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