Skip to content

Commit 6d23d8f

Browse files
committed
fixed compat with PolyfillInterceptor
1 parent a9b4e69 commit 6d23d8f

6 files changed

Lines changed: 417 additions & 4 deletions

‎src/ExpressiveSharp.Generator/Emitter/SynthesizedPropertyEmitter.cs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ namespace ExpressiveSharp.Generator.Emitter;
1414
static internal class SynthesizedPropertyEmitter
1515
{
1616
public static void Emit(SynthesizedPropertySpec spec, string generatedFileName, SourceProductionContext context)
17+
{
18+
var source = BuildSource(spec);
19+
context.AddSource(generatedFileName, SourceText.From(source, Encoding.UTF8));
20+
}
21+
22+
/// <summary>
23+
/// Produces the synthesized partial-class C# source as a string. Pure function of the spec —
24+
/// safe to call from incremental pipeline transforms (no <see cref="SourceProductionContext"/> required).
25+
/// </summary>
26+
public static string BuildSource(SynthesizedPropertySpec spec)
1727
{
1828
var sb = new StringBuilder();
1929
sb.AppendLine("// <auto-generated/>");
@@ -57,7 +67,7 @@ public static void Emit(SynthesizedPropertySpec spec, string generatedFileName,
5767
sb.AppendLine("}");
5868
}
5969

60-
context.AddSource(generatedFileName, SourceText.From(sb.ToString(), Encoding.UTF8));
70+
return sb.ToString();
6171
}
6272

6373
private static string BuildCoalesceBody(SynthesizedPropertySpec spec, string indent)

‎src/ExpressiveSharp.Generator/Interpretation/ExpressivePropertyInterpreter.cs‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,54 @@ public static (ExpressiveDescriptor Descriptor, SynthesizedPropertySpec Spec)? G
130130
return (descriptor, spec);
131131
}
132132

133+
/// <summary>
134+
/// Builds the <see cref="SynthesizedPropertySpec"/> for a stub if it passes the same validation
135+
/// rules as <see cref="GetDescriptor"/>, but reports no diagnostics. Used by
136+
/// <c>PolyfillInterceptorGenerator</c> to compute the synthesized partial-class source for
137+
/// in-memory compilation augmentation; diagnostic ownership stays with <see cref="GetDescriptor"/>
138+
/// inside <c>ExpressiveGenerator</c>.
139+
/// </summary>
140+
public static SynthesizedPropertySpec? TryBuildSpec(
141+
PropertyDeclarationSyntax stubProperty,
142+
IPropertySymbol stubSymbol,
143+
ExpressivePropertyAttributeData attributeData)
144+
{
145+
var targetName = attributeData.TargetName;
146+
if (string.IsNullOrWhiteSpace(targetName)) return null;
147+
if (stubSymbol.IsStatic) return null;
148+
if (stubProperty.ExpressionBody is null) return null;
149+
150+
var containingType = stubSymbol.ContainingType;
151+
if (!IsPartialType(containingType)) return null;
152+
153+
if (containingType.GetMembers(targetName!).Any(m =>
154+
m is IPropertySymbol or IMethodSymbol or IFieldSymbol or IEventSymbol))
155+
return null;
156+
157+
if (FindInheritedMember(containingType, targetName!) is not null) return null;
158+
159+
var returnType = stubSymbol.Type;
160+
var useTernary = IsNullablePropertyType(returnType);
161+
var propertyTypeFqn = returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
162+
var backingFieldTypeFqn = useTernary ? propertyTypeFqn : MakeNullableTypeFqn(returnType);
163+
164+
return new SynthesizedPropertySpec
165+
{
166+
PropertyTypeFqn = propertyTypeFqn,
167+
PropertyName = targetName!,
168+
StubMemberName = stubSymbol.Name,
169+
StubIsMethod = false,
170+
UseTernaryShape = useTernary,
171+
BackingFieldTypeFqn = backingFieldTypeFqn,
172+
ContainingTypeName = containingType.Name,
173+
ContainingTypeNamespace = containingType.ContainingNamespace.IsGlobalNamespace
174+
? null
175+
: containingType.ContainingNamespace.ToDisplayString(),
176+
ContainingTypePath = GetNestedInClassPath(containingType).ToList(),
177+
ContainingTypeKeyword = GetTypeKeyword(containingType),
178+
};
179+
}
180+
133181
private static ExpressiveDescriptor? BuildDescriptor(
134182
SemanticModel semanticModel,
135183
SourceProductionContext context,

‎src/ExpressiveSharp.Generator/PolyfillInterceptorGenerator.cs‎

Lines changed: 187 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
using System.Collections.Immutable;
12
using System.Text;
3+
using ExpressiveSharp.Generator.Emitter;
24
using ExpressiveSharp.Generator.Infrastructure;
5+
using ExpressiveSharp.Generator.Interpretation;
6+
using ExpressiveSharp.Generator.Models;
37
using Microsoft.CodeAnalysis;
48
using Microsoft.CodeAnalysis.CSharp;
59
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Operations;
611
using Microsoft.CodeAnalysis.Text;
712

813
namespace 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
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// <auto-generated/>
2+
#nullable disable
3+
4+
namespace ExpressiveSharp.Generated.Interceptors
5+
{
6+
internal static partial class PolyfillInterceptors
7+
{
8+
[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(/* scrubbed */)]
9+
internal static global::ExpressiveSharp.IExpressiveQueryable<string> __Polyfill_Select_361d_19_18(
10+
this global::ExpressiveSharp.IExpressiveQueryable<global::TestNs.Account> source,
11+
global::System.Func<global::TestNs.Account, string> __func)
12+
{
13+
// Source: a => a.FullName
14+
var i361d19c18_p_a = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.Account), "a");
15+
var i361d19c18_expr_0 = global::System.Linq.Expressions.Expression.Property(i361d19c18_p_a, typeof(global::TestNs.Account).GetProperty("FullName")); // a.FullName
16+
var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::TestNs.Account, string>>(i361d19c18_expr_0, i361d19c18_p_a);
17+
return global::ExpressiveSharp.ExpressiveQueryableExtensions.AsExpressive(
18+
global::System.Linq.Queryable.Select(
19+
(global::System.Linq.IQueryable<global::TestNs.Account>)source,
20+
__lambda));
21+
}
22+
}
23+
}
24+
25+
namespace System.Runtime.CompilerServices
26+
{
27+
[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
28+
file sealed class InterceptsLocationAttribute : global::System.Attribute
29+
{
30+
public InterceptsLocationAttribute(int version, string data) { }
31+
}
32+
}

0 commit comments

Comments
 (0)