Skip to content

Commit 98d1657

Browse files
authored
Merge pull request #45 from EFNext/feat/synthesized-sources
ExpressiveProperties are now visible to other Expressives
2 parents 8d7ad60 + b7fd5df commit 98d1657

6 files changed

Lines changed: 320 additions & 58 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Collections.Immutable;
2+
3+
namespace ExpressiveSharp.Generator.Comparers;
4+
5+
/// <summary>
6+
/// Sequence equality on the synthesized (HintName, Source) array. ImmutableArray's default
7+
/// equality is reference, which would invalidate every downstream consumer on every edit.
8+
/// </summary>
9+
internal sealed class SynthesizedSourceArrayComparer
10+
: IEqualityComparer<ImmutableArray<(string HintName, string Source)>>
11+
{
12+
public readonly static SynthesizedSourceArrayComparer Instance = new();
13+
14+
private SynthesizedSourceArrayComparer() { }
15+
16+
public bool Equals(
17+
ImmutableArray<(string HintName, string Source)> x,
18+
ImmutableArray<(string HintName, string Source)> y)
19+
{
20+
if (x.IsDefault != y.IsDefault) return false;
21+
if (x.IsDefault) return true;
22+
if (x.Length != y.Length) return false;
23+
for (var i = 0; i < x.Length; i++)
24+
{
25+
if (x[i].HintName != y[i].HintName) return false;
26+
if (x[i].Source != y[i].Source) return false;
27+
}
28+
return true;
29+
}
30+
31+
public int GetHashCode(ImmutableArray<(string HintName, string Source)> obj)
32+
{
33+
if (obj.IsDefault) return 0;
34+
unchecked
35+
{
36+
var hash = 17;
37+
for (var i = 0; i < obj.Length; i++)
38+
{
39+
hash = hash * 31 + (obj[i].HintName?.GetHashCode() ?? 0);
40+
hash = hash * 31 + (obj[i].Source?.GetHashCode() ?? 0);
41+
}
42+
return hash;
43+
}
44+
}
45+
}

src/ExpressiveSharp.Generator/ExpressiveGenerator.cs

Lines changed: 153 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
using ExpressiveSharp.Services;
22
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
34
using Microsoft.CodeAnalysis.CSharp.Syntax;
45
using Microsoft.CodeAnalysis.Text;
56
using System.Collections.Immutable;
67
using System.Text;
7-
using ExpressiveSharp.Generator.Infrastructure;
8+
using System.Threading;
89
using ExpressiveSharp.Generator.Comparers;
10+
using ExpressiveSharp.Generator.Emitter;
11+
using ExpressiveSharp.Generator.Infrastructure;
912
using ExpressiveSharp.Generator.Interpretation;
1013
using ExpressiveSharp.Generator.Models;
1114
using 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;

src/ExpressiveSharp.Generator/Interpretation/ExpressivePropertyInterpreter.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ static internal class ExpressivePropertyInterpreter
1919
SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions
2020
| SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
2121

22+
// semanticModel must be from the *original* (non-augmented) compilation so that the EXP0031
23+
// conflict check and ChooseBackingNames don't observe siblings synthesized by this pipeline.
24+
// bodyBindingSemanticModel may come from a compilation augmented with synthesized partials so
25+
// that one [ExpressiveProperty] body can reference another's synthesized target. Pass null to
26+
// bind the body against the original compilation.
2227
public static (ExpressiveDescriptor Descriptor, SynthesizedPropertySpec Spec)? GetDescriptor(
2328
SemanticModel semanticModel,
2429
PropertyDeclarationSyntax stubProperty,
2530
IPropertySymbol stubSymbol,
2631
ExpressivePropertyAttributeData attributeData,
27-
SourceProductionContext context)
32+
SourceProductionContext context,
33+
SemanticModel? bodyBindingSemanticModel = null)
2834
{
2935
var stubLocation = stubProperty.Identifier.GetLocation();
3036
var containingType = stubSymbol.ContainingType;
@@ -92,7 +98,7 @@ public static (ExpressiveDescriptor Descriptor, SynthesizedPropertySpec Spec)? G
9298
}
9399

94100
var descriptor = BuildDescriptor(
95-
semanticModel, context, stubProperty, stubSymbol,
101+
bodyBindingSemanticModel ?? semanticModel, context, stubProperty, stubSymbol,
96102
attributeData, containingType, targetName!);
97103
if (descriptor is null) return null;
98104

0 commit comments

Comments
 (0)