| File: RouteEmbeddedLanguage\RoutePatternHighlighter.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.Linq; using System.Threading; using Microsoft.AspNetCore.Analyzers.Infrastructure.RoutePattern; using Microsoft.AspNetCore.Analyzers.Infrastructure.VirtualChars; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.EmbeddedLanguages; namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; [ExportAspNetCoreEmbeddedLanguageDocumentHighlighter(name: "Route", language: LanguageNames.CSharp)] internal class RoutePatternHighlighter : IAspNetCoreEmbeddedLanguageDocumentHighlighter { public ImmutableArray<AspNetCoreDocumentHighlights> GetDocumentHighlights( SemanticModel semanticModel, SyntaxToken token, int position, CancellationToken cancellationToken) { var routeUsageCache = RouteUsageCache.GetOrCreate(semanticModel.Compilation); var routeUsage = routeUsageCache.Get(token, cancellationToken); if (routeUsage is null) { return ImmutableArray<AspNetCoreDocumentHighlights>.Empty; } return GetHighlights(routeUsage, semanticModel, position, cancellationToken); } private static ImmutableArray<AspNetCoreDocumentHighlights> GetHighlights( RouteUsageModel routeUsage, SemanticModel semanticModel, int position, CancellationToken cancellationToken) { var routePattern = routeUsage.RoutePattern; var virtualChar = routePattern.Text.Find(position); if (virtualChar == null) { return ImmutableArray<AspNetCoreDocumentHighlights>.Empty; } var node = FindParameterNode(routePattern.Root, virtualChar.Value); if (node == null) { return ImmutableArray<AspNetCoreDocumentHighlights>.Empty; } var highlightSpans = ImmutableArray.CreateBuilder<AspNetCoreHighlightSpan>(); // Highlight the parameter in the route string, e.g. "{id}" highlights "id". highlightSpans.Add(new AspNetCoreHighlightSpan(node.GetSpan(), AspNetCoreHighlightSpanKind.Reference)); if (routeUsage.UsageContext.MethodSymbol is { } methodSymbol) { // Resolve possible parameter symbols. Includes properties from AsParametersAttribute. var resolvedParameters = routeUsage.UsageContext.ResolvedParameters; // Match route parameter to method parameter. Parameters in a route aren't case sensitive. // It's possible to match multiple parameters, either based on parameter name, or [FromRoute(Name = "XXX")] attribute. var parameterName = node.ParameterNameToken.Value!.ToString(); foreach (var matchingParameter in resolvedParameters.Where(s => string.Equals(s.RouteParameterName, parameterName, StringComparison.OrdinalIgnoreCase))) { HighlightSymbol(semanticModel, methodSymbol, highlightSpans, matchingParameter.Symbol, cancellationToken); } } return ImmutableArray.Create(new AspNetCoreDocumentHighlights(highlightSpans.ToImmutable())); } private static void HighlightSymbol(SemanticModel semanticModel, IMethodSymbol methodSymbol, IList<AspNetCoreHighlightSpan> highlightSpans, ISymbol matchingParameter, CancellationToken cancellationToken) { // The highlighter provides Roslyn a collection of TextSpan. // Those spans don't carry the document or syntax tree they are referring to. // So, they are always assumed to be on the originally given document. // Hence, before providing any highlight spans, we should ensure we are in the // correct syntax tree, or skip the analysis otherwise. // Highlight parameter in method signature. // e.g. "{id}" in route highlights id in "void Foo(string id) {}" foreach (var item in matchingParameter.DeclaringSyntaxReferences) { if (item.SyntaxTree != semanticModel.SyntaxTree) { continue; } var syntaxNode = item.GetSyntax(cancellationToken); if (syntaxNode is ParameterSyntax parameterSyntax) { highlightSpans.Add(new AspNetCoreHighlightSpan(parameterSyntax.Identifier.Span, AspNetCoreHighlightSpanKind.Definition)); } } // Highlight parameter references inside method. // e.g. "{id}" in route highlights id in "_repository.GetBy(id)" foreach (var item in methodSymbol.DeclaringSyntaxReferences) { if (item.SyntaxTree != semanticModel.SyntaxTree) { continue; } var methodSyntax = item.GetSyntax(cancellationToken); // Have to call GetSymbolInfo because it's easy to have identifiers with the same name // that reference a different API. For example, a type with the same name as parameter. // GetSymbolInfo can be slow. To reduce calls to it we only get IdentifierNameSyntax // nodes, filter them by name first, then check GetSymbolInfo. var parameterReferences = methodSyntax .DescendantNodes() .OfType<IdentifierNameSyntax>() .Where(i => i.Identifier.Text == matchingParameter.Name) .Where(i => semanticModel.GetSymbolInfo(i) is var symbolInfo && SymbolEqualityComparer.Default.Equals(symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault(), matchingParameter)); foreach (var reference in parameterReferences) { highlightSpans.Add(new AspNetCoreHighlightSpan(reference.Identifier.Span, AspNetCoreHighlightSpanKind.Reference)); } } } private static RoutePatternNameParameterPartNode? FindParameterNode(RoutePatternNode node, VirtualChar ch) => FindNode<RoutePatternNameParameterPartNode>(node, ch, (parameter, c) => parameter.ParameterNameToken.VirtualChars.Contains(c)); private static TNode? FindNode<TNode>(RoutePatternNode node, VirtualChar ch, Func<TNode, VirtualChar, bool> predicate) where TNode : RoutePatternNode { if (node is TNode nodeMatch && predicate(nodeMatch, ch)) { return nodeMatch; } foreach (var child in node) { if (child.IsNode) { var result = FindNode(child.Node, ch, predicate); if (result != null) { return result; } } } return null; } }