diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx
index b6637e9d86..7043a53a1f 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx
@@ -225,4 +225,7 @@
Remove duplicate TestMethod attribute
-
\ No newline at end of file
+
+ Use async assertion
+
+
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs
new file mode 100644
index 0000000000..3ff4437e19
--- /dev/null
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/PreferAsyncAssertionFixer.cs
@@ -0,0 +1,440 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Immutable;
+using System.Composition;
+
+using Analyzer.Utilities;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Editing;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.Operations;
+
+using MSTest.Analyzers.Helpers;
+
+namespace MSTest.Analyzers;
+
+///
+/// Code fixer for .
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferAsyncAssertionFixer))]
+[Shared]
+public sealed class PreferAsyncAssertionFixer : CodeFixProvider
+{
+ ///
+ public override ImmutableArray FixableDiagnosticIds { get; }
+ = ImmutableArray.Create(DiagnosticIds.PreferAsyncAssertionRuleId);
+
+ ///
+ public override FixAllProvider GetFixAllProvider()
+ // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
+ => WellKnownFixAllProviders.BatchFixer;
+
+ ///
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ SyntaxNode diagnosticNode = root.FindNode(context.Span);
+
+ if (diagnosticNode.AncestorsAndSelf().OfType().FirstOrDefault() is not { } invocationExpression)
+ {
+ return;
+ }
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: CodeFixResources.UseAsyncAssertionFix,
+ createChangedDocument: ct => UseAsyncAssertionAsync(context.Document, invocationExpression, ct),
+ equivalenceKey: nameof(PreferAsyncAssertionFixer)),
+ context.Diagnostics);
+ }
+
+ private static async Task UseAsyncAssertionAsync(Document document, InvocationExpressionSyntax invocationExpression, CancellationToken cancellationToken)
+ {
+ DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
+ SemanticModel semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
+
+ InvocationExpressionSyntax newInvocationExpression = ReplaceAssertMethodName(invocationExpression);
+ if (TryGetActionArgumentIndex(invocationExpression, semanticModel, cancellationToken, out int actionArgumentIndex) &&
+ TryReplaceAction(newInvocationExpression.ArgumentList.Arguments[actionArgumentIndex], out ArgumentSyntax? newArgument))
+ {
+ newInvocationExpression = newInvocationExpression.WithArgumentList(
+ newInvocationExpression.ArgumentList.WithArguments(newInvocationExpression.ArgumentList.Arguments.Replace(newInvocationExpression.ArgumentList.Arguments[actionArgumentIndex], newArgument)));
+ }
+
+ ExpressionSyntax awaitExpression = CreateAwaitExpression(invocationExpression, newInvocationExpression);
+
+ if (invocationExpression.Ancestors().OfType().FirstOrDefault() is { } methodDeclaration)
+ {
+ MethodDeclarationSyntax newMethodDeclaration = methodDeclaration.ReplaceNode(invocationExpression, awaitExpression);
+ editor.ReplaceNode(methodDeclaration, AddAsyncModifierAndTaskReturnType(newMethodDeclaration, methodDeclaration, semanticModel, cancellationToken));
+ }
+ else
+ {
+ editor.ReplaceNode(invocationExpression, awaitExpression);
+ }
+
+ return editor.GetChangedDocument();
+ }
+
+ private static InvocationExpressionSyntax ReplaceAssertMethodName(InvocationExpressionSyntax invocationExpression)
+ => invocationExpression.Expression switch
+ {
+ MemberAccessExpressionSyntax memberAccessExpression => invocationExpression.WithExpression(memberAccessExpression.WithName(AppendAsyncSuffix(memberAccessExpression.Name))),
+ SimpleNameSyntax simpleName => invocationExpression.WithExpression(AppendAsyncSuffix(simpleName)),
+ _ => invocationExpression,
+ };
+
+ private static SimpleNameSyntax AppendAsyncSuffix(SimpleNameSyntax name)
+ => name switch
+ {
+ GenericNameSyntax genericName => genericName.WithIdentifier(SyntaxFactory.Identifier(
+ genericName.Identifier.LeadingTrivia,
+ genericName.Identifier.ValueText + "Async",
+ genericName.Identifier.TrailingTrivia)),
+ IdentifierNameSyntax identifierName => identifierName.WithIdentifier(SyntaxFactory.Identifier(
+ identifierName.Identifier.LeadingTrivia,
+ identifierName.Identifier.ValueText + "Async",
+ identifierName.Identifier.TrailingTrivia)),
+ _ => name,
+ };
+
+ private static bool TryGetActionArgumentIndex(
+ InvocationExpressionSyntax invocationExpression,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken,
+ out int actionArgumentIndex)
+ {
+ if (semanticModel.GetOperation(invocationExpression, cancellationToken) is not IInvocationOperation invocationOperation)
+ {
+ actionArgumentIndex = -1;
+ return false;
+ }
+
+ foreach (IArgumentOperation argumentOperation in invocationOperation.Arguments)
+ {
+ if (argumentOperation.Parameter?.Name != "action")
+ {
+ continue;
+ }
+
+ ArgumentSyntax? argumentSyntax = argumentOperation.Syntax.AncestorsAndSelf().OfType().FirstOrDefault();
+ if (argumentSyntax is null)
+ {
+ continue;
+ }
+
+ actionArgumentIndex = invocationExpression.ArgumentList.Arguments.IndexOf(argumentSyntax);
+ return actionArgumentIndex >= 0;
+ }
+
+ actionArgumentIndex = -1;
+ return false;
+ }
+
+ private static ExpressionSyntax CreateAwaitExpression(InvocationExpressionSyntax originalInvocationExpression, InvocationExpressionSyntax newInvocationExpression)
+ {
+ AwaitExpressionSyntax awaitExpression = SyntaxFactory.AwaitExpression(newInvocationExpression.WithoutLeadingTrivia())
+ .WithAdditionalAnnotations(Formatter.Annotation);
+
+ return NeedsParenthesizedAwait(originalInvocationExpression)
+ ? SyntaxFactory.ParenthesizedExpression(awaitExpression)
+ .WithLeadingTrivia(originalInvocationExpression.GetLeadingTrivia())
+ .WithAdditionalAnnotations(Formatter.Annotation)
+ : awaitExpression
+ .WithLeadingTrivia(originalInvocationExpression.GetLeadingTrivia())
+ .WithAdditionalAnnotations(Formatter.Annotation);
+ }
+
+ private static bool NeedsParenthesizedAwait(InvocationExpressionSyntax invocationExpression)
+ => invocationExpression.Parent switch
+ {
+ MemberAccessExpressionSyntax memberAccessExpression when memberAccessExpression.Expression == invocationExpression => true,
+ ElementAccessExpressionSyntax elementAccessExpression when elementAccessExpression.Expression == invocationExpression => true,
+ InvocationExpressionSyntax parentInvocationExpression when parentInvocationExpression.Expression == invocationExpression => true,
+ ConditionalAccessExpressionSyntax conditionalAccessExpression when conditionalAccessExpression.Expression == invocationExpression => true,
+ _ => false,
+ };
+
+ private static bool TryReplaceAction(ArgumentSyntax argument, [NotNullWhen(true)] out ArgumentSyntax? newArgument)
+ {
+ if (!TryReplaceActionExpression(argument.Expression, out ExpressionSyntax? newExpression))
+ {
+ newArgument = null;
+ return false;
+ }
+
+ newArgument = argument.WithExpression(newExpression);
+ return true;
+ }
+
+ private static bool TryReplaceActionExpression(ExpressionSyntax expression, [NotNullWhen(true)] out ExpressionSyntax? newExpression)
+ {
+ if (expression is ParenthesizedExpressionSyntax parenthesizedExpression &&
+ TryReplaceActionExpression(parenthesizedExpression.Expression, out ExpressionSyntax? parenthesizedNewExpression))
+ {
+ newExpression = parenthesizedNewExpression.WithTriviaFrom(expression);
+ return true;
+ }
+
+ if (expression is CastExpressionSyntax castExpression &&
+ TryReplaceActionExpression(castExpression.Expression, out ExpressionSyntax? castNewExpression))
+ {
+ newExpression = castNewExpression.WithTriviaFrom(expression);
+ return true;
+ }
+
+ if (expression is AnonymousMethodExpressionSyntax anonymousMethodExpression &&
+ anonymousMethodExpression.ParameterList is null or { Parameters.Count: 0 } &&
+ TryGetBlockedTaskExpressionFromBlock(anonymousMethodExpression.Block, out ExpressionSyntax? anonymousMethodAsyncExpression))
+ {
+ newExpression = SyntaxFactory.ParenthesizedLambdaExpression()
+ .WithParameterList(SyntaxFactory.ParameterList())
+ .WithBody(anonymousMethodAsyncExpression.WithTriviaFrom(anonymousMethodExpression.Block))
+ .WithTriviaFrom(expression);
+ return true;
+ }
+
+ if (expression is not LambdaExpressionSyntax lambdaExpression ||
+ !TryGetBlockedTaskExpressionFromLambda(lambdaExpression, out ExpressionSyntax? asyncExpression))
+ {
+ newExpression = null;
+ return false;
+ }
+
+ LambdaExpressionSyntax newLambdaExpression = lambdaExpression switch
+ {
+ SimpleLambdaExpressionSyntax simpleLambda => simpleLambda.WithBody(asyncExpression.WithTriviaFrom(lambdaExpression.Body)),
+ ParenthesizedLambdaExpressionSyntax parenthesizedLambda => parenthesizedLambda.WithBody(asyncExpression.WithTriviaFrom(lambdaExpression.Body)),
+ _ => lambdaExpression,
+ };
+
+ newExpression = newLambdaExpression;
+ return true;
+ }
+
+ private static MethodDeclarationSyntax AddAsyncModifierAndTaskReturnType(
+ MethodDeclarationSyntax methodDeclaration,
+ MethodDeclarationSyntax originalMethodDeclaration,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken)
+ {
+ MethodDeclarationSyntax newMethodDeclaration = methodDeclaration;
+ bool isAsync = newMethodDeclaration.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.AsyncKeyword));
+
+ if (!isAsync)
+ {
+ newMethodDeclaration = newMethodDeclaration.WithModifiers(newMethodDeclaration.Modifiers.Add(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)));
+ }
+
+ bool originalReturnsVoid = originalMethodDeclaration.ReturnType.IsVoid();
+
+ if (originalReturnsVoid)
+ {
+ newMethodDeclaration = newMethodDeclaration.WithReturnType(GetTaskReturnType(originalMethodDeclaration, semanticModel, cancellationToken).WithTriviaFrom(newMethodDeclaration.ReturnType));
+ if (newMethodDeclaration.ExpressionBody is { } expressionBody)
+ {
+ newMethodDeclaration = ConvertExpressionBodyToBlock(newMethodDeclaration, expressionBody);
+ }
+ }
+ else if (!isAsync && IsTaskOrValueTaskReturnType(originalMethodDeclaration, semanticModel, cancellationToken) && newMethodDeclaration.Body is { } body)
+ {
+ newMethodDeclaration = newMethodDeclaration.WithBody((BlockSyntax)new AwaitableReturnStatementRewriter(body).Visit(body)!);
+ }
+
+ return newMethodDeclaration.WithAdditionalAnnotations(Formatter.Annotation);
+ }
+
+ private static MethodDeclarationSyntax ConvertExpressionBodyToBlock(MethodDeclarationSyntax methodDeclaration, ArrowExpressionClauseSyntax expressionBody)
+ {
+ BlockSyntax block = SyntaxFactory.Block(
+ SyntaxFactory.ExpressionStatement(expressionBody.Expression)
+ .WithLeadingTrivia(expressionBody.GetLeadingTrivia())
+ .WithAdditionalAnnotations(Formatter.Annotation));
+
+ if (!methodDeclaration.SemicolonToken.IsKind(SyntaxKind.None))
+ {
+ block = block.WithCloseBraceToken(block.CloseBraceToken.WithTrailingTrivia(methodDeclaration.SemicolonToken.TrailingTrivia));
+ }
+
+ return methodDeclaration
+ .WithExpressionBody(null)
+ .WithSemicolonToken(default)
+ .WithBody(block);
+ }
+
+ private static TypeSyntax GetTaskReturnType(MethodDeclarationSyntax methodDeclaration, SemanticModel semanticModel, CancellationToken cancellationToken)
+ {
+ INamedTypeSymbol? taskSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask);
+ return taskSymbol is not null &&
+ SymbolEqualityComparer.Default.Equals(
+ semanticModel.GetSpeculativeTypeInfo(methodDeclaration.ReturnType.SpanStart, SyntaxFactory.IdentifierName("Task"), SpeculativeBindingOption.BindAsTypeOrNamespace).Type,
+ taskSymbol)
+ ? SyntaxFactory.IdentifierName("Task")
+ : SyntaxFactory.ParseTypeName("System.Threading.Tasks.Task");
+ }
+
+ private static bool IsTaskOrValueTaskReturnType(MethodDeclarationSyntax methodDeclaration, SemanticModel semanticModel, CancellationToken cancellationToken)
+ {
+ INamedTypeSymbol? taskSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask);
+ INamedTypeSymbol? valueTaskSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask);
+ ITypeSymbol? returnTypeSymbol = semanticModel.GetTypeInfo(methodDeclaration.ReturnType, cancellationToken).Type;
+ return (taskSymbol is not null && SymbolEqualityComparer.Default.Equals(returnTypeSymbol, taskSymbol)) ||
+ (valueTaskSymbol is not null && SymbolEqualityComparer.Default.Equals(returnTypeSymbol, valueTaskSymbol));
+ }
+
+ private static bool TryGetBlockedTaskExpressionFromLambda(LambdaExpressionSyntax lambdaExpression, [NotNullWhen(true)] out ExpressionSyntax? asyncExpression)
+ {
+ if (lambdaExpression.Body is ExpressionSyntax expressionBody)
+ {
+ return TryGetBlockedTaskExpression(expressionBody, out asyncExpression);
+ }
+
+ if (lambdaExpression.Body is BlockSyntax blockSyntax)
+ {
+ return TryGetBlockedTaskExpressionFromBlock(blockSyntax, out asyncExpression);
+ }
+
+ asyncExpression = null;
+ return false;
+ }
+
+ private static bool TryGetBlockedTaskExpressionFromBlock(BlockSyntax blockSyntax, [NotNullWhen(true)] out ExpressionSyntax? asyncExpression)
+ {
+ if (blockSyntax.Statements.Count == 1 &&
+ blockSyntax.Statements[0] is ExpressionStatementSyntax expressionStatement)
+ {
+ return TryGetBlockedTaskExpression(expressionStatement.Expression, out asyncExpression);
+ }
+
+ if (blockSyntax.Statements.Count == 1 &&
+ blockSyntax.Statements[0] is ReturnStatementSyntax { Expression: { } returnExpression })
+ {
+ return TryGetBlockedTaskExpression(returnExpression, out asyncExpression);
+ }
+
+ asyncExpression = null;
+ return false;
+ }
+
+ private static bool TryGetBlockedTaskExpression(ExpressionSyntax expression, [NotNullWhen(true)] out ExpressionSyntax? asyncExpression)
+ {
+ ExpressionSyntax currentExpression = WalkDownParentheses(expression);
+ if (currentExpression is InvocationExpressionSyntax getResultInvocation &&
+ getResultInvocation.ArgumentList.Arguments.Count == 0 &&
+ getResultInvocation.Expression is MemberAccessExpressionSyntax getResultMemberAccess &&
+ getResultMemberAccess.Name.Identifier.ValueText == PreferAsyncAssertionAnalyzer.GetResultMethodName &&
+ WalkDownParentheses(getResultMemberAccess.Expression) is InvocationExpressionSyntax getAwaiterInvocation &&
+ getAwaiterInvocation.ArgumentList.Arguments.Count == 0 &&
+ getAwaiterInvocation.Expression is MemberAccessExpressionSyntax getAwaiterMemberAccess &&
+ getAwaiterMemberAccess.Name.Identifier.ValueText == PreferAsyncAssertionAnalyzer.GetAwaiterMethodName)
+ {
+ asyncExpression = WalkDownParentheses(getAwaiterMemberAccess.Expression);
+ return true;
+ }
+
+ asyncExpression = null;
+ return false;
+ }
+
+ private static ExpressionSyntax WalkDownParentheses(ExpressionSyntax expression)
+ {
+ ExpressionSyntax currentExpression = expression;
+ while (currentExpression is ParenthesizedExpressionSyntax parenthesizedExpression)
+ {
+ currentExpression = parenthesizedExpression.Expression;
+ }
+
+ return currentExpression;
+ }
+
+ private sealed class AwaitableReturnStatementRewriter : CSharpSyntaxRewriter
+ {
+ private readonly BlockSyntax _rootBody;
+
+ public AwaitableReturnStatementRewriter(BlockSyntax rootBody)
+ {
+ _rootBody = rootBody;
+ }
+
+ public override SyntaxNode? VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node)
+ => node;
+
+ public override SyntaxNode? VisitParenthesizedLambdaExpression(ParenthesizedLambdaExpressionSyntax node)
+ => node;
+
+ public override SyntaxNode? VisitAnonymousMethodExpression(AnonymousMethodExpressionSyntax node)
+ => node;
+
+ public override SyntaxNode? VisitLocalFunctionStatement(LocalFunctionStatementSyntax node)
+ => node;
+
+ public override SyntaxNode? VisitBlock(BlockSyntax node)
+ {
+ List? rewrittenStatements = null;
+
+ for (int i = 0; i < node.Statements.Count; i++)
+ {
+ StatementSyntax statement = node.Statements[i];
+ if (statement is ReturnStatementSyntax { Expression: { } returnExpression } returnStatement)
+ {
+ rewrittenStatements ??= AddUnchangedStatements(node.Statements, i);
+ bool isFinalRootReturn = ReferenceEquals(node, _rootBody) && i == node.Statements.Count - 1;
+ rewrittenStatements.AddRange(CreateAwaitAndReturnStatements(returnStatement, returnExpression, includeReturn: !isFinalRootReturn));
+ continue;
+ }
+
+ var rewrittenStatement = (StatementSyntax)Visit(statement)!;
+ if (rewrittenStatements is not null)
+ {
+ rewrittenStatements.Add(rewrittenStatement);
+ }
+ else if (!ReferenceEquals(statement, rewrittenStatement))
+ {
+ rewrittenStatements = AddUnchangedStatements(node.Statements, i);
+ rewrittenStatements.Add(rewrittenStatement);
+ }
+ }
+
+ return rewrittenStatements is null
+ ? node
+ : node.WithStatements(SyntaxFactory.List(rewrittenStatements));
+ }
+
+ public override SyntaxNode? VisitReturnStatement(ReturnStatementSyntax node)
+ => node.Expression is { } expression
+ ? SyntaxFactory.Block(CreateAwaitAndReturnStatements(node, expression, includeReturn: true)).WithAdditionalAnnotations(Formatter.Annotation)
+ : node;
+
+ private static List AddUnchangedStatements(SyntaxList statements, int endIndex)
+ {
+ var rewrittenStatements = new List(statements.Count + 1);
+ for (int j = 0; j < endIndex; j++)
+ {
+ rewrittenStatements.Add(statements[j]);
+ }
+
+ return rewrittenStatements;
+ }
+
+ private static StatementSyntax[] CreateAwaitAndReturnStatements(ReturnStatementSyntax returnStatement, ExpressionSyntax expression, bool includeReturn)
+ {
+ ExpressionStatementSyntax awaitStatement = SyntaxFactory.ExpressionStatement(
+ SyntaxFactory.AwaitExpression(expression.WithoutLeadingTrivia()))
+ .WithLeadingTrivia(returnStatement.GetLeadingTrivia())
+ .WithAdditionalAnnotations(Formatter.Annotation);
+
+ ReturnStatementSyntax newReturnStatement = returnStatement
+ .WithExpression(null)
+ .WithLeadingTrivia(SyntaxFactory.ElasticMarker)
+ .WithAdditionalAnnotations(Formatter.Annotation);
+
+ return includeReturn ? [awaitStatement, newReturnStatement] : [awaitStatement];
+ }
+ }
+}
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf
index 57f565e7b1..794ebcd1c6 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf
@@ -142,6 +142,11 @@
Transformovat přepsání Execute na ExecuteAsync
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
Přidat [TestMethod]
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf
index 9049dbd63e..eb8a988c6c 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf
@@ -142,6 +142,11 @@
Transformieren Sie die Überschreibung von „Execute“ in „ExecuteAsync“.
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
„[TestMethod]“ hinzufügen
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf
index b430077dbd..1055b4cbe2 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf
@@ -142,6 +142,11 @@
Transformar la invalidación "Execute" en "ExecuteAsync"
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
Agregar '[TestMethod]'
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf
index 5a7e9f87f0..344a069533 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf
@@ -142,6 +142,11 @@
Transformer la redéfinition « Execute » en « ExecuteAsync »
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
Ajouter « [TestMethod] »
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf
index 41f0545805..c0ffac5b87 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf
@@ -142,6 +142,11 @@
Trasformare l'override di 'Execute' in 'ExecuteAsync'
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
Aggiungi '[TestMethod]'
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf
index 412910f62d..d89fc54481 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf
@@ -142,6 +142,11 @@
'Execute' オーバーライドを 'ExecuteAsync' に変換する
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
'[TestMethod]' の追加
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf
index dc0e4e65e2..eee135b73b 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf
@@ -142,6 +142,11 @@
'Execute' 재정의를 'ExecuteAsync'로 변환
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
'[TestMethod]' 추가
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf
index fa4a881c0e..3a517ef2b3 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf
@@ -142,6 +142,11 @@
Zmień zastąpienie „Execute” na „ExecuteAsync”
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
Dodaj „[TestMethod]”
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf
index 6c20555db9..50ac948aaf 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf
@@ -142,6 +142,11 @@
Transformar a substituição "Execute" em "ExecuteAsync"
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
Adicionar ''[TestMethod]"
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf
index 2cd2bb2669..43513c8a82 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf
@@ -142,6 +142,11 @@
Преобразовать переопределение "Execute" в "ExecuteAsync"
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
Добавить "[TestMethod]"
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf
index a26add4c97..8239083779 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf
@@ -142,6 +142,11 @@
'Execute' geçersiz kılmasını 'ExecuteAsync' olarak dönüştür
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
'[TestMethod]' ekle
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf
index 18aa54837b..247082768b 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf
@@ -142,6 +142,11 @@
将‘Execute’重写为‘ExecuteAsync’
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
添加“[TestMethod]”
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf
index 597c0b07da..cd0cc9b92a 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf
@@ -142,6 +142,11 @@
將 'Execute' 覆寫改為 'ExecuteAsync'
+
+ Use async assertion
+ Use async assertion
+
+
Add '[TestMethod]'
新增 '[TestMethod]'
diff --git a/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md
index f2b7fad657..4711d4232a 100644
--- a/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md
@@ -1,2 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+MSTEST0064 | Usage | Info | PreferAsyncAssertionAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0064)
diff --git a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs
index 2c7b428295..9b1c979410 100644
--- a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs
+++ b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs
@@ -68,4 +68,5 @@ internal static class DiagnosticIds
public const string UseOSConditionAttributeInsteadOfRuntimeCheckRuleId = "MSTEST0061";
public const string AvoidOutRefTestMethodParametersRuleId = "MSTEST0062";
public const string TestClassConstructorShouldBeValidRuleId = "MSTEST0063";
+ public const string PreferAsyncAssertionRuleId = "MSTEST0064";
}
diff --git a/src/Analyzers/MSTest.Analyzers/PreferAsyncAssertionAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/PreferAsyncAssertionAnalyzer.cs
new file mode 100644
index 0000000000..fe735aed72
--- /dev/null
+++ b/src/Analyzers/MSTest.Analyzers/PreferAsyncAssertionAnalyzer.cs
@@ -0,0 +1,235 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Immutable;
+
+using Analyzer.Utilities.Extensions;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+using MSTest.Analyzers.Helpers;
+using MSTest.Analyzers.RoslynAnalyzerHelpers;
+
+namespace MSTest.Analyzers;
+
+///
+/// MSTEST0064: .
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
+public sealed class PreferAsyncAssertionAnalyzer : DiagnosticAnalyzer
+{
+ internal const string GetAwaiterMethodName = "GetAwaiter";
+ internal const string GetResultMethodName = "GetResult";
+
+ private static readonly LocalizableResourceString Title = new(nameof(Resources.PreferAsyncAssertionTitle), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableResourceString Description = new(nameof(Resources.PreferAsyncAssertionDescription), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableResourceString MessageFormat = new(nameof(Resources.PreferAsyncAssertionMessageFormat), Resources.ResourceManager, typeof(Resources));
+
+ internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
+ DiagnosticIds.PreferAsyncAssertionRuleId,
+ Title,
+ MessageFormat,
+ Description,
+ Category.Usage,
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true);
+
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; }
+ = ImmutableArray.Create(Rule);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(context =>
+ {
+ if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingAssert, out INamedTypeSymbol? assertSymbol) ||
+ !context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute, out INamedTypeSymbol? testMethodAttributeSymbol) ||
+ !context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask, out INamedTypeSymbol? taskSymbol))
+ {
+ return;
+ }
+
+ context.RegisterOperationAction(
+ context => AnalyzeInvocation(context, assertSymbol, testMethodAttributeSymbol, taskSymbol),
+ OperationKind.Invocation);
+ });
+ }
+
+ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol assertSymbol, INamedTypeSymbol testMethodAttributeSymbol, INamedTypeSymbol taskSymbol)
+ {
+ var operation = (IInvocationOperation)context.Operation;
+ IMethodSymbol targetMethod = operation.TargetMethod;
+ if (
+ !SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, assertSymbol) ||
+ targetMethod.Name is not ("Throws" or "ThrowsExactly") ||
+ HasInterpolatedStringHandlerParameter(targetMethod) ||
+ context.ContainingSymbol is not IMethodSymbol containingMethod ||
+ !containingMethod.GetAttributes().Any(attr => attr.AttributeClass.Inherits(testMethodAttributeSymbol)) ||
+ IsUnsupportedVoidTestMethod(containingMethod) ||
+ IsInsideUnsupportedAwaitContext(operation) ||
+ !TryGetActionArgument(operation, out IArgumentOperation? actionArgument) ||
+ !TryGetBlockedTaskOperationFromArgument(actionArgument.Value, out IOperation? asyncOperation))
+ {
+ return;
+ }
+
+ ITypeSymbol? asyncExpressionType = asyncOperation.Type;
+ if (asyncExpressionType is null || !context.Compilation.ClassifyCommonConversion(asyncExpressionType, taskSymbol).IsImplicit)
+ {
+ return;
+ }
+
+ context.ReportDiagnostic(operation.CreateDiagnostic(Rule, targetMethod.Name + "Async", targetMethod.Name));
+ }
+
+ private static bool HasInterpolatedStringHandlerParameter(IMethodSymbol targetMethod)
+ => targetMethod.Parameters.Any(static parameter =>
+ parameter.Type.GetAttributes().Any(static attr => attr.AttributeClass?.Name == "InterpolatedStringHandlerAttribute"));
+
+ private static bool IsUnsupportedVoidTestMethod(IMethodSymbol containingMethod)
+ => containingMethod.ReturnsVoid &&
+ (containingMethod.IsOverride || containingMethod.IsImplementationOfAnyInterfaceMember());
+
+ private static bool TryGetActionArgument(IInvocationOperation operation, [NotNullWhen(true)] out IArgumentOperation? actionArgument)
+ {
+ foreach (IArgumentOperation argument in operation.Arguments)
+ {
+ if (argument.Parameter?.Name == "action")
+ {
+ actionArgument = argument;
+ return true;
+ }
+ }
+
+ actionArgument = null;
+ return false;
+ }
+
+ private static bool IsInsideUnsupportedAwaitContext(IOperation operation)
+ {
+ for (IOperation? current = operation.Parent; current is not null; current = current.Parent)
+ {
+ if (current is IAnonymousFunctionOperation or ILocalFunctionOperation ||
+ current.Kind == OperationKind.Lock ||
+ (current.Parent is ICatchClauseOperation catchClauseOperation && ReferenceEquals(catchClauseOperation.Filter, current)))
+ {
+ return true;
+ }
+ }
+
+ return IsInsideUnsafeSyntax(operation.Syntax);
+ }
+
+ private static bool IsInsideUnsafeSyntax(SyntaxNode syntax)
+ {
+ foreach (SyntaxNode node in syntax.AncestorsAndSelf())
+ {
+ // This analyzer targets both C# and VB from a project that intentionally references
+ // Microsoft.CodeAnalysis.Common only. VB has no unsafe keyword, so use syntax type
+ // names to detect C# unsafe constructs without adding a Microsoft.CodeAnalysis.CSharp
+ // dependency that would make the VB analyzer unsupported (RS1038).
+ string syntaxTypeName = node.GetType().Name;
+ if (syntaxTypeName == "UnsafeStatementSyntax" ||
+ (syntaxTypeName == "MethodDeclarationSyntax" && HasUnsafeKeyword(node)))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool HasUnsafeKeyword(SyntaxNode node)
+ => node.ChildTokens().Any(static token => token.ValueText == "unsafe");
+
+ private static bool TryGetBlockedTaskOperationFromArgument(IOperation argumentValueOperation, [NotNullWhen(true)] out IOperation? asyncOperation)
+ {
+ if (argumentValueOperation.WalkDownConversion() is not IDelegateCreationOperation delegateCreationOperation ||
+ delegateCreationOperation.Target is not IAnonymousFunctionOperation anonymousFunctionOperation ||
+ !TryGetSingleOperation(anonymousFunctionOperation.Body, out IOperation? lambdaOperation))
+ {
+ asyncOperation = null;
+ return false;
+ }
+
+ return TryGetBlockedTaskOperation(lambdaOperation, out asyncOperation);
+ }
+
+ private static bool TryGetSingleOperation(IBlockOperation blockOperation, [NotNullWhen(true)] out IOperation? operation)
+ {
+ operation = null;
+
+ foreach (IOperation childOperation in blockOperation.Operations)
+ {
+ IOperation? candidateOperation = childOperation switch
+ {
+ IExpressionStatementOperation expressionStatementOperation => expressionStatementOperation.Operation,
+
+ // For any return-with-value, treat the returned value as the candidate, except for the
+ // synthetic implicit return that VB 'Function' lambdas emit: it returns an implicit
+ // ILocalReferenceOperation to the function-name local. Treating that as a candidate
+ // would cause single-statement VB Function lambdas to look as if they contain
+ // multiple operations, and the diagnostic would be missed.
+ IReturnOperation { ReturnedValue: { } returnedValue }
+ => GetExplicitReturnedValue(returnedValue),
+
+ // Implicit return with no value contributes no candidate (e.g. end of void lambda).
+ IReturnOperation => null,
+
+ // Skip any other compiler-synthesized operation (e.g. VB exit-function labels).
+ _ when childOperation.IsImplicit => null,
+
+ _ => childOperation,
+ };
+
+ if (candidateOperation is null)
+ {
+ continue;
+ }
+
+ if (operation is not null)
+ {
+ operation = null;
+ return false;
+ }
+
+ operation = candidateOperation;
+ }
+
+ return operation is not null;
+ }
+
+ private static IOperation? GetExplicitReturnedValue(IOperation returnedValue)
+ {
+ // The synthetic implicit return that VB Function lambdas emit returns the (also implicit)
+ // function-name local. Detect that case via the unwrapped operation.
+ IOperation unwrapped = returnedValue.WalkDownConversion();
+ return unwrapped is ILocalReferenceOperation { IsImplicit: true }
+ ? null
+ : returnedValue;
+ }
+
+ private static bool TryGetBlockedTaskOperation(IOperation operation, [NotNullWhen(true)] out IOperation? asyncOperation)
+ {
+ if (operation.WalkDownConversion() is IInvocationOperation getResultInvocation &&
+ getResultInvocation.Arguments.Length == 0 &&
+ getResultInvocation.TargetMethod.Name == GetResultMethodName &&
+ getResultInvocation.Instance?.WalkDownConversion() is IInvocationOperation getAwaiterInvocation &&
+ getAwaiterInvocation.Arguments.Length == 0 &&
+ getAwaiterInvocation.TargetMethod.Name == GetAwaiterMethodName &&
+ getAwaiterInvocation.Instance is { } instance)
+ {
+ asyncOperation = instance.WalkDownConversion();
+ return true;
+ }
+
+ asyncOperation = null;
+ return false;
+ }
+}
diff --git a/src/Analyzers/MSTest.Analyzers/Resources.resx b/src/Analyzers/MSTest.Analyzers/Resources.resx
index 5f6bc6df64..1c336cccd8 100644
--- a/src/Analyzers/MSTest.Analyzers/Resources.resx
+++ b/src/Analyzers/MSTest.Analyzers/Resources.resx
@@ -747,4 +747,13 @@ The type declaring these methods should also respect the following rules:
Test classes must have a public constructor that is either parameterless or accepts a single TestContext parameter. This allows the test framework to instantiate the test class properly.
-
\ No newline at end of file
+
+ Prefer async assertion methods
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf
index ca7323dd0a..3547bb455e 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf
@@ -532,6 +532,21 @@ Typ deklarující tyto metody by měl také respektovat následující pravidla:
Místo trvalého neúspěšného vyhodnocovacího výrazu použijte „Assert.Fail“.
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
Možnost DataTestMethodAttribute je zastaralá a neposkytuje žádné funkce navíc oproti možnosti TestMethodAttribute. Pro všechny testovací metody, včetně parametrizovaných testů, použijte možnost TestMethodAttribute.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf
index 1ff5042ac3..c13aeffae6 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf
@@ -533,6 +533,21 @@ Der Typ, der diese Methoden deklariert, sollte auch die folgenden Regeln beachte
Verwenden Sie „Assert.Fail“ anstelle einer Assert-Anweisung, bei der immer ein Fehler auftritt.
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
„DataTestMethodAttribute“ ist veraltet und bietet keine zusätzliche Funktionalität im Vergleich zu „TestMethodAttribute“. Verwenden Sie „TestMethodAttribute“ für alle Testmethoden, einschließlich parametrisierter Tests.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf
index 0d5e6904d4..96840ad0e2 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf
@@ -532,6 +532,21 @@ El tipo que declara estos métodos también debe respetar las reglas siguientes:
Usar "Assert.Fail" en lugar de una aserción que siempre tiene errores
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
"DataTestMethodAttribute" está obsoleto y no proporciona ninguna funcionalidad adicional sobre "TestMethodAttribute". Use "TestMethodAttribute" para todos los métodos de prueba, incluidas las pruebas parametrizadas.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf
index c9a3918612..071170d8d7 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf
@@ -532,6 +532,21 @@ Le type déclarant ces méthodes doit également respecter les règles suivantes
Utilisez « Assert.Fail » à la place d’une assertion toujours en échec
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
« DataTestMethodAttribute » est obsolète et ne fournit aucune fonctionnalité supplémentaire par rapport à « TestMethodAttribute ». Utiliser « TestMethodAttribute » pour toutes les méthodes de test, y compris les tests paramétrés.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf
index 80116284de..3edc211f41 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf
@@ -532,6 +532,21 @@ Anche il tipo che dichiara questi metodi deve rispettare le regole seguenti:
Usare 'Assert.Fail' invece di un'asserzione che ha sempre esito negativo
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
'DataTestMethodAttribute' è obsoleto e non offre funzionalità aggiuntive rispetto a 'TestMethodAttribute'. Utilizzare 'TestMethodAttribute' per tutti i metodi di test, inclusi i test con parametri.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf
index cd8b59f2bf..cab8c15274 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf
@@ -532,6 +532,21 @@ The type declaring these methods should also respect the following rules:
常に失敗しているアサートの代わりに 'Assert.Fail' を使用する
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
'DataTestMethodAttribute' は廃止されており、'TestMethodAttribute' に対して追加の機能は提供しません。パラメーター化されたテストを含むすべてのテスト メソッドには、'TestMethodAttribute' を使用してください。
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf
index adb8ae1b8b..36376da69b 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf
@@ -532,6 +532,21 @@ The type declaring these methods should also respect the following rules:
항상 실패하는 어설션 대신 'Assert.Fail' 사용
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
'DataTestMethodAttribute'는 더 이상 사용되지 않으며 'TestMethodAttribute'에 대한 추가 기능을 제공하지 않습니다. 매개 변수가 있는 테스트를 포함하여 모든 테스트 메서드에 'TestMethodAttribute'를 사용하세요.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf
index 58fccdba9b..8c4d97f335 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf
@@ -532,6 +532,21 @@ Typ deklarujący te metody powinien również przestrzegać następujących regu
Użyj trybu „Assert.Fail” zamiast kończącej się zawsze niepowodzeniem instrukcji asercji
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
Atrybut „DataTestMethodAttribute” jest przestarzały i nie zapewnia dodatkowych funkcji w stosunku do atrybutu „TestMethodAttribute”. Użyj atrybutu „TestMethodAttribute” dla wszystkich metod testowych, w tym testów sparametryzowanych.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf
index d6b69ecae4..d570cb836a 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf
@@ -532,6 +532,21 @@ O tipo que declara esses métodos também deve respeitar as seguintes regras:
Usar "Assert.Fail" em vez de uma asserção sempre com falha
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
'DataTestMethodAttribute' está obsoleto e não oferece funcionalidade adicional em relação a 'TestMethodAttribute'. Use 'TestMethodAttribute' para todos os métodos de teste, inclusive testes parametrizados.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf
index 08c1723229..721c0e5841 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf
@@ -538,6 +538,21 @@ The type declaring these methods should also respect the following rules:
Используйте "Assert.Fail" вместо утверждения с постоянным сбоем
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
Атрибут "DataTestMethodAttribute" устарел и не предоставляет дополнительных функций по сравнению с "TestMethodAttribute". Используйте '"TestMethodAttribute" для всех методов тестирования, включая параметризованные тесты.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf
index ee076b0d93..3247324853 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf
@@ -532,6 +532,21 @@ Bu metotları bildiren türün ayrıca aşağıdaki kurallara uyması gerekir:
Her zaman başarısız olan onaylama yerine 'Assert.Fail' seçeneğini kullanın
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
'DataTestMethodAttribute' artık kullanılmıyor ve 'TestMethodAttribute' üzerinde ek bir işlevsellik sunmuyor. Parametreli testler de dahil, tüm test yöntemleri için 'TestMethodAttribute' kullanın.
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf
index f194914d24..29c2d2428c 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf
@@ -532,6 +532,21 @@ The type declaring these methods should also respect the following rules:
使用 “Assert.Fail” 而不是始终失败的断言
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
'DataTestMethodAttribute' 已过时,并且不再提供 'TestMethodAttribute' 之外的其他功能。对所有测试方法(包括参数化测试)使用 'TestMethodAttribute'。
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf
index b492f375dc..143670aa7f 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf
@@ -532,6 +532,21 @@ The type declaring these methods should also respect the following rules:
使用 'Assert.Fail',而不是一直失敗的聲明
+
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+ When verifying exceptions from asynchronous code, use the async assertion methods instead of blocking the asynchronous operation with GetAwaiter().GetResult().
+
+
+
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+ Use 'Assert.{0}' instead of blocking an async call in 'Assert.{1}'
+
+
+
+ Prefer async assertion methods
+ Prefer async assertion methods
+
+
'DataTestMethodAttribute' is obsolete and provides no additional functionality over 'TestMethodAttribute'. Use 'TestMethodAttribute' for all test methods, including parameterized tests.
'DataTestMethodAttribute' 已過時,未提供比 'TestMethodAttribute' 更多的功能。針對所有測試方法使用 'TestMethodAttribute',包括參數化測試。
diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/PreferAsyncAssertionAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/PreferAsyncAssertionAnalyzerTests.cs
new file mode 100644
index 0000000000..d6daf569d5
--- /dev/null
+++ b/test/UnitTests/MSTest.Analyzers.UnitTests/PreferAsyncAssertionAnalyzerTests.cs
@@ -0,0 +1,823 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier<
+ MSTest.Analyzers.PreferAsyncAssertionAnalyzer,
+ MSTest.Analyzers.PreferAsyncAssertionFixer>;
+
+using VerifyVB = MSTest.Analyzers.Test.VisualBasicCodeFixVerifier<
+ MSTest.Analyzers.PreferAsyncAssertionAnalyzer,
+ Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>;
+
+namespace MSTest.Analyzers.Test;
+
+[TestClass]
+public sealed class PreferAsyncAssertionAnalyzerTests
+{
+ [TestMethod]
+ public async Task WhenAssertThrowsExactlyBlocksOnTask_CodeFixUsesAsyncAssertionAndUpdatesSignature()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ [|Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult())|];
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ await Assert.ThrowsExactlyAsync(() => BarAsync());
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenVoidTestMethodDoesNotHaveTaskInScope_CodeFixUsesFullyQualifiedTaskReturnType()
+ {
+ string code = """
+ using System;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ [|Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult())|];
+ }
+
+ private System.Threading.Tasks.Task BarAsync() => System.Threading.Tasks.Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async System.Threading.Tasks.Task MyTestMethod()
+ {
+ await Assert.ThrowsExactlyAsync(() => BarAsync());
+ }
+
+ private System.Threading.Tasks.Task BarAsync() => System.Threading.Tasks.Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertThrowsBlocksOnGenericTask_CodeFixUsesAsyncAssertion()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ Exception exception = [|Assert.Throws(() => BarAsync().GetAwaiter().GetResult())|];
+ Assert.IsNotNull(exception);
+ await Task.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.FromResult(42);
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ Exception exception = await Assert.ThrowsAsync(() => BarAsync());
+ Assert.IsNotNull(exception);
+ await Task.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.FromResult(42);
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertThrowsBlocksOnGenericTaskInBlockLambda_CodeFixRemovesBlockingReturnStatement()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ Exception exception = [|Assert.Throws(() => { return BarAsync().GetAwaiter().GetResult(); })|];
+ Assert.IsNotNull(exception);
+ await Task.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.FromResult(42);
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ Exception exception = await Assert.ThrowsAsync(() => BarAsync());
+ Assert.IsNotNull(exception);
+ await Task.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.FromResult(42);
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenStaticallyImportedAssertionBlocksOnTask_CodeFixUpdatesMethodName()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ Exception exception = [|ThrowsExactly(() => BarAsync().GetAwaiter().GetResult())|];
+ Assert.IsNotNull(exception);
+ await Task.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ Exception exception = await ThrowsExactlyAsync(() => BarAsync());
+ Assert.IsNotNull(exception);
+ await Task.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenNonAsyncTaskReturningTestMethodHasReturnExpression_CodeFixConvertsReturnToAwait()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public Task MyTestMethod()
+ {
+ [|Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult())|];
+ return Task.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ await Assert.ThrowsExactlyAsync(() => BarAsync());
+ await Task.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenNonAsyncValueTaskReturningTestMethodHasReturnExpression_CodeFixConvertsReturnToAwait()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public ValueTask MyTestMethod()
+ {
+ [|Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult())|];
+ return ValueTask.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async ValueTask MyTestMethod()
+ {
+ await Assert.ThrowsExactlyAsync(() => BarAsync());
+ await ValueTask.CompletedTask;
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertThrowsExactlyBlocksOnTask_WithNamedArgumentsOutOfOrder_CodeFixUsesAsyncAssertion()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ [|Assert.ThrowsExactly(message: "boom", action: () => BarAsync().GetAwaiter().GetResult())|];
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ await Assert.ThrowsExactlyAsync(message: "boom", action: () => BarAsync());
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionActionIsExplicitlyCast_CodeFixUsesAsyncAssertionAndRemovesCast()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ [|Assert.ThrowsExactly(((Action)(() => BarAsync().GetAwaiter().GetResult())))|];
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ await Assert.ThrowsExactlyAsync(() => BarAsync());
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionActionIsAnonymousMethod_CodeFixUsesAsyncAssertionAndRemovesBlockingCall()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ [|Assert.ThrowsExactly(delegate { BarAsync().GetAwaiter().GetResult(); })|];
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ await Assert.ThrowsExactlyAsync(() => BarAsync());
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionResultIsUsedInMemberAccess_CodeFixParenthesizesAwait()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ string message = [|Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult())|].Message;
+ Assert.IsNotNull(message);
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ string message = (await Assert.ThrowsExactlyAsync(() => BarAsync())).Message;
+ Assert.IsNotNull(message);
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenVoidTestMethodIsExpressionBodied_CodeFixConvertsBodyToBlock()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod() => [|Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult())|];
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ await Assert.ThrowsExactlyAsync(() => BarAsync());
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionIsInsideLockStatement_NoDiagnostic()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ private readonly object _gate = new();
+
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ lock (_gate)
+ {
+ Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult());
+ }
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(code);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionIsInsideExceptionFilter_NoDiagnostic()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ try
+ {
+ throw new Exception();
+ }
+ catch (Exception) when (Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult()) is not null)
+ {
+ }
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(code);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionIsInsideUnsafeBlock_NoDiagnostic()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ unsafe
+ {
+ Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult());
+ }
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ var test = new VerifyCS.Test
+ {
+ TestCode = code,
+ };
+
+ test.SolutionTransforms.Add((solution, projectId) =>
+ {
+ var compilationOptions = (CSharpCompilationOptions)solution.GetProject(projectId)!.CompilationOptions!;
+ return solution.WithProjectCompilationOptions(projectId, compilationOptions.WithAllowUnsafe(true));
+ });
+
+ await test.RunAsync(CancellationToken.None);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionUsesInterpolatedStringHandlerOverload_NoDiagnostic()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult(), $"Message {GetMessage()}");
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ private string GetMessage() => "message";
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(code);
+ }
+
+ [TestMethod]
+ public async Task WhenVoidTestMethodCannotChangeReturnType_NoDiagnostic()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ public interface ITest
+ {
+ void MyTestMethod();
+ }
+
+ public class BaseTestClass
+ {
+ public virtual void MyTestMethod()
+ {
+ }
+ }
+
+ [TestClass]
+ public class OverrideTestClass : BaseTestClass
+ {
+ [TestMethod]
+ public override void MyTestMethod()
+ {
+ Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult());
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+
+ [TestClass]
+ public class ImplicitInterfaceTestClass : ITest
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult());
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+
+ [TestClass]
+ public class ExplicitInterfaceTestClass : ITest
+ {
+ [TestMethod]
+ void ITest.MyTestMethod()
+ {
+ Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult());
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(code);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionIsInsideNestedLambda_NoDiagnostic()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ Action assertion = () => Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult());
+ assertion();
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(code);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionIsInsideLocalFunction_NoDiagnostic()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void MyTestMethod()
+ {
+ void RunAssertion()
+ {
+ Assert.ThrowsExactly(() => BarAsync().GetAwaiter().GetResult());
+ }
+
+ RunAssertion();
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(code);
+ }
+
+ [TestMethod]
+ public async Task WhenVisualBasicFunctionLambdaBlocksOnGenericTask_Diagnostic()
+ {
+ string code = """
+ Imports System
+ Imports System.Threading.Tasks
+ Imports Microsoft.VisualStudio.TestTools.UnitTesting
+
+
+ Public Class MyTestClass
+
+ Public Sub MyTestMethod()
+ [|Assert.ThrowsExactly(Of InvalidOperationException)(Function() BarAsync().GetAwaiter().GetResult())|]
+ End Sub
+
+ Private Function BarAsync() As Task(Of Integer)
+ Return Task.FromResult(42)
+ End Function
+ End Class
+ """;
+
+ await VerifyVB.VerifyAnalyzerAsync(code);
+ }
+
+ [TestMethod]
+ public async Task WhenAssertionDoesNotBlockOnTask_NoDiagnostic()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task MyTestMethod()
+ {
+ await Assert.ThrowsExactlyAsync(() => BarAsync());
+ Assert.ThrowsExactly(() => throw new InvalidOperationException());
+ }
+
+ private Task BarAsync() => Task.CompletedTask;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(code);
+ }
+}