Skip to content

Add MSTEST0064 to prefer async assertions#8256

Merged
Evangelink merged 17 commits into
mainfrom
copilot/add-analyzer-favorizing-async-assertions
May 18, 2026
Merged

Add MSTEST0064 to prefer async assertions#8256
Evangelink merged 17 commits into
mainfrom
copilot/add-analyzer-favorizing-async-assertions

Conversation

Copilot AI commented May 15, 2026

Copy link
Copy Markdown
Contributor

New Feature

What does this feature do?

Adds an MSTest analyzer/code fix that flags synchronous exception assertions blocking on async work and recommends async assertion APIs instead.

Assert.ThrowsExactly<SomeException>(() => foo.BarAsync().GetAwaiter().GetResult());

becomes:

await Assert.ThrowsExactlyAsync<SomeException>(() => foo.BarAsync());

Implementation details

  • Analyzer

    • Adds MSTEST0064 for Assert.Throws / Assert.ThrowsExactly calls that block Task/Task<T> via GetAwaiter().GetResult().
    • Limits diagnostics to MSTest test methods.
  • Code fix

    • Rewrites to Assert.ThrowsAsync / Assert.ThrowsExactlyAsync.
    • Removes the blocking GetAwaiter().GetResult() chain from the asserted lambda.
    • Updates void test methods to async Task when required.
  • Resources and coverage

    • Adds analyzer/code-fix resources and localization updates.
    • Adds focused C# analyzer/code-fix tests for Task, Task<T>, and non-diagnostic cases.

Copilot AI requested review from Copilot and removed request for Copilot May 15, 2026 12:26
Copilot AI linked an issue May 15, 2026 that may be closed by this pull request
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 15, 2026 12:40
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 15, 2026 12:42
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 15, 2026 12:43
Copilot AI changed the title [WIP] Add analyzer favorizing async assertions Add MSTEST0064 to prefer async assertions May 15, 2026
Copilot AI requested a review from Evangelink May 15, 2026 12:44
@Evangelink Evangelink marked this pull request as ready for review May 15, 2026 13:30
Copilot AI review requested due to automatic review settings May 15, 2026 13:30

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

Adds MSTEST0064 to detect MSTest synchronous exception assertions that block on async Task work and provide a C# code fix to use async assertion APIs.

Changes:

  • Adds PreferAsyncAssertionAnalyzer and registers MSTEST0064.
  • Adds PreferAsyncAssertionFixer to rewrite assertions and async test signatures.
  • Adds resources, localization entries, release tracking, and analyzer/code-fix tests.
Show a summary per file
File Description
src/Analyzers/MSTest.Analyzers/PreferAsyncAssertionAnalyzer.cs Adds MSTEST0064 analyzer logic.
src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs Adds C# code fix for async assertions.
src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs Registers MSTEST0064 ID.
src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md Adds release tracking entry.
src/Analyzers/MSTest.Analyzers/Resources.resx Adds analyzer resource strings.
src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx Adds code-fix title resource.
test/UnitTests/MSTest.Analyzers.UnitTests/PreferAsyncAssertionAnalyzerTests.cs Adds focused analyzer/code-fix tests.
src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf Adds localized analyzer resource placeholders.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf Adds localized code-fix resource placeholder.
src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf Adds localized code-fix resource placeholder.

Copilot's findings

Comments suppressed due to low confidence (5)

src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs:90

  • The code fix does not handle static-imported assertions such as using static ...Assert; Throws<Exception>(...). The analyzer still reports those invocations, but this method returns the original Throws name, so the fix wraps await around the synchronous assertion instead of calling ThrowsAsync, which can break compilation after the lambda is rewritten to return a Task.
        if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpression)
        {
            return invocationExpression;
        }

src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs:140

  • Changing a void test method to return unqualified Task requires System.Threading.Tasks to already be in scope. If the original test uses fully qualified task types or implicit usings are disabled, applying the fix introduces an unresolved Task; the fixer should add the using or use a qualified/simplified type.
        if (newMethodDeclaration.ReturnType.IsVoid())
        {
            newMethodDeclaration = newMethodDeclaration.WithReturnType(SyntaxFactory.IdentifierName("Task").WithTriviaFrom(newMethodDeclaration.ReturnType));
        }

src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs:160

  • The analyzer accepts block-bodied lambdas whose single operation is a return value, but the fixer only rewrites block bodies with an expression statement. For Task<T> cases like () => { return BarAsync().GetAwaiter().GetResult(); }, the fix renames to ThrowsAsync and adds await while leaving a lambda that returns the synchronous result, causing the fixed code not to compile.
        if (lambdaExpression.Body is BlockSyntax blockSyntax &&
            blockSyntax.Statements.Count == 1 &&
            blockSyntax.Statements[0] is ExpressionStatementSyntax expressionStatement)

src/Analyzers/MSTest.Analyzers/PreferAsyncAssertionAnalyzer.cs:127

  • This treats every non-return child operation as meaningful, but VB lambdas can include implicit ILabeledOperation nodes in the block. Because those labels are not skipped, otherwise valid VB cases such as a single GetAwaiter().GetResult() statement can look like multiple operations and fail to produce the diagnostic despite the analyzer being registered for Visual Basic.
            IOperation? candidateOperation = childOperation switch
            {
                IExpressionStatementOperation expressionStatementOperation => expressionStatementOperation.Operation,
                IReturnOperation { ReturnedValue: { } returnedValue } => returnedValue,
                IReturnOperation { IsImplicit: true } => null,
                _ => childOperation,

src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs:111

  • The analyzer unwraps conversions/delegate creation when identifying the lambda, but the fixer only handles a direct lambda syntax node. If the diagnosed action is explicitly cast (for example (Action)(() => ...)), the fix still renames to the async assertion and adds await while leaving the blocking casted lambda unchanged, which can make the fixed call fail overload resolution.
        if (argument.Expression is not LambdaExpressionSyntax lambdaExpression ||
            !TryGetBlockedTaskExpressionFromLambda(lambdaExpression, out ExpressionSyntax? asyncExpression))
  • Files reviewed: 33/33 changed files
  • Comments generated: 2

Comment thread src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers/PreferAsyncAssertionAnalyzer.cs
Evangelink and others added 2 commits May 15, 2026 20:46
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 16, 2026 11:58

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.

Copilot's findings

  • Files reviewed: 33/33 changed files
  • Comments generated: 6

Comment thread src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers/PreferAsyncAssertionAnalyzer.cs Outdated
@Evangelink

Copy link
Copy Markdown
Member

@copilot address review comments

Copilot AI requested review from Copilot and removed request for Copilot May 17, 2026 19:23

Copilot AI commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address review comments

Addressed in 687b203. The fixer now unwraps cast/parenthesized lambda arguments before rewriting, the analyzer skips exception filters, and the current PR file list no longer includes the unrelated TestFramework XLF file.

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

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.

Copilot's findings

  • Files reviewed: 33/33 changed files
  • Comments generated: 7

Comment on lines +206 to +208
else if (!isAsync && IsTaskReturnType(originalMethodDeclaration, semanticModel, cancellationToken) && newMethodDeclaration.Body is { } body)
{
newMethodDeclaration = newMethodDeclaration.WithBody((BlockSyntax)new TaskReturnStatementRewriter().Visit(body)!);
Comment on lines +108 to +110
if (current is IAnonymousFunctionOperation or ILocalFunctionOperation ||
current.Kind == OperationKind.Lock ||
(current.Parent is ICatchClauseOperation catchClauseOperation && ReferenceEquals(catchClauseOperation.Filter, current)))
Comment on lines +62 to +64
InvocationExpressionSyntax newInvocationExpression = ReplaceAssertMethodName(invocationExpression);
if (TryGetActionArgumentIndex(invocationExpression, semanticModel, cancellationToken, out int actionArgumentIndex) &&
TryReplaceLambda(newInvocationExpression.ArgumentList.Arguments[actionArgumentIndex], out ArgumentSyntax? newArgument))
Comment on lines +202 to +204
if (newMethodDeclaration.ReturnType.IsVoid())
{
newMethodDeclaration = newMethodDeclaration.WithReturnType(GetTaskReturnType(originalMethodDeclaration, semanticModel, cancellationToken).WithTriviaFrom(newMethodDeclaration.ReturnType));
Comment on lines +70 to +72
AwaitExpressionSyntax awaitExpression = SyntaxFactory.AwaitExpression(newInvocationExpression.WithoutLeadingTrivia())
.WithLeadingTrivia(invocationExpression.GetLeadingTrivia())
.WithAdditionalAnnotations(Formatter.Annotation);
Comment on lines +170 to +174
if (expression is not LambdaExpressionSyntax lambdaExpression ||
!TryGetBlockedTaskExpressionFromLambda(lambdaExpression, out ExpressionSyntax? asyncExpression))
{
newExpression = null;
return false;
Comment on lines +74 to +77
if (invocationExpression.Ancestors().OfType<MethodDeclarationSyntax>().FirstOrDefault() is { } methodDeclaration)
{
MethodDeclarationSyntax newMethodDeclaration = methodDeclaration.ReplaceNode(invocationExpression, awaitExpression);
editor.ReplaceNode(methodDeclaration, AddAsyncModifierAndTaskReturnType(newMethodDeclaration, methodDeclaration, semanticModel, cancellationToken));
@Evangelink

Copy link
Copy Markdown
Member

@copilot address review comments

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 18, 2026 13:29
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 18, 2026 13:32
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 18, 2026 13:34
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 18, 2026 13:36

Copilot AI commented May 18, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address review comments

Addressed in dc11ebe. MSTEST0064 now avoids unsafe/unfixable contexts and interpolated-string-handler overloads, and the fixer handles ValueTask returns, result member access, anonymous delegates, and expression-bodied void tests with regression coverage.

@Evangelink Evangelink merged commit 9eb2438 into main May 18, 2026
10 checks passed
@Evangelink Evangelink deleted the copilot/add-analyzer-favorizing-async-assertions branch May 18, 2026 15:02
Evangelink added a commit that referenced this pull request May 19, 2026
…explicit delegate creations (#8256 follow-up) (#8329)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@Evangelink Evangelink added this to the MSTest 4.3 / MTP 2.3 milestone Jun 16, 2026
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.

Add analyzer favorizing Async assertions

4 participants