File: FindSymbols\FindReferences\FindReferencesSearchEngine.PartialParts.cs
Web Access
Project: 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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.FindSymbols;

/// <remarks>
/// The parts of a partial member are one symbol to the user, like the copies of a symbol in linked files: both parts
/// are reported in the same <see cref="SymbolGroup"/>.  A search of either part finds the uses of both, as <see
/// cref="SymbolFinder.OriginalSymbolsMatch"/> does not distinguish partial parts, so when cascading only the definition
/// part is searched.  A parameter or type parameter named differently in the two parts is the exception: its uses in
/// the implementation part are spelled with the other name, so both parts are searched.
/// </remarks>
internal sealed partial class FindReferencesSearchEngine
{
    /// <summary>
    /// Returns the symbols that Find References reports as one definition with <paramref name="symbol"/>, including
    /// <paramref name="symbol"/> itself.  A use can bind to any of them: each project that compiles a linked file binds
    /// to its own copy of the symbol, and a use of a partial member binds to either part, such as a call to the
    /// definition part or a parameter reference in the implementation part's body.
    /// </summary>
    private static async Task<ImmutableArray<ISymbol>> FindSameSymbolsAsync(
        ISymbol symbol, Solution solution, CancellationToken cancellationToken)
    {
        var linkedSymbols = await SymbolFinder.FindLinkedSymbolsAsync(symbol, solution, cancellationToken).ConfigureAwait(false);
        return [.. linkedSymbols, .. linkedSymbols.Select(GetOtherPartialPart).WhereNotNull()];
    }

    /// <summary>
    /// Whether <paramref name="symbol"/> doesn't need to be searched, because searching the corresponding symbol on
    /// the definition part of its partial member already finds its uses.  Searching it as well would report each of
    /// its uses twice.
    /// </summary>
    private static bool IsFoundThroughPartialDefinitionPart(ISymbol symbol)
    {
        if (!IsOnPartialImplementationPart(symbol))
            return false;

        return symbol switch
        {
            // A search looks for tokens spelled with the searched symbol's name, and OriginalSymbolsMatch ignores
            // partial parts but compares names.  So searching the definition part's parameter or type parameter finds
            // the implementation part's uses only when both parts use the same name.  They can differ (CS8826), and
            // then the implementation part's uses are only found by searching it too.
            IParameterSymbol or ITypeParameterSymbol
                => GetOtherPartialPart(symbol) is { } definition && definition.Name == symbol.Name,

            // Both parts of a partial member have the member's name, and OriginalSymbolsMatch ignores partial parts, so
            // searching the definition part finds the uses of both.
            _ => true,
        };
    }

    /// <summary>
    /// Returns the symbol corresponding to <paramref name="symbol"/> on the other part of its partial member, or <see
    /// langword="null"/> if <paramref name="symbol"/> isn't a partial member, or a parameter or type parameter of one.
    /// </summary>
    private static ISymbol? GetOtherPartialPart(ISymbol symbol)
        => symbol switch
        {
            IMethodSymbol method => method.PartialDefinitionPart ?? method.PartialImplementationPart,
            IPropertySymbol property => property.PartialDefinitionPart ?? property.PartialImplementationPart,
            IEventSymbol @event => @event.PartialDefinitionPart ?? @event.PartialImplementationPart,
            IParameterSymbol parameter => GetOtherPartialPart(parameter.ContainingSymbol) switch
            {
                IMethodSymbol method => ElementAtOrNull(method.Parameters, parameter.Ordinal),
                IPropertySymbol property => ElementAtOrNull(property.Parameters, parameter.Ordinal),
                _ => null,
            },
            ITypeParameterSymbol { TypeParameterKind: TypeParameterKind.Method } typeParameter
                => GetOtherPartialPart(typeParameter.ContainingSymbol) is IMethodSymbol method
                    ? ElementAtOrNull(method.TypeParameters, typeParameter.Ordinal)
                    : null,
            _ => null,
        };

    private static bool IsOnPartialImplementationPart(ISymbol symbol)
        => symbol switch
        {
            IMethodSymbol method => method.PartialDefinitionPart != null,
            IPropertySymbol property => property.PartialDefinitionPart != null,
            IEventSymbol @event => @event.PartialDefinitionPart != null,
            IParameterSymbol parameter => IsOnPartialImplementationPart(parameter.ContainingSymbol),
            ITypeParameterSymbol { TypeParameterKind: TypeParameterKind.Method } typeParameter => IsOnPartialImplementationPart(typeParameter.ContainingSymbol),
            _ => false,
        };

    private static T? ElementAtOrNull<T>(ImmutableArray<T> array, int index) where T : class
        => (uint)index < (uint)array.Length ? array[index] : null;
}