|
// 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 partial class SolutionState
{
private partial class CompilationTracker
{
private readonly struct CompilationTrackerGeneratorInfo
{
/// <summary>
/// The best generated documents we have for the current state. <see cref="DocumentsAreFinal"/>
/// specifies whether the documents are to be considered final and can be reused, or whether they're from
/// a prior snapshot which needs to be recomputed.
/// </summary>
public readonly TextDocumentStates<SourceGeneratedDocumentState> Documents;
/// <summary>
/// The <see cref="GeneratorDriver"/> that was used for the last run, to allow for incremental reuse. May
/// be null if we don't have generators in the first place, haven't ran generators yet for this project,
/// or had to get rid of our driver for some reason.
/// </summary>
public readonly GeneratorDriver? Driver;
/// <summary>
/// Whether the generated documents in <see cref="Documents"/> are final and should not be regenerated.
/// It's important that once we've ran generators once we don't want to run them again. Once we've ran
/// them the first time, those syntax trees are visible from other parts of the Workspaces model; if we
/// run them a second time we'd end up with new trees which would confuse our snapshot model -- once the
/// tree has been handed out we can't make a second tree later.
/// </summary>
public readonly bool DocumentsAreFinal;
/// <summary>
/// Whether the generated documents 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>
/// </summary>
public readonly bool DocumentsAreFinalAndFrozen;
public CompilationTrackerGeneratorInfo(
TextDocumentStates<SourceGeneratedDocumentState> documents,
GeneratorDriver? driver,
bool documentsAreFinal,
bool documentsAreFinalAndFrozen = false)
{
Documents = documents;
Driver = driver;
DocumentsAreFinal = documentsAreFinal;
DocumentsAreFinalAndFrozen = documentsAreFinalAndFrozen;
// If we're frozen, that implies final as well
Contract.ThrowIfTrue(documentsAreFinalAndFrozen && !documentsAreFinal);
}
public CompilationTrackerGeneratorInfo WithDocumentsAreFinal(bool documentsAreFinal)
{
// If we're already frozen, then we won't do anything even if somebody calls WithDocumentsAreFinal(false);
// this for example would happen if we had a frozen snapshot, and then we fork it further with additional changes.
// In that case we would be calling WithDocumentsAreFinal(false) to force generators to run again, but if we've
// frozen in partial semantics, we're done running them period. So we'll just keep treating them as final,
// no matter the wishes of the caller.
if (DocumentsAreFinalAndFrozen || DocumentsAreFinal == documentsAreFinal)
return this;
else
return new(Documents, Driver, documentsAreFinal);
}
public CompilationTrackerGeneratorInfo WithDocumentsAreFinalAndFrozen()
{
return DocumentsAreFinalAndFrozen ? this : new(Documents, Driver, documentsAreFinal: true, documentsAreFinalAndFrozen: true);
}
public CompilationTrackerGeneratorInfo WithDriver(GeneratorDriver? driver)
=> Driver == driver ? this : new(Documents, driver, DocumentsAreFinal, DocumentsAreFinalAndFrozen);
}
/// <summary>
/// The base type of all <see cref="CompilationTracker"/> states. The state of a <see cref="CompilationTracker" />
/// starts at <see cref="Empty"/>, and then will progress through the other states until it finally reaches
/// <see cref="FinalState" />.
/// </summary>
private abstract class CompilationTrackerState
{
/// <summary>
/// The base <see cref="CompilationTrackerState"/> that starts with everything empty.
/// </summary>
public static readonly CompilationTrackerState Empty = new NoCompilationState(
new CompilationTrackerGeneratorInfo(
documents: TextDocumentStates<SourceGeneratedDocumentState>.Empty,
driver: null,
documentsAreFinal: false));
/// <summary>
/// The best compilation that is available that source generators have not ran on. May be an
/// in-progress, full declaration, a final compilation, or <see langword="null"/>.
/// </summary>
public Compilation? CompilationWithoutGeneratedDocuments { get; }
public CompilationTrackerGeneratorInfo GeneratorInfo { get; }
/// <summary>
/// Specifies whether <see cref="FinalCompilationWithGeneratedDocuments"/> and all compilations it depends on contain full information or not. This can return
/// <see langword="null"/> if the state isn't at the point where it would know, and it's necessary to transition to <see cref="FinalState"/> to figure that out.
/// </summary>
public virtual bool? HasSuccessfullyLoaded => null;
/// <summary>
/// The final compilation is potentially available, otherwise <see langword="null"/>.
/// </summary>
public virtual Compilation? FinalCompilationWithGeneratedDocuments => null;
protected CompilationTrackerState(
Compilation? compilationWithoutGeneratedDocuments,
CompilationTrackerGeneratorInfo generatorInfo)
{
CompilationWithoutGeneratedDocuments = compilationWithoutGeneratedDocuments;
GeneratorInfo = generatorInfo;
#if DEBUG
// As a sanity check, we should never see the generated trees inside of the compilation that should not
// have generated trees.
var compilation = compilationWithoutGeneratedDocuments;
if (compilation != null)
{
foreach (var generatedDocument in generatorInfo.Documents.States.Values)
{
Contract.ThrowIfTrue(compilation.SyntaxTrees.Contains(generatedDocument.GetSyntaxTree(CancellationToken.None)));
}
}
#endif
}
public static CompilationTrackerState Create(
Compilation compilation,
CompilationTrackerGeneratorInfo generatorInfo,
Compilation? compilationWithGeneratedDocuments,
ImmutableList<(ProjectState state, CompilationAndGeneratorDriverTranslationAction action)> intermediateProjects)
{
Contract.ThrowIfTrue(intermediateProjects is null);
// If we don't have any intermediate projects to process, just initialize our
// DeclarationState now. We'll pass false for generatedDocumentsAreFinal because this is being called
// if our referenced projects are changing, so we'll have to rerun to consume changes.
return intermediateProjects.IsEmpty
? new AllSyntaxTreesParsedState(compilation, generatorInfo.WithDocumentsAreFinal(false))
: new InProgressState(compilation, generatorInfo, compilationWithGeneratedDocuments, intermediateProjects);
}
}
/// <summary>
/// State used when we potentially have some information (like prior generated documents)
/// but no compilation.
/// </summary>
private sealed class NoCompilationState : CompilationTrackerState
{
public NoCompilationState(CompilationTrackerGeneratorInfo generatorInfo)
: base(compilationWithoutGeneratedDocuments: null, 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
{
/// <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<(ProjectState oldState, CompilationAndGeneratorDriverTranslationAction action)> IntermediateProjects { get; }
/// <summary>
/// The result of taking the original completed compilation that had generated documents and updating them by
/// apply the <see cref="CompilationAndGeneratorDriverTranslationAction" />; 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 Compilation? CompilationWithGeneratedDocuments { get; }
public InProgressState(
Compilation inProgressCompilation,
CompilationTrackerGeneratorInfo generatorInfo,
Compilation? compilationWithGeneratedDocuments,
ImmutableList<(ProjectState state, CompilationAndGeneratorDriverTranslationAction action)> intermediateProjects)
: base(compilationWithoutGeneratedDocuments: inProgressCompilation,
generatorInfo.WithDocumentsAreFinal(false)) // since we have a set of transformations to make, we'll always have to run generators again
{
Contract.ThrowIfTrue(intermediateProjects is null);
Contract.ThrowIfFalse(intermediateProjects.Count > 0);
this.IntermediateProjects = intermediateProjects;
this.CompilationWithGeneratedDocuments = compilationWithGeneratedDocuments;
}
}
/// <summary>
/// A built compilation for the tracker that contains the fully built DeclarationTable,
/// but may not have references initialized
/// </summary>
private sealed class AllSyntaxTreesParsedState : CompilationTrackerState
{
public AllSyntaxTreesParsedState(Compilation declarationCompilation, CompilationTrackerGeneratorInfo generatorInfo)
: base(declarationCompilation, generatorInfo)
{
}
}
/// <summary>
/// The final state a compilation tracker reaches. The real <see cref="CompilationTrackerState.FinalCompilationWithGeneratedDocuments"/> is available. It is a
/// requirement that any <see cref="Compilation"/> provided to any clients of the <see cref="Solution"/>
/// (for example, through <see cref="Project.GetCompilationAsync"/> or <see
/// cref="Project.TryGetCompilation"/> must be from a <see cref="FinalState"/>. This is because <see
/// cref="FinalState"/> stores extra information in it about that compilation that the <see
/// cref="Solution"/> 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 FinalState : CompilationTrackerState
{
public override bool? HasSuccessfullyLoaded { get; }
/// <summary>
/// Weak set of the assembly, module and dynamic symbols that this compilation tracker has created.
/// This can be used to determine which project an assembly symbol came from after the fact. This is
/// needed as the compilation an assembly came from can be GC'ed and further requests to get that
/// compilation (or any of it's assemblies) may produce new assembly symbols.
/// </summary>
public readonly UnrootedSymbolSet UnrootedSymbolSet;
/// <summary>
/// The final compilation, with all references and source generators run. This is distinct from <see
/// cref="Compilation"/>, which in the <see cref="FinalState"/> 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 override Compilation FinalCompilationWithGeneratedDocuments { get; }
private FinalState(
Compilation finalCompilation,
Compilation compilationWithoutGeneratedFiles,
bool hasSuccessfullyLoaded,
CompilationTrackerGeneratorInfo generatorInfo,
UnrootedSymbolSet unrootedSymbolSet)
: base(compilationWithoutGeneratedFiles,
generatorInfo.WithDocumentsAreFinal(true)) // when we're in a final state, we've ran generators and should not run again
{
Contract.ThrowIfNull(finalCompilation);
HasSuccessfullyLoaded = hasSuccessfullyLoaded;
FinalCompilationWithGeneratedDocuments = finalCompilation;
UnrootedSymbolSet = unrootedSymbolSet;
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(finalCompilation, compilationWithoutGeneratedFiles));
}
}
/// <param name="finalCompilation">Not held onto</param>
/// <param name="projectId">Not held onto</param>
/// <param name="metadataReferenceToProjectId">Not held onto</param>
public static FinalState Create(
Compilation finalCompilationSource,
Compilation compilationWithoutGeneratedFiles,
bool hasSuccessfullyLoaded,
CompilationTrackerGeneratorInfo generatorInfo,
Compilation finalCompilation,
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.
var unrootedSymbolSet = UnrootedSymbolSet.Create(finalCompilation);
RecordAssemblySymbols(projectId, finalCompilation, metadataReferenceToProjectId);
return new FinalState(
finalCompilationSource,
compilationWithoutGeneratedFiles,
hasSuccessfullyLoaded,
generatorInfo,
unrootedSymbolSet);
}
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);
}
}
}
}
}
}
|