File: FindSymbols\FindReferences\Finders\PropertySymbolReferenceFinder.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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.FindSymbols.Finders;
 
internal sealed class PropertySymbolReferenceFinder : AbstractMethodOrPropertyOrEventSymbolReferenceFinder<IPropertySymbol>
{
    public static readonly PropertySymbolReferenceFinder Instance = new();
 
    private PropertySymbolReferenceFinder()
    {
    }
 
    protected override bool CanFind(IPropertySymbol symbol)
        => true;
 
    protected override ValueTask<ImmutableArray<ISymbol>> DetermineCascadedSymbolsAsync(
        IPropertySymbol symbol,
        Solution solution,
        FindReferencesSearchOptions options,
        CancellationToken cancellationToken)
    {
        using var _ = ArrayBuilder<ISymbol>.GetInstance(out var result);
 
        CascadeToOtherPartOfPartial(symbol, result);
        CascadeToBackingFields(symbol, result);
        CascadeToAccessors(symbol, result);
        CascadeToPrimaryConstructorParameters(symbol, result, cancellationToken);
 
        return new(result.ToImmutable());
    }
 
    private static void CascadeToOtherPartOfPartial(IPropertySymbol symbol, ArrayBuilder<ISymbol> result)
    {
        result.AddIfNotNull(symbol.PartialDefinitionPart);
        result.AddIfNotNull(symbol.PartialImplementationPart);
    }
 
    private static void CascadeToBackingFields(IPropertySymbol symbol, ArrayBuilder<ISymbol> result)
    {
        foreach (var member in symbol.ContainingType.GetMembers())
        {
            if (member is IFieldSymbol field &&
                symbol.Equals(field.AssociatedSymbol))
            {
                result.Add(field);
            }
        }
    }
 
    private static void CascadeToAccessors(IPropertySymbol symbol, ArrayBuilder<ISymbol> result)
    {
        result.AddIfNotNull(symbol.GetMethod);
        result.AddIfNotNull(symbol.SetMethod);
    }
 
    private static void CascadeToPrimaryConstructorParameters(IPropertySymbol property, ArrayBuilder<ISymbol> result, CancellationToken cancellationToken)
    {
        if (property is
            {
                IsStatic: false,
                DeclaringSyntaxReferences.Length: > 0,
                ContainingType:
                {
                    IsRecord: true,
                    DeclaringSyntaxReferences.Length: > 0,
                } containingType,
            })
        {
            // OK, we have a property in a record.  See if we can find a primary constructor that has a parameter that synthesized this
            var containingTypeSyntaxes = containingType.DeclaringSyntaxReferences.SelectAsArray(r => r.GetSyntax(cancellationToken));
            foreach (var constructor in containingType.Constructors)
            {
                if (constructor.DeclaringSyntaxReferences.Length > 0)
                {
                    var constructorSyntax = constructor.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken);
                    if (containingTypeSyntaxes.Contains(constructorSyntax))
                    {
                        // OK found the primary construct.  Try to find a parameter that corresponds to this property.
                        foreach (var parameter in constructor.Parameters)
                        {
                            if (property.Name.Equals(parameter.Name) &&
                                property.Equals(parameter.GetAssociatedSynthesizedRecordProperty(cancellationToken)))
                            {
                                result.Add(parameter);
                            }
                        }
                    }
                }
            }
        }
    }
 
    protected sealed override async Task DetermineDocumentsToSearchAsync<TData>(
        IPropertySymbol symbol,
        HashSet<string>? globalAliases,
        Project project,
        IImmutableSet<Document>? documents,
        Action<Document, TData> processResult,
        TData processResultData,
        FindReferencesSearchOptions options,
        CancellationToken cancellationToken)
    {
        await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false);
 
        if (IsForEachProperty(symbol))
            await FindDocumentsWithForEachStatementsAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false);
 
        if (symbol.IsIndexer)
            await FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false);
 
        if (symbol.IsIndexer)
            await FindDocumentWithIndexerMemberCrefAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false);
 
        await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false);
    }
 
    private static bool IsForEachProperty(IPropertySymbol symbol)
        => symbol.Name == WellKnownMemberNames.CurrentPropertyName;
 
    protected sealed override void FindReferencesInDocument<TData>(
        IPropertySymbol symbol,
        FindReferencesDocumentState state,
        Action<FinderLocation, TData> processResult,
        TData processResultData,
        FindReferencesSearchOptions options,
        CancellationToken cancellationToken)
    {
        FindReferencesInDocumentUsingSymbolName(
            symbol,
            state,
            static (loc, data) =>
            {
                var useResult = true;
                if (data.options.AssociatePropertyReferencesWithSpecificAccessor)
                {
                    // We want to associate property references to a specific accessor (if an accessor
                    // is being referenced).  Check if this reference would match an accessor. If so, do
                    // not add it.  It will be added by PropertyAccessorSymbolReferenceFinder.
                    var accessors = GetReferencedAccessorSymbols(
                        data.state, data.symbol, loc.Node, data.cancellationToken);
                    useResult = accessors.IsEmpty;
                }
 
                if (useResult)
                    data.processResult(loc, data.processResultData);
            },
            processResultData: (self: this, symbol, state, processResult, processResultData, options, cancellationToken),
            cancellationToken);
 
        if (IsForEachProperty(symbol))
            FindReferencesInForEachStatements(symbol, state, processResult, processResultData, cancellationToken);
 
        if (symbol.IsIndexer)
            FindIndexerReferences(symbol, state, processResult, processResultData, options, cancellationToken);
 
        FindReferencesInDocumentInsideGlobalSuppressions(
            symbol, state, processResult, processResultData, cancellationToken);
    }
 
    private static Task FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync<TData>(
        Project project, IImmutableSet<Document>? documents, Action<Document, TData> processResult, TData processResultData, CancellationToken cancellationToken)
    {
        return FindDocumentsWithPredicateAsync(
            project, documents, static index => index.ContainsExplicitOrImplicitElementAccessExpression, processResult, processResultData, cancellationToken);
    }
 
    private static Task FindDocumentWithIndexerMemberCrefAsync<TData>(
        Project project, IImmutableSet<Document>? documents, Action<Document, TData> processResult, TData processResultData, CancellationToken cancellationToken)
    {
        return FindDocumentsWithPredicateAsync(
            project, documents, static index => index.ContainsIndexerMemberCref, processResult, processResultData, cancellationToken);
    }
 
    private static void FindIndexerReferences<TData>(
        IPropertySymbol symbol,
        FindReferencesDocumentState state,
        Action<FinderLocation, TData> processResult,
        TData processResultData,
        FindReferencesSearchOptions options,
        CancellationToken cancellationToken)
    {
        if (options.AssociatePropertyReferencesWithSpecificAccessor)
        {
            // Looking for individual get/set references.  Don't find anything here. 
            // these results will be provided by the PropertyAccessorSymbolReferenceFinder
            return;
        }
 
        var syntaxFacts = state.SyntaxFacts;
 
        var indexerReferenceExpressions = state.Root.DescendantNodes(descendIntoTrivia: true)
            .Where(node =>
                syntaxFacts.IsElementAccessExpression(node) ||
                syntaxFacts.IsImplicitElementAccess(node) ||
                syntaxFacts.IsConditionalAccessExpression(node) ||
                syntaxFacts.IsIndexerMemberCref(node));
 
        foreach (var node in indexerReferenceExpressions)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            var (matched, candidateReason, indexerReference) = ComputeIndexerInformation(symbol, state, node, cancellationToken);
            if (!matched)
                continue;
 
            var location = state.SyntaxTree.GetLocation(new TextSpan(indexerReference.SpanStart, 0));
            var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken);
 
            var result = new FinderLocation(node,
                new ReferenceLocation(
                    state.Document, alias: null, location, isImplicit: false, symbolUsageInfo,
                    GetAdditionalFindUsagesProperties(node, state),
                    candidateReason));
            processResult(result, processResultData);
        }
    }
 
    private static (bool matched, CandidateReason reason, SyntaxNode indexerReference) ComputeIndexerInformation(
        IPropertySymbol symbol,
        FindReferencesDocumentState state,
        SyntaxNode node,
        CancellationToken cancellationToken)
    {
        var syntaxFacts = state.SyntaxFacts;
 
        if (syntaxFacts.IsElementAccessExpression(node))
        {
            // The indexerReference for an element access expression will not be null
            return ComputeElementAccessInformation(symbol, node, state, cancellationToken)!;
        }
        else if (syntaxFacts.IsImplicitElementAccess(node))
        {
            return ComputeImplicitElementAccessInformation(symbol, node, state, cancellationToken)!;
        }
        else if (syntaxFacts.IsConditionalAccessExpression(node))
        {
            return ComputeConditionalAccessInformation(symbol, node, state, cancellationToken);
        }
        else
        {
            Debug.Assert(syntaxFacts.IsIndexerMemberCref(node));
            return ComputeIndexerMemberCRefInformation(symbol, state, node, cancellationToken);
        }
    }
 
    private static (bool matched, CandidateReason reason, SyntaxNode indexerReference) ComputeIndexerMemberCRefInformation(
        IPropertySymbol symbol, FindReferencesDocumentState state, SyntaxNode node, CancellationToken cancellationToken)
    {
        var (matched, reason) = SymbolsMatch(symbol, state, node, cancellationToken);
 
        // For an IndexerMemberCRef the node itself is the indexer we are looking for.
        return (matched, reason, node);
    }
 
    private static (bool matched, CandidateReason reason, SyntaxNode indexerReference) ComputeConditionalAccessInformation(
        IPropertySymbol symbol, SyntaxNode node, FindReferencesDocumentState state, CancellationToken cancellationToken)
    {
        // For a ConditionalAccessExpression the whenNotNull component is the indexer reference we are looking for
        var syntaxFacts = state.SyntaxFacts;
        syntaxFacts.GetPartsOfConditionalAccessExpression(node, out _, out var indexerReference);
 
        if (syntaxFacts.IsInvocationExpression(indexerReference))
        {
            // call to something like: goo?.bar(1)
            //
            // this will already be handled by the existing method ref finder.
            return default;
        }
 
        var (matched, reason) = SymbolsMatch(symbol, state, indexerReference, cancellationToken);
        return (matched, reason, indexerReference);
    }
 
    private static (bool matched, CandidateReason reason, SyntaxNode? indexerReference) ComputeElementAccessInformation(
        IPropertySymbol symbol, SyntaxNode node, FindReferencesDocumentState state, CancellationToken cancellationToken)
    {
        // For an ElementAccessExpression the indexer we are looking for is the argumentList component.
        state.SyntaxFacts.GetPartsOfElementAccessExpression(node, out var expression, out var indexerReference);
        if (expression != null && SymbolsMatch(symbol, state, expression, cancellationToken).matched)
        {
            // Element access with explicit member name (allowed in VB). We will have
            // already added a reference location for the member name identifier, so skip
            // this one.
            return default;
        }
 
        var (matched, reason) = SymbolsMatch(symbol, state, node, cancellationToken);
        return (matched, reason, indexerReference);
    }
 
    private static (bool matched, CandidateReason reason, SyntaxNode indexerReference) ComputeImplicitElementAccessInformation(
        IPropertySymbol symbol, SyntaxNode node, FindReferencesDocumentState state, CancellationToken cancellationToken)
    {
        var argumentList = state.SyntaxFacts.GetArgumentListOfImplicitElementAccess(node);
        var (matched, reason) = SymbolsMatch(symbol, state, node, cancellationToken);
        return (matched, reason, argumentList);
    }
}