File: FindSymbols\SymbolFinder.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.FindSymbols;
 
public static partial class SymbolFinder
{
    /// <summary>
    /// Obsolete.  Use <see cref="FindSymbolAtPositionAsync(SemanticModel, int, Workspace, CancellationToken)"/>.
    /// </summary>
    [Obsolete("Use FindSymbolAtPositionAsync instead.")]
    public static ISymbol FindSymbolAtPosition(
        SemanticModel semanticModel,
        int position,
        Workspace workspace,
        CancellationToken cancellationToken = default)
    {
        return FindSymbolAtPositionAsync(semanticModel, position, workspace, cancellationToken).WaitAndGetResult(cancellationToken);
    }
 
    /// <summary>
    /// Finds the symbol that is associated with a position in the text of a document.
    /// </summary>
    /// <param name="semanticModel">The semantic model associated with the document.</param>
    /// <param name="position">The character position within the document.</param>
    /// <param name="workspace">A workspace to provide context.</param>
    /// <param name="cancellationToken">A CancellationToken.</param>
    public static Task<ISymbol> FindSymbolAtPositionAsync(
        SemanticModel semanticModel,
        int position,
        Workspace workspace,
        CancellationToken cancellationToken = default)
    {
        if (workspace is null)
            throw new ArgumentNullException(nameof(workspace));
 
        return FindSymbolAtPositionAsync(semanticModel, position, workspace.Services.SolutionServices, cancellationToken);
    }
 
    /// <summary>
    /// Finds the symbol that is associated with a position in the text of a document.
    /// </summary>
    /// <param name="semanticModel">The semantic model associated with the document.</param>
    /// <param name="position">The character position within the document.</param>
    /// <param name="cancellationToken">A CancellationToken.</param>
    internal static async Task<ISymbol> FindSymbolAtPositionAsync(
        SemanticModel semanticModel,
        int position,
        SolutionServices services,
        CancellationToken cancellationToken = default)
    {
        if (semanticModel is null)
            throw new ArgumentNullException(nameof(semanticModel));
        if (services is null)
            throw new ArgumentNullException(nameof(services));
 
        var semanticInfo = await GetSemanticInfoAtPositionAsync(
            semanticModel, position, services, cancellationToken).ConfigureAwait(false);
        return semanticInfo.GetAnySymbol(includeType: false);
    }
 
    internal static async Task<TokenSemanticInfo> GetSemanticInfoAtPositionAsync(
        SemanticModel semanticModel,
        int position,
        SolutionServices services,
        CancellationToken cancellationToken)
    {
        var token = await GetTokenAtPositionAsync(semanticModel, position, services, cancellationToken).ConfigureAwait(false);
 
        if (token != default &&
            token.Span.IntersectsWith(position))
        {
            return semanticModel.GetSemanticInfo(token, services, cancellationToken);
        }
 
        return TokenSemanticInfo.Empty;
    }
 
    private static Task<SyntaxToken> GetTokenAtPositionAsync(
        SemanticModel semanticModel,
        int position,
        SolutionServices services,
        CancellationToken cancellationToken)
    {
        var syntaxTree = semanticModel.SyntaxTree;
        var syntaxFacts = services.GetRequiredLanguageService<ISyntaxFactsService>(semanticModel.Language);
 
        return syntaxTree.GetTouchingTokenAsync(position, syntaxFacts.IsBindableToken, cancellationToken, findInsideTrivia: true);
    }
 
    public static async Task<ISymbol> FindSymbolAtPositionAsync(
        Document document,
        int position,
        CancellationToken cancellationToken = default)
    {
        if (document is null)
            throw new ArgumentNullException(nameof(document));
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        return await FindSymbolAtPositionAsync(semanticModel, position, document.Project.Solution.Services, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Finds the definition symbol declared in source code for a corresponding reference symbol. 
    /// Returns null if no such symbol can be found in the specified solution.
    /// </summary>
    public static Task<ISymbol?> FindSourceDefinitionAsync(ISymbol? symbol, Solution solution, CancellationToken cancellationToken = default)
        => Task.FromResult(FindSourceDefinition(symbol, solution, cancellationToken));
 
    /// <inheritdoc cref="FindSourceDefinitionAsync"/>
    internal static ISymbol? FindSourceDefinition(
        ISymbol? symbol, Solution solution, CancellationToken cancellationToken)
    {
        if (symbol != null)
        {
            symbol = symbol.GetOriginalUnreducedDefinition();
            switch (symbol.Kind)
            {
                case SymbolKind.Event:
                case SymbolKind.Field:
                case SymbolKind.Method:
                case SymbolKind.Local:
                case SymbolKind.NamedType:
                case SymbolKind.Parameter:
                case SymbolKind.Property:
                case SymbolKind.TypeParameter:
                case SymbolKind.Namespace:
                    return FindSourceDefinitionWorker(symbol, solution, cancellationToken);
            }
        }
 
        return null;
    }
 
    private static ISymbol? FindSourceDefinitionWorker(
        ISymbol symbol,
        Solution solution,
        CancellationToken cancellationToken)
    {
        // If it's already in source, then we might already be done
        if (InSource(symbol))
        {
            // If our symbol doesn't have a containing assembly, there's nothing better we can do to map this
            // symbol somewhere else. The common case for this is a merged INamespaceSymbol that spans assemblies.
            if (symbol.ContainingAssembly == null)
                return symbol;
 
            // Just because it's a source symbol doesn't mean we have the final symbol we actually want. In retargeting cases,
            // the retargeted symbol is from "source" but isn't equal to the actual source definition in the other project. Thus,
            // we only want to return symbols from source here if it actually came from a project's compilation's assembly. If it isn't
            // then we have a retargeting scenario and want to take our usual path below as if it was a metadata reference
            foreach (var sourceProject in solution.Projects)
            {
                // If our symbol is actually a "regular" source symbol, then we know the compilation is holding the symbol alive
                // and thus TryGetCompilation is sufficient. For another example of this pattern, see Solution.GetProject(IAssemblySymbol)
                // which we happen to call below.
                if (sourceProject.TryGetCompilation(out var compilation) &&
                    symbol.ContainingAssembly.Equals(compilation.Assembly))
                {
                    return symbol;
                }
            }
        }
        else if (!symbol.Locations.Any(static loc => loc.IsInMetadata))
        {
            // We have a symbol that's neither in source nor metadata
            return null;
        }
 
        var project = solution.GetProject(symbol.ContainingAssembly, cancellationToken);
 
        // Note: if the assembly came from a particular project, then we should be able to get the compilation without
        // building it.  That's because once we create the compilation, we'll hold onto it for the lifetime of the
        // project, to avoid unnecessary recomputation.
        if (project?.TryGetCompilation(out var projectCompilation) is true)
        {
            var symbolId = symbol.GetSymbolKey(cancellationToken);
            var result = symbolId.Resolve(projectCompilation, ignoreAssemblyKey: true, cancellationToken: cancellationToken);
 
            return InSource(result.Symbol) ? result.Symbol : result.CandidateSymbols.FirstOrDefault(InSource);
        }
 
        return null;
 
        static bool InSource([NotNullWhen(true)] ISymbol? symbol)
           => symbol != null && symbol.Locations.Any(static loc => loc.IsInSource);
    }
 
    /// <summary>
    /// Finds symbols in the given compilation that are similar to the specified symbol.
    /// 
    /// A found symbol may be the exact same symbol instance if the compilation is the origin of the specified symbol, 
    /// or it may be a different symbol instance if the compilation is not the originating compilation.
    /// 
    /// Multiple symbols may be returned if there are ambiguous matches.
    /// No symbols may be returned if the compilation does not define or have access to a similar symbol.
    /// </summary>
    /// <param name="symbol">The symbol to find corresponding matches for.</param>
    /// <param name="compilation">A compilation to find the corresponding symbol within. The compilation may or may not be the origin of the symbol.</param>
    /// <param name="cancellationToken">A CancellationToken.</param>
    /// <returns></returns>
    public static IEnumerable<TSymbol> FindSimilarSymbols<TSymbol>(TSymbol symbol, Compilation compilation, CancellationToken cancellationToken = default)
        where TSymbol : ISymbol
    {
        if (symbol is null)
            throw new ArgumentNullException(nameof(symbol));
 
        if (compilation is null)
            throw new ArgumentNullException(nameof(compilation));
 
        var key = symbol.GetSymbolKey(cancellationToken);
 
        // We may be talking about different compilations.  So do not try to resolve locations.
        var result = new HashSet<TSymbol>();
        var resolution = key.Resolve(compilation, cancellationToken: cancellationToken);
        foreach (var current in resolution.OfType<TSymbol>())
        {
            result.Add(current);
        }
 
        return result;
    }
 
    /// <summary>
    /// If <paramref name="symbol"/> is declared in a linked file, then this function returns all the symbols that
    /// are defined by the same symbol's syntax in the all projects that the linked file is referenced from.
    /// <para/>
    /// In order to be returned the other symbols must have the same <see cref="ISymbol.Name"/> and <see
    /// cref="ISymbol.Kind"/> as <paramref name="symbol"/>.  This matches general user intuition that these are all
    /// the 'same' symbol, and should be examined, regardless of the project context and <see cref="ISymbol"/> they
    /// originally started with.
    /// </summary>
    internal static async Task<ImmutableArray<ISymbol>> FindLinkedSymbolsAsync(
        ISymbol symbol, Solution solution, CancellationToken cancellationToken)
    {
        // Add the original symbol to the result set.
        var linkedSymbols = new HashSet<ISymbol> { symbol };
 
        foreach (var location in symbol.DeclaringSyntaxReferences)
        {
            var originalDocument = solution.GetDocument(location.SyntaxTree);
 
            // GetDocument will return null for locations in #load'ed trees. TODO:  Remove this check and add logic
            // to fetch the #load'ed tree's Document once https://github.com/dotnet/roslyn/issues/5260 is fixed.
            if (originalDocument == null)
            {
                Debug.Assert(solution.WorkspaceKind is WorkspaceKind.Interactive or WorkspaceKind.MiscellaneousFiles);
                continue;
            }
 
            var originalRoot = await location.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
            foreach (var linkedDocumentId in originalDocument.GetLinkedDocumentIds())
            {
                var linkedDocument = solution.GetRequiredDocument(linkedDocumentId);
 
                // It's possible for us to have a solution snapshot where only part of a linked set of documents has
                // been updated.  As such, the other linked docs may have different contents/sizes than the original
                // doc we started with.  Skip those files as there's no sensible way to say that we have linked
                // symbols here when the contents are not the same.
                var linkedSyntaxRoot = await linkedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                if (originalRoot.FullSpan != linkedSyntaxRoot.FullSpan)
                    continue;
 
                var linkedNode = linkedSyntaxRoot.FindNode(location.Span, getInnermostNodeForTie: true);
 
                var semanticModel = await linkedDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                var linkedSymbol = semanticModel.GetDeclaredSymbol(linkedNode, cancellationToken);
                if (linkedSymbol is null)
                    continue;
 
                if (linkedSymbol.Kind != symbol.Kind)
                {
                    // With primary constructors, the declaring node of the primary constructor is the type
                    // declaration node itself.  So, see if we're in that situation, and try to find the
                    // corresponding primary constructor in the linked file.
                    if (linkedSymbol is INamedTypeSymbol linkedNamedType &&
                        symbol.IsConstructor())
                    {
                        linkedSymbol = linkedNamedType.Constructors.FirstOrDefault(
                            c => c.DeclaringSyntaxReferences.Any(r => linkedNode.Equals(r.GetSyntax(cancellationToken))));
                        if (linkedSymbol is null)
                            continue;
                    }
                }
 
                if (linkedSymbol.Name == symbol.Name)
                    linkedSymbols.Add(linkedSymbol);
            }
        }
 
        return [.. linkedSymbols];
    }
}