File: Dependencies\ExtensionMethodsCompletionProvider.cs
Web Access
Project: src\src\Framework\AspNetCoreAnalyzers\src\CodeFixes\Microsoft.AspNetCore.App.CodeFixes.csproj (Microsoft.AspNetCore.App.CodeFixes)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
 
namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage;
 
/// <summary>
/// This completion provider expands the completion list of target symbols defined in the
/// ExtensionMethodsCache to include extension methods that can be invoked on the target
/// type that are defined in auxillary packages. This completion provider is designed to be
/// used in conjunction with the `AddPackageFixer` to recommend adding the missing packages
/// extension methods are defined in.
/// </summary>
[ExportCompletionProvider(nameof(ExtensionMethodsCompletionProvider), LanguageNames.CSharp)]
[Shared]
public sealed class ExtensionMethodsCompletionProvider : CompletionProvider
{
    public override async Task ProvideCompletionsAsync(CompletionContext context)
    {
        if (!context.Document.SupportsSemanticModel)
        {
            return;
        }
 
        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
        if (root == null)
        {
            return;
        }
 
        var span = context.CompletionListSpan;
        var token = root.FindToken(span.Start);
        if (token.Parent == null)
        {
            return;
        }
 
        var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
        if (semanticModel == null)
        {
            return;
        }
 
        var wellKnownTypes = WellKnownTypes.GetOrCreate(semanticModel.Compilation);
        var wellKnownExtensionMethodCache = ExtensionMethodsCache.ConstructFromWellKnownTypes(wellKnownTypes);
 
        // We find the nearest member access expression to the adjacent expression to resolve the
        // target type of the extension method that the user is invoking. For example, `app.` should
        // allow us to resolve to a `WebApplication` instance and `builder.Services.Add` should resolve
        // to an `IServiceCollection`.
        var nearestMemberAccessExpression = FindNearestMemberAccessExpression(token.Parent);
        if (nearestMemberAccessExpression is not null && nearestMemberAccessExpression is MemberAccessExpressionSyntax memberAccess)
        {
            var symbol = semanticModel.GetSymbolInfo(memberAccess.Expression);
            var symbolType = symbol.Symbol switch
            {
                IMethodSymbol methodSymbol => methodSymbol.ReturnType,
                IPropertySymbol propertySymbol => propertySymbol.Type,
                ILocalSymbol localSymbol => localSymbol.Type,
                _ => null
            };
 
            var matchingExtensionMethods = wellKnownExtensionMethodCache.Where(pair => IsMatchingExtensionMethod(pair, symbolType, token));
            foreach (var item in matchingExtensionMethods)
            {
                context.CompletionListSpan = span;
                context.AddItem(CompletionItem.Create(
                    displayText: item.Key.ExtensionMethod,
                    sortText: item.Key.ExtensionMethod,
                    filterText: item.Key.ExtensionMethod
                ));
            }
        }
    }
 
    private static SyntaxNode? FindNearestMemberAccessExpression(SyntaxNode? node)
    {
        var current = node;
        while (current != null)
        {
            if (current?.IsKind(SyntaxKind.SimpleMemberAccessExpression) ?? false)
            {
                return current;
            }
 
            current = current?.Parent;
        }
 
        return null;
    }
 
    private static bool IsMatchingExtensionMethod(
        KeyValuePair<ThisAndExtensionMethod, PackageSourceAndNamespace> pair,
        ISymbol? symbolType,
        SyntaxToken token)
    {
        if (symbolType is null)
        {
            return false;
        }
 
        // If the token that we are parsing is some sort of identifier, this indicates that the user
        // has triggered a completion with characters already inserted into the invocation (e.g. `builder.Services.Ad$$).
        // In this case, we only want to provide completions that match the characters that have been inserted.
        var isIdentifierToken = token.IsKind(SyntaxKind.IdentifierName) || token.IsKind(SyntaxKind.IdentifierToken);
        return SymbolEqualityComparer.Default.Equals(pair.Key.ThisType, symbolType) &&
            (!isIdentifierToken || pair.Key.ExtensionMethod.Contains(token.ValueText));
    }
}