@@ -36,10 +36,12 @@ private sealed partial class RegularCompilationTracker : ICompilationTracker
3636 Compilation ? compilationWithStaleGeneratedTrees ,
3737 CancellationToken cancellationToken )
3838 {
39- if ( creationPolicy . GeneratedDocumentCreationPolicy is GeneratedDocumentCreationPolicy . DoNotCreate )
39+ var canSkipRunningGenerators = await CanSkipRunningGeneratorsAsync ( creationPolicy , compilationState , cancellationToken ) . ConfigureAwait ( false ) ;
40+ if ( canSkipRunningGenerators )
4041 {
41- // We're frozen. So we do not want to go through the expensive cost of running generators. Instead, we
42- // just whatever prior generated docs we have.
42+ // We're either frozen, or we only want required generators and know that there aren't any to run, so we
43+ // do not want to go through the expensive cost of running generators. Instead, we just use whatever
44+ // prior generated docs we have.
4345 var generatedSyntaxTrees = await generatorInfo . Documents . States . Values . SelectAsArrayAsync (
4446 static ( state , cancellationToken ) => state . GetSyntaxTreeAsync ( cancellationToken ) , cancellationToken ) . ConfigureAwait ( false ) ;
4547
@@ -69,9 +71,33 @@ private sealed partial class RegularCompilationTracker : ICompilationTracker
6971 generatorInfo . Documents ,
7072 generatorInfo . Driver ,
7173 compilationWithStaleGeneratedTrees ,
74+ creationPolicy . GeneratedDocumentCreationPolicy ,
7275 cancellationToken ) . ConfigureAwait ( false ) ;
7376 return ( compilationWithGeneratedFiles , new ( nextGeneratedDocuments , nextGeneratorDriver ) ) ;
7477 }
78+
79+ async ValueTask < bool > CanSkipRunningGeneratorsAsync ( CreationPolicy creationPolicy , SolutionCompilationState compilationState , CancellationToken cancellationToken )
80+ {
81+ // if we don't want to create generated documents, we can skip
82+ if ( creationPolicy . GeneratedDocumentCreationPolicy is GeneratedDocumentCreationPolicy . DoNotCreate )
83+ return true ;
84+
85+ // if we only want required documents, we can skip if we don't have any required generators
86+ if ( creationPolicy . GeneratedDocumentCreationPolicy is GeneratedDocumentCreationPolicy . CreateOnlyRequired )
87+ {
88+ var hasRequiredGenerators = await HasRequiredGeneratorsAsync ( compilationState , cancellationToken ) . ConfigureAwait ( false ) ;
89+ return ! hasRequiredGenerators ;
90+ }
91+
92+ // we need to run generators
93+ return false ;
94+ }
95+ }
96+
97+ private async Task < bool > HasRequiredGeneratorsAsync ( SolutionCompilationState compilationState , CancellationToken cancellationToken )
98+ {
99+ var presence = await compilationState . GetProjectGeneratorPresenceAsync ( ProjectState . Id , cancellationToken ) . ConfigureAwait ( false ) ;
100+ return presence is SourceGeneratorPresence . ContainsRequiredSourceGenerators ;
75101 }
76102
77103 private async Task < ( Compilation compilationWithGeneratedFiles , TextDocumentStates < SourceGeneratedDocumentState > generatedDocuments ) ? > TryComputeNewGeneratorInfoInRemoteProcessAsync (
@@ -236,12 +262,16 @@ await newGeneratedDocuments.States.Values.SelectAsArrayAsync(
236262 TextDocumentStates < SourceGeneratedDocumentState > oldGeneratedDocuments ,
237263 GeneratorDriver ? generatorDriver ,
238264 Compilation ? compilationWithStaleGeneratedTrees ,
265+ GeneratedDocumentCreationPolicy creationPolicy ,
239266 CancellationToken cancellationToken )
240267 {
241268 // If we don't have any source generators. Trivially bail out.
242269 if ( ! await compilationState . HasSourceGeneratorsAsync ( this . ProjectState . Id , cancellationToken ) . ConfigureAwait ( false ) )
243270 return ( compilationWithoutGeneratedFiles , TextDocumentStates < SourceGeneratedDocumentState > . Empty , generatorDriver ) ;
244271
272+ // Hold onto the prior results so we can compare when filtering
273+ var priorRunResult = generatorDriver ? . GetRunResult ( ) ;
274+
245275 // If we don't already have an existing generator driver, create one from scratch
246276 generatorDriver ??= CreateGeneratorDriver ( this . ProjectState ) ;
247277
@@ -268,7 +298,7 @@ await newGeneratedDocuments.States.Values.SelectAsArrayAsync(
268298 var compilationToRunGeneratorsOn = compilationWithoutGeneratedFiles . RemoveSyntaxTrees ( treesToRemove ) ;
269299 // END HACK HACK HACK HACK.
270300
271- generatorDriver = generatorDriver . RunGenerators ( compilationToRunGeneratorsOn , cancellationToken ) ;
301+ generatorDriver = generatorDriver . RunGenerators ( compilationToRunGeneratorsOn , ShouldGeneratorRun , cancellationToken ) ;
272302
273303 Contract . ThrowIfNull ( generatorDriver ) ;
274304
@@ -425,6 +455,46 @@ static void CheckGeneratorDriver(GeneratorDriver generatorDriver, ProjectState p
425455
426456 Contract . ThrowIfFalse ( additionalTexts . Length == projectState . AdditionalDocumentStates . Count ) ;
427457 }
458+
459+ bool ShouldGeneratorRun ( GeneratorFilterContext context )
460+ {
461+ // We should never try and run a generator driver if we're not expecting to do any work
462+ Contract . ThrowIfTrue ( creationPolicy is GeneratedDocumentCreationPolicy . DoNotCreate ) ;
463+
464+ // If we're in Create mode, we're always going to run all generators
465+ if ( creationPolicy is GeneratedDocumentCreationPolicy . Create )
466+ return true ;
467+
468+ // If we get here we expect to be in CreateOnlyRequired. Throw to ensure we catch if someone adds a new state
469+ Contract . ThrowIfFalse ( creationPolicy is GeneratedDocumentCreationPolicy . CreateOnlyRequired ) ;
470+
471+ // We want to only run required generators, but it's also possible that there are generators that
472+ // have never been run (for instance, an AddGenerator operation might have occurred between runs).
473+ // Our model is that it's acceptable for documents to be slightly out of date, but it is
474+ // fundamentally incorrect to have *no* documents for a generator that could be producing them.
475+
476+ // If there was no prior run result, then we can't have any documents for this generator, so we
477+ // need to re-run it.
478+ if ( priorRunResult is null )
479+ return true ;
480+
481+ // Next we need to check if this particular generator was run as part of the prior driver execution.
482+ // Either we have no state for the generator, in which case it can't have run. If we do have state,
483+ // the contract from the generator driver is that a generator that hasn't run yet produces a default
484+ // ImmutableArray for GeneratedSources. Note that this is different from an empty array, which
485+ // indicates that the generator ran, but didn't produce any documents:
486+
487+ // - GeneratedSources == default ImmutableArray: the generator was not invoked during that run (must run).
488+ // - GeneratedSources == non-default empty array: the generator ran but produced no documents (may skip).
489+ // - GeneratedSources == non-default non-empty array: the generator ran and produced documents (may skip).
490+
491+ if ( ! priorRunResult . Results . Any ( r => r . Generator == context . Generator && ! r . GeneratedSources . IsDefault ) )
492+ return true ;
493+
494+ // We have results for this generator, and we're in CreateOnlyRequired, so only run this generator if
495+ // we consider it to be required.
496+ return context . Generator . IsRequiredGenerator ( ) ;
497+ }
428498 }
429499 }
430500}
0 commit comments