File: Workspace\Solution\SolutionCompilationState.CompilationTracker.CompilationTrackerState.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
internal sealed partial class SolutionCompilationState
{
    private sealed partial class RegularCompilationTracker
    {
        /// <summary>
        /// The base type of all <see cref="RegularCompilationTracker"/> states. The state of a <see
        /// cref="RegularCompilationTracker" /> starts at null, and then will progress through the other states until it
        /// finally reaches <see cref="FinalCompilationTrackerState" />.
        /// </summary>
        private abstract class CompilationTrackerState
        {
            /// <summary>
            /// Whether the generated documents in <see cref="GeneratorInfo"/> are frozen and generators should
            /// never be ran again, ever, even if a document is later changed. This is used to ensure that when we
            /// produce a frozen solution for partial semantics, further downstream forking of that solution won't
            /// rerun generators. This is because of two reasons:
            /// <list type="number">
            /// <item>Generally once we've produced a frozen solution with partial semantics, we now want speed rather
            /// than accuracy; a generator running in a later path will still cause issues there.</item>
            /// <item>The frozen solution with partial semantics makes no guarantee that other syntax trees exist or
            /// whether we even have references -- it's pretty likely that running a generator might produce worse results
            /// than what we originally had.</item>
            /// </list>
            /// This also controls if we will generate skeleton references for cross-language P2P references when
            /// creating the compilation for a particular project.  When entirely frozen, we do not want to do this due
            /// to the enormous cost of emitting ref assemblies for cross language cases.
            /// </summary>
            public readonly CreationPolicy CreationPolicy;
 
            /// <summary>
            /// The best compilation that is available that source generators have not ran on. May be an
            /// in-progress, full declaration, a final compilation.
            /// </summary>
            public abstract Compilation CompilationWithoutGeneratedDocuments { get; }
 
            public CompilationTrackerGeneratorInfo GeneratorInfo { get; }
 
            protected CompilationTrackerState(
                CreationPolicy creationPolicy,
                CompilationTrackerGeneratorInfo generatorInfo)
            {
                CreationPolicy = creationPolicy;
                GeneratorInfo = generatorInfo;
            }
        }
 
        /// <summary>
        /// A state where we are holding onto a previously built compilation, and have a known set of transformations
        /// that could get us to a more final state.
        /// </summary>
        private sealed class InProgressState : CompilationTrackerState
        {
            public readonly Lazy<Compilation> LazyCompilationWithoutGeneratedDocuments;
 
            /// <summary>
            /// The result of taking the original completed compilation that had generated documents and updating
            /// them by apply the <see cref="TranslationAction" />; this is not a
            /// correct snapshot in that the generators have not been rerun, but may be reusable if the generators
            /// are later found to give the same output.
            /// </summary>
            public readonly CancellableLazy<Compilation?> LazyStaleCompilationWithGeneratedDocuments;
 
            /// <summary>
            /// The list of changes that have happened since we last computed a compilation. The oldState corresponds to
            /// the state of the project prior to the mutation.
            /// </summary>
            public ImmutableList<TranslationAction> PendingTranslationActions { get; }
 
            public override Compilation CompilationWithoutGeneratedDocuments => LazyCompilationWithoutGeneratedDocuments.Value;
 
            public InProgressState(
                CreationPolicy creationPolicy,
                Lazy<Compilation> compilationWithoutGeneratedDocuments,
                CompilationTrackerGeneratorInfo generatorInfo,
                CancellableLazy<Compilation?> staleCompilationWithGeneratedDocuments,
                ImmutableList<TranslationAction> pendingTranslationActions)
                : base(creationPolicy, generatorInfo)
            {
                // Note: Intermediate projects can be empty.
                Contract.ThrowIfTrue(pendingTranslationActions is null);
 
                LazyCompilationWithoutGeneratedDocuments = compilationWithoutGeneratedDocuments;
                LazyStaleCompilationWithGeneratedDocuments = staleCompilationWithGeneratedDocuments;
                PendingTranslationActions = pendingTranslationActions;
 
#if DEBUG
                // As a sanity check, we should never see the generated trees inside of the compilation that should
                // not have generated trees.
                foreach (var generatedDocument in generatorInfo.Documents.States.Values)
                {
                    Contract.ThrowIfTrue(this.CompilationWithoutGeneratedDocuments.SyntaxTrees.Contains(generatedDocument.GetSyntaxTree(CancellationToken.None)));
                }
#endif
            }
 
            public InProgressState(
                CreationPolicy creationPolicy,
                Compilation compilationWithoutGeneratedDocuments,
                CompilationTrackerGeneratorInfo generatorInfo,
                Compilation? staleCompilationWithGeneratedDocuments,
                ImmutableList<TranslationAction> pendingTranslationActions)
                : this(
                      creationPolicy,
                      new Lazy<Compilation>(() => compilationWithoutGeneratedDocuments),
                      generatorInfo,
                      // Extracted as a method call to prevent captures.
                      staleCompilationWithGeneratedDocuments is null ? s_lazyNullCompilation : CreateLazyCompilation(staleCompilationWithGeneratedDocuments),
                      pendingTranslationActions)
            {
            }
 
            private static CancellableLazy<Compilation?> CreateLazyCompilation(Compilation? staleCompilationWithGeneratedDocuments)
                => new(staleCompilationWithGeneratedDocuments);
        }
 
        /// <summary>
        /// The final state a compilation tracker reaches. At this point <see
        /// cref="FinalCompilationWithGeneratedDocuments"/> is now available. It is a requirement that any <see
        /// cref="Compilation"/> provided to any clients of the <see cref="SolutionState"/> (for example, through
        /// <see cref="Project.GetCompilationAsync"/> or <see cref="Project.TryGetCompilation"/> must be from a <see
        /// cref="FinalCompilationTrackerState"/>.  This is because <see cref="FinalCompilationTrackerState"/>
        /// stores extra information in it about that compilation that the <see cref="SolutionState"/> can be
        /// queried for (for example: <see cref="Solution.GetOriginatingProject(ISymbol)"/>.  If <see
        /// cref="Compilation"/>s from other <see cref="CompilationTrackerState"/>s are passed out, then these other
        /// APIs will not function correctly.
        /// </summary>
        private sealed class FinalCompilationTrackerState : CompilationTrackerState
        {
            /// <summary>
            /// Specifies whether <see cref="FinalCompilationWithGeneratedDocuments"/> and all compilations it
            /// depends on contain full information or not.
            /// </summary>
            public readonly bool HasSuccessfullyLoaded;
 
            /// <summary>
            /// Used to determine which project an assembly symbol came from after the fact.
            /// </summary>
            private SingleInitNullable<RootedSymbolSet> _rootedSymbolSet;
 
            /// <summary>
            /// The final compilation, with all references and source generators run. This is distinct from <see
            /// cref="Compilation"/>, which in the <see cref="FinalCompilationTrackerState"/> case will be the
            /// compilation before any source generators were ran. This ensures that a later invocation of the
            /// source generators consumes <see cref="Compilation"/> which will avoid generators being ran a second
            /// time on a compilation that already contains the output of other generators. If source generators are
            /// not active, this is equal to <see cref="Compilation"/>.
            /// </summary>
            public readonly Compilation FinalCompilationWithGeneratedDocuments;
 
            /// <summary>
            /// Whether or not this final compilation state *just* generated documents which exactly correspond to the
            /// state of the compilation.  False if the generated documents came from a point in the past, and are being
            /// carried forward until the next time we run generators.
            /// </summary>
            public readonly bool GeneratedDocumentsUpToDate;
 
            public override Compilation CompilationWithoutGeneratedDocuments { get; }
 
            private FinalCompilationTrackerState(
                CreationPolicy creationPolicy,
                bool generatedDocumentsUpToDate,
                Compilation finalCompilationWithGeneratedDocuments,
                Compilation compilationWithoutGeneratedDocuments,
                bool hasSuccessfullyLoaded,
                CompilationTrackerGeneratorInfo generatorInfo)
                : base(creationPolicy, generatorInfo)
            {
                Contract.ThrowIfNull(finalCompilationWithGeneratedDocuments);
 
                this.GeneratedDocumentsUpToDate = generatedDocumentsUpToDate;
 
                // As a policy, all partial-state projects are said to have incomplete references, since the
                // state has no guarantees.
                this.CompilationWithoutGeneratedDocuments = compilationWithoutGeneratedDocuments;
                HasSuccessfullyLoaded = hasSuccessfullyLoaded;
                FinalCompilationWithGeneratedDocuments = finalCompilationWithGeneratedDocuments;
 
                if (this.GeneratorInfo.Documents.IsEmpty)
                {
                    // If we have no generated files, the pre-generator compilation and post-generator compilation
                    // should be the exact same instance; that way we're not creating more compilations than
                    // necessary that would be unable to share source symbols.
                    Debug.Assert(object.ReferenceEquals(finalCompilationWithGeneratedDocuments, compilationWithoutGeneratedDocuments));
                }
 
#if DEBUG
                // As a sanity check, we should never see the generated trees inside of the compilation that should
                // not have generated trees.
                foreach (var generatedDocument in generatorInfo.Documents.States.Values)
                {
                    Contract.ThrowIfTrue(compilationWithoutGeneratedDocuments.SyntaxTrees.Contains(generatedDocument.GetSyntaxTree(CancellationToken.None)));
                }
#endif
            }
 
            /// <param name="projectId">Not held onto</param>
            /// <param name="metadataReferenceToProjectId">Not held onto</param>
            public static FinalCompilationTrackerState Create(
                CreationPolicy creationPolicy,
                bool generatedDocumentsUpToDate,
                Compilation finalCompilationWithGeneratedDocuments,
                Compilation compilationWithoutGeneratedDocuments,
                bool hasSuccessfullyLoaded,
                CompilationTrackerGeneratorInfo generatorInfo,
                ProjectId projectId,
                Dictionary<MetadataReference, ProjectId>? metadataReferenceToProjectId)
            {
                // Keep track of information about symbols from this Compilation.  This will help support other APIs
                // the solution exposes that allows the user to map back from symbols to project information.
 
                RecordAssemblySymbols(projectId, finalCompilationWithGeneratedDocuments, metadataReferenceToProjectId);
 
                return new FinalCompilationTrackerState(
                    creationPolicy,
                    generatedDocumentsUpToDate,
                    finalCompilationWithGeneratedDocuments,
                    compilationWithoutGeneratedDocuments,
                    hasSuccessfullyLoaded,
                    generatorInfo);
            }
 
            public RootedSymbolSet RootedSymbolSet => _rootedSymbolSet.Initialize(
                static finalCompilationWithGeneratedDocuments => RootedSymbolSet.Create(finalCompilationWithGeneratedDocuments),
                this.FinalCompilationWithGeneratedDocuments);
 
            public FinalCompilationTrackerState WithCreationPolicy(CreationPolicy creationPolicy)
                => creationPolicy == this.CreationPolicy
                    ? this
                    : new(creationPolicy,
                        GeneratedDocumentsUpToDate,
                        FinalCompilationWithGeneratedDocuments,
                        CompilationWithoutGeneratedDocuments,
                        HasSuccessfullyLoaded,
                        GeneratorInfo);
 
            private static void RecordAssemblySymbols(ProjectId projectId, Compilation compilation, Dictionary<MetadataReference, ProjectId>? metadataReferenceToProjectId)
            {
                RecordSourceOfAssemblySymbol(compilation.Assembly, projectId);
 
                if (metadataReferenceToProjectId != null)
                {
                    foreach (var (metadataReference, currentID) in metadataReferenceToProjectId)
                    {
                        var symbol = compilation.GetAssemblyOrModuleSymbol(metadataReference);
                        RecordSourceOfAssemblySymbol(symbol, currentID);
                    }
                }
            }
 
            private static void RecordSourceOfAssemblySymbol(ISymbol? assemblyOrModuleSymbol, ProjectId projectId)
            {
                // TODO: how would we ever get a null here?
                if (assemblyOrModuleSymbol == null)
                {
                    return;
                }
 
                Contract.ThrowIfNull(projectId);
                // remember which project is associated with this assembly
                if (!s_assemblyOrModuleSymbolToProjectMap.TryGetValue(assemblyOrModuleSymbol, out var tmp))
                {
                    // use GetValue to avoid race condition exceptions from Add.
                    // the first one to set the value wins.
                    s_assemblyOrModuleSymbolToProjectMap.GetValue(assemblyOrModuleSymbol, _ => projectId);
                }
                else
                {
                    // sanity check: this should always be true, no matter how many times
                    // we attempt to record the association.
                    Debug.Assert(tmp == projectId);
                }
            }
        }
    }
}