Initial end to end working completion with tests

This commit is contained in:
Daniel Cazzulino 2018-11-05 14:58:47 -03:00
Родитель f819973720
Коммит dc849d3adf
12 изменённых файлов: 340 добавлений и 68 удалений

Просмотреть файл

@ -1,2 +1,12 @@
# codeanalysis
# Xamarin CodeAnalyis
Analyzers, code fixers and custom completion for Xamarin projects
## Building
Just open Xamarin.CodeAnalysis.sln and build.
> NOTE: the *first* build ever needs to be run from an administrator command prompt,
> because the extension provides MSBuild targets that need to be symlinked from the
> `Exp` hive location to the `VsInstallDir\MSBuild` location, which requires elevation.

Просмотреть файл

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28218.60
# Visual Studio Version 16
VisualStudioVersion = 16.0.28223.52
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.CodeAnalysis", "src\Xamarin.CodeAnalysis\Xamarin.CodeAnalysis.csproj", "{A83DFC4B-4270-4A1A-8438-897A8AA61AE0}"
EndProject
@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
src\GitInfo.txt = src\GitInfo.txt
src\NuGet.Config = src\NuGet.Config
Packages.props = Packages.props
README.md = README.md
EndProjectSection
EndProject
Global

Просмотреть файл

@ -13,8 +13,9 @@
</PropertyGroup>
<PropertyGroup>
<GitSkipCache Condition="'$(CI)' == 'true'">true</GitSkipCache>
<Configuration Condition="'$(Configuration)' == '' and '$(CI)' == 'true'">Release</Configuration>
<GitSkipCache Condition="$(CI)">true</GitSkipCache>
<Configuration Condition="'$(Configuration)' == '' and $(CI)">Release</Configuration>
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
</PropertyGroup>
<!-- Redeclared by GitInfo -->

Просмотреть файл

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace Xamarin.CodeAnalysis
{
internal static class Extensions
{
//public static async Task<SemanticModel> GetSemanticModelForSpanAsync(this Document document, TextSpan span, CancellationToken cancellationToken)
//{
// var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
// var token = root.FindToken(span.Start);
// if (token.Parent == null)
// {
// return await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
// }
//}
}
}

Просмотреть файл

@ -0,0 +1,129 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
namespace Xamarin.CodeAnalysis
{
[ExportCompletionProvider(nameof(ResourceCompletionProvider), LanguageNames.CSharp)]
public class ResourceCompletionProvider : CompletionProvider
{
private static readonly CompletionItemRules StandardCompletionRules = CompletionItemRules.Default.WithSelectionBehavior(CompletionItemSelectionBehavior.SoftSelection);
public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
{
// TODO: should trigger if we're inside a string
return base.ShouldTriggerCompletion(text, caretPosition, trigger, options);
}
public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
{
// TODO: get the actual xml file location.
return Task.FromResult(CompletionDescription.FromText($"{item.Properties["Path"]}({item.Properties["Line"]},{item.Properties["Position"]})"));
}
public override Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
{
// Determine change to insert
return base.GetChangeAsync(document, item, commitKey, cancellationToken);
}
static readonly Regex isResourceValue = new Regex(@"Resources\\values\\.*.xml", RegexOptions.Compiled);
public override async Task ProvideCompletionsAsync(CompletionContext completionContext)
{
if (!completionContext.Document.SupportsSemanticModel)
return;
var position = completionContext.Position;
var document = completionContext.Document;
var cancellationToken = completionContext.CancellationToken;
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var span = completionContext.CompletionListSpan;
var token = root.FindToken(span.Start);
var node = token.Parent?.AncestorsAndSelf().FirstOrDefault(a => a.FullSpan.Contains(span));
if (node is LiteralExpressionSyntax literal &&
node?.Parent is AttributeArgumentSyntax argument &&
node?.Parent?.Parent?.Parent is AttributeSyntax attribute)
{
var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
var symbol = semanticModel.GetSymbolInfo(attribute, cancellationToken).Symbol;
if (symbol?.ContainingType.ToDisplayString() == "Android.App.ActivityAttribute" ||
(symbol == null && attribute.Name.ToString() == "Activity"))
{
var name = argument.NameEquals.Name.ToString();
// TODO: consider resource files some other way?
var valueDocs = document.Project.AdditionalDocuments.Where(doc => isResourceValue.IsMatch(doc.FilePath));
var elementName = "string";
if (name == "Theme")
elementName = "style";
var strings = new HashSet<string>();
var styles = new HashSet<string>();
var xmlSettings = new XmlReaderSettings
{
IgnoreComments = true,
IgnoreProcessingInstructions = true,
IgnoreWhitespace = true,
};
foreach (var doc in valueDocs)
{
XmlReader reader = null;
try
{
if (File.Exists(doc.FilePath))
{
reader = XmlReader.Create(doc.FilePath, xmlSettings);
}
else
{
// In tests, the file doesn't exist.
var writer = new StringWriter();
(await doc.GetTextAsync(cancellationToken)).Write(writer, cancellationToken);
reader = XmlReader.Create(new StringReader(writer.ToString()), xmlSettings);
}
if (reader.MoveToContent() == XmlNodeType.Element)
{
// TODO: cache already parsed results as long as the text version remains the same?
while (reader.Read())
{
if (reader.LocalName == elementName && reader.GetAttribute("name") is string id)
{
completionContext.AddItem(CompletionItem.Create($"@{reader.LocalName}/{id}",
tags: ImmutableArray.Create(WellKnownTags.Constant, "Xamarin"),
properties: ImmutableDictionary.Create<string, string>()
.Add("Path", doc.FilePath)
.Add("Line", ((IXmlLineInfo)reader).LineNumber.ToString())
.Add("Position", ((IXmlLineInfo)reader).LinePosition.ToString()),
rules: StandardCompletionRules));
}
}
}
}
finally
{
reader?.Dispose();
}
}
}
}
await Task.CompletedTask;
}
}
}

Просмотреть файл

@ -1,46 +0,0 @@
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
namespace Xamarin.CodeAnalysis
{
[ExportCompletionProvider(nameof(StringCompletionProvider), LanguageNames.CSharp)]
public class StringCompletionProvider : CompletionProvider
{
private static readonly CompletionItemRules StandardCompletionRules = CompletionItemRules.Default.WithSelectionBehavior(CompletionItemSelectionBehavior.SoftSelection);
public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
{
// TODO: should trigger if we're inside a string
return base.ShouldTriggerCompletion(text, caretPosition, trigger, options);
}
public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
{
// TODO: get the actual xml file location.
return Task.FromResult(CompletionDescription.FromText("strings.xml(120,34)"));
}
public override Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
{
// Determine change to insert
return base.GetChangeAsync(document, item, commitKey, cancellationToken);
}
public override async Task ProvideCompletionsAsync(CompletionContext context)
{
context.AddItem(CompletionItem.Create("@style/MainTheme",
tags: ImmutableArray.Create(WellKnownTags.Constant),
// TODO: props should contain the file location it was retrieved from to use in description
// properties: ImmutableDictionary.CreateBuilder<string, string>().Add(,
rules: StandardCompletionRules));
await Task.CompletedTask;
}
}
}

Просмотреть файл

@ -0,0 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AdditionalFiles Include="@(AndroidResource -> WithMetadataValue('RelativeDir', 'Resources\values\'))" />
</ItemGroup>
</Project>

Просмотреть файл

@ -20,12 +20,15 @@
<IsExperimental>true</IsExperimental>
<IsSystemComponent>false</IsSystemComponent>
<!--<CreateVsixContainer>true</CreateVsixContainer>-->
<VsixVersion>42.42.42</VsixVersion>
<DefaultItemExcludes>*.targets</DefaultItemExcludes>
<RestoreSources>$(RestoreSources);https://api.nuget.org/v3/index.json;https://nugets.blob.core.windows.net/xvssdk/index.json</RestoreSources>
</PropertyGroup>
<PropertyGroup Condition="'$(CI)' == 'true'">
<PropertyGroup Condition="$(CI)">
<IsProductComponent>true</IsProductComponent>
<IsExperimental>false</IsExperimental>
<IsSystemComponent>true</IsSystemComponent>
@ -59,9 +62,16 @@
<Resource Include="Properties\ICN_Xamarin.ico" />
</ItemGroup>
<ItemGroup>
<Content Include="*.targets" IncludeInVSIX="true" VSIXSubPath="Xamarin" SymLink="true" />
<Content Update="Xamarin.CodeAnalysis.ImportAfter.targets" VSIXSubPath="Current\Microsoft.Common.Targets\ImportAfter" />
<Content Update="@(Content)" Condition="$(CI)" InstallRoot="MSBuild" />
<UpToDateCheckInput Include="@(Content)" />
</ItemGroup>
<Target Name="GetVsixVersion" DependsOnTargets="SetVersions" Returns="$(VsixVersion)">
<PropertyGroup>
<VsixVersion Condition="'$(CI)' == 'true'">$(GitSemVerMajor).$(GitSemVerMinor).$(GitSemVerPatch)</VsixVersion>
<VsixVersion Condition="$(CI)">$(GitSemVerMajor).$(GitSemVerMinor).$(GitSemVerPatch)</VsixVersion>
</PropertyGroup>
</Target>
@ -83,4 +93,71 @@
<Target Name="IsExperimental" Returns="$(IsExperimental)" />
<Target Name="IsSystemComponent" Returns="$(IsSystemComponent)" />
<PropertyGroup Condition="'$(OS)' == 'Windows_NT'">
<BuildDependsOn Condition="!$(CI)">
$(BuildDependsOn);
SymLink
</BuildDependsOn>
</PropertyGroup>
<Target Name="SymLink" DependsOnTargets="IsAdministrator;CollectLinkItems;ReplaceLinkItems" />
<Target Name="IsAdministrator">
<IsAdministrator>
<Output TaskParameter="Result" PropertyName="IsAdministrator" />
</IsAdministrator>
<Warning Text="Current user isn't an Administrator, so MSBuild artifacts won't be symlinked." Condition="'$(IsAdministrator)' == 'false'" />
</Target>
<ItemDefinitionGroup>
<MkLinkCandidate>
<Exists>false</Exists>
<IsSymLink>false</IsSymLink>
</MkLinkCandidate>
<VSIXSourceItem>
<VSIXSubPath />
<SymLink />
</VSIXSourceItem>
</ItemDefinitionGroup>
<Target Name="CollectLinkItems" DependsOnTargets="IsAdministrator;GetVsixDeploymentPath;GetVsixSourceItems" Condition="'$(IsAdministrator)' == 'true'">
<ItemGroup>
<MkLinkCandidate Include="@(VSIXSourceItem -> '$(MSBuildExtensionsPath)\%(VSIXSubPath)\%(Filename)%(Extension)')" Condition="'%(SymLink)' == 'true'">
<LinkTarget>$(VsixDeploymentPath)\%(VSIXSubPath)\%(Filename)%(Extension)</LinkTarget>
</MkLinkCandidate>
<MkLinkCandidate Condition="Exists('%(FullPath)')">
<IsSymLink Condition="$([MSBuild]::BitwiseAnd(1024, $([System.IO.File]::GetAttributes('%(FullPath)')))) == '1024'">true</IsSymLink>
<Exists>true</Exists>
</MkLinkCandidate>
<MkLinkSource Include="@(MkLinkCandidate)" Condition="!Exists('%(FullPath)') Or '%(IsSymLink)' == 'false'" />
</ItemGroup>
</Target>
<Target Name="ReplaceLinkItems" Condition="'@(MkLinkSource)' != '' And '$(IsAdministrator)' == 'true'">
<Message Text="In $(Configuration) builds, we attempt to symlink MSBuild files with current project output." Importance="high" />
<ItemGroup>
<_FilesToDelete Include="@(MkLinkSource -&gt; WithMetadataValue('Exists', 'true'))" />
</ItemGroup>
<Exec Command='del "%(_FilesToDelete.FullPath)"' EchoOff="true" Condition="'@(_FilesToDelete)' != ''" />
<Exec Command='mklink "%(MkLinkSource.Identity)" "%(MkLinkSource.LinkTarget)"'
ConsoleToMSBuild="true"
EchoOff="true"
Condition="Exists('%(MkLinkSource.RootDir)%(MkLinkSource.Directory)')">
<Output TaskParameter="ConsoleOutput" ItemName="MkLinked" />
</Exec>
<Message Importance="high" Text="%(MkLinked.Identity)" Condition="'@(MkLinked)' != ''" />
</Target>
<UsingTask TaskName="IsAdministrator" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<Result ParameterType="System.Boolean" Output="true" />
</ParameterGroup>
<Task>
<Using Namespace="System.Security.Principal" />
<Code Type="Fragment" Language="cs">
Result = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
</Code>
</Task>
</UsingTask>
</Project>

Просмотреть файл

@ -0,0 +1,9 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<XamarinCodeAnalysisTargets Condition="'$(XamarinCodeAnalysisTargets)' == ''">$(MSBuildExtensionsPath)\Xamarin\Xamarin.CodeAnalysis.targets</XamarinCodeAnalysisTargets>
</PropertyGroup>
<Import Condition="Exists('$(XamarinCodeAnalysisTargets)')" Project="$(XamarinCodeAnalysisTargets)" />
</Project>

Просмотреть файл

@ -0,0 +1,5 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="Xamarin.CodeAnalysis.Android.targets" Condition="'$(TargetFrameworkIdentifier)' == 'MonoAndroid'" />
</Project>

Просмотреть файл

@ -1,6 +1,4 @@

using System;
using System.Linq;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
@ -13,21 +11,24 @@ namespace Xamarin.CodeAnalysis.Tests
public class CompletionTests
{
[Theory]
[InlineData(@"using System;
public class Foo
[InlineData(@"using Android.App;
using Android.Support.Design.Widget;
using Android.Support.V7.App;
using Android.Views;
[Activity(Label = ""`"", MainLauncher = true)]
public class MainActivity : AppCompatActivity, NavigationView.IOnNavigationItemSelectedListener
{
public void Do()
{
Console.WriteLine(""`"");
}
}")]
public async Task can_retrieve_completion(string code)
public bool OnNavigationItemSelected(IMenuItem menuItem) => true;
}
", "@string/app_name")]
public async Task can_retrieve_completion(string code, string completion)
{
var hostServices = MefHostServices.Create(MefHostServices.DefaultAssemblies.Concat(
new[]
{
typeof(CompletionService).Assembly,
typeof(StringCompletionProvider).Assembly,
typeof(ResourceCompletionProvider).Assembly,
}));
var workspace = new AdhocWorkspace(hostServices);
@ -40,7 +41,17 @@ public class Foo
MetadataReference.CreateFromFile("Xamarin.CodeAnalysis.dll"),
MetadataReference.CreateFromFile("Xamarin.CodeAnalysis.Completion.dll"),
})
.AddDocument("TestDocument.cs", code);
.AddAdditionalDocument("strings.xml", @"<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name='app_name'>TestApp</string>
</resources>", new[] { "Resources\\values" }, "Resources\\values\\strings.xml")
.Project
.AddAdditionalDocument("styles.xml", @"<?xml version='1.0' encoding='utf-8'?>
<resources>
<style name='AppTheme' parent='Theme.AppCompat.Light.DarkActionBar' />
</resources>", new[] { "Resources\\values" }, "Resources\\values\\styles.xml")
.Project
.AddDocument("TestDocument.cs", code.Replace("`", ""));
var service = CompletionService.GetService(document);
Assert.NotNull(service);
@ -51,7 +62,50 @@ public class Foo
var completions = await service.GetCompletionsAsync(document, caret);
Assert.NotNull(completions);
Assert.NotEmpty(completions.Items);
Assert.Contains(completions.Items, x => x.Tags.Contains("Xamarin"));
Assert.Contains(completions.Items, x => x.DisplayText == completion);
}
[Theory]
[InlineData(@"using System;
public class Foo
{
public void Do()
{
Console.`WriteLine("""");
}
}")]
public async Task does_not_trigger_completion(string code)
{
var hostServices = MefHostServices.Create(MefHostServices.DefaultAssemblies.Concat(
new[]
{
typeof(CompletionService).Assembly,
typeof(ResourceCompletionProvider).Assembly,
}));
var workspace = new AdhocWorkspace(hostServices);
var document = workspace
.AddProject("TestProject", LanguageNames.CSharp)
.WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.WithMetadataReferences(new MetadataReference[]
{
MetadataReference.CreateFromFile(ThisAssembly.Metadata.NETStandardReference),
MetadataReference.CreateFromFile("Xamarin.CodeAnalysis.dll"),
MetadataReference.CreateFromFile("Xamarin.CodeAnalysis.Completion.dll"),
})
.AddDocument("TestDocument.cs", code.Replace("`", ""));
var service = CompletionService.GetService(document);
Assert.NotNull(service);
var caret = code.IndexOf('`');
Assert.NotEqual(-1, caret);
var completions = await service.GetCompletionsAsync(document, caret);
Assert.NotNull(completions);
Assert.DoesNotContain(completions.Items, x => x.Tags.Contains("Xamarin"));
}
}
}

Просмотреть файл

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<TargetFramework Condition="'$(VisualStudioVersion)' == '16.0'">net472</TargetFramework>
</PropertyGroup>
<ItemGroup>