Skip to content

Instantly share code, notes, and snippets.

@Josrph
Last active November 9, 2022 17:55
Show Gist options
  • Save Josrph/5142203220df907a80d5c0ada7858576 to your computer and use it in GitHub Desktop.
Save Josrph/5142203220df907a80d5c0ada7858576 to your computer and use it in GitHub Desktop.
Roslyn-based Refit Route Analyzer.
[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();
}
}
[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