Add MSTEST0064 to prefer async assertions#8256
Merged
Evangelink merged 17 commits intoMay 18, 2026
Merged
Conversation
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Add analyzer favorizing async assertions
Add MSTEST0064 to prefer async assertions
May 15, 2026
Contributor
There was a problem hiding this comment.
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
PreferAsyncAssertionAnalyzerand registers MSTEST0064. - Adds
PreferAsyncAssertionFixerto 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 originalThrowsname, so the fix wrapsawaitaround the synchronous assertion instead of callingThrowsAsync, which can break compilation after the lambda is rewritten to return aTask.
if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpression)
{
return invocationExpression;
}
src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs:140
- Changing a
voidtest method to return unqualifiedTaskrequiresSystem.Threading.Tasksto already be in scope. If the original test uses fully qualified task types or implicit usings are disabled, applying the fix introduces an unresolvedTask; 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 toThrowsAsyncand addsawaitwhile 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
ILabeledOperationnodes in the block. Because those labels are not skipped, otherwise valid VB cases such as a singleGetAwaiter().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 addsawaitwhile 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
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…favorizing-async-assertions
Member
|
@copilot address review comments |
Contributor
Author
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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)); |
Member
|
@copilot address review comments |
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
becomes:
Implementation details
Analyzer
MSTEST0064forAssert.Throws/Assert.ThrowsExactlycalls that blockTask/Task<T>viaGetAwaiter().GetResult().Code fix
Assert.ThrowsAsync/Assert.ThrowsExactlyAsync.GetAwaiter().GetResult()chain from the asserted lambda.voidtest methods toasync Taskwhen required.Resources and coverage
Task,Task<T>, and non-diagnostic cases.