From db398c357c0d943b7acf2a1013f77cb94b4eca73 Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Tue, 6 Oct 2026 15:53:54 -0400 Subject: [PATCH 1/2] Add failing tests for ConfigureAwait on a call which does not bind Generated with Claude Code --- ...aitOnAnUnboundCall#ReadAsync.g.verified.cs | 5 +++++ ...CallInADeclaration#ReadAsync.g.verified.cs | 6 ++++++ tests/Generator.Tests/TaskTests.cs | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 tests/Generator.Tests/Snapshots/TaskTests.ConfigureAwaitOnAnUnboundCall#ReadAsync.g.verified.cs create mode 100644 tests/Generator.Tests/Snapshots/TaskTests.ConfigureAwaitOnAnUnboundCallInADeclaration#ReadAsync.g.verified.cs diff --git a/tests/Generator.Tests/Snapshots/TaskTests.ConfigureAwaitOnAnUnboundCall#ReadAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/TaskTests.ConfigureAwaitOnAnUnboundCall#ReadAsync.g.verified.cs new file mode 100644 index 0000000..7981ca7 --- /dev/null +++ b/tests/Generator.Tests/Snapshots/TaskTests.ConfigureAwaitOnAnUnboundCall#ReadAsync.g.verified.cs @@ -0,0 +1,5 @@ +//HintName: Test.Class.ReadAsync.g.cs +static void Read(dynamic d) +{ + d.ReadPayload(); +} diff --git a/tests/Generator.Tests/Snapshots/TaskTests.ConfigureAwaitOnAnUnboundCallInADeclaration#ReadAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/TaskTests.ConfigureAwaitOnAnUnboundCallInADeclaration#ReadAsync.g.verified.cs new file mode 100644 index 0000000..af0b6d8 --- /dev/null +++ b/tests/Generator.Tests/Snapshots/TaskTests.ConfigureAwaitOnAnUnboundCallInADeclaration#ReadAsync.g.verified.cs @@ -0,0 +1,6 @@ +//HintName: Test.Class.ReadAsync.g.cs +static dynamic Read(dynamic d) +{ + dynamic payload = d.ReadPayload(); + return payload; +} diff --git a/tests/Generator.Tests/TaskTests.cs b/tests/Generator.Tests/TaskTests.cs index cfb7a02..a2c2b6d 100644 --- a/tests/Generator.Tests/TaskTests.cs +++ b/tests/Generator.Tests/TaskTests.cs @@ -64,5 +64,24 @@ public async Task MethodAsync() private async Task FooAsync() => await Task.CompletedTask; private void Foo() { } +""".Verify(); + + [Fact] + public Task ConfigureAwaitOnAnUnboundCall() => """ +[CreateSyncVersion] +static async Task ReadAsync(dynamic d) +{ + await d.ReadPayloadAsync().ConfigureAwait(false); +} +""".Verify(); + + [Fact] + public Task ConfigureAwaitOnAnUnboundCallInADeclaration() => """ +[CreateSyncVersion] +static async Task ReadAsync(dynamic d) +{ + dynamic payload = await d.ReadPayloadAsync().ConfigureAwait(false); + return payload; +} """.Verify(); } From 003b870947ec50d85fa2a30c6a21b3c499a361d2 Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Tue, 6 Oct 2026 15:53:59 -0400 Subject: [PATCH 2/2] Drop an awaited ConfigureAwait from a call which does not bind A call whose argument another generator supplies cannot bind, since generators do not see each other's output, and the removal ran off the bound symbol. Reported from dotnet/SqlClient, where the argument came from the System.Text.Json generator. Generated with Claude Code --- .../AsyncToSyncRewriter.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs b/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs index 26576c1..24e7a66 100644 --- a/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs +++ b/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs @@ -34,6 +34,7 @@ internal sealed class AsyncToSyncRewriter(SemanticModel semanticModel, bool disa // Members private const string CompletedTask = nameof(Task.CompletedTask); + private const string ConfigureAwait = nameof(Task.ConfigureAwait); private const string Delay = nameof(Task<>.Delay); private const string FromResult = nameof(Task<>.FromResult); private const string WaitAsync = "WaitAsync"; @@ -493,8 +494,23 @@ bool InitializedToMemory(SyntaxNode node) if (symbol is null) { - // A missing symbol usually means the code doesn't compile due to an error or a missing reference / usings. - return base.VisitInvocationExpression(node); + // A missing symbol usually means the code doesn't compile due to an error or a missing + // reference / usings, but a call whose argument another generator supplies cannot bind + // either, generators not seeing each other's output. An awaited ConfigureAwait has no + // place in the synchronized method whether or not the call it follows binds, so drop + // it by its shape, as the Async suffix is already dropped by its shape. + var unbound = base.VisitInvocationExpression(node); + + var awaitedConfigureAwait = node is + { + Parent: AwaitExpressionSyntax, + Expression: MemberAccessExpressionSyntax { Name.Identifier.ValueText: ConfigureAwait }, + ArgumentList.Arguments.Count: 1, + }; + + return awaitedConfigureAwait && unbound is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax rewritten } + ? KeepExpressionBeforeDot(rewritten) + : unbound; } if (symbol is not IMethodSymbol methodSymbol)