Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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<MetadataReference> metadataReferences)
{
return WithChange(static (c, r) => c.WithReferences(r), metadataReferences);
}

private CompilationPair WithChange<TArg>(Func<Compilation, TArg, Compilation> 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);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,21 +41,55 @@ private readonly struct CompilationTrackerGeneratorInfo
/// </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 documentsAreFinal,
bool documentsAreFinalAndFrozen = false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would prefer this not be optional. callers should have to think about this.

Copy link
Member

@genlu genlu Oct 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider using a enum here excessive? It seems to me that having something like enum GeneratedDocumentsState with 3 members NotFinal, Final and FinalAndFrozen would make it clearer. i.e. you can only change the state in one direction.

Actually the one-direction comment is false.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bad idea, OK to defer just to limit churn here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

{
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);
}

/// <summary>
Expand Down Expand Up @@ -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(
Expand Down
Loading