[WIP] Fix DelegateBodyTests to ensure UseProvidedBody accepts any body#94
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
90d0e3d to
0bce1ab
Compare
…neration 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
There was a problem hiding this comment.
Pull request overview
This PR aims to fix failing DelegateBodyTests by making fluent UseProvidedBody(...) delegate bodies get emitted into the generated partial method “as written”, preferring syntax-based extraction over runtime execution.
Changes:
- Added syntax-tree extraction of
UseProvidedBody(...)lambda bodies and emit support for arbitrary method bodies. - Updated the fluent generation pipeline to try syntax extraction first, falling back to runtime execution if not found.
- Updated tests/examples and extended the fluent builder API surface (compile-time constants staging).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| EasySourceGenerators.sln.DotSettings.user | Adds Rider/ReSharper user settings entry. |
| EasySourceGenerators.Tests/EasySourceGenerators.Tests.csproj | Adds System.Text as a global using for tests. |
| EasySourceGenerators.Tests/DelegateBodyTests.cs | Fixes lambda parameter naming and expected body formatting to match emitted bodies. |
| EasySourceGenerators.Generators/SourceEmitting/PartialMethodSourceEmitter.cs | Adds EmitWithBody to emit arbitrary method bodies. |
| EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodPatternSourceBuilder.cs | Adds GeneratePartialMethodWithBody wrapper around EmitWithBody. |
| EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs | Attempts delegate-body syntax extraction before runtime execution; adds delegate-body formatting helper. |
| EasySourceGenerators.Generators/IncrementalGenerators/DelegateBodySyntaxExtractor.cs | New extractor to pull lambda bodies from UseProvidedBody(...) calls and re-indent them. |
| EasySourceGenerators.Generators/DataBuilding/DataRecords.cs | Extends BodyGenerationData with CompileTimeConstants. |
| EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs | Removes old BodyWithSwitchStatement stub/commented plan block. |
| EasySourceGenerators.Examples/PiExampleFluent.cs | Removes the old commented-out fluent PI example. |
| EasySourceGenerators.Examples/PiExample.cs | Introduces a new fluent PI example using WithCompileTimeConstants + UseProvidedBody. |
| EasySourceGenerators.Abstractions/MethodBody/IMethodBodyBuilder.cs | Adds WithCompileTimeConstants and new stage-5 interfaces/overloads. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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) => |
There was a problem hiding this comment.
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.
| bool isBlockBody = delegateBody.Contains("\n"); | ||
|
|
||
| if (isBlockBody) | ||
| { | ||
| return delegateBody; | ||
| } | ||
|
|
||
| if (isVoidReturn) | ||
| { | ||
| return $" {delegateBody};"; | ||
| } | ||
|
|
||
| return $" return {delegateBody};"; |
There was a problem hiding this comment.
FormatDelegateBodyForEmit determines whether the extracted lambda was a block by checking delegateBody.Contains("\n"). DelegateBodySyntaxExtractor trims trailing newlines, so a single-statement block lambda can produce a body with no \n and will be treated as an expression body, resulting in invalid emitted code (e.g., double return or ;). Consider returning a (body, kind) result from the extractor (expression vs block), or otherwise passing an explicit flag so block bodies are never inferred via newline presence.
| bool isBlockBody = delegateBody.Contains("\n"); | |
| if (isBlockBody) | |
| { | |
| return delegateBody; | |
| } | |
| if (isVoidReturn) | |
| { | |
| return $" {delegateBody};"; | |
| } | |
| return $" return {delegateBody};"; | |
| // Treat the body as a block/statement body if it spans multiple lines or already | |
| // appears to be a complete statement (e.g., ends with a semicolon). This avoids | |
| // misclassifying single-statement block lambdas (like "return x;") as expression | |
| // bodies, which would otherwise lead to invalid emitted code (double "return" or ";"). | |
| string trimmedBody = delegateBody.TrimEnd(); | |
| bool isBlockBody = trimmedBody.Contains("\n") || trimmedBody.EndsWith(";"); | |
| if (isBlockBody) | |
| { | |
| // For block/statement bodies, use the text as provided by the extractor. | |
| return delegateBody; | |
| } | |
| if (isVoidReturn) | |
| { | |
| return $" {trimmedBody};"; | |
| } | |
| return $" return {trimmedBody};"; |
| for (int i = 0; i < lines.Length; i++) | ||
| { | ||
| if (openIndex == -1 && lines[i].TrimEnd().EndsWith("{", StringComparison.Ordinal)) | ||
| { | ||
| openIndex = i; |
There was a problem hiding this comment.
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.
| /// <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. |
There was a problem hiding this comment.
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.
| /// 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). |
| public record DataMethodBodyBuilderStage4<TParam1, TReturnType>(BodyGenerationData Data) : IMethodBodyBuilderStage4<TParam1, TReturnType> | ||
| { | ||
| public IMethodBodyGenerator UseProvidedBody(Func<TParam1, TReturnType> body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); | ||
|
|
||
| public IMethodBodyGenerator BodyReturningConstant(Func<TReturnType> constantValueFactory) => |
There was a problem hiding this comment.
IMethodBodyBuilderStage4* interfaces now include WithCompileTimeConstants(...), but the concrete builder stages here (e.g., DataMethodBodyBuilderStage4<TParam1, TReturnType>) do not implement that member (nor the new stage-5 types). As written, this will not compile once the updated interfaces are referenced. Add the WithCompileTimeConstants implementations and the corresponding stage-5 builder records, and ensure they populate BodyGenerationData appropriately.
| <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConditionalAttribute_002Ecs_002Fl_003AC_0021_003FUsers_003Fdex3r_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5878d76d3b81484eafbba90b77c960fed2b38_003F80_003F1d2bb8bd_003FConditionalAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||
| <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACSharpCompilationReference_002Ecs_002Fl_003AC_0021_003FUsers_003Fdex3r_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd18742e05d9d34467e6d756ef95f2def337dc61bd1678295c715359e839bb5_003FCSharpCompilationReference_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||
| <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGeneratorAttribute_002Ecs_002Fl_003AC_0021_003FUsers_003Fdex3r_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff1fac3c62ca6565cc950105beae419771ba372ddd8be9ee61ee7edba4bcc22_003FGeneratorAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||
| <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObject_002Ecs_002Fl_003AC_0021_003FUsers_003Fdex3r_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F51505665371d472f8bdbc333fa4d888cf49938_003Fc8_003F5d7360d0_003FObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> |
There was a problem hiding this comment.
This .DotSettings.user file contains IDE/user-specific configuration (including machine-specific cache paths). These files typically shouldn't be committed because they create noisy diffs and are not portable across environments. Consider removing it from the repo/PR and adding it to .gitignore (or using a non-.user shared settings file if you need team-wide defaults).
DelegateBodySyntaxExtractorclass to extract lambda body from syntax treeEmitWithBodymethod toPartialMethodSourceEmitterfor emitting arbitrary method bodiesGeneratePartialMethodWithBodytoGeneratesMethodPatternSourceBuilderGeneratesMethodGenerationPipeline.GenerateFromFluentBodyPatternto try syntax extraction firstsomeParamHere→someParam) to match actual method parametersSystem.Textglobal using to test project forStringBuilderavailability in generated codeOriginal prompt
📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.