diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionServiceTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionServiceTests.cs
index e684569debd6f..a9cb2c69b273d 100644
--- a/src/EditorFeatures/CSharpTest/Completion/CompletionServiceTests.cs
+++ b/src/EditorFeatures/CSharpTest/Completion/CompletionServiceTests.cs
@@ -52,7 +52,7 @@ public class C1
var generatorRanCount = 0;
var generator = new CallbackGenerator(onInit: _ => { }, onExecute: _ => Interlocked.Increment(ref generatorRanCount));
- using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartalSemantics();
+ using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var analyzerReference = new TestGeneratorReference(generator);
var project = SolutionUtilities.AddEmptyProject(workspace.CurrentSolution)
.AddAnalyzerReference(analyzerReference)
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationPair.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationPair.cs
new file mode 100644
index 0000000000000..08d010013e74d
--- /dev/null
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationPair.cs
@@ -0,0 +1,68 @@
+// 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;
+
+namespace Microsoft.CodeAnalysis
+{
+ internal partial class SolutionState
+ {
+ private partial class CompilationTracker
+ {
+ ///
+ /// When we're working with compilations, we often have two: a compilation that does not contain generated files
+ /// (which we might need later to run generators again), and one that has the stale generated files that we might
+ /// be able to reuse as well. In those cases we have to do the same transformations to both, and this gives us
+ /// a handy way to do precisely that while not forking compilations twice if there are no generated files anywhere.
+ ///
+ internal readonly struct CompilationPair
+ {
+ public CompilationPair(Compilation withoutGeneratedDocuments, Compilation withGeneratedDocuments) : this()
+ {
+ CompilationWithoutGeneratedDocuments = withoutGeneratedDocuments;
+ CompilationWithGeneratedDocuments = withGeneratedDocuments;
+ }
+
+ public Compilation CompilationWithoutGeneratedDocuments { get; }
+ public Compilation CompilationWithGeneratedDocuments { get; }
+
+ public CompilationPair ReplaceSyntaxTree(SyntaxTree oldTree, SyntaxTree newTree)
+ {
+ return WithChange(static (compilation, trees) => compilation.ReplaceSyntaxTree(trees.oldTree, trees.newTree), (oldTree, newTree));
+ }
+
+ public CompilationPair AddSyntaxTree(SyntaxTree newTree)
+ {
+ return WithChange(static (compilation, t) => compilation.AddSyntaxTrees(t), newTree);
+ }
+
+ public CompilationPair WithPreviousScriptCompilation(Compilation previousScriptCompilation)
+ {
+ return WithChange(static (compilation, priorCompilation) => compilation.WithScriptCompilationInfo(compilation.ScriptCompilationInfo!.WithPreviousScriptCompilation(priorCompilation)), previousScriptCompilation);
+ }
+
+ public CompilationPair WithReferences(IReadOnlyCollection metadataReferences)
+ {
+ return WithChange(static (c, r) => c.WithReferences(r), metadataReferences);
+ }
+
+ private CompilationPair WithChange(Func change, TArg arg)
+ {
+ var changedWithoutGeneratedDocuments = change(CompilationWithoutGeneratedDocuments, arg);
+
+ if (CompilationWithoutGeneratedDocuments == CompilationWithGeneratedDocuments)
+ {
+ // If we didn't have any generated files, then no reason to transform twice
+ return new CompilationPair(changedWithoutGeneratedDocuments, changedWithoutGeneratedDocuments);
+ }
+
+ var changedWithGeneratedDocuments = change(CompilationWithGeneratedDocuments, arg);
+
+ return new CompilationPair(changedWithoutGeneratedDocuments, changedWithGeneratedDocuments);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.CompilationTrackerState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.CompilationTrackerState.cs
index ada903c8d65b5..1328557666fdf 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.CompilationTrackerState.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.CompilationTrackerState.cs
@@ -6,6 +6,8 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
+using System.Linq;
+using System.Threading;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis
@@ -39,21 +41,55 @@ private readonly struct CompilationTrackerGeneratorInfo
///
public readonly bool DocumentsAreFinal;
+ ///
+ /// 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:
+ ///
+ /// - 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.
+ /// - 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.
+ ///
+ ///
+ public readonly bool DocumentsAreFinalAndFrozen;
+
public CompilationTrackerGeneratorInfo(
TextDocumentStates documents,
GeneratorDriver? driver,
- bool documentsAreFinal)
+ 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)
- => DocumentsAreFinal == documentsAreFinal ? this : new(Documents, Driver, 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);
+ => Driver == driver ? this : new(Documents, driver, DocumentsAreFinal, DocumentsAreFinalAndFrozen);
}
///
@@ -101,6 +137,21 @@ protected CompilationTrackerState(
{
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?.GetValueOrNull();
+
+ 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(
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs
index 0dff6246cad41..6ffc3e21b43fb 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs
@@ -156,22 +156,23 @@ public ICompilationTracker FreezePartialStateWithTree(SolutionState solution, Do
GetPartialCompilationState(
solution, docState.Id,
out var inProgressProject,
- out var inProgressCompilation,
+ out var compilationPair,
out var generatorInfo,
out var metadataReferenceToProjectId,
cancellationToken);
- if (!inProgressCompilation.SyntaxTrees.Contains(tree))
+ // Ensure we actually have the tree we need in there
+ if (!compilationPair.CompilationWithoutGeneratedDocuments.SyntaxTrees.Contains(tree))
{
- var existingTree = inProgressCompilation.SyntaxTrees.FirstOrDefault(t => t.FilePath == tree.FilePath);
+ var existingTree = compilationPair.CompilationWithoutGeneratedDocuments.SyntaxTrees.FirstOrDefault(t => t.FilePath == tree.FilePath);
if (existingTree != null)
{
- inProgressCompilation = inProgressCompilation.ReplaceSyntaxTree(existingTree, tree);
+ compilationPair = compilationPair.ReplaceSyntaxTree(existingTree, tree);
inProgressProject = inProgressProject.UpdateDocument(docState, textChanged: false, recalculateDependentVersions: false);
}
else
{
- inProgressCompilation = inProgressCompilation.AddSyntaxTrees(tree);
+ compilationPair = compilationPair.AddSyntaxTree(tree);
Debug.Assert(!inProgressProject.DocumentStates.Contains(docState.Id));
inProgressProject = inProgressProject.AddDocuments(ImmutableArray.Create(docState));
}
@@ -181,12 +182,12 @@ public ICompilationTracker FreezePartialStateWithTree(SolutionState solution, Do
// 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(
- new ConstantValueSource>(inProgressCompilation),
- new ConstantValueSource>(inProgressCompilation),
- inProgressCompilation,
+ finalCompilationSource: new ConstantValueSource>(compilationPair.CompilationWithGeneratedDocuments),
+ compilationWithoutGeneratedFilesSource: new ConstantValueSource>(compilationPair.CompilationWithoutGeneratedDocuments),
+ compilationWithoutGeneratedFiles: compilationPair.CompilationWithoutGeneratedDocuments,
hasSuccessfullyLoaded: false,
generatorInfo,
- inProgressCompilation,
+ finalCompilation: compilationPair.CompilationWithGeneratedDocuments,
this.ProjectState.Id,
metadataReferenceToProjectId);
@@ -202,12 +203,11 @@ public ICompilationTracker FreezePartialStateWithTree(SolutionState solution, Do
/// The compilation state that is returned will have a compilation that is retained so
/// that it cannot disappear.
///
- /// The compilation to return. Contains any source generated documents that were available already added.
private void GetPartialCompilationState(
SolutionState solution,
DocumentId id,
out ProjectState inProgressProject,
- out Compilation inProgressCompilation,
+ out CompilationPair compilations,
out CompilationTrackerGeneratorInfo generatorInfo,
out Dictionary? metadataReferenceToProjectId,
CancellationToken cancellationToken)
@@ -218,7 +218,7 @@ private void GetPartialCompilationState(
// check whether we can bail out quickly for typing case
var inProgressState = state as InProgressState;
- generatorInfo = state.GeneratorInfo;
+ generatorInfo = state.GeneratorInfo.WithDocumentsAreFinalAndFrozen();
// all changes left for this document is modifying the given document.
// we can use current state as it is since we will replace the document with latest document anyway.
@@ -230,7 +230,9 @@ private void GetPartialCompilationState(
// 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.
- inProgressCompilation = compilationWithoutGeneratedDocuments.AddSyntaxTrees(generatorInfo.Documents.States.Values.Select(state => state.GetSyntaxTree(cancellationToken)));
+ 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.
@@ -248,7 +250,7 @@ private void GetPartialCompilationState(
if (finalCompilation != null)
{
- inProgressCompilation = finalCompilation;
+ 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
@@ -266,14 +268,12 @@ private void GetPartialCompilationState(
if (compilationWithoutGeneratedDocuments == null)
{
inProgressProject = inProgressProject.RemoveAllDocuments();
- inProgressCompilation = CreateEmptyCompilation();
- }
- else
- {
- inProgressCompilation = compilationWithoutGeneratedDocuments;
+ compilationWithoutGeneratedDocuments = CreateEmptyCompilation();
}
- inProgressCompilation = inProgressCompilation.AddSyntaxTrees(generatorInfo.Documents.States.Values.Select(state => state.GetSyntaxTree(cancellationToken)));
+ 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
@@ -297,7 +297,7 @@ private void GetPartialCompilationState(
// previous submission project must support compilation:
RoslynDebug.Assert(previousScriptCompilation != null);
- inProgressCompilation = inProgressCompilation.WithScriptCompilationInfo(inProgressCompilation.ScriptCompilationInfo!.WithPreviousScriptCompilation(previousScriptCompilation));
+ compilations = compilations.WithPreviousScriptCompilation(previousScriptCompilation);
}
else
{
@@ -307,7 +307,7 @@ private void GetPartialCompilationState(
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 = inProgressCompilation;
+ var inProgressCompilationNotRef = compilations.CompilationWithGeneratedDocuments;
metadata = inProgressCompilationNotRef.ExternalReferences.FirstOrDefault(
r => solution.GetProjectState(inProgressCompilationNotRef.GetAssemblyOrModuleSymbol(r) as IAssemblySymbol)?.Id == projectReference.ProjectId);
}
@@ -324,9 +324,9 @@ private void GetPartialCompilationState(
inProgressProject = inProgressProject.WithProjectReferences(newProjectReferences);
- if (!Enumerable.SequenceEqual(inProgressCompilation.ExternalReferences, metadataReferences))
+ if (!Enumerable.SequenceEqual(compilations.CompilationWithoutGeneratedDocuments.ExternalReferences, metadataReferences))
{
- inProgressCompilation = inProgressCompilation.WithReferences(metadataReferences);
+ compilations = compilations.WithReferences(metadataReferences);
}
SolutionLogger.CreatePartialProjectState();
@@ -485,21 +485,12 @@ private async Task BuildCompilationInfoAsync(
compilation = state.CompilationWithoutGeneratedDocuments?.GetValueOrNull(cancellationToken);
- // If we have already reached FinalState in the past but the compilation was garbage collected, we still have the generated documents
- // so we can pass those to FinalizeCompilationAsync to avoid the recomputation. This is necessary for correctness as otherwise
- // we'd be reparsing trees which could result in generated documents changing identity.
- var authoritativeGeneratedDocuments = state.GeneratorInfo.DocumentsAreFinal ? state.GeneratorInfo.Documents : (TextDocumentStates?)null;
- var nonAuthoritativeGeneratedDocuments = state.GeneratorInfo.Documents;
- var generatorDriver = state.GeneratorInfo.Driver;
-
if (compilation == null)
{
// We've got nothing. Build it from scratch :(
return await BuildCompilationInfoFromScratchAsync(
solution,
- authoritativeGeneratedDocuments,
- nonAuthoritativeGeneratedDocuments,
- generatorDriver,
+ state.GeneratorInfo,
cancellationToken).ConfigureAwait(false);
}
@@ -509,10 +500,8 @@ private async Task BuildCompilationInfoAsync(
return await FinalizeCompilationAsync(
solution,
compilation,
- authoritativeGeneratedDocuments,
- nonAuthoritativeGeneratedDocuments,
+ state.GeneratorInfo,
compilationWithStaleGeneratedTrees: null,
- generatorDriver,
cancellationToken).ConfigureAwait(false);
}
else
@@ -525,28 +514,21 @@ private async Task BuildCompilationInfoAsync(
private async Task BuildCompilationInfoFromScratchAsync(
SolutionState solution,
- TextDocumentStates? authoritativeGeneratedDocuments,
- TextDocumentStates nonAuthoritativeGeneratedDocuments,
- GeneratorDriver? generatorDriver,
+ CompilationTrackerGeneratorInfo generatorInfo,
CancellationToken cancellationToken)
{
try
{
var compilation = await BuildDeclarationCompilationFromScratchAsync(
solution.Services,
- new CompilationTrackerGeneratorInfo(
- nonAuthoritativeGeneratedDocuments,
- generatorDriver,
- documentsAreFinal: false),
+ generatorInfo,
cancellationToken).ConfigureAwait(false);
return await FinalizeCompilationAsync(
solution,
compilation,
- authoritativeGeneratedDocuments,
- nonAuthoritativeGeneratedDocuments,
+ generatorInfo,
compilationWithStaleGeneratedTrees: null,
- generatorDriver,
cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
@@ -613,10 +595,8 @@ private async Task BuildFinalStateFromInProgressStateAsync(
return await FinalizeCompilationAsync(
solution,
compilationWithoutGenerators,
- authoritativeGeneratedDocuments: null,
- nonAuthoritativeGeneratedDocuments: state.GeneratorInfo.Documents,
+ state.GeneratorInfo.WithDriver(generatorDriver),
compilationWithGenerators,
- generatorDriver,
cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
@@ -707,26 +687,18 @@ public CompilationInfo(Compilation compilation, bool hasSuccessfullyLoaded, Text
/// Add all appropriate references to the compilation and set it as our final compilation
/// state.
///
- /// The generated documents that can be used since they are already
- /// known to be correct for the given state. This would be non-null in cases where we had computed everything and
- /// ran generators, but then the compilation was garbage collected and are re-creating a compilation but we
- /// still had the prior generated result available.
- /// The generated documents from a previous pass which may
- /// or may not be correct for the current compilation. These states may be used to access cached results, if
- /// and when applicable for the current compilation.
+ /// 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.
/// The compilation from a prior run that contains generated trees, which
- /// match the states included in . If a generator run here produces
- /// the same set of generated documents as are in , and we don't need to make any other
+ /// match the states included in . If a generator run here produces
+ /// the same set of generated documents as are in , 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
/// .
- /// The generator driver that can be reused for this finalization.
private async Task FinalizeCompilationAsync(
SolutionState solution,
Compilation compilationWithoutGenerators,
- TextDocumentStates? authoritativeGeneratedDocuments,
- TextDocumentStates nonAuthoritativeGeneratedDocuments,
+ CompilationTrackerGeneratorInfo generatorInfo,
Compilation? compilationWithStaleGeneratedTrees,
- GeneratorDriver? generatorDriver,
CancellationToken cancellationToken)
{
try
@@ -798,16 +770,16 @@ private async Task FinalizeCompilationAsync(
}
// We will finalize the compilation by adding full contents here.
- // TODO: allow finalize compilation to incrementally update a prior version
- // https://github.com/dotnet/roslyn/issues/46418
Compilation compilationWithGenerators;
- TextDocumentStates generatedDocuments;
- if (authoritativeGeneratedDocuments.HasValue)
+ if (generatorInfo.DocumentsAreFinal)
{
- generatedDocuments = authoritativeGeneratedDocuments.Value;
+ // 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 generatedDocuments.States.Values.SelectAsArrayAsync(state => state.GetSyntaxTreeAsync(cancellationToken)).ConfigureAwait(false));
+ await generatorInfo.Documents.States.Values.SelectAsArrayAsync(state => state.GetSyntaxTreeAsync(cancellationToken)).ConfigureAwait(false));
}
else
{
@@ -816,20 +788,20 @@ private async Task FinalizeCompilationAsync(
if (ProjectState.SourceGenerators.Any())
{
// If we don't already have a generator driver, we'll have to create one from scratch
- if (generatorDriver == null)
+ if (generatorInfo.Driver == null)
{
var additionalTexts = this.ProjectState.AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText);
var compilationFactory = this.ProjectState.LanguageServices.GetRequiredService();
- generatorDriver = compilationFactory.CreateGeneratorDriver(
+ generatorInfo = generatorInfo.WithDriver(compilationFactory.CreateGeneratorDriver(
this.ProjectState.ParseOptions!,
ProjectState.SourceGenerators,
this.ProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider,
- additionalTexts);
+ additionalTexts));
}
- generatorDriver = generatorDriver.RunGenerators(compilationWithoutGenerators, cancellationToken);
- var runResult = generatorDriver.GetRunResult();
+ generatorInfo = generatorInfo.WithDriver(generatorInfo.Driver!.RunGenerators(compilationWithoutGenerators, cancellationToken));
+ 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
@@ -838,7 +810,7 @@ private async Task FinalizeCompilationAsync(
// and the prior generated trees are identical.
if (compilationWithStaleGeneratedTrees != null)
{
- if (nonAuthoritativeGeneratedDocuments.Count != runResult.Results.Sum(r => r.GeneratedSources.Length))
+ if (generatorInfo.Documents.Count != runResult.Results.Sum(r => r.GeneratedSources.Length))
{
compilationWithStaleGeneratedTrees = null;
}
@@ -849,7 +821,7 @@ private async Task FinalizeCompilationAsync(
foreach (var generatedSource in generatorResult.GeneratedSources)
{
var existing = FindExistingGeneratedDocumentState(
- nonAuthoritativeGeneratedDocuments,
+ generatorInfo.Documents,
generatorResult.Generator,
generatedSource.HintName);
@@ -894,14 +866,16 @@ private async Task FinalizeCompilationAsync(
// If we didn't null out this compilation, it means we can actually use it
if (compilationWithStaleGeneratedTrees != null)
{
- generatedDocuments = nonAuthoritativeGeneratedDocuments;
compilationWithGenerators = compilationWithStaleGeneratedTrees;
+ generatorInfo = generatorInfo.WithDocumentsAreFinal(true);
}
else
{
- generatedDocuments = new TextDocumentStates(generatedDocumentsBuilder.ToImmutableAndClear());
+ // We produced new documents, so time to create new state for it
+ var generatedDocuments = new TextDocumentStates(generatedDocumentsBuilder.ToImmutableAndClear());
compilationWithGenerators = compilationWithoutGenerators.AddSyntaxTrees(
await generatedDocuments.States.Values.SelectAsArrayAsync(state => state.GetSyntaxTreeAsync(cancellationToken)).ConfigureAwait(false));
+ generatorInfo = new CompilationTrackerGeneratorInfo(generatedDocuments, generatorInfo.Driver, documentsAreFinal: true);
}
}
@@ -910,17 +884,14 @@ private async Task FinalizeCompilationAsync(
CompilationTrackerState.CreateValueSource(compilationWithoutGenerators, solution.Services),
compilationWithoutGenerators,
hasSuccessfullyLoaded,
- new CompilationTrackerGeneratorInfo(
- generatedDocuments,
- generatorDriver,
- documentsAreFinal: true),
+ generatorInfo,
compilationWithGenerators,
this.ProjectState.Id,
metadataReferenceToProjectId);
this.WriteState(finalState, solution.Services);
- return new CompilationInfo(compilationWithGenerators, hasSuccessfullyLoaded, generatedDocuments);
+ return new CompilationInfo(compilationWithGenerators, hasSuccessfullyLoaded, generatorInfo.Documents);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs
index 90b6a860e2409..77921bd37bce3 100644
--- a/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs
+++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs
@@ -32,6 +32,9 @@ public static Workspace CreateWorkspaceWithRecoverableSyntaxTreesAndWeakCompilat
return workspace;
}
+ public static Workspace CreateWorkspaceWithPartialSemanticsAndWeakCompilations()
+ => WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics(new[] { typeof(TestProjectCacheService), typeof(TestTemporaryStorageService) });
+
#nullable disable
public static void TestProperty(T instance, Func factory, Func getter, TValue validNonDefaultValue, bool defaultThrows = false)
diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs
index 0c8c804d1eb10..8c33312289dd9 100644
--- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs
+++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs
@@ -2569,7 +2569,7 @@ public void TestProjectWithBrokenCrossLanguageReferenceHasIncompleteReferences()
[Fact]
public async Task TestFrozenPartialProjectHasDifferentSemanticVersions()
{
- using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartalSemantics();
+ using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("CSharpProject", "CSharpProject", LanguageNames.CSharp);
project = project.AddDocument("Extra.cs", SourceText.From("class Extra { }")).Project;
diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs
index e0bb0b2d87059..e09fb01fd5e1c 100644
--- a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs
+++ b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs
@@ -2,6 +2,7 @@
// 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.Immutable;
using System.Linq;
using System.Text;
@@ -168,7 +169,7 @@ static async Task AssertCompilationContainsGeneratedFile(Project project, string
[Fact]
public async Task PartialCompilationsIncludeGeneratedFilesAfterFullGeneration()
{
- using var workspace = CreateWorkspaceWithPartalSemantics();
+ using var workspace = CreateWorkspaceWithPartialSemantics();
var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
@@ -267,7 +268,9 @@ public async Task CompilationsInCompilationReferencesIncludeGeneratedSourceFiles
public async Task RequestingGeneratedDocumentsTwiceGivesSameInstance()
{
using var workspace = CreateWorkspaceWithRecoverableSyntaxTreesAndWeakCompilations();
- var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
+
+ var generatorRan = false;
+ var analyzerReference = new TestGeneratorReference(new CallbackGenerator(_ => { }, onExecute: _ => { generatorRan = true; }, source: "// Hello World!"));
var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
@@ -276,7 +279,8 @@ public async Task RequestingGeneratedDocumentsTwiceGivesSameInstance()
var tree = await generatedDocumentFirstTime.GetSyntaxTreeAsync();
// Fetch the compilation, and then wait for it to be GC'ed, then fetch it again. This ensures that
- // finalizing a compilation more than once doesn't recreate things incorrectly.
+ // finalizing a compilation more than once doesn't recreate things incorrectly or run the generator more than once.
+ generatorRan = false;
var compilationReference = ObjectReference.CreateFromFactory(() => project.GetCompilationAsync().Result);
compilationReference.AssertReleased();
var secondCompilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
@@ -285,6 +289,7 @@ public async Task RequestingGeneratedDocumentsTwiceGivesSameInstance()
Assert.Same(generatedDocumentFirstTime, generatedDocumentSecondTime);
Assert.Same(tree, secondCompilation.SyntaxTrees.Single());
+ Assert.False(generatorRan);
}
[Fact]
@@ -304,7 +309,7 @@ public async Task GetDocumentWithGeneratedTreeReturnsGeneratedDocument()
[Fact]
public async Task GetDocumentWithGeneratedTreeForInProgressReturnsGeneratedDocument()
{
- using var workspace = CreateWorkspaceWithPartalSemantics();
+ using var workspace = CreateWorkspaceWithPartialSemantics();
var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
@@ -566,7 +571,7 @@ public async Task FreezingSolutionEnsuresGeneratorsDoNotRun(bool forkBeforeFreez
var generatorRan = false;
var generator = new CallbackGenerator(onInit: _ => { }, onExecute: _ => { generatorRan = true; });
- using var workspace = CreateWorkspaceWithPartalSemantics();
+ using var workspace = CreateWorkspaceWithPartialSemantics();
var analyzerReference = new TestGeneratorReference(generator);
var project = AddEmptyProject(workspace.CurrentSolution)
.AddAnalyzerReference(analyzerReference)
@@ -592,5 +597,65 @@ public async Task FreezingSolutionEnsuresGeneratorsDoNotRun(bool forkBeforeFreez
Assert.False(generatorRan);
}
+
+ [Fact]
+ [WorkItem(56702, "https://github.com/dotnet/roslyn/issues/56702")]
+ public async Task ForkAfterFreezeNoLongerRunsGenerators()
+ {
+ using var workspace = CreateWorkspaceWithPartialSemantics();
+ var generatorRan = false;
+ var analyzerReference = new TestGeneratorReference(new CallbackGenerator(_ => { }, onExecute: _ => { generatorRan = true; }, source: "// Hello World!"));
+ var project = AddEmptyProject(workspace.CurrentSolution)
+ .AddAnalyzerReference(analyzerReference)
+ .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project;
+
+ // Ensure generators are ran
+ var objectReference = await project.GetCompilationAsync();
+
+ Assert.True(generatorRan);
+ generatorRan = false;
+
+ var document = project.Documents.Single().WithFrozenPartialSemantics(CancellationToken.None);
+
+ // And fork with new contents; we'll ensure the contents of this tree are different, but the generator will still not be ran
+ document = document.WithText(SourceText.From("// Something else"));
+
+ var compilation = await document.Project.GetRequiredCompilationAsync(CancellationToken.None);
+ Assert.Equal(2, compilation.SyntaxTrees.Count());
+ Assert.False(generatorRan);
+
+ Assert.Equal("// Something else", (await document.GetRequiredSyntaxRootAsync(CancellationToken.None)).ToFullString());
+ }
+
+ [Fact]
+ [WorkItem(56702, "https://github.com/dotnet/roslyn/issues/56702")]
+ public async Task ForkAfterFreezeNoLongerRunsGeneratorsEvenIfCompilationFallsAwayBeforeFreeze()
+ {
+ using var workspace = CreateWorkspaceWithPartialSemanticsAndWeakCompilations();
+ var generatorRan = false;
+ var analyzerReference = new TestGeneratorReference(new CallbackGenerator(_ => { }, onExecute: _ => { generatorRan = true; }, source: "// Hello World!"));
+ var project = AddEmptyProject(workspace.CurrentSolution)
+ .AddAnalyzerReference(analyzerReference)
+ .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project;
+
+ // Ensure generators are ran
+ var compilationReference = ObjectReference.CreateFromFactory(() => project.GetCompilationAsync().Result);
+
+ Assert.True(generatorRan);
+ generatorRan = false;
+
+ compilationReference.AssertReleased();
+
+ var document = project.Documents.Single().WithFrozenPartialSemantics(CancellationToken.None);
+
+ // And fork with new contents; we'll ensure the contents of this tree are different, but the generator will still not be ran
+ document = document.WithText(SourceText.From("// Something else"));
+
+ var compilation = await document.Project.GetRequiredCompilationAsync(CancellationToken.None);
+ Assert.Equal(2, compilation.SyntaxTrees.Count());
+ Assert.False(generatorRan);
+
+ Assert.Equal("// Something else", (await document.GetRequiredSyntaxRootAsync(CancellationToken.None)).ToFullString());
+ }
}
}
diff --git a/src/Workspaces/CoreTestUtilities/WorkspaceTestUtilities.cs b/src/Workspaces/CoreTestUtilities/WorkspaceTestUtilities.cs
index d23f771b3148d..1a7dd0718af72 100644
--- a/src/Workspaces/CoreTestUtilities/WorkspaceTestUtilities.cs
+++ b/src/Workspaces/CoreTestUtilities/WorkspaceTestUtilities.cs
@@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.UnitTests
{
public static class WorkspaceTestUtilities
{
- public static Workspace CreateWorkspaceWithPartalSemantics(Type[]? additionalParts = null)
+ public static Workspace CreateWorkspaceWithPartialSemantics(Type[]? additionalParts = null)
=> new WorkspaceWithPartialSemantics(FeaturesTestCompositions.Features.AddParts(additionalParts).GetHostServices());
private class WorkspaceWithPartialSemantics : Workspace