Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,49 @@ public interface IMethodBodyBuilderStage3<TReturnType>

public interface IMethodBodyBuilderStage4ReturnVoidNoArg
{
IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory);
IMethodBodyGenerator UseProvidedBody(Action body);
}

public interface IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants>
{
IMethodBodyGenerator UseProvidedBody(Action<TConstants> body);
}

public interface IMethodBodyBuilderStage4NoArg<TReturnType>
{
IMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory);
IMethodBodyGenerator UseProvidedBody(Func<TReturnType> body);
IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValueFactory);
}

public interface IMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants>
{
IMethodBodyGenerator UseProvidedBody(Func<TConstants, TReturnType> body);
IMethodBodyGenerator BodyReturningConstant(Func<TConstants, TReturnType> constantValueFactory);
}

public interface IMethodBodyBuilderStage4ReturnVoid<TParam1>
{
IMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory);
IMethodBodyGenerator UseProvidedBody(Action<TParam1> body);
}

public interface IMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants>
{
IMethodBodyGenerator UseProvidedBody(Action<TConstants, TParam1> body);
}

public interface IMethodBodyBuilderStage4<TParam1, in TReturnType>
{
IMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory);

IMethodBodyGenerator UseProvidedBody(Func<TParam1, TReturnType> body);
IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValueFactory);
IMethodBodyGeneratorSwitchBody<TParam1, TReturnType> BodyWithSwitchStatement();
}

public interface IMethodBodyBuilderStage5WithConstants<TParam1, in TReturnType, TConstants>
{
IMethodBodyGenerator UseProvidedBody(Func<TConstants, TParam1, TReturnType> body);
IMethodBodyGenerator BodyReturningConstant(Func<TConstants, TReturnType> constantValueFactory);
}
37 changes: 20 additions & 17 deletions EasySourceGenerators.Examples/PiExample.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
// SwitchCase/SwitchDefault attribute-based generation is commented out pending replacement with a data-driven approach.
// See DataMethodBodyBuilders.cs for details on the planned replacement.

/*
using EasySourceGenerators.Abstractions;
using EasySourceGenerators.Abstractions;
// ReSharper disable ConvertClosureToMethodGroup

namespace EasySourceGenerators.Examples;

public static partial class PiExample
public static partial class PiExampleFluent
{
public static partial int GetPiDecimal(int decimalNumber);

[MethodBodyGenerator(nameof(GetPiDecimal))]
[SwitchCase(arg1: 0)]
[SwitchCase(arg1: 1)]
[SwitchCase(arg1: 2)]
static int GetPiDecimal_Generator_Specialized(int decimalNumber) =>
SlowMath.CalculatePiDecimal(decimalNumber);

[MethodBodyGenerator(nameof(GetPiDecimal))]
[SwitchDefault]
static Func<int, int> GetPiDecimal_Generator_Fallback() => decimalNumber => SlowMath.CalculatePiDecimal(decimalNumber);
static IMethodBodyGenerator GetPiDecimal_Generator() =>
Generate.MethodBody()
.ForMethod().WithReturnType<int>().WithParameter<int>()
.WithCompileTimeConstants(() => new
{
PrecomputedTargets = (new int[] { 0, 1, 2, 300, 301, 302, 303 }).ToDictionary(i => i, i => SlowMath.CalculatePiDecimal(i))
})
.UseProvidedBody((constants, decimalNumber) =>
Comment on lines +11 to +18
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithCompileTimeConstants(...) is used here, but there is currently no implementation of that method in the concrete fluent builder stages (search shows it exists only in the interface and this example). This will prevent the Examples project/solution from compiling until the builder/runtime support for compile-time constants is added end-to-end.

Copilot uses AI. Check for mistakes.
{
if (constants.PrecomputedTargets.TryGetValue(decimalNumber, out int precomputedResult)) return precomputedResult;

return SlowMath.CalculatePiDecimal(decimalNumber);
});
}
*/

/*
This will generate the following method:
Expand All @@ -34,7 +33,11 @@ public static int GetPiDecimal(int decimalNumber)
case 0: return 3;
case 1: return 1;
case 2: return 4;
case 300: return 3;
case 301: return 7;
case 302: return 2;
case 303: return 4;
default: return CalculatePiDecimal(decimalNumber);
}
}
*/
*/
38 changes: 0 additions & 38 deletions EasySourceGenerators.Examples/PiExampleFluent.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -42,39 +42,6 @@ public record DataMethodBodyBuilderStage4<TParam1, TReturnType>(BodyGenerationDa

public IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValueFactory) =>
new DataMethodBodyGenerator(Data with { ReturnConstantValueFactory = constantValueFactory });

public IMethodBodyGeneratorSwitchBody<TParam1, TReturnType> BodyWithSwitchStatement()
=> throw new NotImplementedException(); //TODO: Remove explicit SwitchStatements with `for` and `constants`, like so:
/*
.ForMethod().WithReturnType<int>().WithParameter<int>()
.BodyWithSwitchStatement()
.ForCases(0, 1, 2, 300, 301, 302, 303).ReturnConstantValue(decimalNumber => SlowMath.CalculatePiDecimal(decimalNumber))
.ForDefaultCase().UseProvidedBody(decimalNumber => SlowMath.CalculatePiDecimal(decimalNumber));

Will be replaced with:

.ForMethod().WithReturnType<int>().WithParameter<int>()
.WithCompileTimeConstants(() => new
{
PrecomputedTargets = new HashSet<int>(new int[] { 0, 1, 2, 300, 301, 302, 303 })
})
.UseProvidedBody(decimalNumber, constants =>
{
if (constants.PrecomputedTargets.Contains(decimalNumber)) return SlowMath.CalculatePiDecimal(decimalNumber);
else return SlowMath.CalculatePiDecimal(decimalNumber);
});

Or (in the case of PI), just simply:

.ForMethod().WithReturnType<int>().WithParameter<int>()
.UseProvidedBody(decimalNumber =>
{
var precomputedTargets = new HashSet<int>(new int[] { 0, 1, 2, 300, 301, 302, 303 })

if (precomputedTargets.Contains(decimalNumber)) return SlowMath.CalculatePiDecimal(decimalNumber);
else return SlowMath.CalculatePiDecimal(decimalNumber);
});
*/
}

public record DataMethodBodyBuilderStage4NoArg<TReturnType>(BodyGenerationData Data) : IMethodBodyBuilderStage4NoArg<TReturnType>
Expand Down
3 changes: 2 additions & 1 deletion EasySourceGenerators.Generators/DataBuilding/DataRecords.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public record BodyGenerationData(
Type? ReturnType = null,
Type[]? ParametersTypes = null,
Delegate? RuntimeDelegateBody = null,
Delegate? ReturnConstantValueFactory = null
Delegate? ReturnConstantValueFactory = null,
object? CompileTimeConstants = null
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using System;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace EasySourceGenerators.Generators.IncrementalGenerators;

/// <summary>
/// Extracts the delegate body source code from a <c>UseProvidedBody(...)</c> invocation
/// within a generator method's syntax tree. The extracted body is re-indented to match
/// the target method body indentation (8 spaces).
/// </summary>
internal static class DelegateBodySyntaxExtractor
{
private const string MethodBodyIndent = " ";

/// <summary>
/// Attempts to find a <c>UseProvidedBody(...)</c> call in the given generator method syntax
/// and extract the lambda body. Returns <c>null</c> if no such call is found.
/// For expression lambdas, returns a single <c>return {expr};</c> line.
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML doc on TryExtractDelegateBody says expression lambdas return a full return {expr}; line, but the implementation returns only the expression text (wrapping happens later in FormatDelegateBodyForEmit). Update the comment to reflect the actual return value to avoid future misuse.

Suggested change
/// For expression lambdas, returns a single <c>return {expr};</c> line.
/// For expression lambdas, returns the expression text (wrapping into a <c>return {expr};</c>
/// statement is performed later when emitting the method body).

Copilot uses AI. Check for mistakes.
/// For block lambdas, returns the block body re-indented to the method body level.
/// </summary>
internal static string? TryExtractDelegateBody(MethodDeclarationSyntax generatorMethodSyntax)
{
InvocationExpressionSyntax? invocation = generatorMethodSyntax
.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.FirstOrDefault(inv =>
inv.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Name.Identifier.Text == "UseProvidedBody");

if (invocation == null)
{
return null;
}

ArgumentSyntax? argument = invocation.ArgumentList.Arguments.FirstOrDefault();
if (argument?.Expression is not LambdaExpressionSyntax lambda)
{
return null;
}

if (lambda.Body is ExpressionSyntax expression)
{
string expressionText = expression.ToFullString().Trim();
return expressionText;
}

if (lambda.Body is BlockSyntax block)
{
return ExtractBlockBody(block);
}

return null;
}

/// <summary>
/// Extracts the content of a block body (between <c>{</c> and <c>}</c>),
/// determines the base indentation, and re-indents all lines to the method body level.
/// Blank lines between statements are preserved with method body indentation.
/// </summary>
private static string? ExtractBlockBody(BlockSyntax block)
{
string blockText = block.ToFullString();
string[] lines = blockText.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');

int openIndex = -1;
int closeIndex = -1;

for (int i = 0; i < lines.Length; i++)
{
if (openIndex == -1 && lines[i].TrimEnd().EndsWith("{", StringComparison.Ordinal))
{
openIndex = i;
Comment on lines +70 to +74
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExtractBlockBody relies on finding {/} on their own lines via EndsWith("{") / StartsWith("}"). This fails for single-line block lambdas like x => { return 42; }, causing extraction to return null and the generator to fall back to runtime execution (which doesn't preserve arbitrary bodies). Consider extracting the span between block.OpenBraceToken and block.CloseBraceToken (or using tokens/statement list) rather than line-scanning the formatted text.

Copilot uses AI. Check for mistakes.
break;
}
}

for (int i = lines.Length - 1; i >= 0; i--)
{
string trimmed = lines[i].Trim();
if (trimmed.StartsWith("}", StringComparison.Ordinal))
{
closeIndex = i;
break;
}
}

if (openIndex == -1 || closeIndex == -1 || closeIndex <= openIndex)
{
return null;
}

string[] contentLines = new string[closeIndex - openIndex - 1];
Array.Copy(lines, openIndex + 1, contentLines, 0, contentLines.Length);

if (contentLines.Length == 0)
{
return null;
}

int minIndent = int.MaxValue;
foreach (string line in contentLines)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}

int indent = 0;
foreach (char c in line)
{
if (c == ' ')
{
indent++;
}
else if (c == '\t')
{
indent += 4;
}
else
{
break;
}
}

if (indent < minIndent)
{
minIndent = indent;
}
}

if (minIndent == int.MaxValue)
{
minIndent = 0;
}

StringBuilder result = new();
for (int i = 0; i < contentLines.Length; i++)
{
string line = contentLines[i];

if (string.IsNullOrWhiteSpace(line))
{
result.AppendLine(MethodBodyIndent);
}
else
{
string stripped = minIndent <= line.Length ? line.Substring(minIndent) : line.TrimStart();
string trimmedEnd = stripped.TrimEnd();
result.AppendLine(MethodBodyIndent + trimmedEnd);
}
}

return result.ToString().TrimEnd('\n', '\r');
}
}
Loading
Loading