File: FindSymbols\FindReferences\DependentTypeFinder_ProjectIndex.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.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Storage;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.FindSymbols;
 
internal static partial class DependentTypeFinder
{
    private sealed class ProjectIndex(
        MultiDictionary<DocumentId, DeclaredSymbolInfo> classesAndRecordsThatMayDeriveFromSystemObject,
        MultiDictionary<DocumentId, DeclaredSymbolInfo> valueTypes,
        MultiDictionary<DocumentId, DeclaredSymbolInfo> enums,
        MultiDictionary<DocumentId, DeclaredSymbolInfo> delegates,
        MultiDictionary<string, (DocumentId, DeclaredSymbolInfo)> namedTypes)
    {
        /// <summary>
        /// We cache the project instance per <see cref="ProjectState"/>.  This allows us to reuse it over a wide set of
        /// changes (for example, changing completely unrelated projects that a particular project doesn't depend on).
        /// However, <see cref="ProjectState"/> doesn't change even when certain things change that will create a
        /// substantively different <see cref="Project"/>.  For example, if the <see
        /// cref="SourceGeneratorExecutionVersion"/> for the project changes, we'll still have the same project state.
        /// As such, we store the <see cref="Checksum"/> of the project as well, ensuring that if anything in it or its
        /// dependencies changes, we recompute the index.
        /// </summary>
        private static readonly ConditionalWeakTable<ProjectState, StrongBox<(Checksum checksum, AsyncLazy<ProjectIndex> lazyProjectIndex)>> s_projectToIndex = new();
 
        public readonly MultiDictionary<DocumentId, DeclaredSymbolInfo> ClassesAndRecordsThatMayDeriveFromSystemObject = classesAndRecordsThatMayDeriveFromSystemObject;
        public readonly MultiDictionary<DocumentId, DeclaredSymbolInfo> ValueTypes = valueTypes;
        public readonly MultiDictionary<DocumentId, DeclaredSymbolInfo> Enums = enums;
        public readonly MultiDictionary<DocumentId, DeclaredSymbolInfo> Delegates = delegates;
        public readonly MultiDictionary<string, (DocumentId, DeclaredSymbolInfo)> NamedTypes = namedTypes;
 
        public static async Task<ProjectIndex> GetIndexAsync(
            Project project, CancellationToken cancellationToken)
        {
            // Use the checksum of the project.  That way if its state *or* SG info changes, we compute a new index with
            // accurate information in it.
            var checksum = await project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false);
            if (!s_projectToIndex.TryGetValue(project.State, out var tuple) ||
                tuple.Value.checksum != checksum)
            {
                tuple = new((checksum, AsyncLazy.Create(CreateIndexAsync, project)));
 
#if NET
                s_projectToIndex.AddOrUpdate(project.State, tuple);
#else
                // Best effort try to update the map with the new data. 
                s_projectToIndex.Remove(project.State);
                // Note: intentionally ignore the return value here.  We want to use the value we've computed even if
                // another thread beats us to adding things here. 
                _ = s_projectToIndex.GetValue(project.State, _ => tuple);
#endif
            }
 
            return await tuple.Value.lazyProjectIndex.GetValueAsync(cancellationToken).ConfigureAwait(false);
        }
 
        private static async Task<ProjectIndex> CreateIndexAsync(Project project, CancellationToken cancellationToken)
        {
            var classesThatMayDeriveFromSystemObject = new MultiDictionary<DocumentId, DeclaredSymbolInfo>();
            var valueTypes = new MultiDictionary<DocumentId, DeclaredSymbolInfo>();
            var enums = new MultiDictionary<DocumentId, DeclaredSymbolInfo>();
            var delegates = new MultiDictionary<DocumentId, DeclaredSymbolInfo>();
 
            var namedTypes = new MultiDictionary<string, (DocumentId, DeclaredSymbolInfo)>(
                project.Services.GetRequiredService<ISyntaxFactsService>().StringComparer);
 
            var solutionKey = SolutionKey.ToSolutionKey(project.Solution);
 
            var regularDocumentStates = project.State.DocumentStates;
            var sourceGeneratorDocumentStates = await project.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(project.State, cancellationToken).ConfigureAwait(false);
 
            var allStates =
                regularDocumentStates.States.Select(kvp => (kvp.Key, kvp.Value)).Concat(
                sourceGeneratorDocumentStates.States.Select(kvp => (kvp.Key, (DocumentState)kvp.Value)));
 
            // Avoid realizing actual Document instances here.  We don't need them, and it can allocate a lot of
            // memory as we do background indexing.
            foreach (var (documentId, document) in allStates)
            {
                var syntaxTreeIndex = await TopLevelSyntaxTreeIndex.GetRequiredIndexAsync(
                    solutionKey, project.State, document, cancellationToken).ConfigureAwait(false);
                foreach (var info in syntaxTreeIndex.DeclaredSymbolInfos)
                {
                    switch (info.Kind)
                    {
                        case DeclaredSymbolInfoKind.Class:
                        case DeclaredSymbolInfoKind.Record:
                            classesThatMayDeriveFromSystemObject.Add(documentId, info);
                            break;
                        case DeclaredSymbolInfoKind.Enum:
                            enums.Add(documentId, info);
                            break;
                        case DeclaredSymbolInfoKind.Struct:
                        case DeclaredSymbolInfoKind.RecordStruct:
                            valueTypes.Add(documentId, info);
                            break;
                        case DeclaredSymbolInfoKind.Delegate:
                            delegates.Add(documentId, info);
                            break;
                    }
 
                    foreach (var inheritanceName in info.InheritanceNames)
                        namedTypes.Add(inheritanceName, (documentId, info));
                }
            }
 
            return new ProjectIndex(classesThatMayDeriveFromSystemObject, valueTypes, enums, delegates, namedTypes);
        }
    }
}