From 89aae3c9cacd066c25504a7750725c3dd113fe21 Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Tue, 6 Oct 2026 15:42:51 -0400 Subject: [PATCH 1/2] Add failing tests for a catch filter which checks cancellation Generated with Claude Code --- .../IsCancellationRequestedTests.cs | 43 +++++++++++++++++++ ...atchFilterChecksCancellation#g.verified.cs | 12 ++++++ ...terChecksCancellationAndMore#g.verified.cs | 9 ++++ ...tchFilterKeepsPreservedToken#g.verified.cs | 12 ++++++ 4 files changed, 76 insertions(+) create mode 100644 tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterChecksCancellation#g.verified.cs create mode 100644 tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterChecksCancellationAndMore#g.verified.cs create mode 100644 tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterKeepsPreservedToken#g.verified.cs diff --git a/tests/Generator.Tests/IsCancellationRequestedTests.cs b/tests/Generator.Tests/IsCancellationRequestedTests.cs index d36d90c..d367817 100644 --- a/tests/Generator.Tests/IsCancellationRequestedTests.cs +++ b/tests/Generator.Tests/IsCancellationRequestedTests.cs @@ -129,4 +129,47 @@ public Task WhileNotCancelledReplacedBySyncOnly() => """ } #endif """.Verify(sourceType: SourceType.MethodBody); + + [Fact] + public Task CatchFilterChecksCancellation() => """ +try +{ + await Task.Delay(1, ct); +} +catch (OperationCanceledException) when (ct.IsCancellationRequested) +{ + throw; +} +catch (Exception) +{ +} +""".Verify(sourceType: SourceType.MethodBody); + + [Fact] + public Task CatchFilterChecksCancellationAndMore() => """ +try +{ + await Task.Delay(1, ct); +} +catch (OperationCanceledException) when (ct.IsCancellationRequested || Environment.TickCount > 0) +{ + throw; +} +""".Verify(sourceType: SourceType.MethodBody); + + [Fact] + public Task CatchFilterKeepsPreservedToken() => """ +[CreateSyncVersion(PreserveCancellationToken = true)] +public async Task MethodAsync(CancellationToken ct) +{ + try + { + await Task.Delay(1, ct); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; + } +} +""".Verify(); } diff --git a/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterChecksCancellation#g.verified.cs b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterChecksCancellation#g.verified.cs new file mode 100644 index 0000000..a9ffebf --- /dev/null +++ b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterChecksCancellation#g.verified.cs @@ -0,0 +1,12 @@ +//HintName: Test.Class.MethodAsync.g.cs +try +{ + global::System.Threading.Thread.Sleep(1); +} +catch (global::System.OperationCanceledException) when (global::System.Threading.CancellationToken.None.IsCancellationRequested) +{ + throw; +} +catch (global::System.Exception) +{ +} diff --git a/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterChecksCancellationAndMore#g.verified.cs b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterChecksCancellationAndMore#g.verified.cs new file mode 100644 index 0000000..57b467a --- /dev/null +++ b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterChecksCancellationAndMore#g.verified.cs @@ -0,0 +1,9 @@ +//HintName: Test.Class.MethodAsync.g.cs +try +{ + global::System.Threading.Thread.Sleep(1); +} +catch (global::System.OperationCanceledException) when (global::System.Threading.CancellationToken.None.IsCancellationRequested || global::System.Environment.TickCount > 0) +{ + throw; +} diff --git a/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterKeepsPreservedToken#g.verified.cs b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterKeepsPreservedToken#g.verified.cs new file mode 100644 index 0000000..e9cb3b0 --- /dev/null +++ b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.CatchFilterKeepsPreservedToken#g.verified.cs @@ -0,0 +1,12 @@ +//HintName: Test.Class.MethodAsync.g.cs +public void Method(global::System.Threading.CancellationToken ct) +{ + try + { + global::System.Threading.Thread.Sleep(1); + } + catch (global::System.OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; + } +} From dac96a669e2037abe1fd4b1336b36806455b2eab Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Tue, 6 Oct 2026 15:43:24 -0400 Subject: [PATCH 2/2] Stand in CancellationToken.None for a removed token A catch filter which checks the token has to keep its shape, so the reference is replaced rather than dropped. None is never cancelled, which is what the synchronized method does. Generated with Claude Code --- .../AsyncToSyncRewriter.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs b/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs index 26576c1..4b57903 100644 --- a/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs +++ b/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs @@ -284,9 +284,24 @@ _ when GetReplacement(symbol) is { } name => @base.WithIdentifier(Identifier(Glo /// public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) - => renamedLocalFunctions.TryGetValue(node.Identifier.ValueText, out var newName) - ? node.WithIdentifier(Identifier(newName)) - : base.VisitIdentifierName(node); + { + if (renamedLocalFunctions.TryGetValue(node.Identifier.ValueText, out var newName)) + { + return node.WithIdentifier(Identifier(newName)); + } + + // A statement or an argument which mentions a removed CancellationToken is dropped whole, + // but an expression such as a catch filter has to keep its shape. CancellationToken.None + // stands in for the parameter: it is never cancelled, which is what the synchronized + // method does. + if (GetSymbol(node) is IParameterSymbol { Type: INamedTypeSymbol { IsCancellationToken: true } } parameter + && removedParameters.Contains(parameter)) + { + return IdentifierName(Global("System.Threading.CancellationToken.None")).WithTriviaFrom(node); + } + + return base.VisitIdentifierName(node); + } public override SyntaxNode? VisitLocalFunctionStatement(LocalFunctionStatementSyntax node) {