File: FindSymbols\FindReferences\FindReferenceCache.cs
Web Access
Project: ..\..\..\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.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.FindSymbols
{
    internal sealed class FindReferenceCache
    {
        private static readonly ConditionalWeakTable<SemanticModel, FindReferenceCache> s_cache = new();
 
        public static FindReferenceCache GetCache(SemanticModel model)
            => s_cache.GetValue(model, static model => new(model));
 
        private readonly SemanticModel _semanticModel;
 
        private readonly ConcurrentDictionary<SyntaxNode, SymbolInfo> _symbolInfoCache = new();
        private readonly ConcurrentDictionary<string, ImmutableArray<SyntaxToken>> _identifierCache;
 
        private ImmutableHashSet<string>? _aliasNameSet;
        private ImmutableArray<SyntaxToken> _constructorInitializerCache;
 
        private FindReferenceCache(SemanticModel semanticModel)
        {
            _semanticModel = semanticModel;
            _identifierCache = new(comparer: semanticModel.Language switch
            {
                LanguageNames.VisualBasic => StringComparer.OrdinalIgnoreCase,
                LanguageNames.CSharp => StringComparer.Ordinal,
                _ => throw ExceptionUtilities.UnexpectedValue(semanticModel.Language)
            });
        }
 
        public SymbolInfo GetSymbolInfo(SyntaxNode node, CancellationToken cancellationToken)
        {
            return _symbolInfoCache.GetOrAdd(node, static (n, arg) => arg._semanticModel.GetSymbolInfo(n, arg.cancellationToken), (_semanticModel, cancellationToken));
        }
 
        public IAliasSymbol? GetAliasInfo(
            ISemanticFactsService semanticFacts, SyntaxToken token, CancellationToken cancellationToken)
        {
            if (_aliasNameSet == null)
            {
                var set = semanticFacts.GetAliasNameSet(_semanticModel, cancellationToken);
                Interlocked.CompareExchange(ref _aliasNameSet, set, null);
            }
 
            if (_aliasNameSet.Contains(token.ValueText))
                return _semanticModel.GetAliasInfo(token.GetRequiredParent(), cancellationToken);
 
            return null;
        }
 
        public async Task<ImmutableArray<SyntaxToken>> FindMatchingIdentifierTokensAsync(
            Document document,
            string identifier,
            CancellationToken cancellationToken)
        {
            // It's very costly to walk an entire tree.  So if the tree is simple and doesn't contain
            // any unicode escapes in it, then we do simple string matching to find the tokens.
            var info = await SyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false);
 
            // If this document doesn't even contain this identifier (escaped or non-escaped) we don't have to search it at all.
            if (!info.ProbablyContainsIdentifier(identifier))
                return ImmutableArray<SyntaxToken>.Empty;
 
            var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
            var root = await _semanticModel.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
 
            // If the identifier was escaped in the file then we'll have to do a more involved search that actually
            // walks the root and checks all identifier tokens.
            //
            // otherwise, we can use the text of the document to quickly find candidates and test those directly.
            if (info.ProbablyContainsEscapedIdentifier(identifier))
            {
                return _identifierCache.GetOrAdd(identifier, _ => FindMatchingIdentifierTokensFromTree());
            }
            else
            {
                var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
                return _identifierCache.GetOrAdd(identifier, _ => FindMatchingIdentifierTokensFromText(text));
            }
 
            bool IsMatch(SyntaxToken token)
                => !token.IsMissing && syntaxFacts.IsIdentifier(token) && syntaxFacts.TextMatch(token.ValueText, identifier);
 
            ImmutableArray<SyntaxToken> FindMatchingIdentifierTokensFromTree()
            {
                using var _ = ArrayBuilder<SyntaxToken>.GetInstance(out var result);
                Recurse(root);
                return result.ToImmutable();
 
                void Recurse(SyntaxNode node)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    foreach (var child in node.ChildNodesAndTokens())
                    {
                        if (child.IsNode)
                        {
                            Recurse(child.AsNode()!);
                        }
                        else if (child.IsToken)
                        {
                            var token = child.AsToken();
                            if (IsMatch(token))
                                result.Add(token);
 
                            if (token.HasStructuredTrivia)
                            {
                                // structured trivia can only be leading trivia
                                foreach (var trivia in token.LeadingTrivia)
                                {
                                    if (trivia.HasStructure)
                                        Recurse(trivia.GetStructure()!);
                                }
                            }
                        }
                    }
                }
            }
 
            ImmutableArray<SyntaxToken> FindMatchingIdentifierTokensFromText(SourceText sourceText)
            {
                using var _ = ArrayBuilder<SyntaxToken>.GetInstance(out var result);
 
                var index = 0;
                while ((index = sourceText.IndexOf(identifier, index, syntaxFacts.IsCaseSensitive)) >= 0)
                {
                    cancellationToken.ThrowIfCancellationRequested();
 
                    var token = root.FindToken(index, findInsideTrivia: true);
                    var span = token.Span;
                    if (span.Start == index && span.Length == identifier.Length && IsMatch(token))
                        result.Add(token);
 
                    var nextIndex = index + identifier.Length;
                    nextIndex = Math.Max(nextIndex, token.SpanStart);
                    index = nextIndex;
                }
 
                return result.ToImmutable();
            }
        }
 
        public IEnumerable<SyntaxToken> GetConstructorInitializerTokens(
            ISyntaxFactsService syntaxFacts, SyntaxNode root, CancellationToken cancellationToken)
        {
            // this one will only get called when we know given document contains constructor initializer.
            // no reason to use text to check whether it exist first.
            if (_constructorInitializerCache.IsDefault)
            {
                var initializers = GetConstructorInitializerTokensWorker(syntaxFacts, root, cancellationToken);
                ImmutableInterlocked.InterlockedInitialize(ref _constructorInitializerCache, initializers);
            }
 
            return _constructorInitializerCache;
        }
 
        private static ImmutableArray<SyntaxToken> GetConstructorInitializerTokensWorker(
            ISyntaxFactsService syntaxFacts, SyntaxNode root, CancellationToken cancellationToken)
        {
            using var _ = ArrayBuilder<SyntaxToken>.GetInstance(out var initializers);
            foreach (var constructor in syntaxFacts.GetConstructors(root, cancellationToken))
            {
                foreach (var token in constructor.DescendantTokens(descendIntoTrivia: false))
                {
                    if (syntaxFacts.IsThisConstructorInitializer(token) || syntaxFacts.IsBaseConstructorInitializer(token))
                        initializers.Add(token);
                }
            }
 
            return initializers.ToImmutable();
        }
    }
}