| File: RouteEmbeddedLanguage\RoutePatternAnalyzer.cs | Web Access |
| Project: src\aspnetcore\src\Framework\AspNetCoreAnalyzers\src\Analyzers\Microsoft.AspNetCore.App.Analyzers.csproj (Microsoft.AspNetCore.App.Analyzers) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Threading; using Microsoft.AspNetCore.Analyzers.Infrastructure.RoutePattern; using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class RoutePatternAnalyzer : DiagnosticAnalyzer { public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(new[] { DiagnosticDescriptors.RoutePatternIssue, DiagnosticDescriptors.RoutePatternUnusedParameter }); private void AnalyzeSemanticModel(SemanticModelAnalysisContext context) { var semanticModel = context.SemanticModel; var syntaxTree = semanticModel.SyntaxTree; var cancellationToken = context.CancellationToken; var root = syntaxTree.GetRoot(cancellationToken); var routeUsageCache = RouteUsageCache.GetOrCreate(context.SemanticModel.Compilation); // Update to use FilterSpan when available. See https://github.com/dotnet/aspnetcore/issues/48157 foreach (var item in root.DescendantTokens()) { cancellationToken.ThrowIfCancellationRequested(); AnalyzeToken(context, routeUsageCache, item, cancellationToken); } } private static void AnalyzeToken(SemanticModelAnalysisContext context, RouteUsageCache routeUsageCache, SyntaxToken token, CancellationToken cancellationToken) { if (!RouteStringSyntaxDetector.IsRouteStringSyntaxToken(token, context.SemanticModel, cancellationToken, out var options)) { return; } var routeUsage = routeUsageCache.Get(token, cancellationToken); if (routeUsage is null) { return; } foreach (var diag in routeUsage.RoutePattern.Diagnostics) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.RoutePatternIssue, Location.Create(context.SemanticModel.SyntaxTree, diag.Span), DiagnosticDescriptors.RoutePatternIssue.DefaultSeverity, additionalLocations: null, properties: null, diag.Message)); } if (routeUsage.UsageContext.MethodSymbol != null) { var routeParameterNames = new HashSet<string>(routeUsage.RoutePattern.RouteParameters.Select(p => p.Name), StringComparer.OrdinalIgnoreCase); foreach (var parameter in routeUsage.UsageContext.ResolvedParameters) { routeParameterNames.Remove(parameter.RouteParameterName); } foreach (var unusedParameterName in routeParameterNames) { var unusedParameter = routeUsage.RoutePattern.GetRouteParameter(unusedParameterName); var parameterInsertIndex = -1; var insertPoint = CalculateInsertPoint( unusedParameter.Name, routeUsage.RoutePattern.RouteParameters, routeUsage.UsageContext.ResolvedParameters); if (insertPoint is { } ip) { parameterInsertIndex = routeUsage.UsageContext.Parameters.IndexOf(ip.ExistingParameter); if (!ip.Before) { parameterInsertIndex++; } } // These properties are used by the fixer. var propertiesBuilder = ImmutableDictionary.CreateBuilder<string, string?>(); propertiesBuilder.Add("RouteParameterName", unusedParameter.Name); propertiesBuilder.Add("RouteParameterPolicy", string.Join(string.Empty, unusedParameter.Policies)); propertiesBuilder.Add("RouteParameterIsOptional", unusedParameter.IsOptional.ToString(CultureInfo.InvariantCulture)); propertiesBuilder.Add("RouteParameterInsertIndex", parameterInsertIndex.ToString(CultureInfo.InvariantCulture)); context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.RoutePatternUnusedParameter, Location.Create(context.SemanticModel.SyntaxTree, unusedParameter.Span), DiagnosticDescriptors.RoutePatternUnusedParameter.DefaultSeverity, additionalLocations: null, properties: propertiesBuilder.ToImmutableDictionary(), unusedParameterName)); } } } private record struct InsertPoint(ISymbol ExistingParameter, bool Before); private static InsertPoint? CalculateInsertPoint(string routeParameterName, ImmutableArray<RouteParameter> routeParameters, ImmutableArray<ParameterSymbol> resolvedParameterSymbols) { InsertPoint? insertPoint = null; var seenRouteParameterName = false; for (var i = 0; i < routeParameters.Length; i++) { var routeParameter = routeParameters[i]; if (string.Equals(routeParameter.Name, routeParameterName, StringComparison.OrdinalIgnoreCase)) { if (insertPoint != null) { break; } seenRouteParameterName = true; continue; } var parameterSymbol = resolvedParameterSymbols.FirstOrDefault(s => string.Equals(s.RouteParameterName, routeParameter.Name, StringComparison.OrdinalIgnoreCase)); if (parameterSymbol.Symbol != null) { var s = parameterSymbol.TopLevelSymbol ?? parameterSymbol.Symbol; if (!seenRouteParameterName) { insertPoint = new InsertPoint(s, Before: false); } else { insertPoint = new InsertPoint(s, Before: true); break; } } } return insertPoint; } public override void Initialize(AnalysisContext context) { // Run on generated code to include routes specified in Razor files. context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterSemanticModelAction(AnalyzeSemanticModel); } }