11using System . Collections . Immutable ;
22using System . Text ;
3+ using System . Threading ;
34using ExpressiveSharp . Generator . Comparers ;
45using ExpressiveSharp . Generator . Emitter ;
56using ExpressiveSharp . Generator . Infrastructure ;
@@ -24,6 +25,9 @@ public class PolyfillInterceptorGenerator : IIncrementalGenerator
2425
2526 private const string ExpressivePropertyAttributeName = "ExpressiveSharp.Mapping.ExpressivePropertyAttribute" ;
2627
28+ /// <summary>Tracking name for the value-equatable interceptor-source node (used by incremental-cache tests).</summary>
29+ public const string InterceptorSourcesTrackingName = "InterceptorSources" ;
30+
2731 private const string ClosureHelperSource = """
2832
2933 file static class __ClosureHelper
@@ -112,12 +116,13 @@ inv.Expression is MemberAccessExpressionSyntax &&
112116 . Where ( static x => x is not null )
113117 . Select ( static ( x , _ ) => x ! ) ;
114118
115- // Reference equality on the CompilationUnitSyntax root: Roslyn keeps unchanged files'
116- // syntax tree roots as the same object across incremental runs, so editing a noise
117- // file leaves all other (file, compilation) pairs equal and skips re-emission.
119+ // A file's interceptors are produced by binding its call sites, which depend on the whole
120+ // compilation (the lambdas reference types/members defined in other files). So we recompute
121+ // when the compilation changes and gate the output on the value-equatable generated source
122+ // below — a comparer keyed on the file's own syntax would serve stale interceptors after a
123+ // cross-file edit.
118124 var filesWithCompilation = candidateFiles
119- . Combine ( context . CompilationProvider )
120- . WithComparer ( CompilationUnitAndCompilationComparer . Instance ) ;
125+ . Combine ( context . CompilationProvider ) ;
121126
122127 // Source generators don't see each other's AddSource output, so SemanticModel can't bind
123128 // references to ExpressiveGenerator's synthesized [ExpressiveProperty] partials. Mirror
@@ -152,20 +157,46 @@ inv.Expression is MemberAccessExpressionSyntax &&
152157 . Collect ( )
153158 . WithComparer ( SynthesizedSourceArrayComparer . Instance ) ;
154159
155- var filesWithCompilationAndSynth = filesWithCompilation
160+ var fileResults = filesWithCompilation
156161 . Combine ( synthesizedSources )
157- . WithComparer ( FileAndSynthesizedSourcesComparer . Instance ) ;
162+ . Select ( static ( pair , ct ) =>
163+ ComputeFileInterceptors ( pair . Left . Left , pair . Left . Right , pair . Right , ct ) ) ;
164+
165+ // Generated interceptor source is value-data: re-emitted only when a file's interceptors
166+ // actually change. Implementation output — interceptors are only needed for the real build
167+ // (the user's code type-checks against the stub methods), so this stays off the live path.
168+ var interceptorSources = fileResults
169+ . Select ( static ( r , _ ) => r . Sources )
170+ . WithTrackingName ( InterceptorSourcesTrackingName ) ;
171+ context . RegisterImplementationSourceOutput ( interceptorSources ,
172+ static ( spc , sources ) =>
173+ {
174+ foreach ( var source in sources . AsImmutableArray )
175+ spc . AddSource ( source . HintName , SourceText . From ( source . Text , Encoding . UTF8 ) ) ;
176+ } ) ;
158177
159- context . RegisterSourceOutput ( filesWithCompilationAndSynth ,
160- static ( spc , pair ) =>
178+ // Diagnostics flow live (real syntax-tree locations); recomputed each run, never stale.
179+ var interceptorDiagnostics = fileResults . Select ( static ( r , _ ) => r . Diagnostics ) ;
180+ context . RegisterImplementationSourceOutput ( interceptorDiagnostics ,
181+ static ( spc , diagnostics ) =>
161182 {
162- // Keeps its existing caching; collector is just the shared emit path, flushed inline.
163- var output = new GeneratorOutputContext ( spc . CancellationToken ) ;
164- ProcessFileAndEmit ( pair . Left . Left , pair . Left . Right , pair . Right , output ) ;
165- output . FlushTo ( spc ) ;
183+ foreach ( var diagnostic in diagnostics )
184+ spc . ReportDiagnostic ( diagnostic ) ;
166185 } ) ;
167186 }
168187
188+ private static ( EquatableArray < GeneratedSource > Sources , ImmutableArray < Diagnostic > Diagnostics )
189+ ComputeFileInterceptors (
190+ CompilationUnitSyntax unit ,
191+ Compilation compilation ,
192+ ImmutableArray < ( string HintName , string Source ) > synthesizedSources ,
193+ CancellationToken cancellationToken )
194+ {
195+ var output = new GeneratorOutputContext ( cancellationToken ) ;
196+ ProcessFileAndEmit ( unit , compilation , synthesizedSources , output ) ;
197+ return ( output . Sources , output . Diagnostics ) ;
198+ }
199+
169200 private static void ProcessFileAndEmit (
170201 CompilationUnitSyntax unit ,
171202 Compilation compilation ,
@@ -858,58 +889,4 @@ private static string ResolveTypeFqn(ITypeSymbol type, Dictionary<ITypeSymbol, s
858889
859890 return type . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) ;
860891 }
861-
862- /// <summary>
863- /// Reference equality on the CompilationUnitSyntax root, ignoring Compilation.
864- /// Roslyn keeps unchanged files' syntax tree roots as the same object across incremental
865- /// runs, so noise-file edits leave untouched (file, compilation) pairs equal and skip
866- /// re-emission — O(1) incremental cost.
867- /// </summary>
868- private sealed class CompilationUnitAndCompilationComparer
869- : IEqualityComparer < ( CompilationUnitSyntax Left , Compilation Right ) >
870- {
871- public readonly static CompilationUnitAndCompilationComparer Instance
872- = new CompilationUnitAndCompilationComparer ( ) ;
873-
874- private CompilationUnitAndCompilationComparer ( ) { }
875-
876- public bool Equals (
877- ( CompilationUnitSyntax Left , Compilation Right ) x ,
878- ( CompilationUnitSyntax Left , Compilation Right ) y )
879- => ReferenceEquals ( x . Left , y . Left ) ;
880-
881- public int GetHashCode ( ( CompilationUnitSyntax Left , Compilation Right ) obj )
882- => System . Runtime . CompilerServices . RuntimeHelpers . GetHashCode ( obj . Left ) ;
883- }
884-
885- // ── FileAndSynthesizedSourcesComparer ──────────────────────────────────────
886-
887- /// <summary>
888- /// ANDs <see cref="CompilationUnitAndCompilationComparer"/> with sequence equality on the
889- /// synthesized array — editing an [ExpressiveProperty] attribute correctly re-emits all
890- /// files since synthesized binding can affect any file's lambdas.
891- /// </summary>
892- private sealed class FileAndSynthesizedSourcesComparer
893- : IEqualityComparer < ( ( CompilationUnitSyntax Left , Compilation Right ) Left , ImmutableArray < ( string HintName , string Source ) > Right ) >
894- {
895- public readonly static FileAndSynthesizedSourcesComparer Instance = new ( ) ;
896-
897- private FileAndSynthesizedSourcesComparer ( ) { }
898-
899- public bool Equals (
900- ( ( CompilationUnitSyntax Left , Compilation Right ) Left , ImmutableArray < ( string HintName , string Source ) > Right ) x ,
901- ( ( CompilationUnitSyntax Left , Compilation Right ) Left , ImmutableArray < ( string HintName , string Source ) > Right ) y )
902- => CompilationUnitAndCompilationComparer . Instance . Equals ( x . Left , y . Left )
903- && SynthesizedSourceArrayComparer . Instance . Equals ( x . Right , y . Right ) ;
904-
905- public int GetHashCode (
906- ( ( CompilationUnitSyntax Left , Compilation Right ) Left , ImmutableArray < ( string HintName , string Source ) > Right ) obj )
907- {
908- unchecked
909- {
910- return CompilationUnitAndCompilationComparer . Instance . GetHashCode ( obj . Left ) * 31
911- + SynthesizedSourceArrayComparer . Instance . GetHashCode ( obj . Right ) ;
912- }
913- }
914- }
915892}
0 commit comments