File: CodeLens\CodeLensFindReferenceProgress.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CodeLens;
 
/// <summary>
/// Tracks incremental progress of a find references search, we use this to
/// count the number of references up until a certain cap is reached and cancel the search
/// or until the search completes, if such a cap is not reached.
/// </summary>
/// <remarks>
/// All public methods of this type could be called from multiple threads.
/// </remarks>
internal sealed class CodeLensFindReferencesProgress(
    ISymbol queriedDefinition,
    SyntaxNode queriedNode,
    int searchCap,
    CancellationToken cancellationToken) : IFindReferencesProgress, IDisposable
{
    private readonly CancellationTokenSource _aggregateCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    private readonly SyntaxNode _queriedNode = queriedNode;
    private readonly ISymbol _queriedSymbol = queriedDefinition;
    private readonly ConcurrentSet<Location> _locations = new ConcurrentSet<Location>(LocationComparer.Instance);
 
    /// <remarks>
    /// If the cap is 0, then there is no cap.
    /// </remarks>
    public int SearchCap { get; } = searchCap;
 
    /// <summary>
    /// The cancellation token that aggregates the original cancellation token + this progress
    /// </summary>
    public CancellationToken CancellationToken => _aggregateCancellationTokenSource.Token;
 
    public bool SearchCapReached => SearchCap != 0 && ReferencesCount > SearchCap;
 
    public int ReferencesCount => _locations.Count;
 
    public ImmutableArray<Location> Locations => [.. _locations];
 
    public void OnStarted()
    {
    }
 
    public void OnCompleted()
    {
    }
 
    public void OnFindInDocumentStarted(Document document)
    {
    }
 
    public void OnFindInDocumentCompleted(Document document)
    {
    }
 
    private static bool FilterDefinition(ISymbol definition)
    {
        return definition.IsImplicitlyDeclared ||
               (definition as IMethodSymbol)?.AssociatedSymbol != null;
    }
 
    // Returns partial symbol locations whose node does not match the queried syntaxNode
    private IEnumerable<Location> GetPartialLocations(ISymbol symbol, CancellationToken cancellationToken)
    {
        // Returns nodes from source not equal to actual location
        return from syntaxReference in symbol.DeclaringSyntaxReferences
               let candidateSyntaxNode = syntaxReference.GetSyntax(cancellationToken)
               where !(_queriedNode.Span == candidateSyntaxNode.Span &&
                       _queriedNode.SyntaxTree.FilePath.Equals(candidateSyntaxNode.SyntaxTree.FilePath,
                           StringComparison.OrdinalIgnoreCase))
               select candidateSyntaxNode.GetLocation();
    }
 
    public void OnDefinitionFound(ISymbol symbol)
    {
        if (FilterDefinition(symbol))
        {
            return;
        }
 
        // Partial types can have more than one declaring syntax references.
        // Add remote locations for all the syntax references except the queried syntax node.
        // To query for the partial locations, filter definition locations that occur in source whose span is part of
        // span of any syntax node from Definition.DeclaringSyntaxReferences except for the queried syntax node.
        var locations = symbol.Locations.Intersect(_queriedSymbol.Locations, LocationComparer.Instance).Any()
            ? GetPartialLocations(symbol, _aggregateCancellationTokenSource.Token)
            : symbol.Locations;
 
        _locations.AddRange(locations.Where(location => location.IsInSource));
 
        if (SearchCapReached)
        {
            _aggregateCancellationTokenSource.Cancel();
        }
    }
 
    /// <summary>
    /// Exclude the following kind of symbols:
    ///  1. Implicitly declared symbols (such as implicit fields backing properties)
    ///  2. Symbols that can't be referenced by name (such as property getters and setters).
    ///  3. Metadata only symbols, i.e. symbols with no location in source.
    /// </summary>
    private bool FilterReference(ISymbol definition, ReferenceLocation reference)
    {
        var isImplicitlyDeclared = definition.IsImplicitlyDeclared || definition.IsAccessor();
        // FindRefs treats a constructor invocation as a reference to the constructor symbol and to the named type symbol that defines it and
        // so should we. Otherwise named types may have a reference count of 0, even if there are calls to its constructors, which might cause
        // people think the class is not in use (#49636).
        // Invocations to implicit parameterless constructors need to be included too.
        var isConstructorInvocation = _queriedSymbol.Kind == SymbolKind.NamedType &&
                                      (definition as IMethodSymbol)?.MethodKind == MethodKind.Constructor;
        return (isImplicitlyDeclared && !isConstructorInvocation) ||
               !reference.Location.IsInSource ||
               !definition.Locations.Any(static loc => loc.IsInSource);
    }
 
    public void OnReferenceFound(ISymbol symbol, ReferenceLocation location)
    {
        if (FilterReference(symbol, location))
        {
            return;
        }
 
        _locations.Add(location.Location);
 
        if (SearchCapReached)
        {
            _aggregateCancellationTokenSource.Cancel();
        }
    }
 
    public void ReportProgress(int current, int maximum)
    {
    }
 
    public void Dispose()
        => _aggregateCancellationTokenSource.Dispose();
}