File: Workspace\Solution\SolutionState.CompilationTracker.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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Logging;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.SourceGeneratorTelemetry;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    internal partial class SolutionState
    {
        /// <summary>
        /// Tracks the changes made to a project and provides the facility to get a lazily built
        /// compilation for that project.  As the compilation is being built, the partial results are
        /// stored as well so that they can be used in the 'in progress' workspace snapshot.
        /// </summary>
        private partial class CompilationTracker : ICompilationTracker
        {
            private static readonly Func<ProjectState, string> s_logBuildCompilationAsync =
                state => string.Join(",", state.AssemblyName, state.DocumentStates.Count);
 
            public ProjectState ProjectState { get; }
 
            /// <summary>
            /// Access via the <see cref="ReadState"/> and <see cref="WriteState"/> methods.
            /// </summary>
            private CompilationTrackerState _stateDoNotAccessDirectly;
 
            // guarantees only one thread is building at a time
            private readonly SemaphoreSlim _buildLock = new(initialCount: 1);
 
            public SkeletonReferenceCache SkeletonReferenceCache { get; }
 
            private CompilationTracker(
                ProjectState project,
                CompilationTrackerState state,
                SkeletonReferenceCache cachedSkeletonReferences)
            {
                Contract.ThrowIfNull(project);
 
                this.ProjectState = project;
                _stateDoNotAccessDirectly = state;
                this.SkeletonReferenceCache = cachedSkeletonReferences;
            }
 
            /// <summary>
            /// Creates a tracker for the provided project.  The tracker will be in the 'empty' state
            /// and will have no extra information beyond the project itself.
            /// </summary>
            public CompilationTracker(ProjectState project)
                : this(project, CompilationTrackerState.Empty, cachedSkeletonReferences: new())
            {
            }
 
            private CompilationTrackerState ReadState()
                => Volatile.Read(ref _stateDoNotAccessDirectly);
 
            private void WriteState(CompilationTrackerState state)
                => Volatile.Write(ref _stateDoNotAccessDirectly, state);
 
            public GeneratorDriver? GeneratorDriver
            {
                get
                {
                    var state = this.ReadState();
                    return state.GeneratorInfo.Driver;
                }
            }
 
            public bool ContainsAssemblyOrModuleOrDynamic(ISymbol symbol, bool primary)
            {
                Debug.Assert(symbol.Kind is SymbolKind.Assembly or
                             SymbolKind.NetModule or
                             SymbolKind.DynamicType);
                var state = this.ReadState();
 
                var unrootedSymbolSet = (state as FinalState)?.UnrootedSymbolSet;
                if (unrootedSymbolSet == null)
                {
                    // this was not a tracker that has handed out a compilation (all compilations handed out must be
                    // owned by a 'FinalState').  So this symbol could not be from us.
                    return false;
                }
 
                return unrootedSymbolSet.Value.ContainsAssemblyOrModuleOrDynamic(symbol, primary);
            }
 
            /// <summary>
            /// Creates a new instance of the compilation info, retaining any already built
            /// compilation state as the now 'old' state
            /// </summary>
            public ICompilationTracker Fork(
                ProjectState newProject,
                CompilationAndGeneratorDriverTranslationAction? translate)
            {
                var state = ReadState();
 
                var baseCompilation = state.CompilationWithoutGeneratedDocuments;
                if (baseCompilation != null)
                {
                    var intermediateProjects = state is InProgressState inProgressState
                        ? inProgressState.IntermediateProjects
                        : ImmutableList<(ProjectState oldState, CompilationAndGeneratorDriverTranslationAction action)>.Empty;
 
                    if (translate is not null)
                    {
                        // We have a translation action; are we able to merge it with the prior one?
                        var merged = false;
                        if (intermediateProjects.Any())
                        {
                            var (priorState, priorAction) = intermediateProjects.Last();
                            var mergedTranslation = translate.TryMergeWithPrior(priorAction);
                            if (mergedTranslation != null)
                            {
                                // We can replace the prior action with this new one
                                intermediateProjects = intermediateProjects.SetItem(intermediateProjects.Count - 1,
                                    (oldState: priorState, mergedTranslation));
                                merged = true;
                            }
                        }
 
                        if (!merged)
                        {
                            // Just add it to the end
                            intermediateProjects = intermediateProjects.Add((oldState: this.ProjectState, translate));
                        }
                    }
 
                    var newState = CompilationTrackerState.Create(
                        baseCompilation, state.GeneratorInfo, state.FinalCompilationWithGeneratedDocuments, intermediateProjects);
 
                    return new CompilationTracker(newProject, newState, this.SkeletonReferenceCache.Clone());
                }
                else
                {
                    // We may still have a cached generator; we'll have to remember to run generators again since we are making some
                    // change here. We'll also need to update the other state of the driver if appropriate.
                    var generatorInfo = state.GeneratorInfo.WithDocumentsAreFinal(false);
 
                    if (generatorInfo.Driver != null && translate != null)
                    {
                        generatorInfo = generatorInfo.WithDriver(translate.TransformGeneratorDriver(generatorInfo.Driver));
                    }
 
                    var newState = new NoCompilationState(generatorInfo);
                    return new CompilationTracker(newProject, newState, this.SkeletonReferenceCache.Clone());
                }
            }
 
            public ICompilationTracker FreezePartialStateWithTree(SolutionState solution, DocumentState docState, SyntaxTree tree, CancellationToken cancellationToken)
            {
                GetPartialCompilationState(
                    solution, docState.Id,
                    out var inProgressProject,
                    out var compilationPair,
                    out var generatorInfo,
                    out var metadataReferenceToProjectId,
                    cancellationToken);
 
                // Ensure we actually have the tree we need in there; note that if the tree is present, then we know the document must also be
                // present in inProgressProject, since those are both updated in parallel.
                //
                // the tree that we have been given was directly returned from the document state that we're also being passed --
                // the only reason we're requesting it earlier is this code is running under a lock in
                // SolutionState.WithFrozenPartialCompilationIncludingSpecificDocument.
                if (!compilationPair.CompilationWithoutGeneratedDocuments.ContainsSyntaxTree(tree))
                {
                    // We do not have the exact tree. It either means this document was recently added, or the tree was recently changed.
                    // We now need to update both the inProgressState and the compilation. There are several possibilities we want to consider:
                    //
                    // 1. An earlier version of the document is present in the compilation, and we just need to update it to the current version
                    // 2. The tree wasn't present in the original snapshot at all, and we just need to add the tree.
                    // 3. The tree wasn't present in the original snapshot, but an older file had been removed that had the same file path.
                    //    As a heuristic, we remove the old one so we don't end up with duplicate trees.
                    //
                    // Note it's possible that we simply had never tried to produce a compilation yet for this project at all, in that case
                    // GetPartialCompilationState would have produced an empty compilation, and it would have updated inProgressProject to
                    // remove all the documents. Thus, that is no different than the "add" case above.
                    if (inProgressProject.DocumentStates.TryGetState(docState.Id, out var oldState))
                    {
                        // Scenario 1. The document had been previously parsed and it's there, so we can update it with our current state
                        // This call should be instant, since the compilation already must exist that contains this tree. Note if no compilation existed
                        // GetPartialCompilationState would have produced an empty one, and removed any documents, so inProgressProject.DocumentStates would
                        // have been empty originally.
                        var oldTree = oldState.GetSyntaxTree(cancellationToken);
 
                        compilationPair = compilationPair.ReplaceSyntaxTree(oldTree, tree);
                        inProgressProject = inProgressProject.UpdateDocument(docState, contentChanged: true);
                    }
                    else
                    {
                        // We're in either scenario 2 or 3. Do we have an existing tree to try replacing? Note: the file path here corresponds to Document.FilePath.
                        // If a document's file path is null, we then substitute Document.Name, so we usually expect there to be a unique string regardless.
                        var oldTree = compilationPair.CompilationWithoutGeneratedDocuments.SyntaxTrees.FirstOrDefault(t => t.FilePath == tree.FilePath);
                        if (oldTree == null)
                        {
                            // Scenario 2.
                            compilationPair = compilationPair.AddSyntaxTree(tree);
                            inProgressProject = inProgressProject.AddDocuments(ImmutableArray.Create(docState));
                        }
                        else
                        {
                            // Scenario 3.
                            compilationPair = compilationPair.ReplaceSyntaxTree(oldTree, tree);
 
                            // The old tree came from some other document with a different ID then we started with -- if the document ID still existed we would have
                            // been in the Scenario 1 case instead. We'll find the old document ID, remove that state, and then add ours.
                            var oldDocumentId = DocumentState.GetDocumentIdForTree(oldTree);
                            Contract.ThrowIfNull(oldDocumentId, $"{nameof(oldTree)} came from the compilation produced by the workspace, so the document ID should have existed.");
                            inProgressProject = inProgressProject
                                .RemoveDocuments(ImmutableArray.Create(oldDocumentId))
                                .AddDocuments(ImmutableArray.Create(docState));
                        }
                    }
                }
 
                // At this point, we now absolutely should have our tree in the compilation
                Contract.ThrowIfFalse(compilationPair.CompilationWithoutGeneratedDocuments.ContainsSyntaxTree(tree));
 
                // The user is asking for an in progress snap.  We don't want to create it and then
                // have the compilation immediately disappear.  So we force it to stay around with a ConstantValueSource.
                // As a policy, all partial-state projects are said to have incomplete references, since the state has no guarantees.
                var finalState = FinalState.Create(
                    finalCompilationSource: compilationPair.CompilationWithGeneratedDocuments,
                    compilationWithoutGeneratedFiles: compilationPair.CompilationWithoutGeneratedDocuments,
                    hasSuccessfullyLoaded: false,
                    generatorInfo,
                    finalCompilation: compilationPair.CompilationWithGeneratedDocuments,
                    this.ProjectState.Id,
                    metadataReferenceToProjectId);
 
                return new CompilationTracker(inProgressProject, finalState, this.SkeletonReferenceCache.Clone());
            }
 
            /// <summary>
            /// Tries to get the latest snapshot of the compilation without waiting for it to be
            /// fully built. This method takes advantage of the progress side-effect produced during
            /// <see cref="BuildCompilationInfoAsync(SolutionState, CancellationToken)"/>.
            /// It will either return the already built compilation, any
            /// in-progress compilation or any known old compilation in that order of preference.
            /// The compilation state that is returned will have a compilation that is retained so
            /// that it cannot disappear.
            /// </summary>
            private void GetPartialCompilationState(
                SolutionState solution,
                DocumentId id,
                out ProjectState inProgressProject,
                out CompilationPair compilations,
                out CompilationTrackerGeneratorInfo generatorInfo,
                out Dictionary<MetadataReference, ProjectId>? metadataReferenceToProjectId,
                CancellationToken cancellationToken)
            {
                var state = ReadState();
                var compilationWithoutGeneratedDocuments = state.CompilationWithoutGeneratedDocuments;
 
                // check whether we can bail out quickly for typing case
                var inProgressState = state as InProgressState;
 
                generatorInfo = state.GeneratorInfo.WithDocumentsAreFinalAndFrozen();
                inProgressProject = inProgressState != null ? inProgressState.IntermediateProjects.First().oldState : this.ProjectState;
 
                // all changes left for this document is modifying the given document; since the compilation is already fully up to date
                // we don't need to do any further checking of it's references
                if (inProgressState != null &&
                    compilationWithoutGeneratedDocuments != null &&
                    inProgressState.IntermediateProjects.All(t => IsTouchDocumentActionForDocument(t.action, id)))
                {
                    // We'll add in whatever generated documents we do have; these may be from a prior run prior to some changes
                    // being made to the project, but it's the best we have so we'll use it.
                    compilations = new CompilationPair(
                        compilationWithoutGeneratedDocuments,
                        compilationWithoutGeneratedDocuments.AddSyntaxTrees(generatorInfo.Documents.States.Values.Select(state => state.GetSyntaxTree(cancellationToken))));
 
                    // This is likely a bug.  It seems possible to pass out a partial compilation state that we don't
                    // properly record assembly symbols for.
                    metadataReferenceToProjectId = null;
                    SolutionLogger.UseExistingPartialProjectState();
                    return;
                }
 
                // if we already have a final compilation we are done.
                if (compilationWithoutGeneratedDocuments != null && state is FinalState finalState)
                {
                    var finalCompilation = finalState.FinalCompilationWithGeneratedDocuments;
                    Contract.ThrowIfNull(finalCompilation, "We have a FinalState, so we must have a non-null final compilation");
 
                    compilations = new CompilationPair(compilationWithoutGeneratedDocuments, finalCompilation);
 
                    // This should hopefully be safe to return as null.  Because we already reached the 'FinalState'
                    // before, we should have already recorded the assembly symbols for it.  So not recording them
                    // again is likely ok (as long as compilations continue to return the same IAssemblySymbols for
                    // the same references across source edits).
                    metadataReferenceToProjectId = null;
                    SolutionLogger.UseExistingFullProjectState();
                    return;
                }
 
                // 1) if we have an in-progress compilation use it.  
                // 2) If we don't, then create a simple empty compilation/project. 
                // 3) then, make sure that all it's p2p refs and whatnot are correct.
                if (compilationWithoutGeneratedDocuments == null)
                {
                    inProgressProject = inProgressProject.RemoveAllDocuments();
                    compilationWithoutGeneratedDocuments = CreateEmptyCompilation();
                }
 
                compilations = new CompilationPair(
                    compilationWithoutGeneratedDocuments,
                    compilationWithoutGeneratedDocuments.AddSyntaxTrees(generatorInfo.Documents.States.Values.Select(state => state.GetSyntaxTree(cancellationToken))));
 
                // Now add in back a consistent set of project references.  For project references
                // try to get either a CompilationReference or a SkeletonReference. This ensures
                // that the in-progress project only reports a reference to another project if it
                // could actually get a reference to that project's metadata.
                var metadataReferences = new List<MetadataReference>();
                var newProjectReferences = new List<ProjectReference>();
                metadataReferences.AddRange(this.ProjectState.MetadataReferences);
 
                metadataReferenceToProjectId = new Dictionary<MetadataReference, ProjectId>();
 
                foreach (var projectReference in this.ProjectState.ProjectReferences)
                {
                    var referencedProject = solution.GetProjectState(projectReference.ProjectId);
                    if (referencedProject != null)
                    {
                        if (referencedProject.IsSubmission)
                        {
                            var previousScriptCompilation = solution.GetCompilationAsync(projectReference.ProjectId, cancellationToken).WaitAndGetResult(cancellationToken);
 
                            // previous submission project must support compilation:
                            RoslynDebug.Assert(previousScriptCompilation != null);
 
                            compilations = compilations.WithPreviousScriptCompilation(previousScriptCompilation);
                        }
                        else
                        {
                            // get the latest metadata for the partial compilation of the referenced project.
                            var metadata = solution.GetPartialMetadataReference(projectReference, this.ProjectState);
 
                            if (metadata == null)
                            {
                                // if we failed to get the metadata, check to see if we previously had existing metadata and reuse it instead.
                                var inProgressCompilationNotRef = compilations.CompilationWithGeneratedDocuments;
                                metadata = inProgressCompilationNotRef.ExternalReferences.FirstOrDefault(
                                    r => solution.GetProjectState(inProgressCompilationNotRef.GetAssemblyOrModuleSymbol(r) as IAssemblySymbol)?.Id == projectReference.ProjectId);
                            }
 
                            if (metadata != null)
                            {
                                newProjectReferences.Add(projectReference);
                                metadataReferences.Add(metadata);
                                metadataReferenceToProjectId.Add(metadata, projectReference.ProjectId);
                            }
                        }
                    }
                }
 
                inProgressProject = inProgressProject.WithProjectReferences(newProjectReferences);
 
                if (!Enumerable.SequenceEqual(compilations.CompilationWithoutGeneratedDocuments.ExternalReferences, metadataReferences))
                {
                    compilations = compilations.WithReferences(metadataReferences);
                }
 
                SolutionLogger.CreatePartialProjectState();
            }
 
            private static bool IsTouchDocumentActionForDocument(CompilationAndGeneratorDriverTranslationAction action, DocumentId id)
                => action is CompilationAndGeneratorDriverTranslationAction.TouchDocumentAction touchDocumentAction &&
                   touchDocumentAction.DocumentId == id;
 
            /// <summary>
            /// Gets the final compilation if it is available.
            /// </summary>
            public bool TryGetCompilation([NotNullWhen(true)] out Compilation? compilation)
            {
                var state = ReadState();
                compilation = state.FinalCompilationWithGeneratedDocuments;
                return compilation != null;
            }
 
            public Task<Compilation> GetCompilationAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                if (this.TryGetCompilation(out var compilation))
                {
                    return Task.FromResult(compilation);
                }
                else if (cancellationToken.IsCancellationRequested)
                {
                    // Handle early cancellation here to avoid throwing/catching cancellation exceptions in the async
                    // state machines. This helps reduce the total number of First Chance Exceptions occurring in IDE
                    // typing scenarios.
                    return Task.FromCanceled<Compilation>(cancellationToken);
                }
                else
                {
                    return GetCompilationSlowAsync(solution, cancellationToken);
                }
            }
 
            private async Task<Compilation> GetCompilationSlowAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                var compilationInfo = await GetOrBuildCompilationInfoAsync(solution, lockGate: true, cancellationToken: cancellationToken).ConfigureAwait(false);
                return compilationInfo.Compilation;
            }
 
            private async Task<CompilationInfo> GetOrBuildCompilationInfoAsync(
                SolutionState solution,
                bool lockGate,
                CancellationToken cancellationToken)
            {
                try
                {
                    using (Logger.LogBlock(FunctionId.Workspace_Project_CompilationTracker_BuildCompilationAsync,
                                           s_logBuildCompilationAsync, ProjectState, cancellationToken))
                    {
                        cancellationToken.ThrowIfCancellationRequested();
 
                        var state = ReadState();
 
                        // Try to get the built compilation.  If it exists, then we can just return that.
                        var finalCompilation = state.FinalCompilationWithGeneratedDocuments;
                        if (finalCompilation != null)
                        {
                            RoslynDebug.Assert(state.HasSuccessfullyLoaded.HasValue);
                            return new CompilationInfo(finalCompilation, state.HasSuccessfullyLoaded.Value, state.GeneratorInfo);
                        }
 
                        // Otherwise, we actually have to build it.  Ensure that only one thread is trying to
                        // build this compilation at a time.
                        if (lockGate)
                        {
                            using (await _buildLock.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
                            {
                                return await BuildCompilationInfoAsync(solution, cancellationToken).ConfigureAwait(false);
                            }
                        }
                        else
                        {
                            return await BuildCompilationInfoAsync(solution, cancellationToken).ConfigureAwait(false);
                        }
                    }
                }
                catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
                {
                    throw ExceptionUtilities.Unreachable();
                }
            }
 
            /// <summary>
            /// Builds the compilation matching the project state. In the process of building, also
            /// produce in progress snapshots that can be accessed from other threads.
            /// </summary>
            private async Task<CompilationInfo> BuildCompilationInfoAsync(
                SolutionState solution,
                CancellationToken cancellationToken)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                var state = ReadState();
 
                // if we already have a compilation, we must be already done!  This can happen if two
                // threads were waiting to build, and we came in after the other succeeded.
                var compilation = state.FinalCompilationWithGeneratedDocuments;
                if (compilation != null)
                {
                    RoslynDebug.Assert(state.HasSuccessfullyLoaded.HasValue);
                    return new CompilationInfo(compilation, state.HasSuccessfullyLoaded.Value, state.GeneratorInfo);
                }
 
                compilation = state.CompilationWithoutGeneratedDocuments;
 
                if (compilation == null)
                {
                    // We've got nothing.  Build it from scratch :(
                    return await BuildCompilationInfoFromScratchAsync(
                        solution,
                        state.GeneratorInfo,
                        cancellationToken).ConfigureAwait(false);
                }
 
                if (state is AllSyntaxTreesParsedState or FinalState)
                {
                    // We have a declaration compilation, use it to reconstruct the final compilation
                    return await FinalizeCompilationAsync(
                        solution,
                        compilation,
                        state.GeneratorInfo,
                        compilationWithStaleGeneratedTrees: null,
                        cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    // We must have an in progress compilation. Build off of that.
                    return await BuildFinalStateFromInProgressStateAsync(
                        solution, (InProgressState)state, compilation, cancellationToken).ConfigureAwait(false);
                }
            }
 
            private async Task<CompilationInfo> BuildCompilationInfoFromScratchAsync(
                SolutionState solution,
                CompilationTrackerGeneratorInfo generatorInfo,
                CancellationToken cancellationToken)
            {
                try
                {
                    var compilation = await BuildDeclarationCompilationFromScratchAsync(
                        generatorInfo, cancellationToken).ConfigureAwait(false);
 
                    return await FinalizeCompilationAsync(
                        solution,
                        compilation,
                        generatorInfo,
                        compilationWithStaleGeneratedTrees: null,
                        cancellationToken).ConfigureAwait(false);
                }
                catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
                {
                    throw ExceptionUtilities.Unreachable();
                }
            }
 
            [PerformanceSensitive(
                "https://github.com/dotnet/roslyn/issues/23582",
                Constraint = "Avoid calling " + nameof(Compilation.AddSyntaxTrees) + " in a loop due to allocation overhead.")]
            private async Task<Compilation> BuildDeclarationCompilationFromScratchAsync(
                CompilationTrackerGeneratorInfo generatorInfo,
                CancellationToken cancellationToken)
            {
                try
                {
                    var compilation = CreateEmptyCompilation();
 
                    using var _ = ArrayBuilder<SyntaxTree>.GetInstance(ProjectState.DocumentStates.Count, out var trees);
                    foreach (var documentState in ProjectState.DocumentStates.GetStatesInCompilationOrder())
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        // Include the tree even if the content of the document failed to load.
                        trees.Add(await documentState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false));
                    }
 
                    compilation = compilation.AddSyntaxTrees(trees);
                    WriteState(new AllSyntaxTreesParsedState(compilation, generatorInfo));
                    return compilation;
                }
                catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
                {
                    throw ExceptionUtilities.Unreachable();
                }
            }
 
            private Compilation CreateEmptyCompilation()
            {
                var compilationFactory = this.ProjectState.LanguageServices.GetRequiredService<ICompilationFactoryService>();
 
                if (this.ProjectState.IsSubmission)
                {
                    return compilationFactory.CreateSubmissionCompilation(
                        this.ProjectState.AssemblyName,
                        this.ProjectState.CompilationOptions!,
                        this.ProjectState.HostObjectType);
                }
                else
                {
                    return compilationFactory.CreateCompilation(
                        this.ProjectState.AssemblyName,
                        this.ProjectState.CompilationOptions!);
                }
            }
 
            private async Task<CompilationInfo> BuildFinalStateFromInProgressStateAsync(
                SolutionState solution, InProgressState state, Compilation inProgressCompilation, CancellationToken cancellationToken)
            {
                try
                {
                    var (compilationWithoutGenerators, compilationWithGenerators, generatorDriver) = await BuildDeclarationCompilationFromInProgressAsync(
                        state, inProgressCompilation, cancellationToken).ConfigureAwait(false);
 
                    return await FinalizeCompilationAsync(
                        solution,
                        compilationWithoutGenerators,
                        state.GeneratorInfo.WithDriver(generatorDriver),
                        compilationWithGenerators,
                        cancellationToken).ConfigureAwait(false);
                }
                catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
                {
                    throw ExceptionUtilities.Unreachable();
                }
            }
 
            private async Task<(Compilation compilationWithoutGenerators, Compilation? compilationWithGenerators, GeneratorDriver? generatorDriver)> BuildDeclarationCompilationFromInProgressAsync(
                InProgressState state, Compilation compilationWithoutGenerators, CancellationToken cancellationToken)
            {
                try
                {
                    var compilationWithGenerators = state.CompilationWithGeneratedDocuments;
                    var generatorDriver = state.GeneratorInfo.Driver;
 
                    // If compilationWithGenerators is the same as compilationWithoutGenerators, then it means a prior run of generators
                    // didn't produce any files. In that case, we'll just make compilationWithGenerators null so we avoid doing any
                    // transformations of it multiple times. Otherwise the transformations below and in FinalizeCompilationAsync will try
                    // to update both at once, which is functionally fine but just unnecessary work. This function is always allowed to return
                    // null for compilationWithGenerators in the end, so there's no harm there.
                    if (compilationWithGenerators == compilationWithoutGenerators)
                    {
                        compilationWithGenerators = null;
                    }
 
                    var intermediateProjects = state.IntermediateProjects;
 
                    while (!intermediateProjects.IsEmpty)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
 
                        // We have a list of transformations to get to our final compilation; take the first transformation and apply it.
                        var intermediateProject = intermediateProjects[0];
 
                        compilationWithoutGenerators = await intermediateProject.action.TransformCompilationAsync(compilationWithoutGenerators, cancellationToken).ConfigureAwait(false);
 
                        if (compilationWithGenerators != null)
                        {
                            // Also transform the compilation that has generated files; we won't do that though if the transformation either would cause problems with
                            // the generated documents, or if don't have any source generators in the first place.
                            if (intermediateProject.action.CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput &&
                                intermediateProject.oldState.SourceGenerators.Any())
                            {
                                compilationWithGenerators = await intermediateProject.action.TransformCompilationAsync(compilationWithGenerators, cancellationToken).ConfigureAwait(false);
                            }
                            else
                            {
                                compilationWithGenerators = null;
                            }
                        }
 
                        if (generatorDriver != null)
                        {
                            generatorDriver = intermediateProject.action.TransformGeneratorDriver(generatorDriver);
                        }
 
                        // We have updated state, so store this new result; this allows us to drop the intermediate state we already processed
                        // even if we were to get cancelled at a later point.
                        intermediateProjects = intermediateProjects.RemoveAt(0);
 
                        this.WriteState(CompilationTrackerState.Create(
                            compilationWithoutGenerators, state.GeneratorInfo.WithDriver(generatorDriver), compilationWithGenerators, intermediateProjects));
                    }
 
                    return (compilationWithoutGenerators, compilationWithGenerators, generatorDriver);
                }
                catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
                {
                    throw ExceptionUtilities.Unreachable();
                }
            }
 
            private readonly struct CompilationInfo
            {
                public Compilation Compilation { get; }
                public bool HasSuccessfullyLoaded { get; }
                public CompilationTrackerGeneratorInfo GeneratorInfo { get; }
 
                public CompilationInfo(Compilation compilation, bool hasSuccessfullyLoaded, CompilationTrackerGeneratorInfo generatorInfo)
                {
                    Compilation = compilation;
                    HasSuccessfullyLoaded = hasSuccessfullyLoaded;
                    GeneratorInfo = generatorInfo;
                }
            }
 
            /// <summary>
            /// Add all appropriate references to the compilation and set it as our final compilation
            /// state.
            /// </summary>
            /// <param name="generatorInfo">The generator info that contains the last run of the documents, if any exists, as
            /// well as the driver that can be used to run if need to.</param>
            /// <param name="compilationWithStaleGeneratedTrees">The compilation from a prior run that contains generated trees, which
            /// match the states included in <paramref name="generatorInfo"/>. If a generator run here produces
            /// the same set of generated documents as are in <paramref name="generatorInfo"/>, and we don't need to make any other
            /// changes to references, we can then use this compilation instead of re-adding source generated files again to the
            /// <paramref name="compilationWithoutGenerators"/>.</param>
            private async Task<CompilationInfo> FinalizeCompilationAsync(
                SolutionState solution,
                Compilation compilationWithoutGenerators,
                CompilationTrackerGeneratorInfo generatorInfo,
                Compilation? compilationWithStaleGeneratedTrees,
                CancellationToken cancellationToken)
            {
                try
                {
                    // Project is complete only if the following are all true:
                    //  1. HasAllInformation flag is set for the project
                    //  2. Either the project has non-zero metadata references OR this is the corlib project.
                    //     For the latter, we use a heuristic if the underlying compilation defines "System.Object" type.
                    var hasSuccessfullyLoaded = this.ProjectState.HasAllInformation &&
                        (this.ProjectState.MetadataReferences.Count > 0 ||
                         compilationWithoutGenerators.GetTypeByMetadataName("System.Object") != null);
 
                    var newReferences = new List<MetadataReference>();
                    var metadataReferenceToProjectId = new Dictionary<MetadataReference, ProjectId>();
                    newReferences.AddRange(this.ProjectState.MetadataReferences);
 
                    foreach (var projectReference in this.ProjectState.ProjectReferences)
                    {
                        var referencedProject = solution.GetProjectState(projectReference.ProjectId);
 
                        // Even though we're creating a final compilation (vs. an in progress compilation),
                        // it's possible that the target project has been removed.
                        if (referencedProject != null)
                        {
                            // If both projects are submissions, we'll count this as a previous submission link
                            // instead of a regular metadata reference
                            if (referencedProject.IsSubmission)
                            {
                                // if the referenced project is a submission project must be a submission as well:
                                Debug.Assert(this.ProjectState.IsSubmission);
 
                                // We now need to (potentially) update the prior submission compilation. That Compilation is held in the
                                // ScriptCompilationInfo that we need to replace as a unit.
                                var previousSubmissionCompilation =
                                    await solution.GetCompilationAsync(projectReference.ProjectId, cancellationToken).ConfigureAwait(false);
 
                                if (compilationWithoutGenerators.ScriptCompilationInfo!.PreviousScriptCompilation != previousSubmissionCompilation)
                                {
                                    compilationWithoutGenerators = compilationWithoutGenerators.WithScriptCompilationInfo(
                                        compilationWithoutGenerators.ScriptCompilationInfo!.WithPreviousScriptCompilation(previousSubmissionCompilation!));
 
                                    compilationWithStaleGeneratedTrees = compilationWithStaleGeneratedTrees?.WithScriptCompilationInfo(
                                        compilationWithStaleGeneratedTrees.ScriptCompilationInfo!.WithPreviousScriptCompilation(previousSubmissionCompilation!));
                                }
                            }
                            else
                            {
                                var metadataReference = await solution.GetMetadataReferenceAsync(
                                    projectReference, this.ProjectState, cancellationToken).ConfigureAwait(false);
 
                                // A reference can fail to be created if a skeleton assembly could not be constructed.
                                if (metadataReference != null)
                                {
                                    newReferences.Add(metadataReference);
                                    metadataReferenceToProjectId.Add(metadataReference, projectReference.ProjectId);
                                }
                                else
                                {
                                    hasSuccessfullyLoaded = false;
                                }
                            }
                        }
                    }
 
                    // Now that we know the set of references this compilation should have, update them if they're not already.
                    // Generators cannot add references, so we can use the same set of references both for the compilation
                    // that doesn't have generated files, and the one we're trying to reuse that has generated files.
                    // Since we updated both of these compilations together in response to edits, we only have to check one
                    // for a potential mismatch.
                    if (!Enumerable.SequenceEqual(compilationWithoutGenerators.ExternalReferences, newReferences))
                    {
                        compilationWithoutGenerators = compilationWithoutGenerators.WithReferences(newReferences);
                        compilationWithStaleGeneratedTrees = compilationWithStaleGeneratedTrees?.WithReferences(newReferences);
                    }
 
                    // We will finalize the compilation by adding full contents here.
                    Compilation compilationWithGenerators;
 
                    if (generatorInfo.DocumentsAreFinal)
                    {
                        // We must have ran generators before, but for some reason had to remake the compilation from scratch.
                        // This could happen if the trees were strongly held, but the compilation was entirely garbage collected.
                        // Just add in the trees we already have. We don't want to rerun since the consumer of this Solution
                        // snapshot has already seen the trees and thus needs to ensure identity of them.
                        compilationWithGenerators = compilationWithoutGenerators.AddSyntaxTrees(
                            await generatorInfo.Documents.States.Values.SelectAsArrayAsync(state => state.GetSyntaxTreeAsync(cancellationToken)).ConfigureAwait(false));
                    }
                    else
                    {
                        using var generatedDocumentsBuilder = new TemporaryArray<SourceGeneratedDocumentState>();
 
                        if (!ProjectState.SourceGenerators.Any())
                        {
                            // We don't have any generators, so if we have a compilation from a previous run with generated files, we definitely can't use it anymore
                            compilationWithStaleGeneratedTrees = null;
                        }
                        else // we have a generator
                        {
                            // If we don't already have a generator driver, we'll have to create one from scratch
                            if (generatorInfo.Driver == null)
                            {
                                var additionalTexts = this.ProjectState.AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText);
                                var compilationFactory = this.ProjectState.LanguageServices.GetRequiredService<ICompilationFactoryService>();
 
                                generatorInfo = generatorInfo.WithDriver(compilationFactory.CreateGeneratorDriver(
                                        this.ProjectState.ParseOptions!,
                                        ProjectState.SourceGenerators.ToImmutableArray(),
                                        this.ProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider,
                                        additionalTexts));
                            }
                            else
                            {
#if DEBUG
 
                                // Assert that the generator driver is in sync with our additional document states; there's not a public
                                // API to get this, but we'll reflect in DEBUG-only.
                                var driverType = generatorInfo.Driver.GetType();
                                var stateMember = driverType.GetField("_state", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
                                Contract.ThrowIfNull(stateMember);
                                var additionalTextsMember = stateMember.FieldType.GetField("AdditionalTexts", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
                                Contract.ThrowIfNull(additionalTextsMember);
                                var state = stateMember.GetValue(generatorInfo.Driver);
                                var additionalTexts = (ImmutableArray<AdditionalText>)additionalTextsMember.GetValue(state)!;
 
                                Contract.ThrowIfFalse(additionalTexts.Length == this.ProjectState.AdditionalDocumentStates.Count);
 
#endif
                            }
 
                            // HACK HACK HACK HACK to address https://github.com/dotnet/roslyn/issues/59818. There, we were running into issues where
                            // a generator being present and consuming syntax was causing all red nodes to be processed. This was problematic when
                            // Razor design time files are also fed in, since those files tend to be quite large. The Razor design time files
                            // aren't produced via a generator, but rather via our legacy IDynamicFileInfo mechanism, so it's also a bit strange
                            // we'd even give them to other generators since that doesn't match the real compiler anyways. This simply removes
                            // all of those trees in an effort to speed things up, and also ensure the design time compilations are a bit more accurate.
                            using var _ = ArrayBuilder<SyntaxTree>.GetInstance(out var treesToRemove);
 
                            foreach (var documentState in ProjectState.DocumentStates.States)
                            {
                                // This matches the logic in CompileTimeSolutionProvider for which documents are removed when we're
                                // activating the generator.
                                if (documentState.Value.Attributes.DesignTimeOnly)
                                {
                                    treesToRemove.Add(await documentState.Value.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false));
                                }
                            }
 
                            var compilationToRunGeneratorsOn = compilationWithoutGenerators.RemoveSyntaxTrees(treesToRemove);
                            // END HACK HACK HACK HACK.
 
                            generatorInfo = generatorInfo.WithDriver(generatorInfo.Driver!.RunGenerators(compilationToRunGeneratorsOn, cancellationToken));
 
                            solution.Services.GetService<ISourceGeneratorTelemetryCollectorWorkspaceService>()?.CollectRunResult(generatorInfo.Driver!.GetRunResult(), generatorInfo.Driver!.GetTimingInfo(), ProjectState);
 
                            var runResult = generatorInfo.Driver!.GetRunResult();
 
                            // We may be able to reuse compilationWithStaleGeneratedTrees if the generated trees are identical. We will assign null
                            // to compilationWithStaleGeneratedTrees if we at any point realize it can't be used. We'll first check the count of trees
                            // if that changed then we absolutely can't reuse it. But if the counts match, we'll then see if each generated tree
                            // content is identical to the prior generation run; if we find a match each time, then the set of the generated trees
                            // and the prior generated trees are identical.
                            if (compilationWithStaleGeneratedTrees != null)
                            {
                                var generatedTreeCount =
                                    runResult.Results.Sum(r => IsGeneratorRunResultToIgnore(r) ? 0 : r.GeneratedSources.Length);
 
                                if (generatorInfo.Documents.Count != generatedTreeCount)
                                {
                                    compilationWithStaleGeneratedTrees = null;
                                }
                            }
 
                            foreach (var generatorResult in runResult.Results)
                            {
                                if (IsGeneratorRunResultToIgnore(generatorResult))
                                {
                                    continue;
                                }
 
                                var generatorAnalyzerReference = this.ProjectState.GetAnalyzerReferenceForGenerator(generatorResult.Generator);
 
                                foreach (var generatedSource in generatorResult.GeneratedSources)
                                {
                                    var existing = FindExistingGeneratedDocumentState(
                                        generatorInfo.Documents,
                                        generatorResult.Generator,
                                        generatorAnalyzerReference,
                                        generatedSource.HintName);
 
                                    if (existing != null)
                                    {
                                        var newDocument = existing.WithUpdatedGeneratedContent(
                                                generatedSource.SourceText,
                                                this.ProjectState.ParseOptions!);
 
                                        generatedDocumentsBuilder.Add(newDocument);
 
                                        if (newDocument != existing)
                                            compilationWithStaleGeneratedTrees = null;
                                    }
                                    else
                                    {
                                        // NOTE: the use of generatedSource.SyntaxTree to fetch the path and options is OK,
                                        // since the tree is a lazy tree and that won't trigger the parse.
                                        var identity = SourceGeneratedDocumentIdentity.Generate(
                                            ProjectState.Id,
                                            generatedSource.HintName,
                                            generatorResult.Generator,
                                            generatedSource.SyntaxTree.FilePath,
                                            generatorAnalyzerReference);
 
                                        generatedDocumentsBuilder.Add(
                                            SourceGeneratedDocumentState.Create(
                                                identity,
                                                generatedSource.SourceText,
                                                generatedSource.SyntaxTree.Options,
                                                ProjectState.LanguageServices));
 
                                        // The count of trees was the same, but something didn't match up. Since we're here, at least one tree
                                        // was added, and an equal number must have been removed. Rather than trying to incrementally update
                                        // this compilation, we'll just toss this and re-add all the trees.
                                        compilationWithStaleGeneratedTrees = null;
                                    }
                                }
                            }
                        }
 
                        // If we didn't null out this compilation, it means we can actually use it
                        if (compilationWithStaleGeneratedTrees != null)
                        {
                            compilationWithGenerators = compilationWithStaleGeneratedTrees;
                            generatorInfo = generatorInfo.WithDocumentsAreFinal(true);
                        }
                        else
                        {
                            // We produced new documents, so time to create new state for it
                            var generatedDocuments = new TextDocumentStates<SourceGeneratedDocumentState>(generatedDocumentsBuilder.ToImmutableAndClear());
                            compilationWithGenerators = compilationWithoutGenerators.AddSyntaxTrees(
                                await generatedDocuments.States.Values.SelectAsArrayAsync(state => state.GetSyntaxTreeAsync(cancellationToken)).ConfigureAwait(false));
                            generatorInfo = new CompilationTrackerGeneratorInfo(generatedDocuments, generatorInfo.Driver, documentsAreFinal: true);
                        }
                    }
 
                    var finalState = FinalState.Create(
                        compilationWithGenerators,
                        compilationWithoutGenerators,
                        hasSuccessfullyLoaded,
                        generatorInfo,
                        compilationWithGenerators,
                        this.ProjectState.Id,
                        metadataReferenceToProjectId);
 
                    this.WriteState(finalState);
 
                    return new CompilationInfo(compilationWithGenerators, hasSuccessfullyLoaded, generatorInfo);
                }
                catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
                {
                    // Explicitly force a yield point here.  This addresses a problem on .net framework where it's
                    // possible that cancelling this task chain ends up stack overflowing as the TPL attempts to
                    // synchronously recurse through the tasks to execute antecedent work.  This will force continuations
                    // here to run asynchronously preventing the stack overflow.
                    // See https://github.com/dotnet/roslyn/issues/56356 for more details.
                    // note: this can be removed if this code only needs to run on .net core (as the stack overflow issue
                    // does not exist there).
                    await Task.Yield().ConfigureAwait(false);
                    throw;
                }
                catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
                {
                    throw ExceptionUtilities.Unreachable();
                }
 
                // Local functions
                static SourceGeneratedDocumentState? FindExistingGeneratedDocumentState(
                    TextDocumentStates<SourceGeneratedDocumentState> states,
                    ISourceGenerator generator,
                    AnalyzerReference analyzerReference,
                    string hintName)
                {
                    var generatorIdentity = new SourceGeneratorIdentity(generator, analyzerReference);
 
                    foreach (var (_, state) in states.States)
                    {
                        if (state.Identity.Generator != generatorIdentity)
                            continue;
 
                        if (state.HintName != hintName)
                            continue;
 
                        return state;
                    }
 
                    return null;
                }
            }
 
            /// <summary>
            /// Attempts to get (without waiting) a metadata reference to a possibly in progress
            /// compilation. Only actual compilation references are returned. Could potentially 
            /// return null if nothing can be provided.
            /// </summary>
            public MetadataReference? GetPartialMetadataReference(ProjectState fromProject, ProjectReference projectReference)
            {
                var state = ReadState();
 
                if (ProjectState.LanguageServices == fromProject.LanguageServices)
                {
                    // if we have a compilation and its the correct language, use a simple compilation reference in any
                    // state it happens to be in right now
                    if (state.CompilationWithoutGeneratedDocuments is { } compilation)
                        return compilation.ToMetadataReference(projectReference.Aliases, projectReference.EmbedInteropTypes);
                }
                else
                {
                    // Cross project reference.  We need a skeleton reference.  Skeletons are too expensive to
                    // generate on demand.  So just try to see if we can grab the last generated skeleton for that
                    // project.
                    var properties = new MetadataReferenceProperties(aliases: projectReference.Aliases, embedInteropTypes: projectReference.EmbedInteropTypes);
                    return this.SkeletonReferenceCache.TryGetAlreadyBuiltMetadataReference(properties);
                }
 
                return null;
            }
 
            public Task<bool> HasSuccessfullyLoadedAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                var state = this.ReadState();
 
                if (state.HasSuccessfullyLoaded.HasValue)
                {
                    return state.HasSuccessfullyLoaded.Value ? SpecializedTasks.True : SpecializedTasks.False;
                }
                else
                {
                    return HasSuccessfullyLoadedSlowAsync(solution, cancellationToken);
                }
            }
 
            private async Task<bool> HasSuccessfullyLoadedSlowAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                var compilationInfo = await GetOrBuildCompilationInfoAsync(solution, lockGate: true, cancellationToken: cancellationToken).ConfigureAwait(false);
                return compilationInfo.HasSuccessfullyLoaded;
            }
 
            public async ValueTask<TextDocumentStates<SourceGeneratedDocumentState>> GetSourceGeneratedDocumentStatesAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                // If we don't have any generators, then we know we have no generated files, so we can skip the computation entirely.
                if (!this.ProjectState.SourceGenerators.Any())
                {
                    return TextDocumentStates<SourceGeneratedDocumentState>.Empty;
                }
 
                var compilationInfo = await GetOrBuildCompilationInfoAsync(solution, lockGate: true, cancellationToken: cancellationToken).ConfigureAwait(false);
                return compilationInfo.GeneratorInfo.Documents;
            }
 
            public async ValueTask<ImmutableArray<Diagnostic>> GetSourceGeneratorDiagnosticsAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                if (!this.ProjectState.SourceGenerators.Any())
                {
                    return ImmutableArray<Diagnostic>.Empty;
                }
 
                var compilationInfo = await GetOrBuildCompilationInfoAsync(solution, lockGate: true, cancellationToken: cancellationToken).ConfigureAwait(false);
 
                var driverRunResult = compilationInfo.GeneratorInfo.Driver?.GetRunResult();
                if (driverRunResult is null)
                {
                    return ImmutableArray<Diagnostic>.Empty;
                }
 
                using var _ = ArrayBuilder<Diagnostic>.GetInstance(capacity: driverRunResult.Diagnostics.Length, out var builder);
 
                foreach (var result in driverRunResult.Results)
                {
                    if (!IsGeneratorRunResultToIgnore(result))
                    {
                        builder.AddRange(result.Diagnostics);
                    }
                }
 
                return builder.ToImmutableAndClear();
            }
 
            public SourceGeneratedDocumentState? TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(DocumentId documentId)
            {
                var state = ReadState();
 
                // If we are in FinalState, then we have correctly ran generators and then know the final contents of the
                // Compilation. The GeneratedDocuments can be filled for intermediate states, but those aren't guaranteed to be
                // correct and can be re-ran later.
                return state is FinalState finalState ? finalState.GeneratorInfo.Documents.GetState(documentId) : null;
            }
 
            // HACK HACK HACK HACK around a problem introduced by https://github.com/dotnet/sdk/pull/24928. The Razor generator is
            // controlled by a flag that lives in an .editorconfig file; in the IDE we generally don't run the generator and instead use
            // the design-time files added through the legacy IDynamicFileInfo API. When we're doing Hot Reload we then
            // remove those legacy files and remove the .editorconfig file that is supposed to disable the generator, for the Hot
            // Reload pass we then are running the generator. This is done in the CompileTimeSolutionProvider.
            //
            // https://github.com/dotnet/sdk/pull/24928 introduced an issue where even though the Razor generator is being told to not
            // run, it still runs anyways. As a tactical fix rather than reverting that PR, for Visual Studio 17.3 Preview 2 we are going
            // to do a hack here which is to rip out generated files.
 
            private bool IsGeneratorRunResultToIgnore(GeneratorRunResult result)
            {
                var globalOptions = this.ProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions;
 
                // This matches the implementation in https://github.com/chsienki/sdk/blob/4696442a24e3972417fb9f81f182420df0add107/src/RazorSdk/SourceGenerators/RazorSourceGenerator.RazorProviders.cs#L27-L28
                var suppressGenerator = globalOptions.TryGetValue("build_property.SuppressRazorSourceGenerator", out var option) && option == "true";
 
                if (!suppressGenerator)
                    return false;
 
                var generatorType = result.Generator.GetGeneratorType();
                return generatorType.FullName == "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator" &&
                       generatorType.Assembly.GetName().Name == "Microsoft.NET.Sdk.Razor.SourceGenerators";
            }
 
            // END HACK HACK HACK HACK, or the setup of it at least; once this hack is removed the calls to IsGeneratorRunResultToIgnore
            // need to be cleaned up.
 
            #region Versions and Checksums
 
            // Dependent Versions are stored on compilation tracker so they are more likely to survive when unrelated solution branching occurs.
 
            private AsyncLazy<VersionStamp>? _lazyDependentVersion;
            private AsyncLazy<VersionStamp>? _lazyDependentSemanticVersion;
            private AsyncLazy<Checksum>? _lazyDependentChecksum;
 
            public Task<VersionStamp> GetDependentVersionAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                if (_lazyDependentVersion == null)
                {
                    var tmp = solution; // temp. local to avoid a closure allocation for the fast path
                    // note: solution is captured here, but it will go away once GetValueAsync executes.
                    Interlocked.CompareExchange(ref _lazyDependentVersion, new AsyncLazy<VersionStamp>(c => ComputeDependentVersionAsync(tmp, c), cacheResult: true), null);
                }
 
                return _lazyDependentVersion.GetValueAsync(cancellationToken);
            }
 
            private async Task<VersionStamp> ComputeDependentVersionAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                var projectState = this.ProjectState;
                var projVersion = projectState.Version;
                var docVersion = await projectState.GetLatestDocumentVersionAsync(cancellationToken).ConfigureAwait(false);
 
                var version = docVersion.GetNewerVersion(projVersion);
                foreach (var dependentProjectReference in projectState.ProjectReferences)
                {
                    cancellationToken.ThrowIfCancellationRequested();
 
                    if (solution.ContainsProject(dependentProjectReference.ProjectId))
                    {
                        var dependentProjectVersion = await solution.GetDependentVersionAsync(dependentProjectReference.ProjectId, cancellationToken).ConfigureAwait(false);
                        version = dependentProjectVersion.GetNewerVersion(version);
                    }
                }
 
                return version;
            }
 
            public Task<VersionStamp> GetDependentSemanticVersionAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                if (_lazyDependentSemanticVersion == null)
                {
                    var tmp = solution; // temp. local to avoid a closure allocation for the fast path
                    // note: solution is captured here, but it will go away once GetValueAsync executes.
                    Interlocked.CompareExchange(ref _lazyDependentSemanticVersion, new AsyncLazy<VersionStamp>(c => ComputeDependentSemanticVersionAsync(tmp, c), cacheResult: true), null);
                }
 
                return _lazyDependentSemanticVersion.GetValueAsync(cancellationToken);
            }
 
            private async Task<VersionStamp> ComputeDependentSemanticVersionAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                var projectState = this.ProjectState;
                var version = await projectState.GetSemanticVersionAsync(cancellationToken).ConfigureAwait(false);
 
                foreach (var dependentProjectReference in projectState.ProjectReferences)
                {
                    cancellationToken.ThrowIfCancellationRequested();
 
                    if (solution.ContainsProject(dependentProjectReference.ProjectId))
                    {
                        var dependentProjectVersion = await solution.GetDependentSemanticVersionAsync(dependentProjectReference.ProjectId, cancellationToken).ConfigureAwait(false);
                        version = dependentProjectVersion.GetNewerVersion(version);
                    }
                }
 
                return version;
            }
 
            public Task<Checksum> GetDependentChecksumAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                if (_lazyDependentChecksum == null)
                {
                    var tmp = solution; // temp. local to avoid a closure allocation for the fast path
                    // note: solution is captured here, but it will go away once GetValueAsync executes.
                    Interlocked.CompareExchange(ref _lazyDependentChecksum, new AsyncLazy<Checksum>(c => ComputeDependentChecksumAsync(tmp, c), cacheResult: true), null);
                }
 
                return _lazyDependentChecksum.GetValueAsync(cancellationToken);
            }
 
            private async Task<Checksum> ComputeDependentChecksumAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                using var tempChecksumArray = TemporaryArray<Checksum>.Empty;
 
                // Get the checksum for the project itself.
                var projectChecksum = await this.ProjectState.GetChecksumAsync(cancellationToken).ConfigureAwait(false);
                tempChecksumArray.Add(projectChecksum);
 
                // Calculate a checksum this project and for each dependent project that could affect semantics for
                // this project. Ensure that the checksum calculation orders the projects consistently so that we get
                // the same checksum across sessions of VS.  Note: we use the project filepath+name as a unique way
                // to reference a project.  This matches the logic in our persistence-service implemention as to how
                // information is associated with a project.
                var transitiveDependencies = solution.GetProjectDependencyGraph().GetProjectsThatThisProjectTransitivelyDependsOn(this.ProjectState.Id);
                var orderedProjectIds = transitiveDependencies.OrderBy(id =>
                {
                    var depProject = solution.GetRequiredProjectState(id);
                    return (depProject.FilePath, depProject.Name);
                });
 
                foreach (var projectId in orderedProjectIds)
                {
                    var referencedProject = solution.GetRequiredProjectState(projectId);
 
                    // Note that these checksums should only actually be calculated once, if the project is unchanged
                    // the same checksum will be returned.
                    var referencedProjectChecksum = await referencedProject.GetChecksumAsync(cancellationToken).ConfigureAwait(false);
                    tempChecksumArray.Add(referencedProjectChecksum);
                }
 
                return Checksum.Create(tempChecksumArray.ToImmutableAndClear());
            }
 
            #endregion
        }
    }
}