From 77250d53612ca9bf32b8191dd1c3e74301812a4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:04:24 +0000 Subject: [PATCH 1/3] Initial plan From 5420b05c68108239fa21683b8afd451f9266d8fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:17:15 +0000 Subject: [PATCH 2/3] 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 <3155725+dex3r@users.noreply.github.com> Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/ea25cd90-2a07-42c1-8091-f255cb2c6687 --- .../DataBuilding/DataMethodBodyBuilders.cs | 38 ++++++++++++++++ .../BodyGenerationDataExtractor.cs | 31 +++++++++---- .../DelegateBodySyntaxExtractor.cs | 43 +++++++++++++------ .../GeneratesMethodExecutionRuntime.cs | 5 ++- .../GeneratesMethodGenerationPipeline.cs | 36 +++++++++------- 5 files changed, 115 insertions(+), 38 deletions(-) diff --git a/EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs b/EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs index cef2554..02e3695 100644 --- a/EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs +++ b/EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs @@ -38,6 +38,9 @@ public IMethodBodyBuilderStage4 WithParameter() => public record DataMethodBodyBuilderStage4(BodyGenerationData Data) : IMethodBodyBuilderStage4 { + public IMethodBodyBuilderStage5WithConstants WithCompileTimeConstants(Func compileTimeConstantsFactory) => + new DataMethodBodyBuilderStage5WithConstants(Data with { CompileTimeConstants = compileTimeConstantsFactory() }); + public IMethodBodyGenerator UseProvidedBody(Func body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => @@ -46,6 +49,9 @@ public IMethodBodyGenerator BodyReturningConstant(Func constantValu public record DataMethodBodyBuilderStage4NoArg(BodyGenerationData Data) : IMethodBodyBuilderStage4NoArg { + public IMethodBodyBuilderStage5NoArgWithConstants WithCompileTimeConstants(Func compileTimeConstantsFactory) => + new DataMethodBodyBuilderStage5NoArgWithConstants(Data with { CompileTimeConstants = compileTimeConstantsFactory() }); + public IMethodBodyGenerator UseProvidedBody(Func body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => @@ -54,10 +60,42 @@ public IMethodBodyGenerator BodyReturningConstant(Func constantValu public record DataMethodBodyBuilderStage4ReturnVoid(BodyGenerationData BodyGenerationData) : IMethodBodyBuilderStage4ReturnVoid { + public IMethodBodyBuilderStage5ReturnVoidWithConstants WithCompileTimeConstants(Func compileTimeConstantsFactory) => + new DataMethodBodyBuilderStage5ReturnVoidWithConstants(BodyGenerationData with { CompileTimeConstants = compileTimeConstantsFactory() }); + public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(BodyGenerationData with { RuntimeDelegateBody = body }); } public record DataMethodBodyBuilderStage4ReturnVoidNoArg(BodyGenerationData BodyGenerationData) : IMethodBodyBuilderStage4ReturnVoidNoArg { + public IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants WithCompileTimeConstants(Func compileTimeConstantsFactory) => + new DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants(BodyGenerationData with { CompileTimeConstants = compileTimeConstantsFactory() }); + public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(BodyGenerationData with { RuntimeDelegateBody = body }); +} + +public record DataMethodBodyBuilderStage5WithConstants(BodyGenerationData Data) : IMethodBodyBuilderStage5WithConstants +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => + new DataMethodBodyGenerator(Data with { ReturnConstantValueFactory = constantValueFactory }); +} + +public record DataMethodBodyBuilderStage5NoArgWithConstants(BodyGenerationData Data) : IMethodBodyBuilderStage5NoArgWithConstants +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => + new DataMethodBodyGenerator(Data with { ReturnConstantValueFactory = constantValueFactory }); +} + +public record DataMethodBodyBuilderStage5ReturnVoidWithConstants(BodyGenerationData Data) : IMethodBodyBuilderStage5ReturnVoidWithConstants +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); +} + +public record DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants(BodyGenerationData Data) : IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); } \ No newline at end of file diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs b/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs index 9130a7a..0de4ff3 100644 --- a/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs +++ b/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs @@ -15,6 +15,8 @@ internal static class BodyGenerationDataExtractor /// Checks for ReturnConstantValueFactory first, then RuntimeDelegateBody. /// Returns a with the extracted value, or null return value /// if neither factory nor body are present. + /// Sets when RuntimeDelegateBody is present, + /// indicating that the delegate body source code should be extracted from the syntax tree. /// internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnType) { @@ -26,13 +28,13 @@ 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(); @@ -40,9 +42,21 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT 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); + } + + /// + /// Checks whether RuntimeDelegateBody is set (non-null) in the body generation data. + /// + private static bool HasRuntimeDelegateBody(Type dataType, object bodyGenerationData) + { + PropertyInfo? runtimeBodyProperty = dataType.GetProperty("RuntimeDelegateBody"); + Delegate? runtimeBody = runtimeBodyProperty?.GetValue(bodyGenerationData) as Delegate; + return runtimeBody != null; } /// @@ -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); } /// @@ -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; @@ -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); } } diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs b/EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs index 4ca71fc..be8b458 100644 --- a/EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs +++ b/EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs @@ -6,8 +6,8 @@ namespace EasySourceGenerators.Generators.IncrementalGenerators; /// -/// Extracts the delegate body source code from a UseProvidedBody(...) 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). /// internal static class DelegateBodySyntaxExtractor @@ -15,21 +15,16 @@ internal static class DelegateBodySyntaxExtractor private const string MethodBodyIndent = " "; /// - /// Attempts to find a UseProvidedBody(...) call in the given generator method syntax - /// and extract the lambda body. Returns null if no such call is found. - /// For expression lambdas, returns a single return {expr}; line. + /// Attempts to find the lambda argument of the outermost invocation in the generator + /// method's return expression and extract the lambda body. Returns null 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. /// internal static string? TryExtractDelegateBody(MethodDeclarationSyntax generatorMethodSyntax) { - InvocationExpressionSyntax? invocation = generatorMethodSyntax - .DescendantNodes() - .OfType() - .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; } @@ -54,6 +49,28 @@ inv.Expression is MemberAccessExpressionSyntax memberAccess && return null; } + /// + /// Gets the return expression from a generator method. Handles both expression-body + /// methods (=> expr) and block-body methods ({ return expr; }). + /// + private static ExpressionSyntax? GetReturnExpression(MethodDeclarationSyntax method) + { + if (method.ExpressionBody != null) + { + return method.ExpressionBody.Expression; + } + + if (method.Body != null) + { + ReturnStatementSyntax? returnStatement = method.Body.Statements + .OfType() + .FirstOrDefault(); + return returnStatement?.Expression; + } + + return null; + } + /// /// Extracts the content of a block body (between { and }), /// determines the base indentation, and re-indents all lines to the method body level. diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs index cb8add1..a1f3a2c 100644 --- a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs +++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs @@ -14,10 +14,13 @@ internal sealed record SwitchBodyData( /// /// Result extracted from after executing a fluent body generator method. +/// indicates that the generator used UseProvidedBody, +/// signaling that the delegate body source code should be extracted from the syntax tree. /// internal sealed record FluentBodyResult( string? ReturnValue, - bool IsVoid); + bool IsVoid, + bool HasDelegateBody); /// /// Orchestrates the execution of generator methods at compile time. diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs index c622a39..0358b5c 100644 --- a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs +++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs @@ -96,9 +96,10 @@ private static string GenerateSourceForGroup( } /// - /// Generates source code from a fluent body pattern. First attempts to extract the delegate - /// body from a UseProvidedBody 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 . If the result indicates a delegate body was + /// provided (via ), attempts to extract the + /// lambda body from the syntax tree. Otherwise, uses the runtime-evaluated return value. /// private static string GenerateFromFluentBodyPattern( SourceProductionContext context, @@ -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, @@ -134,10 +123,25 @@ private static string GenerateFromFluentBodyPattern( return string.Empty; } + if (result!.HasDelegateBody) + { + 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); + } + } + return GeneratesMethodPatternSourceBuilder.GenerateSimplePartialMethod( containingType, partialMethod, - result!.ReturnValue); + result.ReturnValue); } /// From e12637e3afa6363ab659ebc3b9371b82df2746bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:19:46 +0000 Subject: [PATCH 3/3] Improve doc comment on GetReturnExpression about simple structure assumption Co-authored-by: dex3r <3155725+dex3r@users.noreply.github.com> Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/ea25cd90-2a07-42c1-8091-f255cb2c6687 --- .../IncrementalGenerators/DelegateBodySyntaxExtractor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs b/EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs index be8b458..498297e 100644 --- a/EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs +++ b/EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs @@ -52,6 +52,7 @@ internal static class DelegateBodySyntaxExtractor /// /// Gets the return expression from a generator method. Handles both expression-body /// methods (=> expr) and block-body methods ({ return expr; }). + /// Assumes the generator method has a simple structure with at most one return statement. /// private static ExpressionSyntax? GetReturnExpression(MethodDeclarationSyntax method) {