Skip to content

Commit 0046f23

Browse files
authored
Merge pull request #3 from EFNext/feat/consolidated-generated-classes
Consolidate generated expression classes into partial classes
2 parents 4baf560 + 9978ec1 commit 0046f23

221 files changed

Lines changed: 1327 additions & 1854 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs

Lines changed: 34 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,134 +3,114 @@
33
namespace ExpressiveSharp.Generator.Emitter;
44

55
/// <summary>
6-
/// Tracks and deduplicates <c>private static readonly</c> reflection field declarations
7-
/// (<see cref="System.Reflection.MethodInfo"/>, <see cref="System.Reflection.PropertyInfo"/>,
8-
/// <see cref="System.Reflection.ConstructorInfo"/>, <see cref="System.Reflection.FieldInfo"/>)
6+
/// Returns inline reflection expressions for
7+
/// <see cref="System.Reflection.MethodInfo"/>, <see cref="System.Reflection.PropertyInfo"/>,
8+
/// <see cref="System.Reflection.ConstructorInfo"/>, and <see cref="System.Reflection.FieldInfo"/>
99
/// needed by the emitted expression-tree-building code.
10+
/// Each <c>Ensure*</c> method returns a C# expression string that evaluates to the
11+
/// reflection object at runtime, rather than a static field name.
1012
/// </summary>
1113
internal sealed class ReflectionFieldCache
1214
{
1315
private static readonly SymbolDisplayFormat _fullyQualifiedFormat =
1416
SymbolDisplayFormat.FullyQualifiedFormat;
1517

16-
private readonly string _prefix;
17-
private readonly Dictionary<string, string> _fieldNamesByKey = new();
18-
private readonly List<string> _declarations = new();
19-
private int _propertyCounter;
20-
private int _methodCounter;
21-
private int _constructorCounter;
22-
private int _fieldCounter;
18+
private readonly Dictionary<string, string> _expressionsByKey = new();
2319

2420
public ReflectionFieldCache(string prefix = "")
2521
{
26-
_prefix = prefix;
2722
}
2823

2924
/// <summary>
30-
/// Returns the field name for a cached <see cref="System.Reflection.PropertyInfo"/>,
31-
/// creating the declaration if this property hasn't been seen before.
25+
/// Returns an inline reflection expression for a <see cref="System.Reflection.PropertyInfo"/>.
3226
/// </summary>
3327
public string EnsurePropertyInfo(IPropertySymbol property)
3428
{
3529
var typeFqn = property.ContainingType.ToDisplayString(_fullyQualifiedFormat);
3630
var key = $"P:{typeFqn}.{property.Name}";
37-
if (_fieldNamesByKey.TryGetValue(key, out var fieldName))
38-
return fieldName;
31+
if (_expressionsByKey.TryGetValue(key, out var cached))
32+
return cached;
3933

40-
fieldName = $"_{_prefix}p{_propertyCounter++}";
41-
var declaration = $"""private static readonly global::System.Reflection.PropertyInfo {fieldName} = typeof({typeFqn}).GetProperty("{property.Name}");""";
42-
_fieldNamesByKey[key] = fieldName;
43-
_declarations.Add(declaration);
44-
return fieldName;
34+
var expr = $"typeof({typeFqn}).GetProperty(\"{property.Name}\")";
35+
_expressionsByKey[key] = expr;
36+
return expr;
4537
}
4638

4739
/// <summary>
48-
/// Returns the field name for a cached <see cref="System.Reflection.FieldInfo"/>,
49-
/// creating the declaration if this field hasn't been seen before.
40+
/// Returns an inline reflection expression for a <see cref="System.Reflection.FieldInfo"/>.
5041
/// </summary>
5142
public string EnsureFieldInfo(IFieldSymbol field)
5243
{
5344
var typeFqn = field.ContainingType.ToDisplayString(_fullyQualifiedFormat);
5445
var key = $"F:{typeFqn}.{field.Name}";
55-
if (_fieldNamesByKey.TryGetValue(key, out var fieldName))
56-
return fieldName;
46+
if (_expressionsByKey.TryGetValue(key, out var cached))
47+
return cached;
5748

58-
fieldName = $"_{_prefix}f{_fieldCounter++}";
5949
var flags = field.IsStatic
6050
? "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static"
6151
: "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance";
62-
var declaration = $"""private static readonly global::System.Reflection.FieldInfo {fieldName} = typeof({typeFqn}).GetField("{field.Name}", {flags});""";
63-
_fieldNamesByKey[key] = fieldName;
64-
_declarations.Add(declaration);
65-
return fieldName;
52+
var expr = $"typeof({typeFqn}).GetField(\"{field.Name}\", {flags})";
53+
_expressionsByKey[key] = expr;
54+
return expr;
6655
}
6756

6857
/// <summary>
69-
/// Returns the field name for a cached <see cref="System.Reflection.MethodInfo"/>,
70-
/// creating the declaration if this method hasn't been seen before.
58+
/// Returns an inline reflection expression for a <see cref="System.Reflection.MethodInfo"/>.
7159
/// </summary>
7260
public string EnsureMethodInfo(IMethodSymbol method)
7361
{
7462
var typeFqn = method.ContainingType.ToDisplayString(_fullyQualifiedFormat);
7563
var paramTypes = string.Join(", ", method.Parameters.Select(p =>
7664
$"typeof({p.Type.ToDisplayString(_fullyQualifiedFormat)})"));
7765
var key = $"M:{typeFqn}.{method.Name}({paramTypes})";
78-
if (_fieldNamesByKey.TryGetValue(key, out var fieldName))
79-
return fieldName;
66+
if (_expressionsByKey.TryGetValue(key, out var cached))
67+
return cached;
8068

81-
fieldName = $"_{_prefix}m{_methodCounter++}";
8269
var flags = method.IsStatic
8370
? "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static"
8471
: "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance";
8572

86-
string declaration;
73+
string expr;
8774
if (method.IsGenericMethod)
8875
{
89-
// Generic methods: find by name + generic arity + param count, then MakeGenericMethod.
90-
// We can't use GetMethod with parameter types because the definition's parameters
91-
// reference its own type parameters (e.g. IEnumerable<TSource>) which aren't valid C# types.
9276
var originalDef = method.OriginalDefinition;
9377
var genericArity = originalDef.TypeParameters.Length;
9478
var paramCount = originalDef.Parameters.Length;
9579
var typeArgs = string.Join(", ", method.TypeArguments.Select(t =>
9680
$"typeof({t.ToDisplayString(_fullyQualifiedFormat)})"));
97-
declaration = $"private static readonly global::System.Reflection.MethodInfo {fieldName} = global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof({typeFqn}).GetMethods({flags}), m => m.Name == \"{method.Name}\" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == {genericArity} && m.GetParameters().Length == {paramCount})).MakeGenericMethod({typeArgs});";
81+
expr = $"global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof({typeFqn}).GetMethods({flags}), m => m.Name == \"{method.Name}\" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == {genericArity} && m.GetParameters().Length == {paramCount})).MakeGenericMethod({typeArgs})";
9882
}
9983
else
10084
{
101-
declaration = $"private static readonly global::System.Reflection.MethodInfo {fieldName} = typeof({typeFqn}).GetMethod(\"{method.Name}\", {flags}, null, new global::System.Type[] {{ {paramTypes} }}, null);";
85+
expr = $"typeof({typeFqn}).GetMethod(\"{method.Name}\", {flags}, null, new global::System.Type[] {{ {paramTypes} }}, null)";
10286
}
10387

104-
_fieldNamesByKey[key] = fieldName;
105-
_declarations.Add(declaration);
106-
return fieldName;
88+
_expressionsByKey[key] = expr;
89+
return expr;
10790
}
10891

10992
/// <summary>
110-
/// Returns the field name for a cached <see cref="System.Reflection.ConstructorInfo"/>,
111-
/// creating the declaration if this constructor hasn't been seen before.
93+
/// Returns an inline reflection expression for a <see cref="System.Reflection.ConstructorInfo"/>.
11294
/// </summary>
11395
public string EnsureConstructorInfo(IMethodSymbol constructor)
11496
{
11597
var typeFqn = constructor.ContainingType.ToDisplayString(_fullyQualifiedFormat);
11698
var paramTypes = string.Join(", ", constructor.Parameters.Select(p =>
11799
$"typeof({p.Type.ToDisplayString(_fullyQualifiedFormat)})"));
118100
var key = $"C:{typeFqn}({paramTypes})";
119-
if (_fieldNamesByKey.TryGetValue(key, out var fieldName))
120-
return fieldName;
101+
if (_expressionsByKey.TryGetValue(key, out var cached))
102+
return cached;
121103

122-
fieldName = $"_{_prefix}c{_constructorCounter++}";
123-
var declaration = $"private static readonly global::System.Reflection.ConstructorInfo {fieldName} = typeof({typeFqn}).GetConstructor(new global::System.Type[] {{ {paramTypes} }});";
124-
_fieldNamesByKey[key] = fieldName;
125-
_declarations.Add(declaration);
126-
return fieldName;
104+
var expr = $"typeof({typeFqn}).GetConstructor(new global::System.Type[] {{ {paramTypes} }})";
105+
_expressionsByKey[key] = expr;
106+
return expr;
127107
}
128108

129109
/// <summary>
130-
/// Returns all generated <c>private static readonly</c> field declarations.
110+
/// Returns all static field declarations. Always empty since reflection is now inlined.
131111
/// </summary>
132112
public IReadOnlyList<string> GetDeclarations()
133113
{
134-
return _declarations;
114+
return Array.Empty<string>();
135115
}
136116
}

src/ExpressiveSharp.Generator/ExpressiveGenerator.cs

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -117,24 +117,29 @@ private static void Execute(
117117
factoryCandidate.Identifier.Text));
118118
}
119119

120-
var generatedClassName = ExpressionClassNameGenerator.GenerateName(expressive.ClassNamespace, expressive.NestedInClassNames, expressive.MemberName, expressive.ParameterTypeNames);
121-
var generatedFileName = expressive.ClassTypeParameterList is not null ? $"{generatedClassName}-{expressive.ClassTypeParameterList.Parameters.Count}.g.cs" : $"{generatedClassName}.g.cs";
120+
var generatedClassName = ExpressionClassNameGenerator.GenerateClassName(expressive.ClassNamespace, expressive.NestedInClassNames);
121+
var methodSuffix = ExpressionClassNameGenerator.GenerateMethodSuffix(expressive.MemberName, expressive.ParameterTypeNames);
122+
var generatedFileName = expressive.ClassTypeParameterList is not null
123+
? $"{generatedClassName}-{expressive.ClassTypeParameterList.Parameters.Count}.{methodSuffix}.g.cs"
124+
: $"{generatedClassName}.{methodSuffix}.g.cs";
122125

123126
if (expressive.ExpressionTreeEmission is null)
124127
{
125128
throw new InvalidOperationException("ExpressionTreeEmission must be set");
126129
}
127130

128-
EmitExpressionTreeSource(expressive, generatedClassName, generatedFileName, member, compilation, context);
131+
EmitExpressionTreeSource(expressive, generatedClassName, methodSuffix, generatedFileName, member, compilation, context);
129132
}
130133

131134
/// <summary>
132135
/// Emits the generated source file using raw text when <see cref="Emitter.EmitResult"/> is available.
133-
/// This path generates imperative <c>Expression.*</c> factory calls instead of a lambda return.
136+
/// Each file declares the same <c>static partial class</c> — one per declaring type — and adds
137+
/// a uniquely-named <c>{methodSuffix}_Expression()</c> method for this member.
134138
/// </summary>
135139
private static void EmitExpressionTreeSource(
136140
ExpressiveDescriptor expressive,
137141
string generatedClassName,
142+
string methodSuffix,
138143
string generatedFileName,
139144
MemberDeclarationSyntax member,
140145
Compilation? compilation,
@@ -186,21 +191,9 @@ private static void EmitExpressionTreeSource(
186191
? string.Join(" ", expressive.ConstraintClauses.Value.Select(c => c.NormalizeWhitespace().ToFullString()))
187192
: "";
188193

189-
sb.AppendLine($" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");
190-
sb.AppendLine($" static class {generatedClassName}{typeParamList} {constraintClauses}");
194+
sb.AppendLine($" static partial class {generatedClassName}{typeParamList} {constraintClauses}");
191195
sb.AppendLine(" {");
192196

193-
// Static fields for cached reflection info
194-
foreach (var field in emission.StaticFields)
195-
{
196-
sb.AppendLine($" {field}");
197-
}
198-
199-
if (emission.StaticFields.Count > 0)
200-
{
201-
sb.AppendLine();
202-
}
203-
204197
// Source comment showing the original C# member
205198
var sourceText = member.NormalizeWhitespace().ToFullString();
206199
foreach (var line in sourceText.Split('\n'))
@@ -209,19 +202,19 @@ private static void EmitExpressionTreeSource(
209202
sb.AppendLine($" // {trimmed}");
210203
}
211204

212-
// Expression() method
213-
sb.AppendLine($" static {returnType} Expression{methodTypeParamList}() {methodConstraintClauses}");
205+
// {methodSuffix}_Expression() method
206+
sb.AppendLine($" static {returnType} {methodSuffix}_Expression{methodTypeParamList}() {methodConstraintClauses}");
214207
sb.AppendLine(" {");
215208
sb.Append(emission.Body);
216209
sb.AppendLine(" }");
217210

218-
// Transformers property (when declared via attribute)
211+
// Transformers (when declared via attribute)
219212
if (expressive.DeclaredTransformerTypeNames.Count > 0)
220213
{
221214
sb.AppendLine();
222215
var transformerInstances = string.Join(", ",
223216
expressive.DeclaredTransformerTypeNames.Select(t => $"new {t}()"));
224-
sb.AppendLine($" static global::ExpressiveSharp.IExpressionTreeTransformer[] Transformers() => [{transformerInstances}];");
217+
sb.AppendLine($" static global::ExpressiveSharp.IExpressionTreeTransformer[] {methodSuffix}_Transformers() => [{transformerInstances}];");
225218
}
226219

227220
sb.AppendLine(" }");
@@ -239,16 +232,22 @@ private static void EmitExpressionTreeSource(
239232
{
240233
var containingType = memberSymbol.ContainingType;
241234

242-
// Skip C# 14 extension type members — they require special handling (fall back to reflection)
235+
// Determine whether this entry is metadata-only (excluded from runtime registry
236+
// but still used for [EditorBrowsable] attribute-only partial file emission).
237+
var isMetadataOnly = false;
238+
string? classTypeParameters = null;
239+
240+
// C# 14 extension type members — metadata-only (fall back to reflection at runtime)
243241
if (containingType is { IsExtension: true })
244242
{
245-
return null;
243+
isMetadataOnly = true;
246244
}
247245

248-
// Skip generic classes: the registry only supports closed constructed types.
246+
// Generic classes — metadata-only (registry can't represent open generic types)
249247
if (containingType.TypeParameters.Length > 0)
250248
{
251-
return null;
249+
isMetadataOnly = true;
250+
classTypeParameters = "<" + string.Join(", ", containingType.TypeParameters.Select(tp => tp.Name)) + ">";
252251
}
253252

254253
// Determine member kind and lookup name
@@ -258,10 +257,10 @@ private static void EmitExpressionTreeSource(
258257

259258
if (memberSymbol is IMethodSymbol methodSymbol)
260259
{
261-
// Skip generic methods for the same reason as generic classes
260+
// Generic methods — metadata-only (same reason as generic classes)
262261
if (methodSymbol.TypeParameters.Length > 0)
263262
{
264-
return null;
263+
isMetadataOnly = true;
265264
}
266265

267266
if (methodSymbol.MethodKind is MethodKind.Constructor or MethodKind.StaticConstructor)
@@ -285,20 +284,22 @@ private static void EmitExpressionTreeSource(
285284
memberLookupName = memberSymbol.Name;
286285
}
287286

288-
// Build the generated class name using the same logic as Execute
287+
// Build the generated class name and method name using the same logic as Execute
289288
var classNamespace = containingType.ContainingNamespace.IsGlobalNamespace
290289
? null
291290
: containingType.ContainingNamespace.ToDisplayString();
292291

293292
var nestedTypePath = GetRegistryNestedTypePath(containingType);
294293

295-
var generatedClassName = ExpressionClassNameGenerator.GenerateName(
294+
var generatedClassFullName = ExpressionClassNameGenerator.GenerateClassFullName(
296295
classNamespace,
297-
nestedTypePath,
296+
nestedTypePath);
297+
298+
var methodSuffix = ExpressionClassNameGenerator.GenerateMethodSuffix(
298299
memberLookupName,
299300
parameterTypeNames.IsEmpty ? null : parameterTypeNames);
300301

301-
var generatedClassFullName = "ExpressiveSharp.Generated." + generatedClassName;
302+
var expressionMethodName = methodSuffix + "_Expression";
302303

303304
var declaringTypeFullName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
304305

@@ -307,7 +308,10 @@ private static void EmitExpressionTreeSource(
307308
MemberKind: memberKind,
308309
MemberLookupName: memberLookupName,
309310
GeneratedClassFullName: generatedClassFullName,
310-
ParameterTypeNames: parameterTypeNames);
311+
ExpressionMethodName: expressionMethodName,
312+
ParameterTypeNames: parameterTypeNames,
313+
IsMetadataOnly: isMetadataOnly,
314+
ClassTypeParameters: classTypeParameters);
311315
}
312316

313317
private static IEnumerable<string> GetRegistryNestedTypePath(INamedTypeSymbol typeSymbol)

0 commit comments

Comments
 (0)