diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs index 782eb4ba1ac9..1fbffd005e5e 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs @@ -19,6 +19,7 @@ internal enum WellKnownType Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata, Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata, Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata, Microsoft_AspNetCore_Http_HeaderDictionaryExtensions, Microsoft_AspNetCore_Routing_IEndpointRouteBuilder, Microsoft_AspNetCore_Mvc_ControllerAttribute, @@ -71,6 +72,7 @@ internal sealed class WellKnownTypes "Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata", "Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata", "Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata", "Microsoft.AspNetCore.Http.HeaderDictionaryExtensions", "Microsoft.AspNetCore.Routing.IEndpointRouteBuilder", "Microsoft.AspNetCore.Mvc.ControllerAttribute", diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs index 25a70367cc61..dabd6b18c90e 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs @@ -31,11 +31,33 @@ static ImmutableArray ResolvedParametersCore(ISymbol symbol, IS } else { - resolvedParameterSymbols.Add(new ParameterSymbol(child, topLevelSymbol)); + var routeParameterName = ResolveRouteParameterName(child, wellKnownTypes); + resolvedParameterSymbols.Add(new ParameterSymbol(routeParameterName, child, topLevelSymbol)); } } return resolvedParameterSymbols.ToImmutable(); } + + static string ResolveRouteParameterName(ISymbol parameterSymbol, WellKnownTypes wellKnownTypes) + { + var fromRouteMetadata = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata); + if (!parameterSymbol.HasAttributeImplementingInterface(fromRouteMetadata, out var attributeData)) + { + return parameterSymbol.Name; // No route metadata attribute! + } + + foreach (var namedArgument in attributeData.NamedArguments) + { + if (namedArgument.Key == "Name") + { + var routeParameterNameConstant = namedArgument.Value; + var routeParameterName = (string)routeParameterNameConstant.Value!; + return routeParameterName; // Have attribute & name is specified. + } + } + + return parameterSymbol.Name; // We have the attribute, but name isn't specified! + } } public static ImmutableArray GetParameterSymbols(ISymbol symbol) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs index c61319f2e846..24a5a3010b3f 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs @@ -22,7 +22,8 @@ internal enum RouteUsageType Component } -internal record struct ParameterSymbol(ISymbol Symbol, ISymbol? TopLevelSymbol = null) +// RouteParameterName can be different from parameter name using FromRouteAttribute. e.g. [FromRoute(Name = "custom_name")] +internal record struct ParameterSymbol(string RouteParameterName, ISymbol Symbol, ISymbol? TopLevelSymbol = null) { public bool IsNested => TopLevelSymbol != null; } diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SymbolExtensions.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SymbolExtensions.cs index ff6a62a0a5c4..8a23fe3f33fd 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SymbolExtensions.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SymbolExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; @@ -23,6 +24,26 @@ public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeT return false; } + public static bool HasAttributeImplementingInterface(this ISymbol symbol, INamedTypeSymbol interfaceType) + { + return HasAttributeImplementingInterface(symbol, interfaceType, out var _); + } + + public static bool HasAttributeImplementingInterface(this ISymbol symbol, INamedTypeSymbol interfaceType, [NotNullWhen(true)] out AttributeData? matchedAttribute) + { + foreach (var attributeData in symbol.GetAttributes()) + { + if (attributeData.AttributeClass is not null && Implements(attributeData.AttributeClass, interfaceType)) + { + matchedAttribute = attributeData; + return true; + } + } + + matchedAttribute = null; + return false; + } + public static bool Implements(this ITypeSymbol type, ITypeSymbol interfaceType) { foreach (var t in type.AllInterfaces) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternAnalyzer.cs index c369d316c26c..124e59cc676f 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternAnalyzer.cs @@ -82,7 +82,7 @@ private void Analyze( foreach (var parameter in routeUsage.UsageContext.ResolvedParameters) { - routeParameterNames.Remove(parameter.Symbol.Name); + routeParameterNames.Remove(parameter.RouteParameterName); } foreach (var unusedParameterName in routeParameterNames) @@ -143,7 +143,7 @@ private record struct InsertPoint(ISymbol ExistingParameter, bool Before); continue; } - var parameterSymbol = resolvedParameterSymbols.FirstOrDefault(s => string.Equals(s.Symbol.Name, routeParameter.Name, StringComparison.OrdinalIgnoreCase)); + var parameterSymbol = resolvedParameterSymbols.FirstOrDefault(s => string.Equals(s.RouteParameterName, routeParameter.Name, StringComparison.OrdinalIgnoreCase)); if (parameterSymbol.Symbol != null) { var s = parameterSymbol.TopLevelSymbol ?? parameterSymbol.Symbol; diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs index 04b683d79bce..a1e27c3c9010 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs @@ -237,9 +237,9 @@ private static void ProvideParameterCompletions(EmbeddedCompletionContext contex foreach (var parameterSymbol in context.RouteUsage.UsageContext.ResolvedParameters) { // Don't suggest parameter name if it already exists in the route. - if (!context.RouteUsage.RoutePattern.TryGetRouteParameter(parameterSymbol.Symbol.Name, out _)) + if (!context.RouteUsage.RoutePattern.TryGetRouteParameter(parameterSymbol.RouteParameterName, out _)) { - context.AddIfMissing(parameterSymbol.Symbol.Name, suffix: null, description: null, WellKnownTags.Parameter, parentOpt: parentOpt); + context.AddIfMissing(parameterSymbol.RouteParameterName, suffix: null, description: null, WellKnownTags.Parameter, parentOpt: parentOpt); } } } diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternHighlighter.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternHighlighter.cs index e495197a92fa..c17706c40f8c 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternHighlighter.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternHighlighter.cs @@ -8,7 +8,6 @@ using System.Threading; using Microsoft.AspNetCore.Analyzers.Infrastructure.RoutePattern; using Microsoft.AspNetCore.Analyzers.Infrastructure.VirtualChars; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -22,7 +21,6 @@ internal class RoutePatternHighlighter : IAspNetCoreEmbeddedLanguageDocumentHigh public ImmutableArray GetDocumentHighlights( SemanticModel semanticModel, SyntaxToken token, int position, CancellationToken cancellationToken) { - var wellKnownTypes = WellKnownTypes.GetOrCreate(semanticModel.Compilation); var routeUsageCache = RouteUsageCache.GetOrCreate(semanticModel.Compilation); var routeUsage = routeUsageCache.Get(token, cancellationToken); if (routeUsage is null) @@ -30,19 +28,20 @@ public ImmutableArray GetDocumentHighlights( return ImmutableArray.Empty; } - return GetHighlights(routeUsage.RoutePattern, semanticModel, wellKnownTypes, position, routeUsage.UsageContext.MethodSymbol, cancellationToken); + return GetHighlights(routeUsage, semanticModel, position, cancellationToken); } private static ImmutableArray GetHighlights( - RoutePatternTree tree, SemanticModel semanticModel, WellKnownTypes wellKnownTypes, int position, IMethodSymbol? methodSymbol, CancellationToken cancellationToken) + RouteUsageModel routeUsage, SemanticModel semanticModel, int position, CancellationToken cancellationToken) { - var virtualChar = tree.Text.Find(position); + var routePattern = routeUsage.RoutePattern; + var virtualChar = routePattern.Text.Find(position); if (virtualChar == null) { return ImmutableArray.Empty; } - var node = FindParameterNode(tree.Root, virtualChar.Value); + var node = FindParameterNode(routePattern.Root, virtualChar.Value); if (node == null) { return ImmutableArray.Empty; @@ -53,21 +52,17 @@ private static ImmutableArray GetHighlights( // Highlight the parameter in the route string, e.g. "{id}" highlights "id". highlightSpans.Add(new AspNetCoreHighlightSpan(node.GetSpan(), AspNetCoreHighlightSpanKind.Reference)); - if (methodSymbol != null) + if (routeUsage.UsageContext.MethodSymbol is { } methodSymbol) { // Resolve possible parameter symbols. Includes properties from AsParametersAttribute. - var routeParameters = RoutePatternParametersDetector.ResolvedParameters(methodSymbol, wellKnownTypes); - var parameterSymbols = routeParameters.Select(p => p.Symbol).ToArray(); + var resolvedParameters = routeUsage.UsageContext.ResolvedParameters; // Match route parameter to method parameter. Parameters in a route aren't case sensitive. - // First attempt an exact match, then a case insensitive match. + // It's possible to match multiple parameters, either based on parameter name, or [FromRoute(Name = "XXX")] attribute. var parameterName = node.ParameterNameToken.Value!.ToString(); - var matchingParameterSymbol = parameterSymbols.FirstOrDefault(s => s.Name == parameterName) - ?? parameterSymbols.FirstOrDefault(s => string.Equals(s.Name, parameterName, StringComparison.OrdinalIgnoreCase)); - - if (matchingParameterSymbol != null) + foreach (var matchingParameter in resolvedParameters.Where(s => string.Equals(s.RouteParameterName, parameterName, StringComparison.OrdinalIgnoreCase))) { - HighlightSymbol(semanticModel, methodSymbol, highlightSpans, matchingParameterSymbol, cancellationToken); + HighlightSymbol(semanticModel, methodSymbol, highlightSpans, matchingParameter.Symbol, cancellationToken); } } diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs index 1595a60d5421..921e7f7f5d2f 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs @@ -325,6 +325,80 @@ public object TestAction() }); } + [Fact] + public async Task ControllerAction_MatchRouteParameterWithFromRoute_NoDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; + +class Program +{ + static void Main() + { + } +} + +public class TestController +{ + [HttpGet(@""{id}"")] + public object TestAction([FromRoute(Name = ""id"")]string id1) + { + return null; + } +} +"); + + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task ControllerAction_MatchRouteParameterWithMultipleFromRoute_NoDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; + +class Program +{ + static void Main() + { + } +} + +public class TestController +{ + [HttpGet(@""{id}"")] + public object TestAction([CustomFromRoute(Name = ""id"")][FromRoute(Name = ""custom_id"")]string id1) + { + return null; + } +} + +public class CustomFromRouteAttribute : Attribute, IFromRouteMetadata +{ + public string? Name { get; set; } +} +"); + + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Empty(diagnostics); + } + [Fact] public async Task MapGet_AsParameter_NoResults() { diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternCompletionProviderTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternCompletionProviderTests.cs index 2ce4171e5fd9..b59c7b3ec8a2 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternCompletionProviderTests.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternCompletionProviderTests.cs @@ -256,6 +256,31 @@ static void Main() i => Assert.Equal("id", i.DisplayText)); } + [Fact] + public async Task Insertion_ParameterOpenBrace_EndpointMapGet_HasDelegate_FromRouteAttribute_ReturnDelegateParameterItem() + { + // Arrange & Act + var result = await GetCompletionsAndServiceAsync(@" +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; + +class Program +{ + static void Main() + { + EndpointRouteBuilderExtensions.MapGet(null, @""{$$"", ([FromRoute(Name = ""id1"")]string id) => ""); + } +} +"); + + // Assert + Assert.Collection( + result.Completions.ItemsList, + i => Assert.Equal("id1", i.DisplayText)); + } + [Fact] public async Task Insertion_ParameterOpenBrace_EndpointMapGet_HasMethod_ReturnDelegateParameterItem() { diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternHighlighterTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternHighlighterTests.cs index c2a2b3354817..6341f6592efd 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternHighlighterTests.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternHighlighterTests.cs @@ -159,6 +159,72 @@ static void Main() "); } + [Fact] + public async Task InParameterName_ExtensionMethod_MatchingDelegate_RouteMetadataWithoutName_HighlightParameter() + { + // Arrange & Act & Assert + await TestHighlightingAsync(@" +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +class Program +{ + static void Main() + { + IEndpointRouteBuilder builder = null; + builder.MapGet(@""{$$[|id|]}"", ([FromRoute]string [|id|]) => $""{[|id|]}""); + } +} +"); + } + + [Fact] + public async Task InParameterName_ExtensionMethod_MatchingDelegate_RouteMetadataWithName_HighlightParameter() + { + // Arrange & Act & Assert + await TestHighlightingAsync(@" +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +class Program +{ + static void Main() + { + IEndpointRouteBuilder builder = null; + builder.MapGet(@""{$$[|id|]}"", ([FromRoute(Name = ""id"")]string [|id1|]) => $""{[|id1|]}""); + } +} +"); + } + + [Fact] + public async Task InParameterName_ExtensionMethod_MatchingDelegate_RouteMetadataWithName_MultipleMatches_HighlightParameter() + { + // Arrange & Act & Assert + await TestHighlightingAsync(@" +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +class Program +{ + static void Main() + { + IEndpointRouteBuilder builder = null; + builder.MapGet(@""{$$[|id|]}"", ([FromRoute(Name = ""id"")]string [|id1|], string [|id|]) => $""{[|id1|]}""); + } +} +"); + } + [Fact] public async Task InParameterName_MatchingDelegate_HighlightParameter() {