File: FindSymbols\Shared\AbstractSyntaxIndex.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.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Storage;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.FindSymbols;
 
internal abstract partial class AbstractSyntaxIndex<TIndex>
    where TIndex : AbstractSyntaxIndex<TIndex>
{
    protected delegate TIndex? IndexReader(StringTable stringTable, ObjectReader reader, Checksum? checksum);
    protected delegate TIndex IndexCreator(ProjectState project, SyntaxNode root, Checksum checksum, CancellationToken cancellationToken);
 
    private static readonly ConditionalWeakTable<DocumentState, TIndex?> s_documentToIndex = new();
    private static readonly ConditionalWeakTable<DocumentId, TIndex?> s_documentIdToIndex = new();
 
    protected AbstractSyntaxIndex(Checksum? checksum)
    {
        this.Checksum = checksum;
    }
 
    protected static async ValueTask<TIndex> GetRequiredIndexAsync(
        SolutionKey solutionKey, ProjectState project, DocumentState document, IndexReader read, IndexCreator create, CancellationToken cancellationToken)
    {
        var index = await GetIndexAsync(solutionKey, project, document, read, create, cancellationToken).ConfigureAwait(false);
        Contract.ThrowIfNull(index);
        return index;
    }
 
    protected static ValueTask<TIndex?> GetIndexAsync(SolutionKey solutionKey, ProjectState project, DocumentState document, IndexReader read, IndexCreator create, CancellationToken cancellationToken)
        => GetIndexAsync(solutionKey, project, document, loadOnly: false, read, create, cancellationToken);
 
    [PerformanceSensitive("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1224834", OftenCompletesSynchronously = true)]
    protected static async ValueTask<TIndex?> GetIndexAsync(
        SolutionKey solutionKey,
        ProjectState project,
        DocumentState document,
        bool loadOnly,
        IndexReader read,
        IndexCreator create,
        CancellationToken cancellationToken)
    {
        if (!document.SupportsSyntaxTree)
            return null;
 
        // See if we already cached an index with this direct document index.  If so we can just
        // return it with no additional work.
        if (!s_documentToIndex.TryGetValue(document, out var index))
        {
            index = await GetIndexWorkerAsync(solutionKey, project, document, loadOnly, read, create, cancellationToken).ConfigureAwait(false);
            Contract.ThrowIfFalse(index != null || loadOnly == true, "Result can only be null if 'loadOnly: true' was passed.");
 
            if (index == null)
                return null;
 
            // Populate our caches with this data.
#if NET
            s_documentToIndex.TryAdd(document, index);
            s_documentIdToIndex.AddOrUpdate(document.Id, index);
#else
            // Avoid capturing index on the fast path by making a copy for the slow path
            var indexCopy = index;
            s_documentToIndex.GetValue(document, _ => indexCopy);
            s_documentIdToIndex.Remove(document.Id);
            s_documentIdToIndex.GetValue(document.Id, _ => indexCopy);
#endif
        }
 
        return index;
    }
 
    private static async Task<TIndex?> GetIndexWorkerAsync(
        SolutionKey solutionKey,
        ProjectState project,
        DocumentState document,
        bool loadOnly,
        IndexReader read,
        IndexCreator create,
        CancellationToken cancellationToken)
    {
        var (textChecksum, textAndDirectivesChecksum) = await GetChecksumsAsync(project, document, cancellationToken).ConfigureAwait(false);
 
        // Check if we have an index for a another version of this document.  If our
        // checksums match, we can just use that.
        if (s_documentIdToIndex.TryGetValue(document.Id, out var index) &&
            (index?.Checksum == textChecksum || index?.Checksum == textAndDirectivesChecksum))
        {
            // The previous index we stored with this documentId is still valid.  Just
            // return that.
            return index;
        }
 
        // What we have in memory isn't valid.  Try to load from the persistence service.
        index = await LoadAsync(solutionKey, project, document, textChecksum, textAndDirectivesChecksum, read, cancellationToken).ConfigureAwait(false);
        if (index != null || loadOnly)
            return index;
 
        // alright, we don't have cached information, re-calculate them here.
        index = await CreateIndexAsync(project, document, textChecksum, textAndDirectivesChecksum, create, cancellationToken).ConfigureAwait(false);
 
        // okay, persist this info
        await index.SaveAsync(solutionKey, project, document, cancellationToken).ConfigureAwait(false);
 
        return index;
    }
 
    private static async Task<TIndex> CreateIndexAsync(
        ProjectState project,
        DocumentState document,
        Checksum textChecksum,
        Checksum textAndDirectivesChecksum,
        IndexCreator create,
        CancellationToken cancellationToken)
    {
        Contract.ThrowIfFalse(document.SupportsSyntaxTree);
 
        var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
        var syntaxKinds = project.LanguageServices.GetRequiredService<ISyntaxKindsService>();
 
        // if the tree contains `#if`-directives, then include the directives-checksum info in the checksum we
        // produce. We don't want to consider the data reusable if the user changes their parse-option pp-directives
        // as this could change the root generated for this file.
        //
        // It's trivial for us to determine the checksum to use at the index-creation/writing point because we have
        // to have computed the syntax tree anyways to produce the index.  The tradeoff of this design though is
        // that at the reading point we may have to issue two reads to determine which case we're in.  However, this
        // still let's us avoid parsing the doc at the point we're reading in the indices (which would defeat a
        // major reason for having the index in the first place).  Actual measurements show that double reads do not
        // impose any noticeable perf overhead for the features.
        var ifDirectiveKind = syntaxKinds.IfDirectiveTrivia;
 
        var checksum = root.ContainsDirectives && ContainsIfDirective(root, ifDirectiveKind) ? textAndDirectivesChecksum : textChecksum;
 
        return create(project, root, checksum, cancellationToken);
    }
 
    private static bool ContainsIfDirective(SyntaxNode node, int ifDirectiveKind)
    {
        foreach (var child in node.ChildNodesAndTokens())
        {
            if (!child.ContainsDirectives)
                continue;
 
            if (child.AsNode(out var childNode))
            {
                if (ContainsIfDirective(childNode, ifDirectiveKind))
                    return true;
            }
            else
            {
                if (ContainsIfDirective(child.AsToken(), ifDirectiveKind))
                    return true;
            }
        }
 
        return false;
    }
 
    private static bool ContainsIfDirective(SyntaxToken token, int ifDirectiveKind)
    {
        // Only need to check leading trivia as directives can never appear in trailing trivia.
        foreach (var trivia in token.LeadingTrivia)
        {
            if (trivia.RawKind == ifDirectiveKind)
                return true;
        }
 
        return false;
    }
}