1+ using System . Collections . Immutable ;
12using System . Text ;
3+ using ExpressiveSharp . Generator . Emitter ;
24using ExpressiveSharp . Generator . Infrastructure ;
5+ using ExpressiveSharp . Generator . Interpretation ;
6+ using ExpressiveSharp . Generator . Models ;
37using Microsoft . CodeAnalysis ;
48using Microsoft . CodeAnalysis . CSharp ;
59using Microsoft . CodeAnalysis . CSharp . Syntax ;
10+ using Microsoft . CodeAnalysis . Operations ;
611using Microsoft . CodeAnalysis . Text ;
712
813namespace ExpressiveSharp . Generator ;
@@ -26,6 +31,8 @@ public class PolyfillInterceptorGenerator : IIncrementalGenerator
2631 private const string PolyfillTypeName = "ExpressiveSharp.ExpressionPolyfill" ;
2732 private const string PolyfillMethodName = "Create" ;
2833
34+ private const string ExpressivePropertyAttributeName = "ExpressiveSharp.Mapping.ExpressivePropertyAttribute" ;
35+
2936 private const string ClosureHelperSource = """
3037
3138 file static class __ClosureHelper
@@ -133,11 +140,55 @@ inv.Expression is MemberAccessExpressionSyntax &&
133140 . Combine ( context . CompilationProvider )
134141 . WithComparer ( CompilationUnitAndCompilationComparer . Instance ) ;
135142
143+ // ── Synthesized-property sources for compilation augmentation ─────────────
144+ //
145+ // [ExpressiveProperty]-decorated stubs cause ExpressiveGenerator to emit a partial-class
146+ // file declaring the synthesized property (e.g. Rectangle.IsSquareProp). Roslyn source
147+ // generators all run against the SAME input compilation — one generator never sees
148+ // another's AddSource output — so without help, the SemanticModel below cannot bind
149+ // references to synthesized properties inside intercepted lambdas. We mirror the
150+ // synthesis pipeline here, producing the same partial-class source strings, and use
151+ // them to augment the in-memory compilation just for binding (we do NOT call AddSource
152+ // — ExpressiveGenerator owns the user-visible emission).
153+ var synthesizedDeclarations = context . SyntaxProvider
154+ . ForAttributeWithMetadataName (
155+ ExpressivePropertyAttributeName ,
156+ predicate : static ( s , _ ) => s is PropertyDeclarationSyntax ,
157+ transform : static ( c , _ ) => (
158+ Stub : ( PropertyDeclarationSyntax ) c . TargetNode ,
159+ Attribute : new ExpressivePropertyAttributeData ( c . Attributes [ 0 ] )
160+ ) ) ;
161+
162+ var synthesizedSources = synthesizedDeclarations
163+ . Combine ( context . CompilationProvider )
164+ . Select ( static ( pair , ct ) =>
165+ {
166+ ct . ThrowIfCancellationRequested ( ) ;
167+ var ( ( stub , attribute ) , compilation ) = pair ;
168+ var semanticModel = compilation . GetSemanticModel ( stub . SyntaxTree ) ;
169+ if ( semanticModel . GetDeclaredSymbol ( stub , ct ) is not IPropertySymbol stubSymbol )
170+ return default ;
171+
172+ var spec = ExpressivePropertyInterpreter . TryBuildSpec ( stub , stubSymbol , attribute ) ;
173+ if ( spec is null ) return default ;
174+
175+ var hint = $ "{ spec . ContainingTypeName } .{ spec . PropertyName } .Synthesized.augment.cs";
176+ var source = SynthesizedPropertyEmitter . BuildSource ( spec ) ;
177+ return ( HintName : hint , Source : source ) ;
178+ } )
179+ . Where ( static t => t . Source is not null )
180+ . Collect ( )
181+ . WithComparer ( SynthesizedSourceArrayComparer . Instance ) ;
182+
183+ var filesWithCompilationAndSynth = filesWithCompilation
184+ . Combine ( synthesizedSources )
185+ . WithComparer ( FileAndSynthesizedSourcesComparer . Instance ) ;
186+
136187 // All semantic analysis, code emission, and EXP_EMIT_FAILED diagnostic reporting
137188 // happen inside ProcessFileAndEmit. One output file is produced per source file,
138189 // containing all interceptors for that file.
139- context . RegisterSourceOutput ( filesWithCompilation ,
140- static ( spc , pair ) => ProcessFileAndEmit ( pair . Left , pair . Right , spc ) ) ;
190+ context . RegisterSourceOutput ( filesWithCompilationAndSynth ,
191+ static ( spc , pair ) => ProcessFileAndEmit ( pair . Left . Left , pair . Left . Right , pair . Right , spc ) ) ;
141192 }
142193
143194 // ── Per-file processing and emission ────────────────────────────────────
@@ -151,10 +202,33 @@ inv.Expression is MemberAccessExpressionSyntax &&
151202 private static void ProcessFileAndEmit (
152203 CompilationUnitSyntax unit ,
153204 Compilation compilation ,
205+ ImmutableArray < ( string HintName , string Source ) > synthesizedSources ,
154206 SourceProductionContext spc )
155207 {
156208 var ct = spc . CancellationToken ;
157- var model = compilation . GetSemanticModel ( unit . SyntaxTree ) ;
209+
210+ // Augment the compilation in-memory with the synthesized partial-class declarations
211+ // (one per [ExpressiveProperty] stub). The augmented Compilation is local to this
212+ // emitter — it never escapes via AddSource — so there is no double-emission. Without
213+ // this, the SemanticModel below cannot bind references like `Rectangle { IsSquareProp: ... }`
214+ // because ExpressiveGenerator's synthesized property is invisible to this generator.
215+ var modelCompilation = compilation ;
216+ if ( synthesizedSources . Length > 0 )
217+ {
218+ var parseOptions = compilation . SyntaxTrees . FirstOrDefault ( ) ? . Options as CSharpParseOptions
219+ ?? CSharpParseOptions . Default ;
220+ var trees = new SyntaxTree [ synthesizedSources . Length ] ;
221+ for ( var i = 0 ; i < synthesizedSources . Length ; i ++ )
222+ {
223+ trees [ i ] = CSharpSyntaxTree . ParseText (
224+ synthesizedSources [ i ] . Source ,
225+ parseOptions ,
226+ cancellationToken : ct ) ;
227+ }
228+ modelCompilation = compilation . AddSyntaxTrees ( trees ) ;
229+ }
230+
231+ var model = modelCompilation . GetSemanticModel ( unit . SyntaxTree ) ;
158232 var sourcePath = unit . SyntaxTree . FilePath ;
159233 var fileTag = GetFileTag ( sourcePath ) ;
160234
@@ -453,6 +527,22 @@ private static string GetFileTag(string sourcePath)
453527 var bodyNode = lambda . Body is ExpressionSyntax expr ? ( SyntaxNode ) expr : lambda . Body ;
454528 if ( bodyNode is null ) return null ;
455529
530+ // Pre-check: if Roslyn can't bind something in the body (e.g. a synthesized member that
531+ // isn't visible to this generator's compilation), the IOperation tree contains
532+ // IInvalidOperation nodes and the emitted expression tree would be garbage. Surface
533+ // EXP0010 so the user notices instead of silently falling back to the delegate stub.
534+ // We don't fail on a null IOperation — that can mean transparent syntax that the
535+ // emitter unwraps separately, not a binding failure.
536+ var bodyOperation = model . GetOperation ( bodyNode ) ;
537+ if ( bodyOperation is not null && ContainsInvalidOperation ( bodyOperation ) )
538+ {
539+ spc . ReportDiagnostic ( Diagnostic . Create (
540+ Diagnostics . InterceptorEmissionFailed ,
541+ lambda . GetLocation ( ) ,
542+ "lambda body contains unresolvable references — the original delegate stub will be used at runtime" ) ) ;
543+ return null ;
544+ }
545+
456546 var emitter = new Emitter . ExpressionTreeEmitter ( model , spc , varPrefix : varPrefix , delegateVarName : delegateVarName ) ;
457547
458548 if ( typeAliases is not null )
@@ -523,6 +613,20 @@ private static string GetFileTag(string sourcePath)
523613 private static bool IsAnonymousType ( ITypeSymbol type )
524614 => type is INamedTypeSymbol { IsAnonymousType : true } ;
525615
616+ /// <summary>
617+ /// Walks an <see cref="IOperation"/> tree looking for any <c>IInvalidOperation</c>, which
618+ /// Roslyn produces when binding fails (unknown member, malformed reference, etc.).
619+ /// </summary>
620+ private static bool ContainsInvalidOperation ( IOperation op )
621+ {
622+ if ( op is IInvalidOperation ) return true ;
623+ foreach ( var child in op . ChildOperations )
624+ {
625+ if ( ContainsInvalidOperation ( child ) ) return true ;
626+ }
627+ return false ;
628+ }
629+
526630 // ── Formatting helpers ───────────────────────────────────────────────────
527631
528632 private static bool HasLambdaArgument ( InvocationExpressionSyntax inv )
@@ -945,4 +1049,84 @@ public bool Equals(
9451049 public int GetHashCode ( ( CompilationUnitSyntax Left , Compilation Right ) obj )
9461050 => System . Runtime . CompilerServices . RuntimeHelpers . GetHashCode ( obj . Left ) ;
9471051 }
1052+
1053+ // ── SynthesizedSourceArrayComparer ─────────────────────────────────────────
1054+
1055+ /// <summary>
1056+ /// Sequence-equality on the collected (HintName, Source) pairs from the synthesized-property
1057+ /// pipeline. ImmutableArray's default equality is reference, so without this comparer Roslyn
1058+ /// would treat every regenerated array as distinct and invalidate every downstream consumer
1059+ /// on every input edit.
1060+ /// </summary>
1061+ private sealed class SynthesizedSourceArrayComparer
1062+ : IEqualityComparer < ImmutableArray < ( string HintName , string Source ) > >
1063+ {
1064+ public readonly static SynthesizedSourceArrayComparer Instance = new ( ) ;
1065+
1066+ private SynthesizedSourceArrayComparer ( ) { }
1067+
1068+ public bool Equals (
1069+ ImmutableArray < ( string HintName , string Source ) > x ,
1070+ ImmutableArray < ( string HintName , string Source ) > y )
1071+ {
1072+ if ( x . Length != y . Length ) return false ;
1073+ for ( var i = 0 ; i < x . Length ; i ++ )
1074+ {
1075+ if ( x [ i ] . HintName != y [ i ] . HintName ) return false ;
1076+ if ( x [ i ] . Source != y [ i ] . Source ) return false ;
1077+ }
1078+ return true ;
1079+ }
1080+
1081+ public int GetHashCode ( ImmutableArray < ( string HintName , string Source ) > obj )
1082+ {
1083+ unchecked
1084+ {
1085+ var hash = 17 ;
1086+ for ( var i = 0 ; i < obj . Length ; i ++ )
1087+ {
1088+ hash = hash * 31 + ( obj [ i ] . HintName ? . GetHashCode ( ) ?? 0 ) ;
1089+ hash = hash * 31 + ( obj [ i ] . Source ? . GetHashCode ( ) ?? 0 ) ;
1090+ }
1091+ return hash ;
1092+ }
1093+ }
1094+ }
1095+
1096+ // ── FileAndSynthesizedSourcesComparer ──────────────────────────────────────
1097+
1098+ /// <summary>
1099+ /// Combined comparer for the per-file pipeline + synthesized-source array. ANDs the existing
1100+ /// <see cref="CompilationUnitAndCompilationComparer"/> reference-equality on the file root with
1101+ /// sequence-equality on the synthesized array.
1102+ ///
1103+ /// <para>
1104+ /// Editing a file with no <c>[ExpressiveProperty]</c> change → file ref unchanged AND array
1105+ /// unchanged → skip. Editing an <c>[ExpressiveProperty]</c> attribute → array changes → all
1106+ /// files re-emit (correct: synthesized binding may affect any file's lambdas).
1107+ /// </para>
1108+ /// </summary>
1109+ private sealed class FileAndSynthesizedSourcesComparer
1110+ : IEqualityComparer < ( ( CompilationUnitSyntax Left , Compilation Right ) Left , ImmutableArray < ( string HintName , string Source ) > Right ) >
1111+ {
1112+ public readonly static FileAndSynthesizedSourcesComparer Instance = new ( ) ;
1113+
1114+ private FileAndSynthesizedSourcesComparer ( ) { }
1115+
1116+ public bool Equals (
1117+ ( ( CompilationUnitSyntax Left , Compilation Right ) Left , ImmutableArray < ( string HintName , string Source ) > Right ) x ,
1118+ ( ( CompilationUnitSyntax Left , Compilation Right ) Left , ImmutableArray < ( string HintName , string Source ) > Right ) y )
1119+ => CompilationUnitAndCompilationComparer . Instance . Equals ( x . Left , y . Left )
1120+ && SynthesizedSourceArrayComparer . Instance . Equals ( x . Right , y . Right ) ;
1121+
1122+ public int GetHashCode (
1123+ ( ( CompilationUnitSyntax Left , Compilation Right ) Left , ImmutableArray < ( string HintName , string Source ) > Right ) obj )
1124+ {
1125+ unchecked
1126+ {
1127+ return CompilationUnitAndCompilationComparer . Instance . GetHashCode ( obj . Left ) * 31
1128+ + SynthesizedSourceArrayComparer . Instance . GetHashCode ( obj . Right ) ;
1129+ }
1130+ }
1131+ }
9481132}
0 commit comments