[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 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() .Where(x => GetTypeFullName(x, context).Equals("Refit.HttpMethodAttribute")) .GroupBy(x => x.Ancestors().OfType().FirstOrDefault()).ToList(); var locations = new List(); var properties = new Dictionary(); 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 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(); } }