fixed generator cache being stale on cross-file edit#71
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR fixes a stale incremental-generator cache scenario where a cross-file type change could leave generated output out-of-date, by restructuring the pipeline to recompute per-compilation change and then “value-gate” downstream emission using value-equatable projections. It also introduces a regression test to ensure cross-file edits don’t serve stale output and unrelated edits don’t churn generated sources.
Changes:
- Refactors
ExpressiveGenerator(and related interpretation/emission paths) to compute member outputs per compilation change, capturingAddSource/ReportDiagnosticinto a replayableGeneratorOutputContext, and gating emission via value-equatableGeneratedSource/EquatableArray<T>. - Updates
PolyfillInterceptorGeneratorand interpreter/emitter code to acceptGeneratorOutputContextin shared emission paths (so they can run inside incrementalSelect). - Adds
IncrementalCachingTestsand removes previously-used custom comparers that could incorrectly treat cross-file changes as unchanged.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/IncrementalCachingTests.cs | Adds regression coverage for cross-file edits vs incremental staleness and for unrelated edits vs churn. |
| src/ExpressiveSharp.Generator/ExpressiveGenerator.cs | Reworks incremental pipeline to recompute on compilation changes and gate downstream emission on value-equatable projections; adds tracking name for tests. |
| src/ExpressiveSharp.Generator/Infrastructure/GeneratorOutput.cs | Introduces GeneratorOutputContext, GeneratedSource, MemberComputation, and EquatableArray<T> to support value-gated incremental emission. |
| src/ExpressiveSharp.Generator/PolyfillInterceptorGenerator.cs | Switches shared emit path to collect into GeneratorOutputContext and flush within RegisterSourceOutput. |
| src/ExpressiveSharp.Generator/Interpretation/ExpressiveInterpreter.cs | Updates interpretation entrypoint to accept GeneratorOutputContext. |
| src/ExpressiveSharp.Generator/Interpretation/ExpressiveInterpreter.BodyProcessors.cs | Threads GeneratorOutputContext through body processing/emission calls. |
| src/ExpressiveSharp.Generator/Interpretation/ExpressiveInterpreter.Helpers.cs | Updates helper diagnostic reporting to use GeneratorOutputContext. |
| src/ExpressiveSharp.Generator/Interpretation/ExpressiveForInterpreter.cs | Updates [ExpressiveFor] interpretation to use GeneratorOutputContext. |
| src/ExpressiveSharp.Generator/Interpretation/ExpressivePropertyInterpreter.cs | Updates [ExpressiveProperty] interpretation to use GeneratorOutputContext. |
| src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs | Replaces SourceProductionContext? with GeneratorOutputContext? for emission-time diagnostics/source capture. |
| src/ExpressiveSharp.Generator/Emitter/SynthesizedPropertyEmitter.cs | Updates emitter API to accept GeneratorOutputContext. |
| src/ExpressiveSharp.Generator/Comparers/MemberDeclarationSyntaxEqualityComparer.cs | Removes comparer previously used for incremental caching decisions. |
| src/ExpressiveSharp.Generator/Comparers/MemberDeclarationSyntaxAndCompilationEqualityComparer.cs | Removes comparer previously used for incremental caching decisions. |
| src/ExpressiveSharp.Generator/Comparers/ExpressiveForMemberCompilationEqualityComparer.cs | Removes comparer previously used for incremental caching decisions in [ExpressiveFor] pipeline. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var c1 = CreateCompilation(memberTree, modelsV1); | ||
| var driver = CreateDriver().RunGenerators(c1); | ||
|
|
||
| // Edit ONLY Models.cs (Member.cs's tree reference is preserved, so a naive comparer would | ||
| // treat this member as unchanged). | ||
| var modelsV2 = CSharpSyntaxTree.ParseText(ModelsLongVersion, path: "Models.cs"); | ||
| var c2 = c1.ReplaceSyntaxTree(modelsV1, modelsV2); | ||
|
|
||
| driver = driver.RunGenerators(c2); | ||
| var incremental = GetMemberSourceText(driver.GetRunResult()); | ||
|
|
||
| // Ground truth: a fresh driver on the final compilation. | ||
| var fresh = GetMemberSourceText(CreateDriver().RunGenerators(c2).GetRunResult()); | ||
|
|
||
| TestContext.WriteLine("Incremental:\n" + incremental); | ||
| TestContext.WriteLine("Fresh:\n" + fresh); | ||
|
|
||
| Assert.AreEqual(fresh, incremental, | ||
| "After a cross-file type change, incremental output must match a from-scratch run (no stale cache)."); | ||
| // Sanity: the change really did alter the output (int->long removes the Convert), so the test | ||
| // is actually exercising staleness rather than comparing two identical strings. | ||
| StringAssert.Contains(incremental, "long", "Expected the long-typed property to be reflected."); | ||
| Assert.IsFalse(incremental.Contains("Convert"), | ||
| "With Number typed as long, no widening Convert should remain in the generated tree."); |
Example:
long Foo(Bar t) => t.Bazkeeps emitting anint→longConvert afterBar.Bazis changed tolongin another file. Fixed with this PR and simplifies future additions