diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb
index 81b813e0e5abe..b7a0aac19bbd2 100644
--- a/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb
+++ b/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb
@@ -19,7 +19,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer
Public Sub SourceGeneratorsListed()
Dim workspaceXml =
-
+
@@ -39,7 +39,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer
Public Async Function PlaceholderItemCreateIfGeneratorProducesNoFiles() As Task
Dim workspaceXml =
-
+
@@ -63,7 +63,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer
Public Async Function SingleSourceGeneratedFileProducesItem() As Task
Dim workspaceXml =
-
+
@@ -92,7 +92,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer
Public Async Function MultipleSourceGeneratedFilesProducesSortedItem() As Task
Dim workspaceXml =
-
+
@@ -124,7 +124,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer
Public Async Function ChangeToNoGeneratedDocumentsUpdatesListCorrectly() As Task
Dim workspaceXml =
-
+
@@ -153,7 +153,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer
Public Async Function AddingAGeneratedDocumentUpdatesListCorrectly() As Task
Dim workspaceXml =
-
+
diff --git a/src/Workspaces/CSharp/Portable/Workspace/LanguageServices/CSharpCompilationFactoryService.cs b/src/Workspaces/CSharp/Portable/Workspace/LanguageServices/CSharpCompilationFactoryService.cs
index 00f5e22c59bb6..13d91d5fff3c6 100644
--- a/src/Workspaces/CSharp/Portable/Workspace/LanguageServices/CSharpCompilationFactoryService.cs
+++ b/src/Workspaces/CSharp/Portable/Workspace/LanguageServices/CSharpCompilationFactoryService.cs
@@ -41,7 +41,7 @@ Compilation ICompilationFactoryService.CreateSubmissionCompilation(string assemb
CompilationOptions ICompilationFactoryService.GetDefaultCompilationOptions()
=> s_defaultOptions;
- GeneratorDriver? ICompilationFactoryService.CreateGeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider optionsProvider, ImmutableArray additionalTexts)
+ GeneratorDriver ICompilationFactoryService.CreateGeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider optionsProvider, ImmutableArray additionalTexts)
{
return CSharpGeneratorDriver.Create(generators, additionalTexts, (CSharpParseOptions)parseOptions, optionsProvider);
}
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/CompilationFactory/ICompilationFactoryService.cs b/src/Workspaces/Core/Portable/Workspace/Host/CompilationFactory/ICompilationFactoryService.cs
index 2140d43b72719..358cf9d40a6ae 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/CompilationFactory/ICompilationFactoryService.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/CompilationFactory/ICompilationFactoryService.cs
@@ -13,6 +13,6 @@ internal interface ICompilationFactoryService : ILanguageService
Compilation CreateCompilation(string assemblyName, CompilationOptions options);
Compilation CreateSubmissionCompilation(string assemblyName, CompilationOptions options, Type? hostObjectType);
CompilationOptions GetDefaultCompilationOptions();
- GeneratorDriver? CreateGeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider optionsProvider, ImmutableArray additionalTexts);
+ GeneratorDriver CreateGeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider optionsProvider, ImmutableArray additionalTexts);
}
}
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/AdditionalTextWithState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/AdditionalTextWithState.cs
index ab23b1b4a901c..58b6325577cfa 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/AdditionalTextWithState.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/AdditionalTextWithState.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CodeAnalysis.Text;
@@ -14,11 +15,18 @@ namespace Microsoft.CodeAnalysis.Diagnostics
internal sealed class AdditionalTextWithState : AdditionalText
{
private readonly TextDocumentState _documentState;
+ private static readonly ConditionalWeakTable _additionalTextsForDocumentStates = new();
+ private static readonly ConditionalWeakTable.CreateValueCallback s_createAdditionalText = static ts => new AdditionalTextWithState(ts);
+
+ public static AdditionalText FromState(TextDocumentState state)
+ {
+ return _additionalTextsForDocumentStates.GetValue(state, s_createAdditionalText);
+ }
///
/// Create a from a .
///
- public AdditionalTextWithState(TextDocumentState documentState)
+ private AdditionalTextWithState(TextDocumentState documentState)
=> _documentState = documentState ?? throw new ArgumentNullException(nameof(documentState));
///
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs
index 4c96f8797db96..3b521ffc50287 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs
@@ -245,7 +245,7 @@ internal DocumentState CreateDocument(DocumentInfo documentInfo, ParseOptions? p
public AnalyzerOptions AnalyzerOptions
=> _lazyAnalyzerOptions ??= new AnalyzerOptions(
- additionalFiles: AdditionalDocumentStates.SelectAsArray(static state => new AdditionalTextWithState(state)),
+ additionalFiles: AdditionalDocumentStates.SelectAsArray(AdditionalTextWithState.FromState),
optionsProvider: new WorkspaceAnalyzerConfigOptionsProvider(this));
public async Task> GetAnalyzerOptionsForPathAsync(
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction.cs
index 3af5e1a6c1c03..92e824fcc101b 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction.cs
@@ -9,6 +9,10 @@ namespace Microsoft.CodeAnalysis
{
internal partial class SolutionState
{
+ ///
+ /// Represents a change that needs to be made to a , , or both in response to
+ /// some user edit.
+ ///
private abstract partial class CompilationAndGeneratorDriverTranslationAction
{
public virtual Task TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken)
@@ -28,6 +32,8 @@ public virtual Task TransformCompilationAsync(Compilation oldCompil
///
public abstract bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput { get; }
+ public virtual GeneratorDriver? TransformGeneratorDriver(GeneratorDriver generatorDriver) => generatorDriver;
+
///
/// When changes are made to a solution, we make a list of translation actions. If multiple similar changes happen in rapid
/// succession, we may be able to merge them without holding onto intermediate state.
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction_Actions.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction_Actions.cs
index 1ae69d5f48c37..8ecf79c035eab 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction_Actions.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction_Actions.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
@@ -50,11 +51,8 @@ public override Task TransformCompilationAsync(Compilation oldCompi
internal sealed class TouchAdditionalDocumentAction : CompilationAndGeneratorDriverTranslationAction
{
-#pragma warning disable IDE0052 // Remove unread private members
- // https://github.com/dotnet/roslyn/issues/44161: right now there is no way to tell a GeneratorDriver that an additional document changed
private readonly TextDocumentState _oldState;
private readonly TextDocumentState _newState;
-#pragma warning restore IDE0052 // Remove unread private members
public TouchAdditionalDocumentAction(TextDocumentState oldState, TextDocumentState newState)
{
@@ -77,6 +75,17 @@ public TouchAdditionalDocumentAction(TextDocumentState oldState, TextDocumentSta
return null;
}
+
+ public override GeneratorDriver? TransformGeneratorDriver(GeneratorDriver generatorDriver)
+ {
+ var oldText = AdditionalTextWithState.FromState(_oldState);
+ var newText = AdditionalTextWithState.FromState(_newState);
+
+ // TODO: have the compiler add an API for replacing an additional text
+ // https://github.com/dotnet/roslyn/issues/54087
+ return generatorDriver.RemoveAdditionalTexts(ImmutableArray.Create(oldText))
+ .AddAdditionalTexts(ImmutableArray.Create(newText));
+ }
}
internal sealed class RemoveDocumentsAction : CompilationAndGeneratorDriverTranslationAction
@@ -132,10 +141,12 @@ public override async Task TransformCompilationAsync(Compilation ol
internal sealed class ReplaceAllSyntaxTreesAction : CompilationAndGeneratorDriverTranslationAction
{
private readonly ProjectState _state;
+ private readonly bool _isParseOptionChange;
- public ReplaceAllSyntaxTreesAction(ProjectState state)
+ public ReplaceAllSyntaxTreesAction(ProjectState state, bool isParseOptionChange)
{
_state = state;
+ _isParseOptionChange = isParseOptionChange;
}
public override async Task TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken)
@@ -153,15 +164,34 @@ public override async Task TransformCompilationAsync(Compilation ol
// Because this removes all trees, it'd also remove the generated trees.
public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => false;
+
+ public override GeneratorDriver? TransformGeneratorDriver(GeneratorDriver generatorDriver)
+ {
+ if (_isParseOptionChange)
+ {
+ // TODO: update the existing generator driver; the compiler needs to add an API for that.
+ // In the mean time, drop it and we'll recreate it from scratch.
+ // https://github.com/dotnet/roslyn/issues/54087
+ return null;
+ }
+ else
+ {
+ // We are using this as a way to reorder syntax trees -- we don't need to do anything as the driver
+ // will get the new compilation once we pass it to it.
+ return generatorDriver;
+ }
+ }
}
internal sealed class ProjectCompilationOptionsAction : CompilationAndGeneratorDriverTranslationAction
{
private readonly CompilationOptions _options;
+ private readonly bool _isAnalyzerConfigChange;
- public ProjectCompilationOptionsAction(CompilationOptions options)
+ public ProjectCompilationOptionsAction(CompilationOptions options, bool isAnalyzerConfigChange)
{
_options = options;
+ _isAnalyzerConfigChange = isAnalyzerConfigChange;
}
public override Task TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken)
@@ -172,6 +202,23 @@ public override Task TransformCompilationAsync(Compilation oldCompi
// Updating the options of a compilation doesn't require us to reparse trees, so we can use this to update
// compilations with stale generated trees.
public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => true;
+
+ public override GeneratorDriver? TransformGeneratorDriver(GeneratorDriver generatorDriver)
+ {
+ if (_isAnalyzerConfigChange)
+ {
+ // TODO: update the existing generator driver; the compiler needs to add an API for that.
+ // In the mean time, drop it and we'll recreate it from scratch.
+ // https://github.com/dotnet/roslyn/issues/54087
+ return null;
+ }
+ else
+ {
+ // Changing any other option is fine and the driver can be reused. The driver
+ // will get the new compilation once we pass it to it.
+ return generatorDriver;
+ }
+ }
}
internal sealed class ProjectAssemblyNameAction : CompilationAndGeneratorDriverTranslationAction
@@ -195,11 +242,8 @@ public override Task TransformCompilationAsync(Compilation oldCompi
internal sealed class AddAnalyzerReferencesAction : CompilationAndGeneratorDriverTranslationAction
{
-#pragma warning disable IDE0052 // Remove unread private members
- // https://github.com/dotnet/roslyn/issues/44161: right now there is no way to tell a GeneratorDriver that an analyzer reference has been added
private readonly ImmutableArray _analyzerReferences;
private readonly string _language;
-#pragma warning restore IDE0052 // Remove unread private members
public AddAnalyzerReferencesAction(ImmutableArray analyzerReferences, string language)
{
@@ -211,15 +255,17 @@ public AddAnalyzerReferencesAction(ImmutableArray analyzerRef
// translation (which is a no-op). Since we use a 'false' here to mean that it's not worth keeping
// the compilation with stale trees around, answering true is still important.
public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => true;
+
+ public override GeneratorDriver? TransformGeneratorDriver(GeneratorDriver generatorDriver)
+ {
+ return generatorDriver.AddGenerators(_analyzerReferences.SelectMany(r => r.GetGenerators(_language)).ToImmutableArray());
+ }
}
internal sealed class RemoveAnalyzerReferencesAction : CompilationAndGeneratorDriverTranslationAction
{
-#pragma warning disable IDE0052 // Remove unread private members
- // https://github.com/dotnet/roslyn/issues/44161: right now there is no way to tell a GeneratorDriver that an analyzer reference has been removed
private readonly ImmutableArray _analyzerReferences;
private readonly string _language;
-#pragma warning restore IDE0052 // Remove unread private members
public RemoveAnalyzerReferencesAction(ImmutableArray analyzerReferences, string language)
{
@@ -231,14 +277,15 @@ public RemoveAnalyzerReferencesAction(ImmutableArray analyzer
// translation (which is a no-op). Since we use a 'false' here to mean that it's not worth keeping
// the compilation with stale trees around, answering true is still important.
public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => true;
+ public override GeneratorDriver? TransformGeneratorDriver(GeneratorDriver generatorDriver)
+ {
+ return generatorDriver.RemoveGenerators(_analyzerReferences.SelectMany(r => r.GetGenerators(_language)).ToImmutableArray());
+ }
}
internal sealed class AddAdditionalDocumentsAction : CompilationAndGeneratorDriverTranslationAction
{
-#pragma warning disable IDE0052 // Remove unread private members
- // https://github.com/dotnet/roslyn/issues/44161: right now there is no way to tell a GeneratorDriver that an additional file has been added
private readonly ImmutableArray _additionalDocuments;
-#pragma warning restore IDE0052 // Remove unread private members
public AddAdditionalDocumentsAction(ImmutableArray additionalDocuments)
{
@@ -249,14 +296,16 @@ public AddAdditionalDocumentsAction(ImmutableArray additional
// translation (which is a no-op). Since we use a 'false' here to mean that it's not worth keeping
// the compilation with stale trees around, answering true is still important.
public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => true;
+
+ public override GeneratorDriver? TransformGeneratorDriver(GeneratorDriver generatorDriver)
+ {
+ return generatorDriver.AddAdditionalTexts(_additionalDocuments.SelectAsArray(AdditionalTextWithState.FromState));
+ }
}
internal sealed class RemoveAdditionalDocumentsAction : CompilationAndGeneratorDriverTranslationAction
{
-#pragma warning disable IDE0052 // Remove unread private members
- // https://github.com/dotnet/roslyn/issues/44161: right now there is no way to tell a GeneratorDriver that an additional file has been added
private readonly ImmutableArray _additionalDocuments;
-#pragma warning restore IDE0052 // Remove unread private members
public RemoveAdditionalDocumentsAction(ImmutableArray additionalDocuments)
{
@@ -267,6 +316,11 @@ public RemoveAdditionalDocumentsAction(ImmutableArray additio
// translation (which is a no-op). Since we use a 'false' here to mean that it's not worth keeping
// the compilation with stale trees around, answering true is still important.
public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => true;
+
+ public override GeneratorDriver? TransformGeneratorDriver(GeneratorDriver generatorDriver)
+ {
+ return generatorDriver.RemoveAdditionalTexts(_additionalDocuments.SelectAsArray(AdditionalTextWithState.FromState));
+ }
}
}
}
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.State.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.State.cs
index 96ee1b8160480..b03e5a92a2f8a 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.State.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.State.cs
@@ -31,7 +31,8 @@ private class State
compilationWithoutGeneratedDocuments: null,
declarationOnlyCompilation: null,
generatedDocuments: TextDocumentStates.Empty,
- generatedDocumentsAreFinal: false);
+ generatedDocumentsAreFinal: false,
+ generatorDriver: null);
///
/// A strong reference to the declaration-only compilation. This compilation isn't used to produce symbols,
@@ -53,6 +54,13 @@ private class State
///
public TextDocumentStates GeneratedDocuments { get; }
+ ///
+ /// The 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.
+ ///
+ public GeneratorDriver? GeneratorDriver { get; }
+
///
/// Whether the generated documents in 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
@@ -78,6 +86,7 @@ protected State(
ValueSource>? compilationWithoutGeneratedDocuments,
Compilation? declarationOnlyCompilation,
TextDocumentStates generatedDocuments,
+ GeneratorDriver? generatorDriver,
bool generatedDocumentsAreFinal)
{
// Declaration-only compilations should never have any references
@@ -86,12 +95,14 @@ protected State(
CompilationWithoutGeneratedDocuments = compilationWithoutGeneratedDocuments;
DeclarationOnlyCompilation = declarationOnlyCompilation;
GeneratedDocuments = generatedDocuments;
+ GeneratorDriver = generatorDriver;
GeneratedDocumentsAreFinal = generatedDocumentsAreFinal;
}
public static State Create(
Compilation compilation,
TextDocumentStates generatedDocuments,
+ GeneratorDriver? generatorDriver,
Compilation? compilationWithGeneratedDocuments,
ImmutableArray> intermediateProjects)
{
@@ -101,8 +112,8 @@ public static State Create(
// 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.Length == 0
- ? new FullDeclarationState(compilation, generatedDocuments, generatedDocumentsAreFinal: false)
- : (State)new InProgressState(compilation, generatedDocuments, compilationWithGeneratedDocuments, intermediateProjects);
+ ? new FullDeclarationState(compilation, generatedDocuments, generatorDriver, generatedDocumentsAreFinal: false)
+ : new InProgressState(compilation, generatedDocuments, generatorDriver, compilationWithGeneratedDocuments, intermediateProjects);
}
public static ValueSource> CreateValueSource(
@@ -138,11 +149,13 @@ private sealed class InProgressState : State
public InProgressState(
Compilation inProgressCompilation,
TextDocumentStates generatedDocuments,
+ GeneratorDriver? generatorDriver,
Compilation? compilationWithGeneratedDocuments,
ImmutableArray<(ProjectState state, CompilationAndGeneratorDriverTranslationAction action)> intermediateProjects)
: base(compilationWithoutGeneratedDocuments: new ConstantValueSource>(inProgressCompilation),
declarationOnlyCompilation: null,
generatedDocuments,
+ generatorDriver,
generatedDocumentsAreFinal: false) // since we have a set of transformations to make, we'll always have to run generators again
{
Contract.ThrowIfTrue(intermediateProjects.IsDefault);
@@ -160,10 +173,12 @@ private sealed class LightDeclarationState : State
{
public LightDeclarationState(Compilation declarationOnlyCompilation,
TextDocumentStates generatedDocuments,
+ GeneratorDriver? generatorDriver,
bool generatedDocumentsAreFinal)
: base(compilationWithoutGeneratedDocuments: null,
declarationOnlyCompilation,
generatedDocuments,
+ generatorDriver,
generatedDocumentsAreFinal)
{
}
@@ -177,10 +192,12 @@ private sealed class FullDeclarationState : State
{
public FullDeclarationState(Compilation declarationCompilation,
TextDocumentStates generatedDocuments,
+ GeneratorDriver? generatorDriver,
bool generatedDocumentsAreFinal)
: base(new WeakValueSource(declarationCompilation),
declarationCompilation.Clone().RemoveAllReferences(),
generatedDocuments,
+ generatorDriver,
generatedDocumentsAreFinal)
{
}
@@ -224,10 +241,12 @@ private FinalState(
Compilation compilationWithoutGeneratedFiles,
bool hasSuccessfullyLoaded,
TextDocumentStates generatedDocuments,
+ GeneratorDriver? generatorDriver,
UnrootedSymbolSet unrootedSymbolSet)
: base(compilationWithoutGeneratedFilesSource,
compilationWithoutGeneratedFiles.Clone().RemoveAllReferences(),
generatedDocuments,
+ generatorDriver: generatorDriver,
generatedDocumentsAreFinal: true) // when we're in a final state, we've ran generators and should not run again
{
HasSuccessfullyLoaded = hasSuccessfullyLoaded;
@@ -252,6 +271,7 @@ public static FinalState Create(
Compilation compilationWithoutGeneratedFiles,
bool hasSuccessfullyLoaded,
TextDocumentStates generatedDocuments,
+ GeneratorDriver? generatorDriver,
Compilation finalCompilation,
ProjectId projectId,
Dictionary? metadataReferenceToProjectId)
@@ -267,7 +287,9 @@ public static FinalState Create(
compilationWithoutGeneratedFilesSource,
compilationWithoutGeneratedFiles,
hasSuccessfullyLoaded,
- generatedDocuments, unrootedSymbolSet);
+ generatedDocuments,
+ generatorDriver,
+ unrootedSymbolSet);
}
private static void RecordAssemblySymbols(ProjectId projectId, Compilation compilation, Dictionary? metadataReferenceToProjectId)
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs
index 1ccf65b54804c..50ce1391b2442 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs
@@ -155,7 +155,7 @@ public ICompilationTracker Fork(
}
}
- var newState = State.Create(newInProgressCompilation, state.GeneratedDocuments, state.FinalCompilationWithGeneratedDocuments?.GetValueOrNull(cancellationToken), intermediateProjects);
+ var newState = State.Create(newInProgressCompilation, state.GeneratedDocuments, state.GeneratorDriver, state.FinalCompilationWithGeneratedDocuments?.GetValueOrNull(cancellationToken), intermediateProjects);
return new CompilationTracker(newProject, newState);
}
@@ -166,10 +166,10 @@ public ICompilationTracker Fork(
if (translate != null)
{
var intermediateProjects = ImmutableArray.Create((this.ProjectState, translate));
- return new CompilationTracker(newProject, new InProgressState(declarationOnlyCompilation, state.GeneratedDocuments, compilationWithGeneratedDocuments: state.FinalCompilationWithGeneratedDocuments?.GetValueOrNull(cancellationToken), intermediateProjects));
+ return new CompilationTracker(newProject, new InProgressState(declarationOnlyCompilation, state.GeneratedDocuments, state.GeneratorDriver, compilationWithGeneratedDocuments: state.FinalCompilationWithGeneratedDocuments?.GetValueOrNull(cancellationToken), intermediateProjects));
}
- return new CompilationTracker(newProject, new LightDeclarationState(declarationOnlyCompilation, state.GeneratedDocuments, generatedDocumentsAreFinal: false));
+ return new CompilationTracker(newProject, new LightDeclarationState(declarationOnlyCompilation, state.GeneratedDocuments, state.GeneratorDriver, generatedDocumentsAreFinal: false));
}
// We have nothing. Just make a tracker that only points to the new project. We'll have
@@ -188,7 +188,7 @@ public ICompilationTracker FreezePartialStateWithTree(SolutionState solution, Do
GetPartialCompilationState(
solution, docState.Id,
out var inProgressProject, out var inProgressCompilation,
- out var sourceGeneratedDocuments, out var metadataReferenceToProjectId, cancellationToken);
+ out var sourceGeneratedDocuments, out var generatorDriver, out var metadataReferenceToProjectId, cancellationToken);
if (!inProgressCompilation.SyntaxTrees.Contains(tree))
{
@@ -215,6 +215,7 @@ public ICompilationTracker FreezePartialStateWithTree(SolutionState solution, Do
inProgressCompilation,
hasSuccessfullyLoaded: false,
sourceGeneratedDocuments,
+ generatorDriver,
inProgressCompilation,
this.ProjectState.Id,
metadataReferenceToProjectId);
@@ -238,6 +239,7 @@ private void GetPartialCompilationState(
out ProjectState inProgressProject,
out Compilation inProgressCompilation,
out TextDocumentStates sourceGeneratedDocuments,
+ out GeneratorDriver? generatorDriver,
out Dictionary? metadataReferenceToProjectId,
CancellationToken cancellationToken)
{
@@ -248,6 +250,7 @@ private void GetPartialCompilationState(
var inProgressState = state as InProgressState;
sourceGeneratedDocuments = state.GeneratedDocuments;
+ generatorDriver = state.GeneratorDriver;
// 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.
@@ -429,7 +432,7 @@ private async Task GetOrBuildDeclarationCompilationAsync(SolutionSe
// okay, move to full declaration state. do this so that declaration only compilation never
// realize symbols.
var declarationOnlyCompilation = state.DeclarationOnlyCompilation.Clone();
- WriteState(new FullDeclarationState(declarationOnlyCompilation, state.GeneratedDocuments, state.GeneratedDocumentsAreFinal), solutionServices);
+ WriteState(new FullDeclarationState(declarationOnlyCompilation, state.GeneratedDocuments, state.GeneratorDriver, state.GeneratedDocumentsAreFinal), solutionServices);
return declarationOnlyCompilation;
}
@@ -443,7 +446,7 @@ private async Task GetOrBuildDeclarationCompilationAsync(SolutionSe
return compilation;
}
- (compilation, _) = await BuildDeclarationCompilationFromInProgressAsync(solutionServices, (InProgressState)state, compilation, cancellationToken).ConfigureAwait(false);
+ (compilation, _, _) = await BuildDeclarationCompilationFromInProgressAsync(solutionServices, (InProgressState)state, compilation, cancellationToken).ConfigureAwait(false);
// We must have an in progress compilation. Build off of that.
return compilation;
@@ -525,8 +528,8 @@ private Task BuildCompilationInfoAsync(
// 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.GeneratedDocumentsAreFinal ? state.GeneratedDocuments : (TextDocumentStates?)null;
-
var nonAuthoritativeGeneratedDocuments = state.GeneratedDocuments;
+ var generatorDriver = state.GeneratorDriver;
if (compilation == null)
{
@@ -535,7 +538,7 @@ private Task BuildCompilationInfoAsync(
if (state.DeclarationOnlyCompilation != null)
{
// we have declaration only compilation. build final one from it.
- return FinalizeCompilationAsync(solution, state.DeclarationOnlyCompilation, authoritativeGeneratedDocuments, nonAuthoritativeGeneratedDocuments, compilationWithStaleGeneratedTrees: null, cancellationToken);
+ return FinalizeCompilationAsync(solution, state.DeclarationOnlyCompilation, authoritativeGeneratedDocuments, nonAuthoritativeGeneratedDocuments, compilationWithStaleGeneratedTrees: null, generatorDriver, cancellationToken);
}
// We've got nothing. Build it from scratch :(
@@ -551,6 +554,7 @@ private Task BuildCompilationInfoAsync(
authoritativeGeneratedDocuments,
nonAuthoritativeGeneratedDocuments,
compilationWithStaleGeneratedTrees: null,
+ generatorDriver,
cancellationToken);
}
else
@@ -572,6 +576,7 @@ private async Task BuildCompilationInfoFromScratchAsync(
authoritativeGeneratedDocuments: null,
nonAuthoritativeGeneratedDocuments: TextDocumentStates.Empty,
compilationWithStaleGeneratedTrees: null,
+ generatorDriver: null,
cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
@@ -601,7 +606,7 @@ private async Task BuildDeclarationCompilationFromScratchAsync(
compilation = compilation.AddSyntaxTrees(trees);
trees.Free();
- WriteState(new FullDeclarationState(compilation, TextDocumentStates.Empty, generatedDocumentsAreFinal: false), solutionServices);
+ WriteState(new FullDeclarationState(compilation, TextDocumentStates.Empty, generatorDriver: null, generatedDocumentsAreFinal: false), solutionServices);
return compilation;
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
@@ -634,8 +639,15 @@ private async Task BuildFinalStateFromInProgressStateAsync(
{
try
{
- var (compilationWithoutGenerators, compilationWithGenerators) = await BuildDeclarationCompilationFromInProgressAsync(solution.Services, state, inProgressCompilation, cancellationToken).ConfigureAwait(false);
- return await FinalizeCompilationAsync(solution, compilationWithoutGenerators, authoritativeGeneratedDocuments: null, nonAuthoritativeGeneratedDocuments: state.GeneratedDocuments, compilationWithGenerators, cancellationToken).ConfigureAwait(false);
+ var (compilationWithoutGenerators, compilationWithGenerators, generatorDriver) = await BuildDeclarationCompilationFromInProgressAsync(solution.Services, state, inProgressCompilation, cancellationToken).ConfigureAwait(false);
+ return await FinalizeCompilationAsync(
+ solution,
+ compilationWithoutGenerators,
+ authoritativeGeneratedDocuments: null,
+ nonAuthoritativeGeneratedDocuments: state.GeneratedDocuments,
+ compilationWithGenerators,
+ generatorDriver,
+ cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
{
@@ -643,12 +655,13 @@ private async Task BuildFinalStateFromInProgressStateAsync(
}
}
- private async Task<(Compilation compilationWithoutGenerators, Compilation? compilationWithGenerators)> BuildDeclarationCompilationFromInProgressAsync(
+ private async Task<(Compilation compilationWithoutGenerators, Compilation? compilationWithGenerators, GeneratorDriver? generatorDriver)> BuildDeclarationCompilationFromInProgressAsync(
SolutionServices solutionServices, InProgressState state, Compilation compilationWithoutGenerators, CancellationToken cancellationToken)
{
try
{
var compilationWithGenerators = state.CompilationWithGeneratedDocuments;
+ var generatorDriver = state.GeneratorDriver;
// 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
@@ -686,14 +699,19 @@ private async Task BuildFinalStateFromInProgressStateAsync(
}
}
+ 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(State.Create(compilationWithoutGenerators, state.GeneratedDocuments, compilationWithGenerators, intermediateProjects), solutionServices);
+ this.WriteState(State.Create(compilationWithoutGenerators, state.GeneratedDocuments, generatorDriver, compilationWithGenerators, intermediateProjects), solutionServices);
}
- return (compilationWithoutGenerators, compilationWithGenerators);
+ return (compilationWithoutGenerators, compilationWithGenerators, generatorDriver);
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
{
@@ -726,12 +744,19 @@ public CompilationInfo(Compilation compilation, bool hasSuccessfullyLoaded, Text
/// 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 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
+ /// 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,
Compilation? compilationWithStaleGeneratedTrees,
+ GeneratorDriver? generatorDriver,
CancellationToken cancellationToken)
{
try
@@ -820,76 +845,77 @@ private async Task FinalizeCompilationAsync(
if (ProjectState.SourceGenerators.Any())
{
- var additionalTexts = this.ProjectState.AdditionalDocumentStates.SelectAsArray(state => new AdditionalTextWithState(state));
- var compilationFactory = this.ProjectState.LanguageServices.GetRequiredService();
+ // If we don't already have a generator driver, we'll have to create one from scratch
+ if (generatorDriver == null)
+ {
+ var additionalTexts = this.ProjectState.AdditionalDocumentStates.SelectAsArray(AdditionalTextWithState.FromState);
+ var compilationFactory = this.ProjectState.LanguageServices.GetRequiredService();
+
+ generatorDriver = compilationFactory.CreateGeneratorDriver(
+ this.ProjectState.ParseOptions!,
+ ProjectState.SourceGenerators,
+ this.ProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider,
+ additionalTexts);
+ }
- var generatorDriver = compilationFactory.CreateGeneratorDriver(
- this.ProjectState.ParseOptions!,
- ProjectState.SourceGenerators,
- this.ProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider,
- additionalTexts);
+ generatorDriver = generatorDriver.RunGenerators(compilationWithoutGenerators, cancellationToken);
+ var runResult = generatorDriver.GetRunResult();
- if (generatorDriver != null)
+ // 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)
{
- generatorDriver = generatorDriver.RunGenerators(compilationWithoutGenerators, cancellationToken);
- var runResult = generatorDriver.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)
+ if (nonAuthoritativeGeneratedDocuments.Count != runResult.Results.Sum(r => r.GeneratedSources.Length))
{
- if (nonAuthoritativeGeneratedDocuments.Count != runResult.Results.Sum(r => r.GeneratedSources.Length))
- {
- compilationWithStaleGeneratedTrees = null;
- }
+ compilationWithStaleGeneratedTrees = null;
}
+ }
- foreach (var generatorResult in runResult.Results)
+ foreach (var generatorResult in runResult.Results)
+ {
+ foreach (var generatedSource in generatorResult.GeneratedSources)
{
- foreach (var generatedSource in generatorResult.GeneratedSources)
+ var existing = FindExistingGeneratedDocumentState(
+ nonAuthoritativeGeneratedDocuments,
+ generatorResult.Generator,
+ generatedSource.HintName);
+
+ if (existing != null)
{
- var existing = FindExistingGeneratedDocumentState(
- nonAuthoritativeGeneratedDocuments,
- generatorResult.Generator,
- 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);
-
- generatedDocumentsBuilder.Add(
- SourceGeneratedDocumentState.Create(
- identity,
- generatedSource.SourceText,
- generatedSource.SyntaxTree.Options,
- this.ProjectState.LanguageServices,
- solution.Services));
-
- // 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.
+ 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);
+
+ generatedDocumentsBuilder.Add(
+ SourceGeneratedDocumentState.Create(
+ identity,
+ generatedSource.SourceText,
+ generatedSource.SyntaxTree.Options,
+ this.ProjectState.LanguageServices,
+ solution.Services));
+
+ // 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;
}
}
}
@@ -915,6 +941,7 @@ private async Task FinalizeCompilationAsync(
compilationWithoutGenerators,
hasSuccessfullyLoaded,
generatedDocuments,
+ generatorDriver,
compilationWithGenerators,
this.ProjectState.Id,
metadataReferenceToProjectId);
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs
index 0b2fdc942ef0b..6b9a3153ba569 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs
@@ -758,7 +758,7 @@ public SolutionState WithProjectCompilationOptions(ProjectId projectId, Compilat
return this;
}
- return ForkProject(newProject, new CompilationAndGeneratorDriverTranslationAction.ProjectCompilationOptionsAction(options));
+ return ForkProject(newProject, new CompilationAndGeneratorDriverTranslationAction.ProjectCompilationOptionsAction(options, isAnalyzerConfigChange: false));
}
///
@@ -783,7 +783,7 @@ public SolutionState WithProjectParseOptions(ProjectId projectId, ParseOptions o
}
else
{
- return ForkProject(newProject, new CompilationAndGeneratorDriverTranslationAction.ReplaceAllSyntaxTreesAction(newProject));
+ return ForkProject(newProject, new CompilationAndGeneratorDriverTranslationAction.ReplaceAllSyntaxTreesAction(newProject, isParseOptionChange: true));
}
}
@@ -929,7 +929,7 @@ public SolutionState WithProjectDocumentsOrder(ProjectId projectId, ImmutableLis
return this;
}
- return ForkProject(newProject, new CompilationAndGeneratorDriverTranslationAction.ReplaceAllSyntaxTreesAction(newProject));
+ return ForkProject(newProject, new CompilationAndGeneratorDriverTranslationAction.ReplaceAllSyntaxTreesAction(newProject, isParseOptionChange: false));
}
///
@@ -1116,7 +1116,7 @@ public SolutionState AddAnalyzerConfigDocuments(ImmutableArray doc
(oldProject, documents) =>
{
var newProject = oldProject.AddAnalyzerConfigDocuments(documents);
- return (newProject, new CompilationAndGeneratorDriverTranslationAction.ProjectCompilationOptionsAction(newProject.CompilationOptions!));
+ return (newProject, new CompilationAndGeneratorDriverTranslationAction.ProjectCompilationOptionsAction(newProject.CompilationOptions!, isAnalyzerConfigChange: true));
});
}
@@ -1127,7 +1127,7 @@ public SolutionState RemoveAnalyzerConfigDocuments(ImmutableArray do
(oldProject, documentIds, _) =>
{
var newProject = oldProject.RemoveAnalyzerConfigDocuments(documentIds);
- return (newProject, new CompilationAndGeneratorDriverTranslationAction.ProjectCompilationOptionsAction(newProject.CompilationOptions!));
+ return (newProject, new CompilationAndGeneratorDriverTranslationAction.ProjectCompilationOptionsAction(newProject.CompilationOptions!, isAnalyzerConfigChange: true));
});
}
@@ -1448,7 +1448,7 @@ private SolutionState UpdateAnalyzerConfigDocumentState(AnalyzerConfigDocumentSt
Debug.Assert(oldProject != newProject);
return ForkProject(newProject,
- newProject.CompilationOptions != null ? new CompilationAndGeneratorDriverTranslationAction.ProjectCompilationOptionsAction(newProject.CompilationOptions) : null);
+ newProject.CompilationOptions != null ? new CompilationAndGeneratorDriverTranslationAction.ProjectCompilationOptionsAction(newProject.CompilationOptions, isAnalyzerConfigChange: true) : null);
}
///
diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentIdentity.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentIdentity.cs
index 08fa53f00a46a..c2d03ef939c66 100644
--- a/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentIdentity.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentIdentity.cs
@@ -36,10 +36,7 @@ public SourceGeneratedDocumentIdentity(DocumentId documentId, string hintName, s
public static string GetGeneratorTypeName(ISourceGenerator generator)
{
- // PROTOTYPE(source-generators): this will return the incorrect type for wrapped generators
- // there is ongoing work to remove the type dependency, so we'll
- // fix it when that merges in
- return generator.GetType().FullName!;
+ return GeneratorDriver.GetGeneratorType(generator).FullName!;
}
public static string GetGeneratorAssemblyName(ISourceGenerator generator)
diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs
index 5e515eaf59705..a268cc0ba6e43 100644
--- a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs
+++ b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs
@@ -7,6 +7,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
@@ -20,15 +21,25 @@ namespace Microsoft.CodeAnalysis.UnitTests
[UseExportProvider]
public class SolutionWithSourceGeneratorTests : TestBase
{
+ // This is used to add on the preview language version which controls incremental generators being allowed.
+ // TODO: remove this method entirely and the calls once incremental generators are no longer preview
+ private static Project WithPreviewLanguageVersion(Project project)
+ {
+ return project.WithParseOptions(((CSharpParseOptions)project.ParseOptions!).WithLanguageVersion(LanguageVersion.Preview));
+ }
+
[Theory]
[CombinatorialData]
- public async Task SourceGeneratorBasedOnAdditionalFileGeneratesSyntaxTreesOnce(
+ public async Task SourceGeneratorBasedOnAdditionalFileGeneratesSyntaxTrees(
bool fetchCompilationBeforeAddingGenerator,
bool useRecoverableTrees)
{
+ // This test is just the sanity test to make sure generators work at all. There's not a special scenario being
+ // tested.
+
using var workspace = useRecoverableTrees ? CreateWorkspaceWithRecoverableSyntaxTreesAndWeakCompilations() : CreateWorkspace();
- var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented() { });
- var project = AddEmptyProject(workspace.CurrentSolution)
+ var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
+ var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference);
// Optionally fetch the compilation first, which validates that we handle both running the generator
@@ -54,12 +65,50 @@ public async Task SourceGeneratorBasedOnAdditionalFileGeneratesSyntaxTreesOnce(
Assert.Same(generatedTree, await generatedDocument.GetSyntaxTreeAsync());
}
+ [Fact]
+ public async Task IncrementalSourceGeneratorInvokedCorrectNumberOfTimes()
+ {
+ using var workspace = CreateWorkspace();
+ var generator = new GenerateFileForEachAdditionalFileWithContentsCommented();
+ var analyzerReference = new TestGeneratorReference(generator);
+ var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
+ .AddAnalyzerReference(analyzerReference)
+ .AddAdditionalDocument("Test.txt", "Hello, world!").Project
+ .AddAdditionalDocument("Test2.txt", "Hello, world!").Project;
+
+ var compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
+
+ Assert.Equal(2, compilation.SyntaxTrees.Count());
+ Assert.Equal(2, generator.AdditionalFilesConvertedCount);
+
+ // Change one of the additional documents, and rerun; we should only reprocess that one change, since this
+ // is an incremental generator.
+ project = project.AdditionalDocuments.First().WithAdditionalDocumentText(SourceText.From("Changed text!")).Project;
+
+ compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
+
+ Assert.Equal(2, compilation.SyntaxTrees.Count());
+
+ // We should now have converted three additional files -- the two from the original run and then the one that was changed.
+ // The other one should have been kept constant because that didn't change.
+ Assert.Equal(3, generator.AdditionalFilesConvertedCount);
+
+ // Change one of the source documents, and rerun; we should again only reprocess that one change.
+ project = project.AddDocument("Source.cs", SourceText.From("")).Project;
+
+ compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
+
+ // We have one extra syntax tree now, but it did not require any invocations of the incremental generator.
+ Assert.Equal(3, compilation.SyntaxTrees.Count());
+ Assert.Equal(3, generator.AdditionalFilesConvertedCount);
+ }
+
[Fact]
public async Task SourceGeneratorContentStillIncludedAfterSourceFileChange()
{
using var workspace = CreateWorkspace();
- var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented() { });
- var project = AddEmptyProject(workspace.CurrentSolution)
+ var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
+ var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddDocument("Hello.cs", "// Source File").Project
.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
@@ -90,8 +139,8 @@ static async Task AssertCompilationContainsOneRegularAndOneGeneratedFile(Project
public async Task SourceGeneratorContentChangesAfterAdditionalFileChanges()
{
using var workspace = CreateWorkspace();
- var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented() { });
- var project = AddEmptyProject(workspace.CurrentSolution)
+ var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
+ var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
@@ -119,7 +168,7 @@ public async Task PartialCompilationsIncludeGeneratedFilesAfterFullGeneration()
{
using var workspace = CreateWorkspace();
var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
- var project = AddEmptyProject(workspace.CurrentSolution)
+ var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddDocument("Hello.cs", "// Source File").Project
.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
@@ -139,7 +188,7 @@ public async Task DocumentIdOfGeneratedDocumentsIsStable()
{
using var workspace = CreateWorkspace();
var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
- var projectBeforeChange = AddEmptyProject(workspace.CurrentSolution)
+ var projectBeforeChange = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
@@ -178,7 +227,7 @@ public async Task DocumentIdGuidInDifferentProjectsIsDifferent()
static Solution AddProjectWithReference(Solution solution, TestGeneratorReference analyzerReference)
{
- var project = AddEmptyProject(solution);
+ var project = WithPreviewLanguageVersion(AddEmptyProject(solution));
project = project.AddAnalyzerReference(analyzerReference);
project = project.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
@@ -191,7 +240,7 @@ public async Task CompilationsInCompilationReferencesIncludeGeneratedSourceFiles
{
using var workspace = CreateWorkspace();
var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
- var solution = AddEmptyProject(workspace.CurrentSolution)
+ var solution = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddAdditionalDocument("Test.txt", "Hello, world!").Project.Solution;
@@ -207,6 +256,7 @@ public async Task CompilationsInCompilationReferencesIncludeGeneratedSourceFiles
var compilationWithGenerator = await solution.GetRequiredProject(projectIdWithGenerator).GetRequiredCompilationAsync(CancellationToken.None);
+ Assert.NotEmpty(compilationWithGenerator.SyntaxTrees);
Assert.Same(compilationWithGenerator, compilationReference.Compilation);
}
@@ -215,7 +265,7 @@ public async Task RequestingGeneratedDocumentsTwiceGivesSameInstance()
{
using var workspace = CreateWorkspaceWithRecoverableSyntaxTreesAndWeakCompilations();
var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
- var project = AddEmptyProject(workspace.CurrentSolution)
+ var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
@@ -239,7 +289,7 @@ public async Task GetDocumentWithGeneratedTreeReturnsGeneratedDocument()
{
using var workspace = CreateWorkspace();
var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
- var project = AddEmptyProject(workspace.CurrentSolution)
+ var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
@@ -253,7 +303,7 @@ public async Task GetDocumentWithGeneratedTreeForInProgressReturnsGeneratedDocum
{
using var workspace = CreateWorkspace();
var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
- var project = AddEmptyProject(workspace.CurrentSolution)
+ var project = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project
.AddAdditionalDocument("Test.txt", "Hello, world!").Project;
@@ -427,7 +477,7 @@ public async Task OpenSourceGeneratedFileMatchesBufferContentsEvenIfGeneratedFil
{
using var workspace = CreateWorkspace();
var analyzerReference = new TestGeneratorReference(new GenerateFileForEachAdditionalFileWithContentsCommented());
- var originalAdditionalFile = AddEmptyProject(workspace.CurrentSolution)
+ var originalAdditionalFile = WithPreviewLanguageVersion(AddEmptyProject(workspace.CurrentSolution))
.AddAnalyzerReference(analyzerReference)
.AddAdditionalDocument("Test.txt", SourceText.From(""));
diff --git a/src/Workspaces/CoreTestUtilities/GenerateFileForEachAdditionalFileWithContentsCommented.cs b/src/Workspaces/CoreTestUtilities/GenerateFileForEachAdditionalFileWithContentsCommented.cs
index c102036887979..dccf1cb0c88e7 100644
--- a/src/Workspaces/CoreTestUtilities/GenerateFileForEachAdditionalFileWithContentsCommented.cs
+++ b/src/Workspaces/CoreTestUtilities/GenerateFileForEachAdditionalFileWithContentsCommented.cs
@@ -4,38 +4,48 @@
using System.IO;
using System.Text;
+using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Roslyn.Test.Utilities
{
- internal sealed class GenerateFileForEachAdditionalFileWithContentsCommented : ISourceGenerator
+ internal sealed class GenerateFileForEachAdditionalFileWithContentsCommented : IIncrementalGenerator
{
- public void Execute(GeneratorExecutionContext context)
+ ///
+ /// This should only be updated with Interlocked APIs.
+ ///
+ private int _additionalFilesConvertedCount;
+
+ ///
+ /// The number of additional files we converted to a source file. This can be used to assert incrementality.
+ ///
+ public int AdditionalFilesConvertedCount => _additionalFilesConvertedCount;
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
{
- foreach (var file in context.AdditionalFiles)
+ context.RegisterExecutionPipeline(context =>
{
- AddSourceForAdditionalFile(context, file);
- }
+ context.RegisterSourceOutput(context.AdditionalTextsProvider, (context, additionalText) =>
+ context.AddSource(
+ GetGeneratedFileName(additionalText.Path),
+ GenerateSourceForAdditionalFile(additionalText, context.CancellationToken)));
+ });
}
- public void Initialize(GeneratorInitializationContext context)
+ private SourceText GenerateSourceForAdditionalFile(AdditionalText file, CancellationToken cancellationToken)
{
- // TODO: context.RegisterForAdditionalFileChanges(UpdateContext);
- }
+ Interlocked.Increment(ref _additionalFilesConvertedCount);
- private static void AddSourceForAdditionalFile(GeneratorExecutionContext context, AdditionalText file)
- {
// We're going to "comment" out the contents of the file when generating this
- var sourceText = file.GetText(context.CancellationToken);
+ var sourceText = file.GetText(cancellationToken);
Contract.ThrowIfNull(sourceText, "Failed to fetch the text of an additional file.");
var changes = sourceText.Lines.SelectAsArray(l => new TextChange(new TextSpan(l.Start, length: 0), "// "));
var generatedText = sourceText.WithChanges(changes);
- // TODO: remove the generatedText.ToString() when I don't have to specify the encoding
- context.AddSource(GetGeneratedFileName(file.Path), SourceText.From(generatedText.ToString(), encoding: Encoding.UTF8));
+ return SourceText.From(generatedText.ToString(), encoding: Encoding.UTF8);
}
private static string GetGeneratedFileName(string path) => $"{Path.GetFileNameWithoutExtension(path)}.generated";
diff --git a/src/Workspaces/CoreTestUtilities/TestGeneratorReference.cs b/src/Workspaces/CoreTestUtilities/TestGeneratorReference.cs
index 301521aa7175d..6650c252fe590 100644
--- a/src/Workspaces/CoreTestUtilities/TestGeneratorReference.cs
+++ b/src/Workspaces/CoreTestUtilities/TestGeneratorReference.cs
@@ -33,6 +33,11 @@ public TestGeneratorReference(ISourceGenerator generator)
_checksum = Checksum.From(checksumArray);
}
+ public TestGeneratorReference(IIncrementalGenerator generator)
+ : this(GeneratorDriver.WrapGenerator(generator))
+ {
+ }
+
public override string? FullPath => null;
public override object Id => this;
public Guid Guid { get; }