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 @@ -38,6 +38,9 @@ public IMethodBodyBuilderStage4<TParam1, T> WithParameter<TParam1>() =>

public record DataMethodBodyBuilderStage4<TParam1, TReturnType>(BodyGenerationData Data) : IMethodBodyBuilderStage4<TParam1, TReturnType>
{
public IMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory) =>
new DataMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants>(Data with { CompileTimeConstants = compileTimeConstantsFactory() });

public IMethodBodyGenerator UseProvidedBody(Func<TParam1, TReturnType> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });

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

public record DataMethodBodyBuilderStage4NoArg<TReturnType>(BodyGenerationData Data) : IMethodBodyBuilderStage4NoArg<TReturnType>
{
public IMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory) =>
new DataMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants>(Data with { CompileTimeConstants = compileTimeConstantsFactory() });

public IMethodBodyGenerator UseProvidedBody(Func<TReturnType> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });

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

public record DataMethodBodyBuilderStage4ReturnVoid<TParam1>(BodyGenerationData BodyGenerationData) : IMethodBodyBuilderStage4ReturnVoid<TParam1>
{
public IMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory) =>
new DataMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants>(BodyGenerationData with { CompileTimeConstants = compileTimeConstantsFactory() });

public IMethodBodyGenerator UseProvidedBody(Action<TParam1> body) => new DataMethodBodyGenerator(BodyGenerationData with { RuntimeDelegateBody = body });
}

public record DataMethodBodyBuilderStage4ReturnVoidNoArg(BodyGenerationData BodyGenerationData) : IMethodBodyBuilderStage4ReturnVoidNoArg
{
public IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants> WithCompileTimeConstants<TConstants>(Func<TConstants> compileTimeConstantsFactory) =>
new DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants>(BodyGenerationData with { CompileTimeConstants = compileTimeConstantsFactory() });

public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(BodyGenerationData with { RuntimeDelegateBody = body });
}

public record DataMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants>(BodyGenerationData Data) : IMethodBodyBuilderStage5WithConstants<TParam1, TReturnType, TConstants>
{
public IMethodBodyGenerator UseProvidedBody(Func<TConstants, TParam1, TReturnType> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });

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

public record DataMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants>(BodyGenerationData Data) : IMethodBodyBuilderStage5NoArgWithConstants<TReturnType, TConstants>
{
public IMethodBodyGenerator UseProvidedBody(Func<TConstants, TReturnType> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });

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

public record DataMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants>(BodyGenerationData Data) : IMethodBodyBuilderStage5ReturnVoidWithConstants<TParam1, TConstants>
{
public IMethodBodyGenerator UseProvidedBody(Action<TConstants, TParam1> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });
}

public record DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants>(BodyGenerationData Data) : IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants<TConstants>
{
public IMethodBodyGenerator UseProvidedBody(Action<TConstants> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body });
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ internal static class BodyGenerationDataExtractor
/// Checks for <c>ReturnConstantValueFactory</c> first, then <c>RuntimeDelegateBody</c>.
/// Returns a <see cref="FluentBodyResult"/> with the extracted value, or <c>null</c> return value
/// if neither factory nor body are present.
/// Sets <see cref="FluentBodyResult.HasDelegateBody"/> when <c>RuntimeDelegateBody</c> is present,
/// indicating that the delegate body source code should be extracted from the syntax tree.
/// </summary>
internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnType)
{
Expand All @@ -26,23 +28,35 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT
{
// The method returned something that isn't a DataMethodBodyGenerator.
// This may happen when the fluent chain is incomplete (e.g., user returned an intermediate builder).
return new FluentBodyResult(null, isVoidReturnType);
return new FluentBodyResult(null, isVoidReturnType, HasDelegateBody: false);
}

object? bodyGenerationData = dataProperty.GetValue(methodResult);
if (bodyGenerationData == null)
{
return new FluentBodyResult(null, isVoidReturnType);
return new FluentBodyResult(null, isVoidReturnType, HasDelegateBody: false);
}

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

bool hasDelegateBody = HasRuntimeDelegateBody(dataType, bodyGenerationData);

return TryExtractFromConstantFactory(dataType, bodyGenerationData, isVoid)
?? TryExtractFromRuntimeBody(dataType, bodyGenerationData, isVoid)
?? new FluentBodyResult(null, isVoid);
?? TryExtractFromRuntimeBody(dataType, bodyGenerationData, isVoid, hasDelegateBody)
?? new FluentBodyResult(null, isVoid, hasDelegateBody);
}

/// <summary>
/// Checks whether <c>RuntimeDelegateBody</c> is set (non-null) in the body generation data.
/// </summary>
private static bool HasRuntimeDelegateBody(Type dataType, object bodyGenerationData)
{
PropertyInfo? runtimeBodyProperty = dataType.GetProperty("RuntimeDelegateBody");
Delegate? runtimeBody = runtimeBodyProperty?.GetValue(bodyGenerationData) as Delegate;
return runtimeBody != null;
}

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

object? constantValue = constantFactory.DynamicInvoke();
return new FluentBodyResult(constantValue?.ToString(), isVoid);
return new FluentBodyResult(constantValue?.ToString(), isVoid, HasDelegateBody: false);
}

/// <summary>
Expand All @@ -72,7 +86,8 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT
private static FluentBodyResult? TryExtractFromRuntimeBody(
Type dataType,
object bodyGenerationData,
bool isVoid)
bool isVoid,
bool hasDelegateBody)
{
PropertyInfo? runtimeBodyProperty = dataType.GetProperty("RuntimeDelegateBody");
Delegate? runtimeBody = runtimeBodyProperty?.GetValue(bodyGenerationData) as Delegate;
Expand All @@ -85,10 +100,10 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT
if (bodyParams.Length == 0)
{
object? bodyResult = runtimeBody.DynamicInvoke();
return new FluentBodyResult(bodyResult?.ToString(), isVoid);
return new FluentBodyResult(bodyResult?.ToString(), isVoid, hasDelegateBody);
}

// For delegates with parameters, we can't invoke at compile time without values
return new FluentBodyResult(null, isVoid);
return new FluentBodyResult(null, isVoid, hasDelegateBody);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,25 @@
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
/// Extracts the delegate body source code from the outermost invocation's lambda argument
/// in a generator method's return expression. 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.
/// Attempts to find the lambda argument of the outermost invocation in the generator
/// method's return expression and extract the lambda body. Returns <c>null</c> if no
/// such lambda is found.
/// For expression lambdas, returns the expression text.
/// 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)
ExpressionSyntax? returnExpression = GetReturnExpression(generatorMethodSyntax);
if (returnExpression is not InvocationExpressionSyntax invocation)
{
return null;
}
Expand All @@ -54,6 +49,29 @@ inv.Expression is MemberAccessExpressionSyntax memberAccess &&
return null;
}

/// <summary>
/// Gets the return expression from a generator method. Handles both expression-body
/// methods (<c>=&gt; expr</c>) and block-body methods (<c>{ return expr; }</c>).
/// Assumes the generator method has a simple structure with at most one return statement.
/// </summary>
private static ExpressionSyntax? GetReturnExpression(MethodDeclarationSyntax method)
{
if (method.ExpressionBody != null)
{
return method.ExpressionBody.Expression;
}

if (method.Body != null)
{
ReturnStatementSyntax? returnStatement = method.Body.Statements
.OfType<ReturnStatementSyntax>()
.FirstOrDefault();
return returnStatement?.Expression;
}
Comment on lines +64 to +70
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

GetReturnExpression’s comment says the method “assumes … at most one return statement”, but the implementation uses FirstOrDefault(), which will silently pick the first return if there are multiple. Consider enforcing the assumption (e.g., SingleOrDefault + diagnostic) or updating the comment/logic so unexpected shapes don’t lead to extracting the wrong expression without any signal.

Copilot uses AI. Check for mistakes.

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ internal sealed record SwitchBodyData(

/// <summary>
/// Result extracted from <see cref="DataBuilding.BodyGenerationData"/> after executing a fluent body generator method.
/// <see cref="HasDelegateBody"/> indicates that the generator used <c>UseProvidedBody</c>,
/// signaling that the delegate body source code should be extracted from the syntax tree.
/// </summary>
internal sealed record FluentBodyResult(
string? ReturnValue,
bool IsVoid);
bool IsVoid,
bool HasDelegateBody);

/// <summary>
/// Orchestrates the execution of generator methods at compile time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,10 @@ private static string GenerateSourceForGroup(
}

/// <summary>
/// Generates source code from a fluent body pattern. First attempts to extract the delegate
/// body from a <c>UseProvidedBody</c> call in the syntax tree. If no such call is found,
/// falls back to executing the generator method and extracting the return value.
/// Generates source code from a fluent body pattern. Executes the generator method first
/// to obtain <see cref="FluentBodyResult"/>. If the result indicates a delegate body was
/// provided (via <see cref="FluentBodyResult.HasDelegateBody"/>), attempts to extract the
/// lambda body from the syntax tree. Otherwise, uses the runtime-evaluated return value.
/// </summary>
private static string GenerateFromFluentBodyPattern(
SourceProductionContext context,
Expand All @@ -107,18 +108,6 @@ private static string GenerateFromFluentBodyPattern(
INamedTypeSymbol containingType,
Compilation compilation)
{
string? delegateBody = DelegateBodySyntaxExtractor.TryExtractDelegateBody(methodInfo.Syntax);
if (delegateBody != null)
{
bool isVoidReturn = partialMethod.ReturnType.SpecialType == SpecialType.System_Void;
string bodyLines = FormatDelegateBodyForEmit(delegateBody, isVoidReturn);

return GeneratesMethodPatternSourceBuilder.GeneratePartialMethodWithBody(
containingType,
partialMethod,
bodyLines);
}

(FluentBodyResult? result, string? error) = GeneratesMethodExecutionRuntime.ExecuteFluentBodyGeneratorMethod(
methodInfo.Symbol,
partialMethod,
Expand All @@ -134,10 +123,25 @@ private static string GenerateFromFluentBodyPattern(
return string.Empty;
}

if (result!.HasDelegateBody)
{
string? delegateBody = DelegateBodySyntaxExtractor.TryExtractDelegateBody(methodInfo.Syntax);
if (delegateBody != null)
{
Comment on lines +126 to +130
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

When HasDelegateBody is true but TryExtractDelegateBody returns null (e.g., non-lambda argument or unexpected return-expression shape), the code silently falls back to GenerateSimplePartialMethod. For parameterized delegates, ReturnValue will be null and the generated partial method will return default, which is incorrect and hard to diagnose. Consider reporting a diagnostic (and failing generation for that target) when HasDelegateBody is true but extraction fails, instead of falling back to the runtime-evaluated ReturnValue.

Copilot uses AI. Check for mistakes.
bool isVoidReturn = partialMethod.ReturnType.SpecialType == SpecialType.System_Void;
string bodyLines = FormatDelegateBodyForEmit(delegateBody, isVoidReturn);

return GeneratesMethodPatternSourceBuilder.GeneratePartialMethodWithBody(
containingType,
partialMethod,
bodyLines);
}
}

return GeneratesMethodPatternSourceBuilder.GenerateSimplePartialMethod(
containingType,
partialMethod,
result!.ReturnValue);
result.ReturnValue);
}

/// <summary>
Expand Down
Loading