Created
November 2, 2020 08:02
-
-
Save emir01/9858246f84964fd5bde923fc334fe4fe to your computer and use it in GitHub Desktop.
The full Code for the Roslyn Blog Post Part 2 - Analyzer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Immutable; | |
using System.Linq; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
using Microsoft.CodeAnalysis.Diagnostics; | |
namespace MultipleMethodCallAnalyzer.Diagnostics | |
{ | |
[DiagnosticAnalyzer(LanguageNames.CSharp)] | |
public class Analyzer : DiagnosticAnalyzer | |
{ | |
public const string DiagnosticId = "MultipleMethodCallDiagnosticAnalyzer"; | |
private static readonly string Title = "Multiple Method Invocation"; | |
public static readonly string MessageFormat = | |
@"{0} called multiple times with identical arguments: [{1}]"; | |
private static readonly string Description = "Multiple Identical Method Invocation"; | |
private const string Category = "Usage"; | |
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, | |
Category, DiagnosticSeverity.Warning, true, Description); | |
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | |
public override void Initialize(AnalysisContext context) | |
{ | |
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | | |
GeneratedCodeAnalysisFlags.None); | |
context.EnableConcurrentExecution(); | |
context.RegisterCodeBlockStartAction<SyntaxKind>(analysisContext => | |
{ | |
if (analysisContext.OwningSymbol.Kind != SymbolKind.Method) | |
{ | |
return; | |
} | |
// create a new analyzer for this code block | |
var analyzer = new StatefulNodeAnalyzer(); | |
analysisContext.RegisterSyntaxNodeAction( | |
ctx => analyzer.AnalyzeSyntaxNode(ctx, analysisContext.CodeBlock), | |
SyntaxKind.InvocationExpression); | |
}); | |
} | |
private class StatefulNodeAnalyzer | |
{ | |
public void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context, | |
SyntaxNode methodCodeBlock) | |
{ | |
// we are going to run the analysis in the context of the code block | |
// which for us would be the method. | |
// | |
// We want to search for any invocations within that method code block - for now! | |
var semanticModel = context.SemanticModel; | |
var node = (InvocationExpressionSyntax)context.Node; | |
var methodSymbol = (IMethodSymbol)semanticModel.GetSymbolInfo(node).Symbol; | |
// todo We should maybe only be interested in Methods returning Reference Types ? | |
if (methodSymbol != null && methodSymbol.ReturnsVoid) | |
{ | |
return; | |
} | |
// traverse the code block | |
var allInvocationExpressions = | |
methodCodeBlock.DescendantNodes().OfType<InvocationExpressionSyntax>().ToList(); | |
// if there are no other invocations except the current one tracked by the 'node' | |
// don't do any other processing. Example we only have a call to our single Invocation | |
if (allInvocationExpressions.Count == 1) | |
{ | |
return; | |
} | |
// I want to get only the nodes that refer to the same function I'm checking for in the current | |
// If for example we had a call to OurMethod(input) and Console.WriteLine() | |
var currentNodeExpression = node.Expression.ToString(); | |
var invocationsOnlyWithCurrentNodeExpression = | |
allInvocationExpressions.Where(expressionNode => | |
expressionNode.Expression.ToString().Equals(currentNodeExpression)).ToList(); | |
// if we end up with 1 then there are no other invocations matching current node. | |
if (invocationsOnlyWithCurrentNodeExpression.Count == 1) | |
{ | |
return; | |
} | |
// Now we get the invocations that are not the Current one by looking at | |
// the location where they start in the code block | |
var otherInvocationsMatchingCurrent = | |
invocationsOnlyWithCurrentNodeExpression.Where(inv => inv.Span.Start != node.Span.Start).ToList(); | |
// simple argument matching | |
var currentArgumentsList = node.ArgumentList.Arguments; | |
var invocationsWithMatchingArgumentList = otherInvocationsMatchingCurrent.Where(inv => | |
ArgumentListsMatch(currentArgumentsList, inv.ArgumentList.Arguments)).ToList(); | |
if (invocationsWithMatchingArgumentList.Count > 0) | |
{ | |
var methodName = currentNodeExpression; | |
var argumentList = node.ArgumentList.Arguments.ToString(); | |
var diagnostic = Diagnostic.Create(Rule, node.GetLocation(), methodName, argumentList); | |
context.ReportDiagnostic(diagnostic); | |
} | |
} | |
// todo: move to helpers | |
private bool ArgumentListsMatch(SeparatedSyntaxList<ArgumentSyntax> originalArgumentList, | |
SeparatedSyntaxList<ArgumentSyntax> toMatchArgumentList) | |
{ | |
if (originalArgumentList.Count != toMatchArgumentList.Count) | |
{ | |
return false; | |
} | |
for (int i = 0; i < originalArgumentList.Count; i++) | |
{ | |
var originalCandidate = originalArgumentList[i]; | |
var toMatchCandidate = toMatchArgumentList[i]; | |
if (originalCandidate.Expression.ToString() != toMatchCandidate.Expression.ToString()) | |
{ | |
return false; | |
} | |
} | |
return true; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment