11using ExpressiveSharp . Services ;
22using Microsoft . CodeAnalysis ;
3+ using Microsoft . CodeAnalysis . CSharp ;
34using Microsoft . CodeAnalysis . CSharp . Syntax ;
45using Microsoft . CodeAnalysis . Text ;
56using System . Collections . Immutable ;
67using System . Text ;
7- using ExpressiveSharp . Generator . Infrastructure ;
8+ using System . Threading ;
89using ExpressiveSharp . Generator . Comparers ;
10+ using ExpressiveSharp . Generator . Emitter ;
11+ using ExpressiveSharp . Generator . Infrastructure ;
912using ExpressiveSharp . Generator . Interpretation ;
1013using ExpressiveSharp . Generator . Models ;
1114using ExpressiveSharp . Generator . Registry ;
@@ -25,8 +28,26 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
2528 var globalOptions = context . AnalyzerConfigOptionsProvider
2629 . Select ( static ( opts , _ ) => new ExpressiveGlobalOptions ( opts . GlobalOptions ) ) ;
2730
28- // No live Roslyn objects (AttributeData, SemanticModel, Compilation, ISymbol) in the
29- // transform — they're always new instances and defeat incremental caching entirely.
31+ // [ExpressiveProperty] synthesizes new property declarations into separate generated
32+ // files. Source generators don't see each other's (or their own pipelines') AddSource
33+ // output, so the SemanticModel built from the input compilation can't bind references
34+ // from one [ExpressiveProperty] body to another's synthesized target. Mirror the
35+ // synthesis here and augment our local compilation for binding only — never AddSource.
36+ var synthesizedSources = BuildSynthesizedSourcesPipeline ( context ) ;
37+
38+ // Augment the compilation once per (compilation, synth) pair. The result is reused
39+ // across every member of every pipeline, so we don't re-parse synthesized partials
40+ // and rebuild a Compilation per [Expressive] / [ExpressiveFor] / [ExpressiveProperty]
41+ // member.
42+ var bindingCompilationProvider = context . CompilationProvider
43+ . Combine ( synthesizedSources )
44+ . Select ( static ( pair , ct ) => AugmentCompilation ( pair . Left , pair . Right , ct ) ) ;
45+
46+ // ── [Expressive] pipeline ──────────────────────────────────────────────
47+
48+ // Extract only pure stable data from the attribute in the transform.
49+ // No live Roslyn objects (no AttributeData, SemanticModel, Compilation, ISymbol) —
50+ // those are always new instances and defeat incremental caching entirely.
3051 var memberDeclarations = context . SyntaxProvider
3152 . ForAttributeWithMetadataName (
3253 ExpressiveAttributeName ,
@@ -44,30 +65,33 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4465 GlobalOptions : pair . Right
4566 ) ) ;
4667
68+ // Combine with the augmented compilation directly: validation has no synthesized-sibling
69+ // conflicts in this pipeline (only [ExpressiveProperty] does), so binding-against-augmented
70+ // is correct everywhere.
4771 var compilationAndMemberPairs = memberDeclarationsWithGlobalOptions
48- . Combine ( context . CompilationProvider )
72+ . Combine ( bindingCompilationProvider )
4973 . WithComparer ( new MemberDeclarationSyntaxAndCompilationEqualityComparer ( ) ) ;
5074
5175 context . RegisterImplementationSourceOutput ( compilationAndMemberPairs ,
5276 static ( spc , source ) =>
5377 {
54- var ( ( member , attribute , globalOptions ) , compilation ) = source ;
55- var semanticModel = compilation . GetSemanticModel ( member . SyntaxTree ) ;
78+ var ( ( member , attribute , globalOptions ) , bindingCompilation ) = source ;
79+ var semanticModel = bindingCompilation . GetSemanticModel ( member . SyntaxTree ) ;
5680 var memberSymbol = semanticModel . GetDeclaredSymbol ( member ) ;
5781
5882 if ( memberSymbol is null )
5983 {
6084 return ;
6185 }
6286
63- Execute ( member , semanticModel , memberSymbol , attribute , globalOptions , compilation , spc ) ;
87+ Execute ( member , semanticModel , memberSymbol , attribute , globalOptions , bindingCompilation , spc ) ;
6488 } ) ;
6589
6690 var registryEntries = compilationAndMemberPairs . Select (
6791 static ( source , cancellationToken ) => {
68- var ( ( member , _, _) , compilation ) = source ;
92+ var ( ( member , _, _) , bindingCompilation ) = source ;
6993
70- var semanticModel = compilation . GetSemanticModel ( member . SyntaxTree ) ;
94+ var semanticModel = bindingCompilation . GetSemanticModel ( member . SyntaxTree ) ;
7195 var memberSymbol = semanticModel . GetDeclaredSymbol ( member , cancellationToken ) ;
7296
7397 if ( memberSymbol is null )
@@ -79,17 +103,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
79103 } ) ;
80104
81105 var expressiveForDeclarations = CreateExpressiveForPipeline (
82- context , globalOptions , ExpressiveForAttributeName , ExpressiveForMemberKind . MethodOrProperty ) ;
106+ context , globalOptions , bindingCompilationProvider , ExpressiveForAttributeName , ExpressiveForMemberKind . MethodOrProperty ) ;
83107
84108 var expressiveForConstructorDeclarations = CreateExpressiveForPipeline (
85- context , globalOptions , ExpressiveForConstructorAttributeName , ExpressiveForMemberKind . Constructor ) ;
109+ context , globalOptions , bindingCompilationProvider , ExpressiveForConstructorAttributeName , ExpressiveForMemberKind . Constructor ) ;
86110
87111 var expressiveForRegistryEntries = expressiveForDeclarations . Select (
88112 static ( source , _ ) => ExtractRegistryEntryForExternal ( source ) ) ;
89113 var expressiveForConstructorRegistryEntries = expressiveForConstructorDeclarations . Select (
90114 static ( source , _ ) => ExtractRegistryEntryForExternal ( source ) ) ;
91115
92- var expressivePropertyDeclarations = CreateExpressivePropertyPipeline ( context ) ;
116+ // ── [ExpressiveProperty] pipeline ───────────────────────────────────────
117+
118+ var expressivePropertyDeclarations = CreateExpressivePropertyPipeline ( context , bindingCompilationProvider ) ;
93119
94120 var expressivePropertyRegistryEntries = expressivePropertyDeclarations . Select (
95121 static ( source , _ ) => ExtractRegistryEntryForExpressiveProperty ( source ) ) ;
@@ -119,6 +145,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
119145 CreateExpressiveForPipeline (
120146 IncrementalGeneratorInitializationContext context ,
121147 IncrementalValueProvider < ExpressiveGlobalOptions > globalOptions ,
148+ IncrementalValueProvider < Compilation > bindingCompilationProvider ,
122149 string attributeFullName ,
123150 ExpressiveForMemberKind memberKind )
124151 {
@@ -139,8 +166,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
139166 GlobalOptions : pair . Right
140167 ) ) ;
141168
169+ // Combine with the augmented compilation directly. The augmented compilation is
170+ // shared across the entire run via bindingCompilationProvider's Select cache, so
171+ // we don't re-parse synthesized partials per member.
142172 var compilationAndPairs = declarationsWithGlobalOptions
143- . Combine ( context . CompilationProvider )
173+ . Combine ( bindingCompilationProvider )
144174 . WithComparer ( new ExpressiveForMemberCompilationEqualityComparer ( ) ) ;
145175
146176 // Collect all items and emit in a single batch to detect duplicates before AddSource.
@@ -153,15 +183,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
153183
154184 foreach ( var source in items )
155185 {
156- var ( ( member , attribute , globalOptions ) , compilation ) = source ;
157- var semanticModel = compilation . GetSemanticModel ( member . SyntaxTree ) ;
186+ var ( ( member , attribute , globalOptions ) , bindingCompilation ) = source ;
187+ var semanticModel = bindingCompilation . GetSemanticModel ( member . SyntaxTree ) ;
158188 var stubSymbol = semanticModel . GetDeclaredSymbol ( member ) ;
159189
160190 if ( stubSymbol is not ( IMethodSymbol or IPropertySymbol ) )
161191 continue ;
162192
163193 ExecuteFor ( member , semanticModel , stubSymbol , attribute , globalOptions ,
164- compilation , spc , emittedFileNames ) ;
194+ bindingCompilation , spc , emittedFileNames ) ;
165195 }
166196 } ) ;
167197
@@ -555,8 +585,97 @@ private static IEnumerable<string> GetRegistryNestedTypePath(INamedTypeSymbol ty
555585 yield return typeSymbol . Name ;
556586 }
557587
588+ private static IncrementalValueProvider < ImmutableArray < ( string HintName , string Source ) > >
589+ BuildSynthesizedSourcesPipeline ( IncrementalGeneratorInitializationContext context )
590+ {
591+ var augmentations = context . SyntaxProvider
592+ . ForAttributeWithMetadataName (
593+ ExpressivePropertyAttributeName ,
594+ predicate : static ( s , _ ) => s is PropertyDeclarationSyntax ,
595+ transform : static ( c , _ ) => (
596+ Stub : ( PropertyDeclarationSyntax ) c . TargetNode ,
597+ Attribute : new ExpressivePropertyAttributeData ( c . Attributes [ 0 ] ) ) )
598+ . Combine ( context . CompilationProvider )
599+ . Select ( static ( pair , ct ) =>
600+ {
601+ ct . ThrowIfCancellationRequested ( ) ;
602+ var ( ( stub , attribute ) , compilation ) = pair ;
603+ var sm = compilation . GetSemanticModel ( stub . SyntaxTree ) ;
604+ if ( sm . GetDeclaredSymbol ( stub , ct ) is not IPropertySymbol stubSymbol )
605+ return default ( ( string HintName , string Source , string DedupKey ) ) ;
606+ var spec = ExpressivePropertyInterpreter . TryBuildSpec ( stub , stubSymbol , attribute ) ;
607+ if ( spec is null ) return default ;
608+ var hint = $ "{ spec . ContainingTypeName } .{ spec . PropertyName } .Synthesized.augment.cs";
609+ var dedupKey = BuildDedupKey ( spec ) ;
610+ return ( HintName : hint , Source : SynthesizedPropertyEmitter . BuildSource ( spec ) , DedupKey : dedupKey ) ;
611+ } )
612+ . Where ( static t => t . Source is not null ) ;
613+
614+ return augmentations
615+ . Collect ( )
616+ . Select ( static ( arr , _ ) =>
617+ {
618+ if ( arr . Length == 0 )
619+ return ImmutableArray < ( string HintName , string Source ) > . Empty ;
620+ // Two stubs targeting the same (containing type, property name) is an EXP-error
621+ // case but both pass TryBuildSpec. Dedup so the augmented compilation doesn't
622+ // get duplicate-member binding errors that would mask the real diagnostic.
623+ var seen = new HashSet < string > ( ) ;
624+ var builder = ImmutableArray . CreateBuilder < ( string HintName , string Source ) > ( arr . Length ) ;
625+ foreach ( var item in arr . Where ( item => seen . Add ( item . DedupKey ) ) )
626+ builder . Add ( ( item . HintName , item . Source ) ) ;
627+ return builder . Count == arr . Length ? builder . MoveToImmutable ( ) : builder . ToImmutable ( ) ;
628+ } )
629+ . WithComparer ( SynthesizedSourceArrayComparer . Instance ) ;
630+ }
631+
632+ private static string BuildDedupKey ( SynthesizedPropertySpec spec )
633+ {
634+ var sb = new StringBuilder ( ) ;
635+ if ( spec . ContainingTypeNamespace is not null )
636+ {
637+ sb . Append ( spec . ContainingTypeNamespace ) ;
638+ sb . Append ( '.' ) ;
639+ }
640+ for ( var i = 0 ; i < spec . ContainingTypePath . Count ; i ++ )
641+ {
642+ if ( i > 0 ) sb . Append ( '+' ) ;
643+ sb . Append ( spec . ContainingTypePath [ i ] ) ;
644+ }
645+ sb . Append ( '.' ) ;
646+ sb . Append ( spec . PropertyName ) ;
647+ return sb . ToString ( ) ;
648+ }
649+
650+ private static Compilation AugmentCompilation (
651+ Compilation compilation ,
652+ ImmutableArray < ( string HintName , string Source ) > synthesizedSources ,
653+ CancellationToken ct )
654+ {
655+ if ( synthesizedSources . IsDefaultOrEmpty )
656+ return compilation ;
657+ var parseOptions = compilation . SyntaxTrees . FirstOrDefault ( ) ? . Options as CSharpParseOptions
658+ ?? CSharpParseOptions . Default ;
659+ var trees = new SyntaxTree [ synthesizedSources . Length ] ;
660+ for ( var i = 0 ; i < trees . Length ; i ++ )
661+ {
662+ trees [ i ] = CSharpSyntaxTree . ParseText (
663+ synthesizedSources [ i ] . Source ,
664+ parseOptions ,
665+ cancellationToken : ct ) ;
666+ }
667+ return compilation . AddSyntaxTrees ( trees ) ;
668+ }
669+
670+ /// <summary>
671+ /// Incremental pipeline for <c>[ExpressiveProperty]</c>. Discovers property stubs, runs the
672+ /// interpreter, and emits both the expression-tree factory and the synthesized partial-class
673+ /// declaration.
674+ /// </summary>
558675 private static IncrementalValuesProvider < ( ( PropertyDeclarationSyntax Stub , ExpressivePropertyAttributeData Attribute ) , Compilation ) >
559- CreateExpressivePropertyPipeline ( IncrementalGeneratorInitializationContext context )
676+ CreateExpressivePropertyPipeline (
677+ IncrementalGeneratorInitializationContext context ,
678+ IncrementalValueProvider < Compilation > bindingCompilationProvider )
560679 {
561680 var declarations = context . SyntaxProvider
562681 . ForAttributeWithMetadataName (
@@ -569,18 +688,29 @@ private static IEnumerable<string> GetRegistryNestedTypePath(INamedTypeSymbol ty
569688
570689 var compilationAndPairs = declarations . Combine ( context . CompilationProvider ) ;
571690
572- context . RegisterSourceOutput ( compilationAndPairs . Collect ( ) ,
691+ // Pair every (decl, originalCompilation) with the augmented compilation. Validation
692+ // and ChooseBackingNames must use the original compilation (augmentation would otherwise
693+ // false-positive EXP0031 against synthesized siblings); body-binding uses the augmented
694+ // SemanticModel so cross-references between [ExpressiveProperty] stubs resolve.
695+ var compilationAndPairsWithBinding = compilationAndPairs . Combine ( bindingCompilationProvider ) ;
696+
697+ context . RegisterSourceOutput ( compilationAndPairsWithBinding . Collect ( ) ,
573698 static ( spc , items ) =>
574699 {
575700 var emittedFileNames = new HashSet < string > ( ) ;
576701 foreach ( var source in items )
577702 {
578- var ( ( stub , attribute ) , compilation ) = source ;
703+ var ( ( ( stub , attribute ) , compilation ) , bindingCompilation ) = source ;
579704 var semanticModel = compilation . GetSemanticModel ( stub . SyntaxTree ) ;
580705 if ( semanticModel . GetDeclaredSymbol ( stub ) is not IPropertySymbol stubSymbol )
581706 continue ;
582707
583- ExecuteExpressiveProperty ( stub , stubSymbol , semanticModel , attribute , spc , emittedFileNames ) ;
708+ var bindingSemanticModel = ReferenceEquals ( bindingCompilation , compilation )
709+ ? semanticModel
710+ : bindingCompilation . GetSemanticModel ( stub . SyntaxTree ) ;
711+
712+ ExecuteExpressiveProperty ( stub , stubSymbol , semanticModel , bindingSemanticModel ,
713+ attribute , spc , emittedFileNames ) ;
584714 }
585715 } ) ;
586716
@@ -591,12 +721,14 @@ private static void ExecuteExpressiveProperty(
591721 PropertyDeclarationSyntax stub ,
592722 IPropertySymbol stubSymbol ,
593723 SemanticModel semanticModel ,
724+ SemanticModel bodyBindingSemanticModel ,
594725 ExpressivePropertyAttributeData attribute ,
595726 SourceProductionContext context ,
596727 HashSet < string > emittedFileNames )
597728 {
598729 var result = ExpressivePropertyInterpreter . GetDescriptor (
599- semanticModel , stub , stubSymbol , attribute , context ) ;
730+ semanticModel , stub , stubSymbol , attribute , context ,
731+ bodyBindingSemanticModel : bodyBindingSemanticModel ) ;
600732 if ( result is null ) return ;
601733
602734 var ( descriptor , synthesisSpec ) = result . Value ;
0 commit comments