Skip to content

Commit bfb4dfc

Browse files
Copilotdex3r
andauthored
Remove UseProvidedBody string matching from DelegateBodySyntaxExtractor (#96)
* Initial plan * Remove UseProvidedBody string matching from DelegateBodySyntaxExtractor Refactor the delegate body extraction to use structural syntax analysis instead of searching for the "UseProvidedBody" method name string. Changes: - DelegateBodySyntaxExtractor now finds the lambda by looking at the outermost invocation's lambda argument in the method's return expression, instead of scanning for "UseProvidedBody" string - FluentBodyResult gets HasDelegateBody flag indicating RuntimeDelegateBody was set in BodyGenerationData - BodyGenerationDataExtractor sets HasDelegateBody when RuntimeDelegateBody is non-null - GeneratesMethodGenerationPipeline executes the method first, then uses HasDelegateBody to decide if syntax extraction is needed - Add missing WithCompileTimeConstants implementations to fix pre-existing build errors in DataMethodBodyBuilders Co-authored-by: dex3r <[email protected]> Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/ea25cd90-2a07-42c1-8091-f255cb2c6687 * Improve doc comment on GetReturnExpression about simple structure assumption Co-authored-by: dex3r <[email protected]> Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/ea25cd90-2a07-42c1-8091-f255cb2c6687 --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: dex3r <[email protected]>
1 parent 651af09 commit bfb4dfc

5 files changed

Lines changed: 116 additions & 38 deletions

File tree

EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public IMethodBodyBuilderStage4<TParam1, T> WithParameter<TParam1>() =>
3838

3939
public record DataMethodBodyBuilderStage4<TParam1, TReturnType>(BodyGenerationData Data) : IMethodBodyBuilderStage4<TParam1, TReturnType>
4040
{
41+
public IMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory) =>
42+
new DataMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants>(Data with { CompileTimeConstants = compileTimeConstantsFactory() });
43+
4144
public IMethodBodyGenerator UseProvidedBody(Func<TParam1, TReturnType> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });
4245

4346
public IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValueFactory) =>
@@ -46,6 +49,9 @@ public IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValu
4649

4750
public record DataMethodBodyBuilderStage4NoArg<TReturnType>(BodyGenerationData Data) : IMethodBodyBuilderStage4NoArg<TReturnType>
4851
{
52+
public IMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory) =>
53+
new DataMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants>(Data with { CompileTimeConstants = compileTimeConstantsFactory() });
54+
4955
public IMethodBodyGenerator UseProvidedBody(Func<TReturnType> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });
5056

5157
public IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValueFactory) =>
@@ -54,10 +60,42 @@ public IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValu
5460

5561
public record DataMethodBodyBuilderStage4ReturnVoid<TParam1>(BodyGenerationData BodyGenerationData) : IMethodBodyBuilderStage4ReturnVoid<TParam1>
5662
{
63+
public IMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory) =>
64+
new DataMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants>(BodyGenerationData with { CompileTimeConstants = compileTimeConstantsFactory() });
65+
5766
public IMethodBodyGenerator UseProvidedBody(Action<TParam1> body) => new DataMethodBodyGenerator(BodyGenerationData with { RuntimeDelegateBody = body });
5867
}
5968

6069
public record DataMethodBodyBuilderStage4ReturnVoidNoArg(BodyGenerationData BodyGenerationData) : IMethodBodyBuilderStage4ReturnVoidNoArg
6170
{
71+
public IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory) =>
72+
new DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants>(BodyGenerationData with { CompileTimeConstants = compileTimeConstantsFactory() });
73+
6274
public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(BodyGenerationData with { RuntimeDelegateBody = body });
75+
}
76+
77+
public record DataMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants>(BodyGenerationData Data) : IMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants>
78+
{
79+
public IMethodBodyGenerator UseProvidedBody(Func<TConstants, TParam1, TReturnType> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });
80+
81+
public IMethodBodyGenerator BodyReturningConstant(Func<TConstants, TReturnType> constantValueFactory) =>
82+
new DataMethodBodyGenerator(Data with { ReturnConstantValueFactory = constantValueFactory });
83+
}
84+
85+
public record DataMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants>(BodyGenerationData Data) : IMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants>
86+
{
87+
public IMethodBodyGenerator UseProvidedBody(Func<TConstants, TReturnType> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });
88+
89+
public IMethodBodyGenerator BodyReturningConstant(Func<TConstants, TReturnType> constantValueFactory) =>
90+
new DataMethodBodyGenerator(Data with { ReturnConstantValueFactory = constantValueFactory });
91+
}
92+
93+
public record DataMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants>(BodyGenerationData Data) : IMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants>
94+
{
95+
public IMethodBodyGenerator UseProvidedBody(Action<TConstants, TParam1> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });
96+
}
97+
98+
public record DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants>(BodyGenerationData Data) : IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants>
99+
{
100+
public IMethodBodyGenerator UseProvidedBody(Action<TConstants> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });
63101
}

EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ internal static class BodyGenerationDataExtractor
1515
/// Checks for <c>ReturnConstantValueFactory</c> first, then <c>RuntimeDelegateBody</c>.
1616
/// Returns a <see cref="FluentBodyResult"/> with the extracted value, or <c>null</c> return value
1717
/// if neither factory nor body are present.
18+
/// Sets <see cref="FluentBodyResult.HasDelegateBody"/> when <c>RuntimeDelegateBody</c> is present,
19+
/// indicating that the delegate body source code should be extracted from the syntax tree.
1820
/// </summary>
1921
internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnType)
2022
{
@@ -26,23 +28,35 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT
2628
{
2729
// The method returned something that isn't a DataMethodBodyGenerator.
2830
// This may happen when the fluent chain is incomplete (e.g., user returned an intermediate builder).
29-
return new FluentBodyResult(null, isVoidReturnType);
31+
return new FluentBodyResult(null, isVoidReturnType, HasDelegateBody: false);
3032
}
3133

3234
object? bodyGenerationData = dataProperty.GetValue(methodResult);
3335
if (bodyGenerationData == null)
3436
{
35-
return new FluentBodyResult(null, isVoidReturnType);
37+
return new FluentBodyResult(null, isVoidReturnType, HasDelegateBody: false);
3638
}
3739

3840
Type dataType = bodyGenerationData.GetType();
3941
PropertyInfo? returnTypeProperty = dataType.GetProperty("ReturnType");
4042
Type? dataReturnType = returnTypeProperty?.GetValue(bodyGenerationData) as Type;
4143
bool isVoid = dataReturnType == typeof(void);
4244

45+
bool hasDelegateBody = HasRuntimeDelegateBody(dataType, bodyGenerationData);
46+
4347
return TryExtractFromConstantFactory(dataType, bodyGenerationData, isVoid)
44-
?? TryExtractFromRuntimeBody(dataType, bodyGenerationData, isVoid)
45-
?? new FluentBodyResult(null, isVoid);
48+
?? TryExtractFromRuntimeBody(dataType, bodyGenerationData, isVoid, hasDelegateBody)
49+
?? new FluentBodyResult(null, isVoid, hasDelegateBody);
50+
}
51+
52+
/// <summary>
53+
/// Checks whether <c>RuntimeDelegateBody</c> is set (non-null) in the body generation data.
54+
/// </summary>
55+
private static bool HasRuntimeDelegateBody(Type dataType, object bodyGenerationData)
56+
{
57+
PropertyInfo? runtimeBodyProperty = dataType.GetProperty("RuntimeDelegateBody");
58+
Delegate? runtimeBody = runtimeBodyProperty?.GetValue(bodyGenerationData) as Delegate;
59+
return runtimeBody != null;
4660
}
4761

4862
/// <summary>
@@ -61,7 +75,7 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT
6175
}
6276

6377
object? constantValue = constantFactory.DynamicInvoke();
64-
return new FluentBodyResult(constantValue?.ToString(), isVoid);
78+
return new FluentBodyResult(constantValue?.ToString(), isVoid, HasDelegateBody: false);
6579
}
6680

6781
/// <summary>
@@ -72,7 +86,8 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT
7286
private static FluentBodyResult? TryExtractFromRuntimeBody(
7387
Type dataType,
7488
object bodyGenerationData,
75-
bool isVoid)
89+
bool isVoid,
90+
bool hasDelegateBody)
7691
{
7792
PropertyInfo? runtimeBodyProperty = dataType.GetProperty("RuntimeDelegateBody");
7893
Delegate? runtimeBody = runtimeBodyProperty?.GetValue(bodyGenerationData) as Delegate;
@@ -85,10 +100,10 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT
85100
if (bodyParams.Length == 0)
86101
{
87102
object? bodyResult = runtimeBody.DynamicInvoke();
88-
return new FluentBodyResult(bodyResult?.ToString(), isVoid);
103+
return new FluentBodyResult(bodyResult?.ToString(), isVoid, hasDelegateBody);
89104
}
90105

91106
// For delegates with parameters, we can't invoke at compile time without values
92-
return new FluentBodyResult(null, isVoid);
107+
return new FluentBodyResult(null, isVoid, hasDelegateBody);
93108
}
94109
}

EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,25 @@
66
namespace EasySourceGenerators.Generators.IncrementalGenerators;
77

88
/// <summary>
9-
/// Extracts the delegate body source code from a <c>UseProvidedBody(...)</c> invocation
10-
/// within a generator method's syntax tree. The extracted body is re-indented to match
9+
/// Extracts the delegate body source code from the outermost invocation's lambda argument
10+
/// in a generator method's return expression. The extracted body is re-indented to match
1111
/// the target method body indentation (8 spaces).
1212
/// </summary>
1313
internal static class DelegateBodySyntaxExtractor
1414
{
1515
private const string MethodBodyIndent = " ";
1616

1717
/// <summary>
18-
/// Attempts to find a <c>UseProvidedBody(...)</c> call in the given generator method syntax
19-
/// and extract the lambda body. Returns <c>null</c> if no such call is found.
20-
/// For expression lambdas, returns a single <c>return {expr};</c> line.
18+
/// Attempts to find the lambda argument of the outermost invocation in the generator
19+
/// method's return expression and extract the lambda body. Returns <c>null</c> if no
20+
/// such lambda is found.
21+
/// For expression lambdas, returns the expression text.
2122
/// For block lambdas, returns the block body re-indented to the method body level.
2223
/// </summary>
2324
internal static string? TryExtractDelegateBody(MethodDeclarationSyntax generatorMethodSyntax)
2425
{
25-
InvocationExpressionSyntax? invocation = generatorMethodSyntax
26-
.DescendantNodes()
27-
.OfType<InvocationExpressionSyntax>()
28-
.FirstOrDefault(inv =>
29-
inv.Expression is MemberAccessExpressionSyntax memberAccess &&
30-
memberAccess.Name.Identifier.Text == "UseProvidedBody");
31-
32-
if (invocation == null)
26+
ExpressionSyntax? returnExpression = GetReturnExpression(generatorMethodSyntax);
27+
if (returnExpression is not InvocationExpressionSyntax invocation)
3328
{
3429
return null;
3530
}
@@ -54,6 +49,29 @@ inv.Expression is MemberAccessExpressionSyntax memberAccess &&
5449
return null;
5550
}
5651

52+
/// <summary>
53+
/// Gets the return expression from a generator method. Handles both expression-body
54+
/// methods (<c>=&gt; expr</c>) and block-body methods (<c>{ return expr; }</c>).
55+
/// Assumes the generator method has a simple structure with at most one return statement.
56+
/// </summary>
57+
private static ExpressionSyntax? GetReturnExpression(MethodDeclarationSyntax method)
58+
{
59+
if (method.ExpressionBody != null)
60+
{
61+
return method.ExpressionBody.Expression;
62+
}
63+
64+
if (method.Body != null)
65+
{
66+
ReturnStatementSyntax? returnStatement = method.Body.Statements
67+
.OfType<ReturnStatementSyntax>()
68+
.FirstOrDefault();
69+
return returnStatement?.Expression;
70+
}
71+
72+
return null;
73+
}
74+
5775
/// <summary>
5876
/// Extracts the content of a block body (between <c>{</c> and <c>}</c>),
5977
/// determines the base indentation, and re-indents all lines to the method body level.

EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ internal sealed record SwitchBodyData(
1414

1515
/// <summary>
1616
/// Result extracted from <see cref="DataBuilding.BodyGenerationData"/> after executing a fluent body generator method.
17+
/// <see cref="HasDelegateBody"/> indicates that the generator used <c>UseProvidedBody</c>,
18+
/// signaling that the delegate body source code should be extracted from the syntax tree.
1719
/// </summary>
1820
internal sealed record FluentBodyResult(
1921
string? ReturnValue,
20-
bool IsVoid);
22+
bool IsVoid,
23+
bool HasDelegateBody);
2124

2225
/// <summary>
2326
/// Orchestrates the execution of generator methods at compile time.

EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,10 @@ private static string GenerateSourceForGroup(
9696
}
9797

9898
/// <summary>
99-
/// Generates source code from a fluent body pattern. First attempts to extract the delegate
100-
/// body from a <c>UseProvidedBody</c> call in the syntax tree. If no such call is found,
101-
/// falls back to executing the generator method and extracting the return value.
99+
/// Generates source code from a fluent body pattern. Executes the generator method first
100+
/// to obtain <see cref="FluentBodyResult"/>. If the result indicates a delegate body was
101+
/// provided (via <see cref="FluentBodyResult.HasDelegateBody"/>), attempts to extract the
102+
/// lambda body from the syntax tree. Otherwise, uses the runtime-evaluated return value.
102103
/// </summary>
103104
private static string GenerateFromFluentBodyPattern(
104105
SourceProductionContext context,
@@ -107,18 +108,6 @@ private static string GenerateFromFluentBodyPattern(
107108
INamedTypeSymbol containingType,
108109
Compilation compilation)
109110
{
110-
string? delegateBody = DelegateBodySyntaxExtractor.TryExtractDelegateBody(methodInfo.Syntax);
111-
if (delegateBody != null)
112-
{
113-
bool isVoidReturn = partialMethod.ReturnType.SpecialType == SpecialType.System_Void;
114-
string bodyLines = FormatDelegateBodyForEmit(delegateBody, isVoidReturn);
115-
116-
return GeneratesMethodPatternSourceBuilder.GeneratePartialMethodWithBody(
117-
containingType,
118-
partialMethod,
119-
bodyLines);
120-
}
121-
122111
(FluentBodyResult? result, string? error) = GeneratesMethodExecutionRuntime.ExecuteFluentBodyGeneratorMethod(
123112
methodInfo.Symbol,
124113
partialMethod,
@@ -134,10 +123,25 @@ private static string GenerateFromFluentBodyPattern(
134123
return string.Empty;
135124
}
136125

126+
if (result!.HasDelegateBody)
127+
{
128+
string? delegateBody = DelegateBodySyntaxExtractor.TryExtractDelegateBody(methodInfo.Syntax);
129+
if (delegateBody != null)
130+
{
131+
bool isVoidReturn = partialMethod.ReturnType.SpecialType == SpecialType.System_Void;
132+
string bodyLines = FormatDelegateBodyForEmit(delegateBody, isVoidReturn);
133+
134+
return GeneratesMethodPatternSourceBuilder.GeneratePartialMethodWithBody(
135+
containingType,
136+
partialMethod,
137+
bodyLines);
138+
}
139+
}
140+
137141
return GeneratesMethodPatternSourceBuilder.GenerateSimplePartialMethod(
138142
containingType,
139143
partialMethod,
140-
result!.ReturnValue);
144+
result.ReturnValue);
141145
}
142146

143147
/// <summary>

0 commit comments

Comments
 (0)