Skip to content

Commit 651af09

Browse files
Copilotdex3r
andauthored
[WIP] Fix DelegateBodyTests to ensure UseProvidedBody accepts any body (#94)
* Initial plan * 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 <[email protected]> Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/e1b5542a-d819-423d-a4b7-f97407a7d1b4 * Compile time constats WIP * Compile time constants WIP --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: dex3r <[email protected]> Co-authored-by: mateusz.krzaczek <[email protected]>
1 parent 0bce1ab commit 651af09

12 files changed

Lines changed: 282 additions & 105 deletions

File tree

EasySourceGenerators.Abstractions/MethodBody/IMethodBodyBuilder.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,49 @@ public interface IMethodBodyBuilderStage3<TReturnType>
2727

2828
public interface IMethodBodyBuilderStage4ReturnVoidNoArg
2929
{
30+
IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory);
3031
IMethodBodyGenerator UseProvidedBody(Action body);
3132
}
3233

34+
public interface IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants>
35+
{
36+
IMethodBodyGenerator UseProvidedBody(Action<TConstants> body);
37+
}
38+
3339
public interface IMethodBodyBuilderStage4NoArg<TReturnType>
3440
{
41+
IMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory);
3542
IMethodBodyGenerator UseProvidedBody(Func<TReturnType> body);
3643
IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValueFactory);
3744
}
3845

46+
public interface IMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants>
47+
{
48+
IMethodBodyGenerator UseProvidedBody(Func<TConstants, TReturnType> body);
49+
IMethodBodyGenerator BodyReturningConstant(Func<TConstants, TReturnType> constantValueFactory);
50+
}
51+
3952
public interface IMethodBodyBuilderStage4ReturnVoid<TParam1>
4053
{
54+
IMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory);
4155
IMethodBodyGenerator UseProvidedBody(Action<TParam1> body);
4256
}
4357

58+
public interface IMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants>
59+
{
60+
IMethodBodyGenerator UseProvidedBody(Action<TConstants, TParam1> body);
61+
}
62+
4463
public interface IMethodBodyBuilderStage4<TParam1, in TReturnType>
4564
{
65+
IMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory);
66+
4667
IMethodBodyGenerator UseProvidedBody(Func<TParam1, TReturnType> body);
4768
IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValueFactory);
48-
IMethodBodyGeneratorSwitchBody<TParam1, TReturnType> BodyWithSwitchStatement();
69+
}
70+
71+
public interface IMethodBodyBuilderStage5WithConstants<TParam1, in TReturnType, TConstants>
72+
{
73+
IMethodBodyGenerator UseProvidedBody(Func<TConstants, TParam1, TReturnType> body);
74+
IMethodBodyGenerator BodyReturningConstant(Func<TConstants, TReturnType> constantValueFactory);
4975
}
Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
1-
// SwitchCase/SwitchDefault attribute-based generation is commented out pending replacement with a data-driven approach.
2-
// See DataMethodBodyBuilders.cs for details on the planned replacement.
3-
4-
/*
5-
using EasySourceGenerators.Abstractions;
1+
using EasySourceGenerators.Abstractions;
62
// ReSharper disable ConvertClosureToMethodGroup
73

84
namespace EasySourceGenerators.Examples;
95

10-
public static partial class PiExample
6+
public static partial class PiExampleFluent
117
{
128
public static partial int GetPiDecimal(int decimalNumber);
139

1410
[MethodBodyGenerator(nameof(GetPiDecimal))]
15-
[SwitchCase(arg1: 0)]
16-
[SwitchCase(arg1: 1)]
17-
[SwitchCase(arg1: 2)]
18-
static int GetPiDecimal_Generator_Specialized(int decimalNumber) =>
19-
SlowMath.CalculatePiDecimal(decimalNumber);
20-
21-
[MethodBodyGenerator(nameof(GetPiDecimal))]
22-
[SwitchDefault]
23-
static Func<int, int> GetPiDecimal_Generator_Fallback() => decimalNumber => SlowMath.CalculatePiDecimal(decimalNumber);
11+
static IMethodBodyGenerator GetPiDecimal_Generator() =>
12+
Generate.MethodBody()
13+
.ForMethod().WithReturnType<int>().WithParameter<int>()
14+
.WithCompileTimeConstants(() => new
15+
{
16+
PrecomputedTargets = (new int[] { 0, 1, 2, 300, 301, 302, 303 }).ToDictionary(i => i, i => SlowMath.CalculatePiDecimal(i))
17+
})
18+
.UseProvidedBody((constants, decimalNumber) =>
19+
{
20+
if (constants.PrecomputedTargets.TryGetValue(decimalNumber, out int precomputedResult)) return precomputedResult;
21+
22+
return SlowMath.CalculatePiDecimal(decimalNumber);
23+
});
2424
}
25-
*/
2625

2726
/*
2827
This will generate the following method:
@@ -34,7 +33,11 @@ public static int GetPiDecimal(int decimalNumber)
3433
case 0: return 3;
3534
case 1: return 1;
3635
case 2: return 4;
36+
case 300: return 3;
37+
case 301: return 7;
38+
case 302: return 2;
39+
case 303: return 4;
3740
default: return CalculatePiDecimal(decimalNumber);
3841
}
3942
}
40-
*/
43+
*/

EasySourceGenerators.Examples/PiExampleFluent.cs

Lines changed: 0 additions & 38 deletions
This file was deleted.

EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -42,39 +42,6 @@ public record DataMethodBodyBuilderStage4<TParam1, TReturnType>(BodyGenerationDa
4242

4343
public IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValueFactory) =>
4444
new DataMethodBodyGenerator(Data with { ReturnConstantValueFactory = constantValueFactory });
45-
46-
public IMethodBodyGeneratorSwitchBody<TParam1, TReturnType> BodyWithSwitchStatement()
47-
=> throw new NotImplementedException(); //TODO: Remove explicit SwitchStatements with `for` and `constants`, like so:
48-
/*
49-
.ForMethod().WithReturnType<int>().WithParameter<int>()
50-
.BodyWithSwitchStatement()
51-
.ForCases(0, 1, 2, 300, 301, 302, 303).ReturnConstantValue(decimalNumber => SlowMath.CalculatePiDecimal(decimalNumber))
52-
.ForDefaultCase().UseProvidedBody(decimalNumber => SlowMath.CalculatePiDecimal(decimalNumber));
53-
54-
Will be replaced with:
55-
56-
.ForMethod().WithReturnType<int>().WithParameter<int>()
57-
.WithCompileTimeConstants(() => new
58-
{
59-
PrecomputedTargets = new HashSet<int>(new int[] { 0, 1, 2, 300, 301, 302, 303 })
60-
})
61-
.UseProvidedBody(decimalNumber, constants =>
62-
{
63-
if (constants.PrecomputedTargets.Contains(decimalNumber)) return SlowMath.CalculatePiDecimal(decimalNumber);
64-
else return SlowMath.CalculatePiDecimal(decimalNumber);
65-
});
66-
67-
Or (in the case of PI), just simply:
68-
69-
.ForMethod().WithReturnType<int>().WithParameter<int>()
70-
.UseProvidedBody(decimalNumber =>
71-
{
72-
var precomputedTargets = new HashSet<int>(new int[] { 0, 1, 2, 300, 301, 302, 303 })
73-
74-
if (precomputedTargets.Contains(decimalNumber)) return SlowMath.CalculatePiDecimal(decimalNumber);
75-
else return SlowMath.CalculatePiDecimal(decimalNumber);
76-
});
77-
*/
7845
}
7946

8047
public record DataMethodBodyBuilderStage4NoArg<TReturnType>(BodyGenerationData Data) : IMethodBodyBuilderStage4NoArg<TReturnType>

EasySourceGenerators.Generators/DataBuilding/DataRecords.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ public record BodyGenerationData(
66
Type? ReturnType = null,
77
Type[]? ParametersTypes = null,
88
Delegate? RuntimeDelegateBody = null,
9-
Delegate? ReturnConstantValueFactory = null
9+
Delegate? ReturnConstantValueFactory = null,
10+
object? CompileTimeConstants = null
1011
);
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+
}

0 commit comments

Comments
 (0)