Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Adds support for generating proxies at design-time from the IDE
Adds analyzer and codefix to detect ProxyGenerator-annotated method invocations
and generates and adds a proxy for them in the IDE via a codefix.

This required a minor refactoring of the proxy generation we had, pretty much
everything is reused as-is.

Created a separate Proxy.Sdk that contains the document visiting/generation
interfaces so that third parties can extend this process via MEF in the IDE
natively, or via the existing MSBuild-based mechanism for the compile-time
generation and discovery.
  • Loading branch information
kzu committed Jul 2, 2017
commit e1af78d13f54795ffeb08a4ec03ddb4cd882155b
2 changes: 1 addition & 1 deletion build.proj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<PropertyGroup>
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
<IntermediateOutputPath>$(RestoreOutputPath)\</IntermediateOutputPath>
<AdditionalProperties>Configuration=$(Configuration)</AdditionalProperties>
<AdditionalProperties>Configuration=$(Configuration);Dev=15.0</AdditionalProperties>
<Out Condition="'$(Out)' == ''">out</Out>
</PropertyGroup>

Expand Down
120 changes: 120 additions & 0 deletions src/Analyzer.Vsix/BindingRedirects.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" InitialTargets="SetupBindingRedirects" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
<BindingRedirects>BindingRedirects.pkgdef</BindingRedirects>
<!-- Expression applied to the %(ReferencePath.FusionName) to determine if binding redirection will be generated -->
<BindingRedirectFusionExpr>^Microsoft.CodeAnalysis|^System.Composition</BindingRedirectFusionExpr>
<!-- Whether to use 99.9.9.9 as the old version max range if no %(BindingRedirected.OldVersionTo) is found.
If not specified, the determined fusion version of the resolved @(ReferencePath) will be used instead. -->
<BindingRedirectAllVersions Condition="'$(BindingRedirectAllVersions)' == ''">true</BindingRedirectAllVersions>
</PropertyGroup>

<Target Name="SetupBindingRedirects">
<PropertyGroup>
<BindingRedirectsDependsOn>
CollectBindingRedirected;
CleanBindingRedirectsPackage;
GenerateBindingRedirectsPackage
</BindingRedirectsDependsOn>
<GetCopyToOutputDirectoryItemsDependsOn>
BindingRedirects;
$(GetCopyToOutputDirectoryItemsDependsOn)
</GetCopyToOutputDirectoryItemsDependsOn>
<ResolveReferencesDependsOn>
$(ResolveReferencesDependsOn);
BindingRedirects
</ResolveReferencesDependsOn>
<BuildDependsOn>
$(BuildDependsOn);
ReportBindingRedirects
</BuildDependsOn>
</PropertyGroup>
</Target>

<ItemDefinitionGroup>
<!-- Facade assemblies don't have this metadata attribute, so default it to something. -->
<ReferencePath>
<FusionName></FusionName>
</ReferencePath>
</ItemDefinitionGroup>

<Target Name="BindingRedirects" DependsOnTargets="$(BindingRedirectsDependsOn)" />

<Target Name="ReportBindingRedirects">
<Message Text="Binding redirects package file: $(IntermediateOutputPath)$(BindingRedirects)" Importance="high" />
<Message Text="Binding redirecting @(XamarinVisualStudioAssembly -> Count()) Xamarin.VisualStudio.* assemblies" Importance="high" />
<Message Text=" - %(XamarinVisualStudioAssembly.Filename)%(XamarinVisualStudioAssembly.Extension)" Importance="normal" />
<Message Text="Binding redirecting @(XamarinMessagingAssembly -> Count()) Xamarin.Messaging.* assemblies" Importance="high" />
<Message Text=" - %(XamarinMessagingAssembly.Filename)%(XamarinMessagingAssembly.Extension)" Importance="normal" />

<ItemGroup>
<ExtraBindingRedirects Include="@(BindingRedirected)" Exclude="@(XamarinVisualStudioAssembly);@(XamarinMessagingAssembly)" />
</ItemGroup>

<Message Text="Binding redirecting @(ExtraBindingRedirects -> Count()) extra assemblies" Importance="high" />
<Message Text=" - %(ExtraBindingRedirects.Filename)%(ExtraBindingRedirects.Extension)" Importance="normal" />
</Target>

<Target Name="CollectBindingRedirected" DependsOnTargets="ResolveAssemblyReferences">
<ItemGroup>
<BindingRedirected Include="@(ReferencePath)" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(FusionName)', $(BindingRedirectFusionExpr)))" />
<!-- Special-case Clide since we don't want to bind-redirect v3 -->
<BindingRedirected Include="@(ReferencePath)" Condition="$([System.String]::new('%(FusionName)').StartsWith('Clide'))">
<OldVersionTo>2.9.9.9999</OldVersionTo>
</BindingRedirected>
</ItemGroup>
</Target>

<Target Name="CleanBindingRedirectsPackage"
Inputs="@(ReferencePath);$(MSBuildThisFileFullPath)$(MSBuildProjectFullPath)"
Outputs="$(IntermediateOutputPath)$(BindingRedirects)">
<!-- If we're in this target, it's because the file is out of date, or it doesn't exist -->
<Delete Files="$(IntermediateOutputPath)$(BindingRedirects)" Condition="Exists('$(IntermediateOutputPath)$(BindingRedirects)')" />
</Target>

<Target Name="GenerateBindingRedirectsPackage" Inputs="@(BindingRedirected)" Outputs="%(Identity)-BATCH">
<PropertyGroup>
<_FusionName>%(BindingRedirected.FusionName)</_FusionName>
<_IsFullName Condition=" $(_FusionName.IndexOf(',')) != '-1' ">true</_IsFullName>
</PropertyGroup>
<PropertyGroup Condition=" '$(_IsFullName)' == 'true' ">
<_Name>$(_FusionName.Substring(0, $(_FusionName.IndexOf(','))))</_Name>
<_IndexOfToken>$(_FusionName.IndexOf('PublicKeyToken='))</_IndexOfToken>
<_IndexOfToken>$([MSBuild]::Add($(_IndexOfToken), 15))</_IndexOfToken>
<_Token>$(_FusionName.Substring($(_IndexOfToken)))</_Token>
</PropertyGroup>

<ItemGroup Condition=" '$(_IsFullName)' == 'true' ">
<BindingRedirected>
<Guid>$([System.Guid]::NewGuid().ToString().ToUpper())</Guid>
<AssemblyName>$(_Name)</AssemblyName>
<PublicKeyToken>$(_Token)</PublicKeyToken>
<OldVersionFrom Condition=" '%(BindingRedirected.OldVersionFrom)' == '' ">0.0.0.0</OldVersionFrom>
<OldVersionTo Condition=" '%(BindingRedirected.OldVersionTo)' == '' And '$(BindingRedirectAllVersions)' == 'true'">99.9.9.9</OldVersionTo>
<OldVersionTo Condition=" '%(BindingRedirected.OldVersionTo)' == '' And '$(BindingRedirectAllVersions)' != 'true'">%(BindingRedirected.Version)</OldVersionTo>
</BindingRedirected>
</ItemGroup>

<WriteLinesToFile File="$(IntermediateOutputPath)$(BindingRedirects)" Overwrite="false"
Condition=" '$(_IsFullName)' == 'true' And '%(BindingRedirected.PublicKeyToken)' != 'null' "
Lines='[$RootKey$\RuntimeConfiguration\dependentAssembly\bindingRedirection\{%(BindingRedirected.Guid)}]
"name"="%(BindingRedirected.AssemblyName)"
"publicKeyToken"="%(BindingRedirected.PublicKeyToken)"
"culture"="neutral"
"oldVersion"="%(BindingRedirected.OldVersionFrom)-%(BindingRedirected.OldVersionTo)"
"newVersion"="%(BindingRedirected.Version)"
"codeBase"="$PackageFolder$\%(BindingRedirected.Filename)%(BindingRedirected.Extension)"
'/>

<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)$(BindingRedirects)" />
<Content Include="$(IntermediateOutputPath)$(BindingRedirects)">
<IncludeInVSIX>true</IncludeInVSIX>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>$(BindingRedirects)</Link>
</Content>
</ItemGroup>
</Target>

</Project>
33 changes: 33 additions & 0 deletions src/Analyzer.Vsix/Moq.Analyzer.Vsix.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net461;net462</TargetFrameworks>
<Dev>14.0</Dev>

<GeneratePkgDefFile>false</GeneratePkgDefFile>
<IncludeAssemblyInVSIXContainer>false</IncludeAssemblyInVSIXContainer>
<IncludeCopyLocalReferencesInVSIXContainer>true</IncludeCopyLocalReferencesInVSIXContainer>
<IncludeDebugSymbolsInVSIXContainer>false</IncludeDebugSymbolsInVSIXContainer>
<IncludeDebugSymbolsInLocalVSIXDeployment>false</IncludeDebugSymbolsInLocalVSIXDeployment>
<CopyBuildOutputToOutputDirectory>false</CopyBuildOutputToOutputDirectory>
<CopyOutputSymbolsToOutputDirectory>false</CopyOutputSymbolsToOutputDirectory>
<VSSDKTargetPlatformRegRootSuffix>Moq</VSSDKTargetPlatformRegRootSuffix>

<TargetVsixContainerName>Moq.Analyzer.vsix</TargetVsixContainerName>
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.VSSDK.BuildTools" Version="15.1.192" />
<PackageReference Include="Xamarin.VSSDK.BuildTools" Version="0.2.1-pre-build0027" />
<PackageReference Include="Roslynator" Version="0.2.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Analyzer\Moq.Analyzer.csproj" />
<ProjectReference Include="..\Proxy\Proxy.Generator\Moq.Proxy.Generator.csproj" />
<ProjectReference Include="..\Sdk.Generator\Moq.Sdk.Generator.csproj" />
</ItemGroup>

<!--<Import Project="BindingRedirects.targets" />-->
</Project>
9 changes: 9 additions & 0 deletions src/Analyzer.Vsix/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"profiles": {
"Moq.Analyzer.Vsix": {
"commandName": "Executable",
"executablePath": "$(VsInstallRoot)\\Common7\\IDE\\devenv.exe",
"commandLineArgs": "/rootSuffix $(VSSDKTargetPlatformRegRootSuffix)"
}
}
}
24 changes: 24 additions & 0 deletions src/Analyzer.Vsix/source.extension.vsixmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
<Metadata>
<Identity Id="Moq" Version="1.0" Language="en-US" Publisher="kzu" />
<DisplayName>Moq</DisplayName>
<Description xml:space="preserve">Provides analyzers and code fixes for Moq.</Description>
</Metadata>
<Installation>
<InstallationTarget Version="[14.0,)" Id="Microsoft.VisualStudio.Community" />
</Installation>
<Assets>
<Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="File" Path="Roslyn.Services.Editor.UnitTests.dll" />
<Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="File" Path="Moq.Proxy.Generator.dll"/>
<Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="File" Path="Moq.Sdk.Generator.dll"/>
<Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="File" Path="Moq.Analyzer.dll"/>
<Asset Type="Microsoft.VisualStudio.Analyzer" d:Source="File" Path="Moq.Analyzer.dll"/>
<!--<Asset Type="Microsoft.VisualStudio.Analyzer" d:Source="File" Path="Roslyn.Services.Editor.UnitTests.dll" />-->
<!--<Asset Type="Microsoft.VisualStudio.VsPackage" d:Source="File" Path="BindingRedirects.pkgdef" />-->
</Assets>
<Prerequisites>
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[15.0,)" DisplayName="Visual Studio core editor" />
<Prerequisite Id="Microsoft.VisualStudio.Component.Roslyn.LanguageServices" Version="[15.0,)" DisplayName="Roslyn Language Services" />
</Prerequisites>
</PackageManifest>
63 changes: 63 additions & 0 deletions src/Analyzer/MissingProxyAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Moq.Analyzer.Properties;
using Moq.Proxy;

namespace Moq.Analyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class MissingProxyAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "MOQ001";

static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.MissingProxyAnalyzer_Title), Resources.ResourceManager, typeof(Resources));
static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.MissingProxyAnalyzer_Description), Resources.ResourceManager, typeof(Resources));
static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.MissingProxyAnalyzer_Message), Resources.ResourceManager, typeof(Resources));

const string Category = "Build";

private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, Microsoft.CodeAnalysis.CSharp.SyntaxKind.InvocationExpression);
context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.InvocationExpression);
}

static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
{
var generator = context.Compilation.GetTypeByMetadataName(typeof(ProxyGeneratorAttribute).FullName);
if (generator == null)
return;

var symbol = context.Compilation.GetSemanticModel(context.Node.SyntaxTree).GetSymbolInfo(context.Node);
if (symbol.Symbol?.Kind == SymbolKind.Method)
{
var method = (IMethodSymbol)symbol.Symbol;
if (method.GetAttributes().Any(x => x.AttributeClass == generator) &&
// Skip generic method definitions since they are typically usability overloads
// like Mock.Of<T>(...)
!method.TypeArguments.Any(x => x.Kind == SymbolKind.TypeParameter))
{
var name = ProxyGenerator.GetProxyFullName(method.TypeArguments);

// See if the proxy already exists
var proxy = context.Compilation.GetTypeByMetadataName(name);
if (proxy == null)
{
var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(),
new [] { new KeyValuePair<string, string>("Name", name) }.ToImmutableDictionary(),
name);

context.ReportDiagnostic(diagnostic);
}
}
}
}
}
}
112 changes: 112 additions & 0 deletions src/Analyzer/MissingProxyCodeFix.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host;
using System.IO;
using Microsoft.CodeAnalysis.Text;
using Moq.Analyzer.Properties;
using Moq.Proxy;
using Microsoft.CodeAnalysis.Editing;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.Simplification;

namespace Moq.Analyzer
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingProxyCodeFix)), Shared]
public class MissingProxyCodeFix : CodeFixProvider
{
ICodeAnalysisServices analysisServices;

[ImportingConstructor]
public MissingProxyCodeFix([Import(AllowDefault = true)] ICodeAnalysisServices analysisServices) => this.analysisServices = analysisServices;

public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get => ImmutableArray.Create(MissingProxyAnalyzer.DiagnosticId);
}

public sealed override FixAllProvider GetFixAllProvider()
{
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
// TODO: implement
return WellKnownFixAllProviders.BatchFixer;
}

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;

// Find the invocation identified by the diagnostic.
var invocation = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<InvocationExpressionSyntax>().First();

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title: Strings.MissingProxyCodeFix.Title,
createChangedSolution: c => GenerateProxyAsync(context.Document, invocation, c),
equivalenceKey: nameof(MissingProxyCodeFix)),
diagnostic);
}

async Task<Solution> GenerateProxyAsync(Document document, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
{
var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
var symbol = semanticModel.GetSymbolInfo(invocation);
if (symbol.Symbol?.Kind == SymbolKind.Method)
{
var method = (IMethodSymbol)symbol.Symbol;

var generator = SyntaxGenerator.GetGenerator(document.Project);
var (name, syntax) = ProxyGenerator.CreateProxy(method.TypeArguments, generator);

var code = syntax.NormalizeWhitespace().ToFullString();

var workspace = new AdhocWorkspace(document.Project.Solution.Workspace.Services.HostServices, "Proxy");
var info = ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"Proxy",
"Proxy",
document.Project.Language,
compilationOptions: document.Project.CompilationOptions,
parseOptions: document.Project.ParseOptions,
metadataReferences: document.Project.MetadataReferences);
var project = workspace.AddProject(info);
var file = Path.Combine(Path.GetDirectoryName(document.Project.FilePath), ProxyGenerator.ProxyNamespace, name + ".cs");
var proxy = project.AddDocument(Path.GetFileName(file),
SourceText.From(code),
new[] { "Proxies" },
file);

proxy = await ProxyGenerator.ApplyVisitors(proxy, analysisServices, cancellationToken);
proxy = await Simplifier.ReduceAsync(proxy);
syntax = await proxy.GetSyntaxRootAsync();

var output = syntax.NormalizeWhitespace().ToFullString();

return document.Project.AddDocument(Path.GetFileName(file),
output,
new[] { "Proxies" },
file)
.Project.Solution;
}
else
{
return document.Project.Solution;
}
}
}
}
Loading