Last active
November 9, 2022 17:55
-
-
Save Josrph/5142203220df907a80d5c0ada7858576 to your computer and use it in GitHub Desktop.
Roslyn-based Refit Route Analyzer.
This file contains 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
[DiagnosticAnalyzer(LanguageNames.CSharp)] | |
public class RefitRouteAnalyzer : DiagnosticAnalyzer | |
{ | |
private const string Category = "Routing"; | |
public const string DiagnosticId = "RefitRouteAnalyzer"; | |
private static readonly DiagnosticDescriptor AttributeRule = new DiagnosticDescriptor( | |
DiagnosticId, | |
title: "Route string should match method signature", | |
messageFormat: "Attribute name '{0}' contains incorrect route", | |
Category, DiagnosticSeverity.Error, | |
isEnabledByDefault: true, | |
description: "Route attribute contains invalid route."); | |
private static readonly DiagnosticDescriptor InterfaceRule = new DiagnosticDescriptor( | |
DiagnosticId, | |
title: "Route string should match method signature", | |
messageFormat: "Interface name '{0}' contains one or more incorrect routes", | |
Category, DiagnosticSeverity.Error, | |
isEnabledByDefault: true, | |
description: "Interface contains one or more invalid routes."); | |
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get => ImmutableArray.Create(AttributeRule, InterfaceRule); } | |
public override void Initialize(AnalysisContext context) | |
{ | |
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | |
context.EnableConcurrentExecution(); | |
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InterfaceDeclaration); | |
} | |
private void AnalyzeNode(SyntaxNodeAnalysisContext context) | |
{ | |
var interfaceSyntax = (InterfaceDeclarationSyntax)context.Node; | |
var methodAttrList = interfaceSyntax.DescendantNodes() | |
.OfType<AttributeSyntax>() | |
.Where(x => GetTypeFullName(x, context).Equals("Refit.HttpMethodAttribute")) | |
.GroupBy(x => x.Ancestors().OfType<MethodDeclarationSyntax>().FirstOrDefault()).ToList(); | |
var locations = new List<Location>(); | |
var properties = new Dictionary<string, string>(); | |
foreach (var group in methodAttrList) | |
{ | |
var method = group.Key; | |
var attribute = group.FirstOrDefault(); | |
var attributeArg = attribute.ArgumentList.Arguments.FirstOrDefault(); | |
if (attributeArg != null && attributeArg.Expression is LiteralExpressionSyntax argLiteral) | |
{ | |
string route = argLiteral.Token.ValueText; | |
string methodName = method.Identifier.ValueText; | |
var strBuilder = new StringBuilder($"/{methodName}"); | |
var paramNames = GetParameterNames(method, context); | |
if (paramNames.Count > 0) | |
{ | |
strBuilder.Append('?'); | |
for (int i = 0; i < paramNames.Count; i++) | |
{ | |
string param = paramNames[i]; | |
strBuilder.Append($"{param}={{{param}}}"); | |
if (i != (paramNames.Count - 1)) | |
{ | |
strBuilder.Append('&'); | |
} | |
} | |
} | |
string validRoute = strBuilder.ToString(); | |
if (!route.Equals(validRoute)) | |
{ | |
// For all such symbols, produce a diagnostic. | |
var location = attributeArg.GetLocation(); | |
properties.Add((properties.Count + 1).ToString(), validRoute); | |
locations.Add(location); | |
var diagnostic = Diagnostic.Create(AttributeRule, location, attribute.Name.ToString()); | |
context.ReportDiagnostic(diagnostic); | |
} | |
} | |
} | |
if (locations.Count > 0 && locations.Count == properties.Count) | |
{ | |
string interfaceName = interfaceSyntax.Identifier.ValueText; | |
Location location = interfaceSyntax.Identifier.GetLocation(); | |
var diagnostic = Diagnostic.Create(InterfaceRule, location, locations, properties.ToImmutableDictionary(), interfaceName); | |
context.ReportDiagnostic(diagnostic); | |
} | |
} | |
private static string GetTypeFullName(ISymbol symbol) => $"{symbol.ContainingNamespace}.{symbol.Name}"; | |
private static string GetTypeFullName(SyntaxNode node, SyntaxNodeAnalysisContext context) | |
{ | |
var symbol = context.SemanticModel.GetSymbolInfo(node, context.CancellationToken).Symbol; | |
return symbol != null ? GetTypeFullName(symbol.ContainingType.BaseType) : string.Empty; | |
} | |
private static IList<string> GetParameterNames(MethodDeclarationSyntax methodDecl, SyntaxNodeAnalysisContext context) | |
{ | |
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDecl, context.CancellationToken); | |
return methodSymbol.Parameters.Where(x => x.Type.IsValueType || GetTypeFullName(x.Type).Equals("System.String")).Select(x => x.Name).ToList(); | |
} | |
} |
This file contains 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
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RefitRouteCodeFixProvider)), Shared] | |
public class RefitRouteCodeFixProvider : CodeFixProvider | |
{ | |
private const string CodeFixTitle = "Fix routes"; | |
public sealed override ImmutableArray<string> FixableDiagnosticIds | |
{ | |
get { return ImmutableArray.Create(RefitRouteAnalyzer.DiagnosticId); } | |
} | |
public sealed override FixAllProvider GetFixAllProvider() | |
{ | |
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(); | |
if (diagnostic.AdditionalLocations.Count > 0) | |
{ | |
var properties = diagnostic.Properties; | |
var locations = diagnostic.AdditionalLocations; | |
// Register a code action that will invoke the fix. | |
var codeAction = CodeAction.Create( | |
title: CodeFixTitle, | |
createChangedDocument: ct => FixeRoutesAsync(context.Document, root, locations, properties, ct), | |
equivalenceKey: CodeFixTitle); | |
context.RegisterCodeFix(codeAction, diagnostic); | |
} | |
} | |
private async Task<Document> FixeRoutesAsync(Document doc, SyntaxNode root, IReadOnlyList<Location> locations, IImmutableDictionary<string, string> properties, CancellationToken cancellationToken) | |
{ | |
var editor = await DocumentEditor.CreateAsync(doc, cancellationToken).ConfigureAwait(false); | |
for (int i = 0; i < locations.Count; i++) | |
{ | |
var location = locations[i]; | |
var validRoute = properties[(i + 1).ToString()]; | |
if (root.FindToken(location.SourceSpan.Start).Parent is LiteralExpressionSyntax oldLiteral) | |
{ | |
var newliteral = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(validRoute)); | |
editor.ReplaceNode(oldLiteral, newliteral); | |
} | |
} | |
return editor.GetChangedDocument(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment