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
Prev Previous commit
Next Next commit
Added outdated analyzer and updating code
Simplified formatting and whitespacing.
  • Loading branch information
kzu committed Jul 8, 2017
commit 1ecc8fc37b1ed1b45b87f3cd66476d158004ee9f
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@

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

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

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

public sealed override FixAllProvider GetFixAllProvider()
Expand All @@ -48,13 +48,13 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
var invocation =
(SyntaxNode)sourceToken.Parent.AncestorsAndSelf().OfType<Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax>().FirstOrDefault() ??
(SyntaxNode)sourceToken.Parent.AncestorsAndSelf().OfType<Microsoft.CodeAnalysis.VisualBasic.Syntax.InvocationExpressionSyntax>().FirstOrDefault();

// 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)),
equivalenceKey: nameof(GenerateProxyCodeFix)),
diagnostic);
}

Expand All @@ -65,66 +65,34 @@ async Task<Solution> GenerateProxyAsync(Document document, SyntaxNode 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 = document.Project.Solution.Workspace;

var projectId = ProjectId.CreateNewId();
var solution = workspace.CurrentSolution.AddProject(ProjectInfo.Create(
projectId,
VersionStamp.Create(),
"Proxy",
"Proxy",
document.Project.Language,
compilationOptions: document.Project.CompilationOptions,
parseOptions: document.Project.ParseOptions,
projectReferences: document.Project.ProjectReferences,
metadataReferences: document.Project.MetadataReferences));
var extension = document.Project.Language == LanguageNames.CSharp ? ".cs" : ".vb";
var file = Path.Combine(Path.GetDirectoryName(document.Project.FilePath), ProxyFactory.ProxyNamespace, name + extension);

// Add the current project's output assembly if it exists, otherwise, all its source docs
if (document.Project.OutputFilePath == null || !File.Exists(document.Project.OutputFilePath))
var proxyDoc = document.Project.Documents.FirstOrDefault(d => d.Name == Path.GetFileName(file) && d.Folders.SequenceEqual(new[] { "Proxies" }));
if (proxyDoc == null)
{
foreach (var doc in document.Project.Documents.Where(d => d.Id != document.Id))
{
solution = solution.AddDocument(DocumentInfo.Create(
DocumentId.CreateNewId(projectId), doc.Name, doc.Folders, doc.SourceCodeKind, filePath: doc.FilePath));
}
proxyDoc = document.Project.AddDocument(Path.GetFileName(file),
syntax,
new[] { "Proxies" },
file);
}
else
{
solution = solution.GetProject(projectId)
.AddMetadataReference(MetadataReference.CreateFromFile(document.Project.OutputFilePath)).Solution;
proxyDoc = proxyDoc.WithSyntaxRoot(syntax);
}

var extension = document.Project.Language == LanguageNames.CSharp ? ".cs" : ".vb";
var file = Path.Combine(Path.GetDirectoryName(document.Project.FilePath), ProxyFactory.ProxyNamespace, name + extension);
var docId = DocumentId.CreateNewId(projectId);
solution = solution.AddDocument(DocumentInfo.Create(
docId,
Path.GetFileName(file),
filePath: file,
folders: new[] { "Proxies" },
loader: TextLoader.From(TextAndVersion.Create(SourceText.From(code), VersionStamp.Create()))));

var proxy = solution.GetDocument(docId);

proxy = await ProxyGenerator.ApplyVisitors(proxy, analysisServices, cancellationToken);
proxyDoc = await ProxyGenerator.ApplyVisitors(proxyDoc, analysisServices, cancellationToken);
// This is somewhat expensive, but since we're adding it to the user' solution, we might
// as well make it look great ;)
proxy = await Simplifier.ReduceAsync(proxy);
proxy = await Formatter.FormatAsync(proxy);
syntax = await proxy.GetSyntaxRootAsync();

code = syntax.NormalizeWhitespace().ToFullString();
proxyDoc = await Simplifier.ReduceAsync(proxyDoc);
proxyDoc = await Formatter.FormatAsync(proxyDoc);
syntax = await proxyDoc.GetSyntaxRootAsync();
//syntax = syntax.NormalizeWhitespace();

return document.Project.AddDocument(Path.GetFileName(file),
code,
new[] { "Proxies" },
file)
.Project.Solution;
return proxyDoc.WithSyntaxRoot(syntax).Project.Solution;
}
else
{
Expand Down
2 changes: 1 addition & 1 deletion src/Analyzer/MissingProxyAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class MissingProxyAnalyzer : DiagnosticAnalyzer

const string Category = "Build";

private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);
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); } }

Expand Down
72 changes: 72 additions & 0 deletions src/Analyzer/OutdatedProxyAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
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 OutdatedProxyAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "MOQ002";

static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.OutdatedProxyAnalyzer_Title), Resources.ResourceManager, typeof(Resources));
static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.OutdatedProxyAnalyzer_Description), Resources.ResourceManager, typeof(Resources));
static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.OutdatedProxyAnalyzer_Message), Resources.ResourceManager, typeof(Resources));

const string Category = "Build";

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);
}

private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
{
var symbol = context.Compilation.GetSemanticModel(context.Node.SyntaxTree).GetSymbolInfo(context.Node);
if (symbol.Symbol?.Kind == SymbolKind.Method)
{
var generator = context.Compilation.GetTypeByMetadataName(typeof(ProxyGeneratorAttribute).FullName);
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)
{
// See if the symbol has any diagnostics associated
var diag = context.Compilation.GetDiagnostics();
var proxyPath = proxy.Locations[0].GetLineSpan().Path;

Func<Location, bool> isProxyLoc = (loc) => loc.IsInSource && loc.GetLineSpan().Path == proxyPath;

var proxyDiag = diag
.Where(d => isProxyLoc(d.Location) || d.AdditionalLocations.Any(a => isProxyLoc(a)))
.Where(d => d.Id == "CS0535");

if (proxyDiag.Any())
{
// If there are compilation errors, we should update the proxy.
var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), name);

context.ReportDiagnostic(diagnostic);
}
}
}
}
}
}
}
29 changes: 28 additions & 1 deletion src/Analyzer/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Analyzer/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@
<data name="MissingProxyCodeFix_Title" xml:space="preserve">
<value>Generate Proxy</value>
</data>
<data name="OutdatedProxyAnalyzer_Description" xml:space="preserve">
<value>Existing generated proxy is no longer up-to-date.</value>
</data>
<data name="OutdatedProxyAnalyzer_Message" xml:space="preserve">
<value>Proxy named '{0}' must be updated.</value>
</data>
<data name="OutdatedProxyAnalyzer_Title" xml:space="preserve">
<value>Proxy must be updated</value>
</data>
<data name="WrongProxyBaseType" xml:space="preserve">
<value>Only one base type can be specified for a proxy to generate and it must be the first type in the list. Invalid set of symbols: {0}.</value>
</data>
Expand Down
73 changes: 54 additions & 19 deletions src/Proxy/Proxy.Generator/Rewrite/CSharpProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,56 @@ public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
.WithModifiers(x.Modifiers))
.ToArray());


node = (ClassDeclarationSyntax)base.VisitClassDeclaration(node);
node = node.AddBaseListTypes(SimpleBaseType(IdentifierName(nameof(IProxy))));

node = (ClassDeclarationSyntax)generator.AddMembers(node,
generator.FieldDeclaration(
"pipeline",
generator.IdentifierName(nameof(BehaviorPipeline)),
initializer: generator.ObjectCreationExpression(generator.IdentifierName(nameof(BehaviorPipeline))))
// #region IProxy
.WithLeadingTrivia(
Whitespace(Environment.NewLine),
Trivia(RegionDirectiveTrivia(false).WithEndOfDirectiveToken(
Token(TriviaList(PreprocessingMessage(nameof(IProxy))),
SyntaxKind.EndOfDirectiveToken,
TriviaList()))),
Whitespace(Environment.NewLine)));

node = node.AddMembers(PropertyDeclaration(
node = node.AddMembers(
FieldDeclaration(
VariableDeclaration(
IdentifierName(Identifier(
TriviaList(
CarriageReturnLineFeed,
Trivia(RegionDirectiveTrivia(true)
.WithRegionKeyword(Token(
TriviaList(),
SyntaxKind.RegionKeyword,
TriviaList(Space)))
.WithEndOfDirectiveToken(Token(
TriviaList(PreprocessingMessage(nameof(IProxy))),
SyntaxKind.EndOfDirectiveToken,
TriviaList(CarriageReturnLineFeed))
)
)
),
nameof(BehaviorPipeline),
TriviaList(Space)
)
)
)
.WithVariables(
SingletonSeparatedList(
VariableDeclarator(Identifier(
TriviaList(),
"pipeline",
TriviaList(Space)))
.WithInitializer(
EqualsValueClause(
ObjectCreationExpression(IdentifierName("BehaviorPipeline"))
.WithNewKeyword(Token(
TriviaList(),
SyntaxKind.NewKeyword,
TriviaList(Space)))
.WithArgumentList(ArgumentList()))
.WithEqualsToken(Token(
TriviaList(),
SyntaxKind.EqualsToken,
TriviaList(Space)))))))
.WithSemicolonToken(Token(
TriviaList(),
SyntaxKind.SemicolonToken,
TriviaList(LineFeed))
),
PropertyDeclaration(
GenericName(Identifier("IList"), TypeArgumentList(SingletonSeparatedList<TypeSyntax>(IdentifierName(nameof(IProxyBehavior))))),
Identifier(nameof(IProxy.Behaviors)))
.WithExplicitInterfaceSpecifier(ExplicitInterfaceSpecifier(IdentifierName(nameof(IProxy))))
Expand All @@ -72,11 +103,15 @@ public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("pipeline"),
IdentifierName("Behaviors"))))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
.WithSemicolonToken(Token(
TriviaList(),
SyntaxKind.SemicolonToken,
TriviaList(CarriageReturnLineFeed)))
// #endregion
.WithTrailingTrivia(TriviaList(
Whitespace(Environment.NewLine),
Trivia(EndRegionDirectiveTrivia(false)))));
CarriageReturnLineFeed,
Trivia(EndRegionDirectiveTrivia(true)),
CarriageReturnLineFeed)));

return node;
}
Expand Down
Loading