Skip to content

Commit c7cb245

Browse files
Copilotdex3r
andcommitted
feat: extract delegate body from UseProvidedBody syntax for source generation
Add DelegateBodySyntaxExtractor to extract lambda body from syntax tree. Support both expression lambdas (e.g. () => 42) and block lambdas with complex control flow (if/else, switch, multi-statement bodies). Add EmitWithBody to PartialMethodSourceEmitter for arbitrary body emission. Fix test lambda parameter names to match actual method parameters. Co-authored-by: dex3r <3155725+dex3r@users.noreply.github.com> Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/e1b5542a-d819-423d-a4b7-f97407a7d1b4
1 parent 0604630 commit c7cb245

6 files changed

Lines changed: 229 additions & 11 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
using System;
2+
using System.Linq;
3+
using System.Text;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
6+
namespace EasySourceGenerators.Generators.IncrementalGenerators;
7+
8+
/// <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
11+
/// the target method body indentation (8 spaces).
12+
/// </summary>
13+
internal static class DelegateBodySyntaxExtractor
14+
{
15+
private const string MethodBodyIndent = " ";
16+
17+
/// <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.
21+
/// For block lambdas, returns the block body re-indented to the method body level.
22+
/// </summary>
23+
internal static string? TryExtractDelegateBody(MethodDeclarationSyntax generatorMethodSyntax)
24+
{
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)
33+
{
34+
return null;
35+
}
36+
37+
ArgumentSyntax? argument = invocation.ArgumentList.Arguments.FirstOrDefault();
38+
if (argument?.Expression is not LambdaExpressionSyntax lambda)
39+
{
40+
return null;
41+
}
42+
43+
if (lambda.Body is ExpressionSyntax expression)
44+
{
45+
string expressionText = expression.ToFullString().Trim();
46+
return expressionText;
47+
}
48+
49+
if (lambda.Body is BlockSyntax block)
50+
{
51+
return ExtractBlockBody(block);
52+
}
53+
54+
return null;
55+
}
56+
57+
/// <summary>
58+
/// Extracts the content of a block body (between <c>{</c> and <c>}</c>),
59+
/// determines the base indentation, and re-indents all lines to the method body level.
60+
/// Blank lines between statements are preserved with method body indentation.
61+
/// </summary>
62+
private static string? ExtractBlockBody(BlockSyntax block)
63+
{
64+
string blockText = block.ToFullString();
65+
string[] lines = blockText.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');
66+
67+
int openIndex = -1;
68+
int closeIndex = -1;
69+
70+
for (int i = 0; i < lines.Length; i++)
71+
{
72+
if (openIndex == -1 && lines[i].TrimEnd().EndsWith("{", StringComparison.Ordinal))
73+
{
74+
openIndex = i;
75+
break;
76+
}
77+
}
78+
79+
for (int i = lines.Length - 1; i >= 0; i--)
80+
{
81+
string trimmed = lines[i].Trim();
82+
if (trimmed.StartsWith("}", StringComparison.Ordinal))
83+
{
84+
closeIndex = i;
85+
break;
86+
}
87+
}
88+
89+
if (openIndex == -1 || closeIndex == -1 || closeIndex <= openIndex)
90+
{
91+
return null;
92+
}
93+
94+
string[] contentLines = new string[closeIndex - openIndex - 1];
95+
Array.Copy(lines, openIndex + 1, contentLines, 0, contentLines.Length);
96+
97+
if (contentLines.Length == 0)
98+
{
99+
return null;
100+
}
101+
102+
int minIndent = int.MaxValue;
103+
foreach (string line in contentLines)
104+
{
105+
if (string.IsNullOrWhiteSpace(line))
106+
{
107+
continue;
108+
}
109+
110+
int indent = 0;
111+
foreach (char c in line)
112+
{
113+
if (c == ' ')
114+
{
115+
indent++;
116+
}
117+
else if (c == '\t')
118+
{
119+
indent += 4;
120+
}
121+
else
122+
{
123+
break;
124+
}
125+
}
126+
127+
if (indent < minIndent)
128+
{
129+
minIndent = indent;
130+
}
131+
}
132+
133+
if (minIndent == int.MaxValue)
134+
{
135+
minIndent = 0;
136+
}
137+
138+
StringBuilder result = new();
139+
for (int i = 0; i < contentLines.Length; i++)
140+
{
141+
string line = contentLines[i];
142+
143+
if (string.IsNullOrWhiteSpace(line))
144+
{
145+
result.AppendLine(MethodBodyIndent);
146+
}
147+
else
148+
{
149+
string stripped = minIndent <= line.Length ? line.Substring(minIndent) : line.TrimStart();
150+
string trimmedEnd = stripped.TrimEnd();
151+
result.AppendLine(MethodBodyIndent + trimmedEnd);
152+
}
153+
}
154+
155+
return result.ToString().TrimEnd('\n', '\r');
156+
}
157+
}

EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs

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

9898
/// <summary>
99-
/// Generates source code from a fluent body pattern, executing the generator method
100-
/// and extracting the return value from the fluent API result.
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.
101102
/// </summary>
102103
private static string GenerateFromFluentBodyPattern(
103104
SourceProductionContext context,
@@ -106,6 +107,18 @@ private static string GenerateFromFluentBodyPattern(
106107
INamedTypeSymbol containingType,
107108
Compilation compilation)
108109
{
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+
109122
(FluentBodyResult? result, string? error) = GeneratesMethodExecutionRuntime.ExecuteFluentBodyGeneratorMethod(
110123
methodInfo.Symbol,
111124
partialMethod,
@@ -127,6 +140,27 @@ private static string GenerateFromFluentBodyPattern(
127140
result!.ReturnValue);
128141
}
129142

143+
/// <summary>
144+
/// Formats the extracted delegate body for emission. Expression bodies are wrapped in
145+
/// <c>return {expr};</c>. Block bodies are used as-is (already re-indented by the extractor).
146+
/// </summary>
147+
private static string FormatDelegateBodyForEmit(string delegateBody, bool isVoidReturn)
148+
{
149+
bool isBlockBody = delegateBody.Contains("\n");
150+
151+
if (isBlockBody)
152+
{
153+
return delegateBody;
154+
}
155+
156+
if (isVoidReturn)
157+
{
158+
return $" {delegateBody};";
159+
}
160+
161+
return $" return {delegateBody};";
162+
}
163+
130164
/// <summary>
131165
/// Generates source code from a simple pattern, executing the generator method
132166
/// and using its return value as the partial method's return expression.

EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodPatternSourceBuilder.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ internal static string GenerateSimplePartialMethod(
2828
return PartialMethodSourceEmitter.Emit(emitData, returnValueLiteral);
2929
}
3030

31+
/// <summary>
32+
/// Generates a complete C# source file containing a partial method implementation
33+
/// with the given body lines (already indented to the method body level).
34+
/// </summary>
35+
internal static string GeneratePartialMethodWithBody(
36+
INamedTypeSymbol containingType,
37+
IMethodSymbol partialMethod,
38+
string bodyLines)
39+
{
40+
PartialMethodEmitData emitData = RoslynSymbolDataMapper.ToPartialMethodEmitData(containingType, partialMethod);
41+
return PartialMethodSourceEmitter.EmitWithBody(emitData, bodyLines);
42+
}
43+
3144
/// <summary>
3245
/// Formats a string value as a C# literal expression based on the target return type.
3346
/// Delegates to <see cref="CSharpLiteralFormatter.FormatValueAsLiteral"/>.

EasySourceGenerators.Generators/SourceEmitting/PartialMethodSourceEmitter.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ internal static string Emit(PartialMethodEmitData data, string? returnValueLiter
2626
return builder.ToString();
2727
}
2828

29+
/// <summary>
30+
/// Generates a complete C# source file containing a partial method implementation
31+
/// with the given body lines (already indented to the method body level).
32+
/// </summary>
33+
internal static string EmitWithBody(PartialMethodEmitData data, string bodyLines)
34+
{
35+
StringBuilder builder = new();
36+
AppendFileHeader(builder, data);
37+
builder.AppendLine(bodyLines);
38+
AppendClosingBraces(builder);
39+
return builder.ToString();
40+
}
41+
2942
/// <summary>
3043
/// Appends the auto-generated file header, namespace declaration, type declaration,
3144
/// and method signature opening to the <see cref="StringBuilder"/>.

EasySourceGenerators.Tests/DelegateBodyTests.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ static partial class DelegateBodyTestClass_WithIf
5757
{
5858
public static partial int PartialMethod(int someParam)
5959
{
60-
if (someParamHere > 0)
60+
if (someParam > 0)
6161
{
6262
return 42;
6363
}
@@ -83,7 +83,7 @@ static partial class DelegateBodyTestClass_WithSwitch
8383
{
8484
public static partial int PartialMethod(int someParam)
8585
{
86-
switch (someParamHere)
86+
switch (someParam)
8787
{
8888
case -1: return 6;
8989
case 0: return 7;
@@ -110,7 +110,7 @@ public static partial int PartialMethod(int someParam)
110110
{
111111
int interResult = 0;
112112
113-
switch (someParamHere)
113+
switch (someParam)
114114
{
115115
case -1: interResult = 6; break;
116116
case 0: interResult = 7; break;
@@ -157,9 +157,9 @@ public static partial class DelegateBodyTestClass_WithIf
157157
public static IMethodBodyGenerator JustReturnConstantGenerator() =>
158158
Generate.MethodBody()
159159
.ForMethod().WithReturnType<int>().WithParameter<int>()
160-
.UseProvidedBody(someParamHere =>
160+
.UseProvidedBody(someParam =>
161161
{
162-
if (someParamHere > 0)
162+
if (someParam > 0)
163163
{
164164
return 42;
165165
}
@@ -178,9 +178,9 @@ public static partial class DelegateBodyTestClass_WithSwitch
178178
public static IMethodBodyGenerator JustReturnConstantGenerator() =>
179179
Generate.MethodBody()
180180
.ForMethod().WithReturnType<int>().WithParameter<int>()
181-
.UseProvidedBody(someParamHere =>
181+
.UseProvidedBody(someParam =>
182182
{
183-
switch (someParamHere)
183+
switch (someParam)
184184
{
185185
case -1: return 6;
186186
case 0: return 7;
@@ -198,11 +198,11 @@ public static partial class DelegateBodyTestClass_WithComplexBody
198198
public static IMethodBodyGenerator JustReturnConstantGenerator() =>
199199
Generate.MethodBody()
200200
.ForMethod().WithReturnType<int>().WithParameter<int>()
201-
.UseProvidedBody(someParamHere =>
201+
.UseProvidedBody(someParam =>
202202
{
203203
int interResult = 0;
204204

205-
switch (someParamHere)
205+
switch (someParam)
206206
{
207207
case -1: interResult = 6; break;
208208
case 0: interResult = 7; break;

EasySourceGenerators.Tests/EasySourceGenerators.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
<ItemGroup>
2727
<Using Include="NUnit.Framework"/>
28+
<Using Include="System.Text"/>
2829
</ItemGroup>
2930

3031
<PropertyGroup>

0 commit comments

Comments
 (0)