Skip to content

MSTEST0064: gate code fix against await-forbidden returns and unwrap explicit delegate creations (#8256 follow-up)#8329

Merged
Evangelink merged 4 commits into
mainfrom
dev/evangelink/pr-8256-followup
May 19, 2026
Merged

MSTEST0064: gate code fix against await-forbidden returns and unwrap explicit delegate creations (#8256 follow-up)#8329
Evangelink merged 4 commits into
mainfrom
dev/evangelink/pr-8256-followup

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Follow-up to #8256. Addresses two issues raised in PR review that survived to the merged code.

What does this PR do?

1. AwaitableReturnStatementRewriter no longer emits await inside await-forbidden contexts

For a non-async Task/ValueTask-returning test method, the fixer rewrites every return X; into await X; return;. The rewriter already skipped lambdas, anonymous methods and local functions, but not lock / unsafe / fixed blocks. As a result, code like:

[TestMethod]
public Task TestMethod()
{
    Assert.ThrowsExactly<E>(() => BarAsync().GetAwaiter().GetResult()); // diagnostic fires (outside lock)
    lock (_gate)
    {
        return Task.CompletedTask;
    }
}

would be rewritten to await Task.CompletedTask; return; inside the lock, producing CS1996 Cannot await in the body of a lock statement.

The fixer now walks the containing method (respecting the same boundaries as the rewriter) and, when a non-async Task/ValueTask test method has any return X; inside a lock, unsafe, or fixed block, it skips registering the code fix. The diagnostic is still reported so the user is aware and can convert manually. (Originally flagged in the low-confidence reviewer feedback on commit 190da15e.)

2. Code fix now unwraps explicit delegate creations (new Action(...))

The analyzer reports Assert.Throws<E>(new Action(() => task.GetAwaiter().GetResult())) because Roslyn represents the wrapper as an IDelegateCreationOperation whose target is the inner anonymous function. The fixer only handled lambda / parenthesized / cast / anonymous-method syntactic arguments, so it would rename to ThrowsAsync while leaving new Action(...) in place — but ThrowsAsync takes Func<Task>, so the result didn't compile.

TryReplaceActionExpression now also unwraps ObjectCreationExpressionSyntax arguments with a single lambda or anonymous-method argument and applies the same transformation to the inner expression.

Tests

Added 4 regression tests in PreferAsyncAssertionAnalyzerTests:

  • WhenAssertionActionIsExplicitDelegateCreation_CodeFixUnwrapsDelegateCreation
  • WhenAssertionActionIsExplicitDelegateCreationWithAnonymousMethod_CodeFixUnwrapsDelegateCreation
  • WhenNonAsyncTaskMethodHasReturnInsideLockBlock_DiagnosticReportedButNoCodeFixOffered
  • WhenNonAsyncValueTaskMethodHasReturnInsideUnsafeBlock_DiagnosticReportedButNoCodeFixOffered

All 2152 MSTest.Analyzers.UnitTests (net8.0 + net472) pass locally.

Built locally with .\build.cmd -c Release (0 warnings, 0 errors).

Follow-up to #8256. Addresses two issues raised in PR review:

1. The fixer's AwaitableReturnStatementRewriter would emit `await`
   inside `lock`, `unsafe`, and `fixed` blocks when a non-async
   `Task`/`ValueTask`-returning test method had `return X;` inside
   such a block, producing CS1996 (Cannot await in body of lock
   statement) and equivalent invalid code. The fixer now scans the
   containing method for return-with-expression statements inside
   await-forbidden blocks and skips offering the code fix when one
   would produce invalid code. The diagnostic is still reported so the
   user can manually convert the test method.

2. The analyzer reported diagnostics on explicit delegate creations
   such as `Assert.Throws<E>(new Action(() => task.GetAwaiter().GetResult()))`,
   but the fixer left the `new Action(...)` wrapper in place while
   renaming to `ThrowsAsync`. Since `ThrowsAsync` takes
   `Func<Task>`, the resulting code did not compile. The fixer now
   unwraps `ObjectCreationExpressionSyntax` arguments with a single
   lambda or anonymous-method argument and transforms the inner
   expression.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 18, 2026 15:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is a follow-up to MSTEST0064 (“prefer async assertions”) that hardens the code fix to avoid producing uncompilable code, and extends the fixer to handle an additional syntax form seen in real-world code.

Changes:

  • Skip registering the MSTEST0064 code fix when it would require emitting await inside lock/unsafe/fixed blocks for non-async Task/ValueTask test methods.
  • Extend the fixer to unwrap explicit delegate creations (e.g., new Action(...)) so the transformed call correctly matches *Async assertion signatures.
  • Add regression tests covering the above scenarios (delegate creation + no-fix-in-unawaitable-context).
Show a summary per file
File Description
test/UnitTests/MSTest.Analyzers.UnitTests/PreferAsyncAssertionAnalyzerTests.cs Adds regression tests for explicit delegate creation unwrapping and for “diagnostic but no code fix” when returns appear in await-forbidden contexts.
src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs Gates code-fix registration when return rewriting would force await into lock/unsafe/fixed contexts; unwraps new Action(...)-style arguments during transformation.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

@Evangelink Evangelink left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

# Dimension Verdict
13 Test Completeness 🟡 1 MODERATE — fixed block path untested
18 Analyzer & Code Fix Quality 🟡 1 MODERATE — target-typed new() not handled

✅ 19/21 dimensions clean.

  • Analyzer Quality — ImplicitObjectCreationExpressionSyntax (new()) not covered by the ObjectCreation unwrapping branch
  • Test Completeness — VisitFixedStatement has no test; lock and unsafe are covered but fixed is not

Overall: The two new features — suppressing the code fix when a non-async Task/ValueTask method returns inside a lock/unsafe/fixed block, and unwrapping new Action(...) delegate wrappers — are logically sound and the UnawaitableReturnDetector walker is correctly implemented. The guard conditions are consistent with the existing AwaitableReturnStatementRewriter scope. The two findings above are coverage gaps, not correctness regressions.

Generated by Expert Code Review (on open) for issue #8329 · ● 6M

@Evangelink

Copy link
Copy Markdown
Member Author

@copilot address review comments

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>

Copilot AI commented May 18, 2026

Copy link
Copy Markdown
Contributor

@copilot address review comments

Done in e6d27bb. I addressed the review comments by adding target-typed new() unwrapping support in the fixer and adding a regression test for the fixed-block unawaitable return path.

@Evangelink

Copy link
Copy Markdown
Member Author

@copilot fix

test/UnitTests/MSTest.Analyzers.UnitTests/Verifiers/CSharpCodeFixVerifier`2.cs#L92

test/UnitTests/MSTest.Analyzers.UnitTests/Verifiers/CSharpCodeFixVerifier`2.cs(92,1): error : WhenAssertionActionIsTargetTypedDelegateCreation_CodeFixUnwrapsDelegateCreation [net8.0]
Test method MSTest.Analyzers.Test.PreferAsyncAssertionAnalyzerTests.WhenAssertionActionIsTargetTypedDelegateCreation_CodeFixUnwrapsDelegateCreation threw exception:
System.InvalidOperationException: Context: Diagnostics of test state
Mismatch between number of diagnostics returned, expected "1" actual "3"

Diagnostics:
// /0/Test0.cs(11,9): info MSTEST0064: Use 'Assert.ThrowsExactlyAsync' instead of blocking an async call in 'Assert.ThrowsExactly'
VerifyCS.Diagnostic().WithSpan(11, 9, 11, 104).WithArguments("ThrowsExactlyAsync", "ThrowsExactly"),
// /0/Test0.cs(11,67): error CS0029: Cannot implicitly convert type 'void' to 'object'
DiagnosticResult.CompilerError("CS0029").WithSpan(11, 67, 11, 102).WithArguments("void", "object"),
// /0/Test0.cs(11,67): error CS1662: Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type
DiagnosticResult.CompilerError("CS1662").WithSpan(11, 67, 11, 102).WithArguments("lambda expression"),

Check failure on line 1067 in test/UnitTests/MSTest.Analyzers.UnitTests/PreferAsyncAssertionAnalyzerTests.cs

@azure-pipelines
azure-pipelines
/ microsoft.testfx (Build Linux Debug)
test/UnitTests/MSTest.Analyzers.UnitTests/PreferAsyncAssertionAnalyzerTests.cs#L1067

test/UnitTests/MSTest.Analyzers.UnitTests/PreferAsyncAssertionAnalyzerTests.cs(1067,1): error : WhenNonAsyncTaskMethodHasReturnInsideFixedBlock_DiagnosticReportedButNoCodeFixOffered [net8.0]
Test method MSTest.Analyzers.Test.PreferAsyncAssertionAnalyzerTests.WhenNonAsyncTaskMethodHasReturnInsideFixedBlock_DiagnosticReportedButNoCodeFixOffered threw exception:
System.InvalidOperationException: Context: Diagnostics of test state
Mismatch between number of diagnostics returned, expected "1" actual "0"

Diagnostics:
NONE.

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 18, 2026 16:46
@Evangelink Evangelink removed the request for review from Copilot May 18, 2026 16:46

Copilot AI commented May 18, 2026

Copy link
Copy Markdown
Contributor

@copilot fix

test/UnitTests/MSTest.Analyzers.UnitTests/Verifiers/CSharpCodeFixVerifier`2.cs#L92
...

Fixed in 1f9479f. I updated the target-typed new() test input to disambiguate the Action overload while still exercising ImplicitObjectCreationExpressionSyntax, and adjusted the fixed-block test to keep the assertion outside unsafe syntax (method is non-unsafe, fixed nested in an unsafe block), which restores the expected MSTEST0064 diagnostic/code-fix behavior.

@Evangelink

Copy link
Copy Markdown
Member Author

@copilot fix

test/UnitTests/MSTest.Analyzers.UnitTests/Verifiers/CSharpCodeFixVerifier`2.cs#L92

test/UnitTests/MSTest.Analyzers.UnitTests/Verifiers/CSharpCodeFixVerifier`2.cs(92,1): error : WhenAssertionActionIsTargetTypedDelegateCreation_CodeFixUnwrapsDelegateCreation [net8.0]
Test method MSTest.Analyzers.Test.PreferAsyncAssertionAnalyzerTests.WhenAssertionActionIsTargetTypedDelegateCreation_CodeFixUnwrapsDelegateCreation threw exception:
System.InvalidOperationException: Context: Diagnostics of test state
Expected diagnostic to start at column "9" was actually at column "61"

Expected diagnostic:
// /0/Test0.cs(11,9,11,114): info MSTEST0064
VerifyCS.Diagnostic().WithSpan(11, 9, 11, 114),

Actual diagnostic:
// /0/Test0.cs(11,61): error CS0407: 'void Action.Invoke()' has the wrong return type
DiagnosticResult.CompilerError("CS0407").WithSpan(11, 61, 11, 112).WithArguments("System.Action.Invoke()", "void"),

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 18, 2026 18:40
@Evangelink Evangelink removed the request for review from Copilot May 18, 2026 18:40

Copilot AI commented May 18, 2026

Copy link
Copy Markdown
Contributor

@copilot fix

test/UnitTests/MSTest.Analyzers.UnitTests/Verifiers/CSharpCodeFixVerifier`2.cs#L92
...

Fixed in 520976a. I changed the target-typed delegate creation test input to Assert.ThrowsExactly<...>((Action)new(() => ...)), which still exercises ImplicitObjectCreationExpressionSyntax but disambiguates the overload and avoids the CS0407 compiler error.

@Evangelink Evangelink merged commit 9217e81 into main May 19, 2026
10 checks passed
@Evangelink Evangelink deleted the dev/evangelink/pr-8256-followup branch May 19, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants