Skip to content

Commit 95c74fe

Browse files
authored
Resolve parameter name with FromRouteAttribute in tooling (#45720)
1 parent 8d8e5a8 commit 95c74fe

File tree

10 files changed

+227
-21
lines changed

10 files changed

+227
-21
lines changed

src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal enum WellKnownType
1919
Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata,
2020
Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata,
2121
Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata,
22+
Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata,
2223
Microsoft_AspNetCore_Http_HeaderDictionaryExtensions,
2324
Microsoft_AspNetCore_Routing_IEndpointRouteBuilder,
2425
Microsoft_AspNetCore_Mvc_ControllerAttribute,
@@ -71,6 +72,7 @@ internal sealed class WellKnownTypes
7172
"Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata",
7273
"Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata",
7374
"Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata",
75+
"Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata",
7476
"Microsoft.AspNetCore.Http.HeaderDictionaryExtensions",
7577
"Microsoft.AspNetCore.Routing.IEndpointRouteBuilder",
7678
"Microsoft.AspNetCore.Mvc.ControllerAttribute",

src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,33 @@ static ImmutableArray<ParameterSymbol> ResolvedParametersCore(ISymbol symbol, IS
3131
}
3232
else
3333
{
34-
resolvedParameterSymbols.Add(new ParameterSymbol(child, topLevelSymbol));
34+
var routeParameterName = ResolveRouteParameterName(child, wellKnownTypes);
35+
resolvedParameterSymbols.Add(new ParameterSymbol(routeParameterName, child, topLevelSymbol));
3536
}
3637
}
3738
return resolvedParameterSymbols.ToImmutable();
3839
}
40+
41+
static string ResolveRouteParameterName(ISymbol parameterSymbol, WellKnownTypes wellKnownTypes)
42+
{
43+
var fromRouteMetadata = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata);
44+
if (!parameterSymbol.HasAttributeImplementingInterface(fromRouteMetadata, out var attributeData))
45+
{
46+
return parameterSymbol.Name; // No route metadata attribute!
47+
}
48+
49+
foreach (var namedArgument in attributeData.NamedArguments)
50+
{
51+
if (namedArgument.Key == "Name")
52+
{
53+
var routeParameterNameConstant = namedArgument.Value;
54+
var routeParameterName = (string)routeParameterNameConstant.Value!;
55+
return routeParameterName; // Have attribute & name is specified.
56+
}
57+
}
58+
59+
return parameterSymbol.Name; // We have the attribute, but name isn't specified!
60+
}
3961
}
4062

4163
public static ImmutableArray<ISymbol> GetParameterSymbols(ISymbol symbol)

src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ internal enum RouteUsageType
2222
Component
2323
}
2424

25-
internal record struct ParameterSymbol(ISymbol Symbol, ISymbol? TopLevelSymbol = null)
25+
// RouteParameterName can be different from parameter name using FromRouteAttribute. e.g. [FromRoute(Name = "custom_name")]
26+
internal record struct ParameterSymbol(string RouteParameterName, ISymbol Symbol, ISymbol? TopLevelSymbol = null)
2627
{
2728
public bool IsNested => TopLevelSymbol != null;
2829
}

src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SymbolExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Immutable;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Linq;
78
using Microsoft.CodeAnalysis;
89

@@ -23,6 +24,26 @@ public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeT
2324
return false;
2425
}
2526

27+
public static bool HasAttributeImplementingInterface(this ISymbol symbol, INamedTypeSymbol interfaceType)
28+
{
29+
return HasAttributeImplementingInterface(symbol, interfaceType, out var _);
30+
}
31+
32+
public static bool HasAttributeImplementingInterface(this ISymbol symbol, INamedTypeSymbol interfaceType, [NotNullWhen(true)] out AttributeData? matchedAttribute)
33+
{
34+
foreach (var attributeData in symbol.GetAttributes())
35+
{
36+
if (attributeData.AttributeClass is not null && Implements(attributeData.AttributeClass, interfaceType))
37+
{
38+
matchedAttribute = attributeData;
39+
return true;
40+
}
41+
}
42+
43+
matchedAttribute = null;
44+
return false;
45+
}
46+
2647
public static bool Implements(this ITypeSymbol type, ITypeSymbol interfaceType)
2748
{
2849
foreach (var t in type.AllInterfaces)

src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternAnalyzer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private void Analyze(
8282

8383
foreach (var parameter in routeUsage.UsageContext.ResolvedParameters)
8484
{
85-
routeParameterNames.Remove(parameter.Symbol.Name);
85+
routeParameterNames.Remove(parameter.RouteParameterName);
8686
}
8787

8888
foreach (var unusedParameterName in routeParameterNames)
@@ -143,7 +143,7 @@ private record struct InsertPoint(ISymbol ExistingParameter, bool Before);
143143
continue;
144144
}
145145

146-
var parameterSymbol = resolvedParameterSymbols.FirstOrDefault(s => string.Equals(s.Symbol.Name, routeParameter.Name, StringComparison.OrdinalIgnoreCase));
146+
var parameterSymbol = resolvedParameterSymbols.FirstOrDefault(s => string.Equals(s.RouteParameterName, routeParameter.Name, StringComparison.OrdinalIgnoreCase));
147147
if (parameterSymbol.Symbol != null)
148148
{
149149
var s = parameterSymbol.TopLevelSymbol ?? parameterSymbol.Symbol;

src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,9 @@ private static void ProvideParameterCompletions(EmbeddedCompletionContext contex
237237
foreach (var parameterSymbol in context.RouteUsage.UsageContext.ResolvedParameters)
238238
{
239239
// Don't suggest parameter name if it already exists in the route.
240-
if (!context.RouteUsage.RoutePattern.TryGetRouteParameter(parameterSymbol.Symbol.Name, out _))
240+
if (!context.RouteUsage.RoutePattern.TryGetRouteParameter(parameterSymbol.RouteParameterName, out _))
241241
{
242-
context.AddIfMissing(parameterSymbol.Symbol.Name, suffix: null, description: null, WellKnownTags.Parameter, parentOpt: parentOpt);
242+
context.AddIfMissing(parameterSymbol.RouteParameterName, suffix: null, description: null, WellKnownTags.Parameter, parentOpt: parentOpt);
243243
}
244244
}
245245
}

src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternHighlighter.cs

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using System.Threading;
99
using Microsoft.AspNetCore.Analyzers.Infrastructure.RoutePattern;
1010
using Microsoft.AspNetCore.Analyzers.Infrastructure.VirtualChars;
11-
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
1211
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
1312
using Microsoft.CodeAnalysis;
1413
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -22,27 +21,27 @@ internal class RoutePatternHighlighter : IAspNetCoreEmbeddedLanguageDocumentHigh
2221
public ImmutableArray<AspNetCoreDocumentHighlights> GetDocumentHighlights(
2322
SemanticModel semanticModel, SyntaxToken token, int position, CancellationToken cancellationToken)
2423
{
25-
var wellKnownTypes = WellKnownTypes.GetOrCreate(semanticModel.Compilation);
2624
var routeUsageCache = RouteUsageCache.GetOrCreate(semanticModel.Compilation);
2725
var routeUsage = routeUsageCache.Get(token, cancellationToken);
2826
if (routeUsage is null)
2927
{
3028
return ImmutableArray<AspNetCoreDocumentHighlights>.Empty;
3129
}
3230

33-
return GetHighlights(routeUsage.RoutePattern, semanticModel, wellKnownTypes, position, routeUsage.UsageContext.MethodSymbol, cancellationToken);
31+
return GetHighlights(routeUsage, semanticModel, position, cancellationToken);
3432
}
3533

3634
private static ImmutableArray<AspNetCoreDocumentHighlights> GetHighlights(
37-
RoutePatternTree tree, SemanticModel semanticModel, WellKnownTypes wellKnownTypes, int position, IMethodSymbol? methodSymbol, CancellationToken cancellationToken)
35+
RouteUsageModel routeUsage, SemanticModel semanticModel, int position, CancellationToken cancellationToken)
3836
{
39-
var virtualChar = tree.Text.Find(position);
37+
var routePattern = routeUsage.RoutePattern;
38+
var virtualChar = routePattern.Text.Find(position);
4039
if (virtualChar == null)
4140
{
4241
return ImmutableArray<AspNetCoreDocumentHighlights>.Empty;
4342
}
4443

45-
var node = FindParameterNode(tree.Root, virtualChar.Value);
44+
var node = FindParameterNode(routePattern.Root, virtualChar.Value);
4645
if (node == null)
4746
{
4847
return ImmutableArray<AspNetCoreDocumentHighlights>.Empty;
@@ -53,21 +52,17 @@ private static ImmutableArray<AspNetCoreDocumentHighlights> GetHighlights(
5352
// Highlight the parameter in the route string, e.g. "{id}" highlights "id".
5453
highlightSpans.Add(new AspNetCoreHighlightSpan(node.GetSpan(), AspNetCoreHighlightSpanKind.Reference));
5554

56-
if (methodSymbol != null)
55+
if (routeUsage.UsageContext.MethodSymbol is { } methodSymbol)
5756
{
5857
// Resolve possible parameter symbols. Includes properties from AsParametersAttribute.
59-
var routeParameters = RoutePatternParametersDetector.ResolvedParameters(methodSymbol, wellKnownTypes);
60-
var parameterSymbols = routeParameters.Select(p => p.Symbol).ToArray();
58+
var resolvedParameters = routeUsage.UsageContext.ResolvedParameters;
6159

6260
// Match route parameter to method parameter. Parameters in a route aren't case sensitive.
63-
// First attempt an exact match, then a case insensitive match.
61+
// It's possible to match multiple parameters, either based on parameter name, or [FromRoute(Name = "XXX")] attribute.
6462
var parameterName = node.ParameterNameToken.Value!.ToString();
65-
var matchingParameterSymbol = parameterSymbols.FirstOrDefault(s => s.Name == parameterName)
66-
?? parameterSymbols.FirstOrDefault(s => string.Equals(s.Name, parameterName, StringComparison.OrdinalIgnoreCase));
67-
68-
if (matchingParameterSymbol != null)
63+
foreach (var matchingParameter in resolvedParameters.Where(s => string.Equals(s.RouteParameterName, parameterName, StringComparison.OrdinalIgnoreCase)))
6964
{
70-
HighlightSymbol(semanticModel, methodSymbol, highlightSpans, matchingParameterSymbol, cancellationToken);
65+
HighlightSymbol(semanticModel, methodSymbol, highlightSpans, matchingParameter.Symbol, cancellationToken);
7166
}
7267
}
7368

src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,80 @@ public object TestAction()
325325
});
326326
}
327327

328+
[Fact]
329+
public async Task ControllerAction_MatchRouteParameterWithFromRoute_NoDiagnostics()
330+
{
331+
// Arrange
332+
var source = TestSource.Read(@"
333+
using System;
334+
using System.Diagnostics.CodeAnalysis;
335+
using Microsoft.AspNetCore.Builder;
336+
using Microsoft.AspNetCore.Mvc;
337+
338+
class Program
339+
{
340+
static void Main()
341+
{
342+
}
343+
}
344+
345+
public class TestController
346+
{
347+
[HttpGet(@""{id}"")]
348+
public object TestAction([FromRoute(Name = ""id"")]string id1)
349+
{
350+
return null;
351+
}
352+
}
353+
");
354+
355+
// Act
356+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
357+
358+
// Assert
359+
Assert.Empty(diagnostics);
360+
}
361+
362+
[Fact]
363+
public async Task ControllerAction_MatchRouteParameterWithMultipleFromRoute_NoDiagnostics()
364+
{
365+
// Arrange
366+
var source = TestSource.Read(@"
367+
using System;
368+
using System.Diagnostics.CodeAnalysis;
369+
using Microsoft.AspNetCore.Builder;
370+
using Microsoft.AspNetCore.Http.Metadata;
371+
using Microsoft.AspNetCore.Mvc;
372+
373+
class Program
374+
{
375+
static void Main()
376+
{
377+
}
378+
}
379+
380+
public class TestController
381+
{
382+
[HttpGet(@""{id}"")]
383+
public object TestAction([CustomFromRoute(Name = ""id"")][FromRoute(Name = ""custom_id"")]string id1)
384+
{
385+
return null;
386+
}
387+
}
388+
389+
public class CustomFromRouteAttribute : Attribute, IFromRouteMetadata
390+
{
391+
public string? Name { get; set; }
392+
}
393+
");
394+
395+
// Act
396+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
397+
398+
// Assert
399+
Assert.Empty(diagnostics);
400+
}
401+
328402
[Fact]
329403
public async Task MapGet_AsParameter_NoResults()
330404
{

src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternCompletionProviderTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,31 @@ static void Main()
256256
i => Assert.Equal("id", i.DisplayText));
257257
}
258258

259+
[Fact]
260+
public async Task Insertion_ParameterOpenBrace_EndpointMapGet_HasDelegate_FromRouteAttribute_ReturnDelegateParameterItem()
261+
{
262+
// Arrange & Act
263+
var result = await GetCompletionsAndServiceAsync(@"
264+
using System;
265+
using System.Diagnostics.CodeAnalysis;
266+
using Microsoft.AspNetCore.Builder;
267+
using Microsoft.AspNetCore.Mvc;
268+
269+
class Program
270+
{
271+
static void Main()
272+
{
273+
EndpointRouteBuilderExtensions.MapGet(null, @""{$$"", ([FromRoute(Name = ""id1"")]string id) => "");
274+
}
275+
}
276+
");
277+
278+
// Assert
279+
Assert.Collection(
280+
result.Completions.ItemsList,
281+
i => Assert.Equal("id1", i.DisplayText));
282+
}
283+
259284
[Fact]
260285
public async Task Insertion_ParameterOpenBrace_EndpointMapGet_HasMethod_ReturnDelegateParameterItem()
261286
{

src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternHighlighterTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,72 @@ static void Main()
159159
");
160160
}
161161

162+
[Fact]
163+
public async Task InParameterName_ExtensionMethod_MatchingDelegate_RouteMetadataWithoutName_HighlightParameter()
164+
{
165+
// Arrange & Act & Assert
166+
await TestHighlightingAsync(@"
167+
using System;
168+
using System.Diagnostics.CodeAnalysis;
169+
using Microsoft.AspNetCore.Builder;
170+
using Microsoft.AspNetCore.Mvc;
171+
using Microsoft.AspNetCore.Routing;
172+
173+
class Program
174+
{
175+
static void Main()
176+
{
177+
IEndpointRouteBuilder builder = null;
178+
builder.MapGet(@""{$$[|id|]}"", ([FromRoute]string [|id|]) => $""{[|id|]}"");
179+
}
180+
}
181+
");
182+
}
183+
184+
[Fact]
185+
public async Task InParameterName_ExtensionMethod_MatchingDelegate_RouteMetadataWithName_HighlightParameter()
186+
{
187+
// Arrange & Act & Assert
188+
await TestHighlightingAsync(@"
189+
using System;
190+
using System.Diagnostics.CodeAnalysis;
191+
using Microsoft.AspNetCore.Builder;
192+
using Microsoft.AspNetCore.Mvc;
193+
using Microsoft.AspNetCore.Routing;
194+
195+
class Program
196+
{
197+
static void Main()
198+
{
199+
IEndpointRouteBuilder builder = null;
200+
builder.MapGet(@""{$$[|id|]}"", ([FromRoute(Name = ""id"")]string [|id1|]) => $""{[|id1|]}"");
201+
}
202+
}
203+
");
204+
}
205+
206+
[Fact]
207+
public async Task InParameterName_ExtensionMethod_MatchingDelegate_RouteMetadataWithName_MultipleMatches_HighlightParameter()
208+
{
209+
// Arrange & Act & Assert
210+
await TestHighlightingAsync(@"
211+
using System;
212+
using System.Diagnostics.CodeAnalysis;
213+
using Microsoft.AspNetCore.Builder;
214+
using Microsoft.AspNetCore.Mvc;
215+
using Microsoft.AspNetCore.Routing;
216+
217+
class Program
218+
{
219+
static void Main()
220+
{
221+
IEndpointRouteBuilder builder = null;
222+
builder.MapGet(@""{$$[|id|]}"", ([FromRoute(Name = ""id"")]string [|id1|], string [|id|]) => $""{[|id1|]}"");
223+
}
224+
}
225+
");
226+
}
227+
162228
[Fact]
163229
public async Task InParameterName_MatchingDelegate_HighlightParameter()
164230
{

0 commit comments

Comments
 (0)