diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidUsingAssertsInAsyncVoidContextFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidUsingAssertsInAsyncVoidContextFixer.cs
new file mode 100644
index 0000000000..6bdb003d33
--- /dev/null
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidUsingAssertsInAsyncVoidContextFixer.cs
@@ -0,0 +1,253 @@
+// 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 MSTest.Analyzers.Helpers;
+
+namespace MSTest.Analyzers;
+
+///
+/// Code fixer for .
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AvoidUsingAssertsInAsyncVoidContextFixer))]
+[Shared]
+public sealed class AvoidUsingAssertsInAsyncVoidContextFixer : CodeFixProvider
+{
+ private const string SystemThreadingTasksNamespace = "System.Threading.Tasks";
+
+ ///
+ public sealed override ImmutableArray FixableDiagnosticIds { get; }
+ = ImmutableArray.Create(DiagnosticIds.AvoidUsingAssertsInAsyncVoidContextRuleId);
+
+ ///
+ 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);
+
+ Diagnostic diagnostic = context.Diagnostics[0];
+ SyntaxNode diagnosticNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
+
+ // Walk up the ancestors to find the nearest async void method or local function.
+ foreach (SyntaxNode ancestor in diagnosticNode.AncestorsAndSelf())
+ {
+ if (ancestor is MethodDeclarationSyntax methodDeclaration)
+ {
+ if (methodDeclaration.Modifiers.Any(SyntaxKind.AsyncKeyword) &&
+ methodDeclaration.ReturnType.IsVoid() &&
+ !methodDeclaration.Modifiers.Any(SyntaxKind.OverrideKeyword) &&
+ !methodDeclaration.Modifiers.Any(SyntaxKind.VirtualKeyword) &&
+ methodDeclaration.ExplicitInterfaceSpecifier is null)
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: CodeFixResources.AvoidUsingAssertsInAsyncVoidContextFix,
+ createChangedDocument: ct => ChangeReturnTypeToTaskAsync(context.Document, methodDeclaration, ct),
+ equivalenceKey: nameof(AvoidUsingAssertsInAsyncVoidContextFixer)),
+ diagnostic);
+ }
+
+ break;
+ }
+
+ if (ancestor is LocalFunctionStatementSyntax localFunction)
+ {
+ if (localFunction.Modifiers.Any(SyntaxKind.AsyncKeyword) &&
+ localFunction.ReturnType.IsVoid())
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: CodeFixResources.AvoidUsingAssertsInAsyncVoidContextFix,
+ createChangedDocument: ct => ChangeReturnTypeToTaskAsync(context.Document, localFunction, ct),
+ equivalenceKey: nameof(AvoidUsingAssertsInAsyncVoidContextFixer)),
+ diagnostic);
+ }
+
+ break;
+ }
+
+ if (ancestor is AnonymousFunctionExpressionSyntax anonymousFunction)
+ {
+ // Only stop at async lambdas/delegates — they represent the async void context.
+ // For non-async lambdas, keep walking up to find the enclosing async void method/local function.
+ if (anonymousFunction.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword))
+ {
+ // For async lambdas/anonymous functions, we don't provide a fix since changing to Task
+ // would require changing the delegate type as well.
+ break;
+ }
+ }
+ }
+ }
+
+ private static Task ChangeReturnTypeToTaskAsync(
+ Document document,
+ MethodDeclarationSyntax methodDeclaration,
+ CancellationToken cancellationToken)
+ => ReplaceReturnTypeAsync(
+ document,
+ methodDeclaration,
+ (node, newType) => ((MethodDeclarationSyntax)node).WithReturnType(newType),
+ cancellationToken);
+
+ private static Task ChangeReturnTypeToTaskAsync(
+ Document document,
+ LocalFunctionStatementSyntax localFunction,
+ CancellationToken cancellationToken)
+ => ReplaceReturnTypeAsync(
+ document,
+ localFunction,
+ (node, newType) => ((LocalFunctionStatementSyntax)node).WithReturnType(newType),
+ cancellationToken);
+
+ private static async Task ReplaceReturnTypeAsync(
+ Document document,
+ SyntaxNode nodeToReplace,
+ Func withNewReturnType,
+ CancellationToken cancellationToken)
+ {
+ SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+
+ TypeSyntax originalReturnType = nodeToReplace switch
+ {
+ MethodDeclarationSyntax m => m.ReturnType,
+ LocalFunctionStatementSyntax l => l.ReturnType,
+ _ => throw new InvalidOperationException(),
+ };
+
+ // Determine whether 'Task' (System.Threading.Tasks.Task) is already in scope at the method's
+ // location. This correctly handles file-scoped, namespace-scoped, global, and SDK-implicit usings.
+ bool needsImport = !await IsTaskInScopeAsync(document, nodeToReplace, cancellationToken).ConfigureAwait(false);
+
+ TypeSyntax newReturnType = SyntaxFactory.IdentifierName("Task").WithTriviaFrom(originalReturnType);
+ SyntaxAnnotation methodMarker = new();
+ SyntaxNode replacement = withNewReturnType(nodeToReplace, newReturnType).WithAdditionalAnnotations(methodMarker);
+ SyntaxNode newRoot = root.ReplaceNode(nodeToReplace, replacement);
+
+ if (needsImport && newRoot is CompilationUnitSyntax compilationUnit)
+ {
+ SyntaxNode newMethodNode = newRoot.GetAnnotatedNodes(methodMarker).First();
+ newRoot = AddSystemThreadingTasksUsing(compilationUnit, newMethodNode);
+ }
+
+ return document.WithSyntaxRoot(newRoot);
+ }
+
+ private static async Task IsTaskInScopeAsync(Document document, SyntaxNode nodeAtPosition, CancellationToken cancellationToken)
+ {
+ SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
+ if (semanticModel is null)
+ {
+ return false;
+ }
+
+ INamedTypeSymbol? taskSymbol = semanticModel.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task");
+ if (taskSymbol is null)
+ {
+ // Reference assembly missing — let the user deal with the resulting error rather than guessing.
+ return true;
+ }
+
+ // Look up the unqualified name "Task" at the method/local function's position. If it resolves
+ // to System.Threading.Tasks.Task, no extra import is needed (covers file-scoped, namespace-scoped,
+ // global, and SDK-implicit usings, including 'using global::System.Threading.Tasks;' and
+ // 'using System.Threading.Tasks;' inside the enclosing namespace).
+ ImmutableArray candidates = semanticModel.LookupNamespacesAndTypes(nodeAtPosition.SpanStart, name: "Task");
+ return candidates.Any(c => SymbolEqualityComparer.Default.Equals(c, taskSymbol));
+ }
+
+ private static CompilationUnitSyntax AddSystemThreadingTasksUsing(CompilationUnitSyntax compilationUnit, SyntaxNode methodNode)
+ {
+ // Match the file's existing line endings to avoid producing mixed CR/LF + LF output (which would
+ // both look ugly and break analyzer-test verifiers that diff text byte-for-byte on Linux/macOS).
+ SyntaxTrivia endOfLineTrivia = DetectEndOfLineTrivia(compilationUnit);
+
+ UsingDirectiveSyntax newUsing = SyntaxFactory
+ .UsingDirective(SyntaxFactory.ParseName(SystemThreadingTasksNamespace).WithLeadingTrivia(SyntaxFactory.Space))
+ .WithTrailingTrivia(endOfLineTrivia);
+
+ // Add the using to the smallest enclosing block-scoped namespace (preserving the file's existing
+ // namespace-scoped style when applicable). For file-scoped namespaces (no NamespaceDeclarationSyntax
+ // ancestor), fall back to file-scope insertion — that is the conventional location for usings
+ // when a file uses 'namespace Foo;' style.
+ NamespaceDeclarationSyntax? containingNs = methodNode.Ancestors().OfType().FirstOrDefault();
+ if (containingNs is not null)
+ {
+ SyntaxList updatedUsings = InsertAlphabetically(containingNs.Usings, newUsing);
+ return compilationUnit.ReplaceNode(containingNs, containingNs.WithUsings(updatedUsings));
+ }
+
+ return compilationUnit.WithUsings(InsertAlphabetically(compilationUnit.Usings, newUsing));
+ }
+
+ private static SyntaxList InsertAlphabetically(SyntaxList existing, UsingDirectiveSyntax newUsing)
+ {
+ // Place 'System.Threading.Tasks' alphabetically among System.* usings, before any non-System usings.
+ // C# 10+ same-file 'global using' directives must precede non-global usings, so always insert
+ // after the global block.
+ int insertionIndex = existing.Count;
+ for (int i = 0; i < existing.Count; i++)
+ {
+ UsingDirectiveSyntax current = existing[i];
+ if (IsGlobalUsing(current))
+ {
+ continue;
+ }
+
+ string? nameText = current.Name?.ToString();
+ if (nameText is null)
+ {
+ continue;
+ }
+
+ bool isSystemNamespace = string.Equals(nameText, "System", StringComparison.Ordinal) ||
+ nameText.StartsWith("System.", StringComparison.Ordinal);
+ if (!isSystemNamespace ||
+ string.Compare(nameText, SystemThreadingTasksNamespace, StringComparison.Ordinal) > 0)
+ {
+ insertionIndex = i;
+ break;
+ }
+ }
+
+ return existing.Insert(insertionIndex, newUsing);
+ }
+
+ private static bool IsGlobalUsing(UsingDirectiveSyntax usingDirective)
+ {
+ // 'global' is a contextual keyword introduced in C# 10. Detect it textually so the build-time
+ // Roslyn 3.11 package does not need to expose UsingDirectiveSyntax.GlobalKeyword.
+ SyntaxToken firstToken = usingDirective.GetFirstToken();
+ return firstToken.Text == "global";
+ }
+
+ private static SyntaxTrivia DetectEndOfLineTrivia(CompilationUnitSyntax compilationUnit)
+ {
+ foreach (SyntaxTrivia trivia in compilationUnit.DescendantTrivia())
+ {
+ if (trivia.IsKind(SyntaxKind.EndOfLineTrivia))
+ {
+ // Use an elastic end-of-line so the formatter can still add the conventional blank line
+ // between the inserted using directive and the following content, while preserving the
+ // file's existing line-ending convention (LF on Unix, CR/LF on Windows).
+ return SyntaxFactory.ElasticEndOfLine(trivia.ToFullString());
+ }
+ }
+
+ return SyntaxFactory.ElasticEndOfLine(Environment.NewLine);
+ }
+}
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx
index e07487d5a9..b6637e9d86 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx
@@ -213,6 +213,9 @@
Remove 'out' and 'ref' modifiers
+
+ Change return type to 'Task'
+
Remove duplicate 'DataRow'
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf
index f9a80d184f..2936f358cb 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf
@@ -37,6 +37,11 @@
Odebrat modifikátory out a ref
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
Změnit přístupnost metody na private
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf
index b046a11672..2c4a7f4283 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf
@@ -37,6 +37,11 @@
Entfernen der Modifizierer „out“ und „ref“
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
Methodenzugriff auf „privat“ ändern
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf
index 48c09bd2a8..5f9a5797db 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf
@@ -37,6 +37,11 @@
Quitar modificadores 'out' y 'ref'
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
Cambiar la accesibilidad del método a "private"
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf
index 74ae8ac268..0033da6b23 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf
@@ -37,6 +37,11 @@
Supprimer les modificateurs « out » et « ref »
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
Remplacer l’accessibilité de la méthode par « privé »
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf
index 46fc3d6088..0d9f9c751e 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf
@@ -37,6 +37,11 @@
Rimuovi i modificatori 'out' e 'ref'
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
Modifica l'accessibilità del metodo in 'privato'
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf
index 5d719d2220..17acb67edd 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf
@@ -37,6 +37,11 @@
'out' 修飾子と 'ref' 修飾子を削除する
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
メソッドのアクセシビリティを 'private' に変更する
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf
index 1022070659..6b0111c2c4 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf
@@ -37,6 +37,11 @@
'out' 및 'ref' 한정자 제거
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
메서드 접근성 '비공개'로 변경하기
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf
index 00c6d4d6f8..94099f9665 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf
@@ -37,6 +37,11 @@
Usuń modyfikatory „out” i „ref”
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
Zmień dostępność metody na „private” (prywatna)
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 8237cf5892..6196c1e16f 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf
@@ -37,6 +37,11 @@
Remova os modificadores ''out'' e ''ref''
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
Alterar a acessibilidade do método para 'privado'
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf
index cc70f61b15..551fb24bdf 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf
@@ -37,6 +37,11 @@
Удалите модификаторы "out" и "ref"
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
Изменить доступность метода на "private"
diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf
index 89e5818cb0..531c9e6bdb 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf
@@ -37,6 +37,11 @@
'out' ve 'ref' değiştiricilerini kaldırın
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
Yöntem erişilebilirliğini ‘özel’ olarak değiştir
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 94927033e5..33af9d81f4 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf
@@ -37,6 +37,11 @@
移除 "out" 或 "ref" 修饰符
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
将方法可访问性更改为“private”
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 7ba71fe74b..90f268ea62 100644
--- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf
+++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf
@@ -37,6 +37,11 @@
移除 'out' 和 'ref' 修飾元
+
+ Change return type to 'Task'
+ Change return type to 'Task'
+
+
Change method accessibility to 'private'
將方法協助工具變更為 'private'
diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidUsingAssertsInAsyncVoidContextAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidUsingAssertsInAsyncVoidContextAnalyzerTests.cs
index d30fa650da..9ae7ce2ad8 100644
--- a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidUsingAssertsInAsyncVoidContextAnalyzerTests.cs
+++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidUsingAssertsInAsyncVoidContextAnalyzerTests.cs
@@ -3,7 +3,7 @@
using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier<
MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextAnalyzer,
- Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>;
+ MSTest.Analyzers.AvoidUsingAssertsInAsyncVoidContextFixer>;
namespace MSTest.Analyzers.UnitTests;
@@ -77,7 +77,108 @@ public async void TestMethod()
}
""";
- await VerifyCS.VerifyCodeFixAsync(code, code);
+ string fixedCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await Task.Delay(1);
+ Assert.Fail("");
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInAsyncVoidMethod_WithoutTaskUsing_AddsTaskUsingInCorrectPosition()
+ {
+ string code = """
+ using System;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async void TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ [|Assert.Fail("")|];
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ Assert.Fail("");
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInNonAsyncLambdaInsideAsyncVoidMethod_Diagnostic()
+ {
+ string code = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async void TestMethod()
+ {
+ await Task.Delay(1);
+ Action action = () =>
+ {
+ [|Assert.Fail("")|];
+ };
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using System;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await Task.Delay(1);
+ Action action = () =>
+ {
+ Assert.Fail("");
+ };
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
}
[TestMethod]
@@ -129,7 +230,26 @@ async void d()
}
""";
- await VerifyCS.VerifyCodeFixAsync(code, code);
+ string fixedCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void TestMethod()
+ {
+ async Task d()
+ {
+ await Task.Delay(1);
+ Assert.Fail("");
+ };
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
}
[TestMethod]
@@ -151,7 +271,23 @@ public async void TestMethod()
}
""";
- await VerifyCS.VerifyCodeFixAsync(code, code);
+ string fixedCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await Task.Delay(1);
+ StringAssert.Contains("abc", "a");
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
}
[TestMethod]
@@ -174,7 +310,24 @@ public async void TestMethod()
}
""";
- await VerifyCS.VerifyCodeFixAsync(code, code);
+ string fixedCode = """
+ using System.Collections;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await Task.Delay(1);
+ CollectionAssert.AreEqual(new[] { 1 }, new[] { 1 });
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
}
[TestMethod]
@@ -199,7 +352,26 @@ async void d()
}
""";
- await VerifyCS.VerifyCodeFixAsync(code, code);
+ string fixedCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void TestMethod()
+ {
+ async Task d()
+ {
+ await Task.Delay(1);
+ StringAssert.Contains("abc", "a");
+ };
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
}
[TestMethod]
@@ -229,4 +401,514 @@ public void TestMethod()
await VerifyCS.VerifyCodeFixAsync(code, code);
}
+
+ [TestMethod]
+ public async Task UseAssertMethodInAsyncVoidLocalFunction_MissingTasksUsing_AddsUsing()
+ {
+ string code = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void TestMethod()
+ {
+ async void d()
+ {
+ [|Assert.Fail("")|];
+ };
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public void TestMethod()
+ {
+ async Task d()
+ {
+ Assert.Fail("");
+ };
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInVirtualAsyncVoidMethod_NoCodeFix()
+ {
+ string code = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ public virtual async void SetUp()
+ {
+ await Task.Delay(1);
+ [|Assert.Fail("")|];
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, code);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInExplicitInterfaceImplAsyncVoidMethod_NoCodeFix()
+ {
+ string code = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ public interface ITestSetup
+ {
+ void SetUp();
+ }
+
+ [TestClass]
+ public class MyTestClass : ITestSetup
+ {
+ async void ITestSetup.SetUp()
+ {
+ await Task.Delay(1);
+ [|Assert.Fail("")|];
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, code);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMultipleTimesInAsyncVoidMethod_BatchFix()
+ {
+ string code = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async void TestMethod()
+ {
+ await Task.Delay(1);
+ [|Assert.IsTrue(true)|];
+ [|Assert.Fail("")|];
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await Task.Delay(1);
+ Assert.IsTrue(true);
+ Assert.Fail("");
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInAsyncVoidMethod_WithNamespaceScopedUsing_NoExtraUsing()
+ {
+ string code = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace MyNamespace
+ {
+ using System.Threading.Tasks;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async void TestMethod()
+ {
+ await Task.Delay(1);
+ [|Assert.Fail("")|];
+ }
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace MyNamespace
+ {
+ using System.Threading.Tasks;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await Task.Delay(1);
+ Assert.Fail("");
+ }
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInOverrideAsyncVoidMethod_NoCodeFix()
+ {
+ string code = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestBase
+ {
+ public virtual async void SetUp()
+ {
+ await Task.Delay(1);
+ }
+ }
+
+ [TestClass]
+ public class MyTestClass : MyTestBase
+ {
+ public override async void SetUp()
+ {
+ await Task.Delay(1);
+ [|Assert.Fail("")|];
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, code);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInAsyncVoidMethod_MissingTasksUsing_Diagnostic()
+ {
+ string code = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async void TestMethod()
+ {
+ [|Assert.Fail("")|];
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ Assert.Fail("");
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInAsyncVoidMethod_UsingsOrderedAlphabetically()
+ {
+ string code = """
+ using System.Collections;
+ using System.Xml;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async void TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ [|Assert.Fail("")|];
+ _ = nameof(XmlReader);
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using System.Collections;
+ using System.Threading.Tasks;
+ using System.Xml;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ Assert.Fail("");
+ _ = nameof(XmlReader);
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInAsyncVoidMethod_MultipleNamespaces_AddsUsingInCorrectScope()
+ {
+ // Namespace A has the using; namespace B contains the async void method to fix.
+ // The fixer must ensure 'Task' resolves at the method's location (namespace B),
+ // not just rely on the using existing somewhere in the file.
+ string code = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace A
+ {
+ using System.Threading.Tasks;
+
+ public class Helper
+ {
+ public Task DoAsync() => Task.CompletedTask;
+ }
+ }
+
+ namespace B
+ {
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async void TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ [|Assert.Fail("")|];
+ }
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace A
+ {
+ using System.Threading.Tasks;
+
+ public class Helper
+ {
+ public Task DoAsync() => Task.CompletedTask;
+ }
+ }
+
+ namespace B
+ {
+ using System.Threading.Tasks;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ Assert.Fail("");
+ }
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInAsyncVoidMethod_FileScopedNamespace_AddsUsingAtFileScope()
+ {
+ string code = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace MyNamespace;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async void TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ [|Assert.Fail("")|];
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace MyNamespace;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ Assert.Fail("");
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInAsyncVoidMethod_BatchFixAcrossNamespaces_AddsUsingInEachNamespace()
+ {
+ // FixAll/BatchFixer scenario: two async void methods in two separate namespaces, both needing
+ // the 'using System.Threading.Tasks;' to be added inside their own namespace.
+ string code = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace A
+ {
+ [TestClass]
+ public class TestClassA
+ {
+ [TestMethod]
+ public async void TestMethodA()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ [|Assert.Fail("")|];
+ }
+ }
+ }
+
+ namespace B
+ {
+ [TestClass]
+ public class TestClassB
+ {
+ [TestMethod]
+ public async void TestMethodB()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ [|Assert.Fail("")|];
+ }
+ }
+ }
+ """;
+
+ string fixedCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace A
+ {
+ using System.Threading.Tasks;
+
+ [TestClass]
+ public class TestClassA
+ {
+ [TestMethod]
+ public async Task TestMethodA()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ Assert.Fail("");
+ }
+ }
+ }
+
+ namespace B
+ {
+ using System.Threading.Tasks;
+
+ [TestClass]
+ public class TestClassB
+ {
+ [TestMethod]
+ public async Task TestMethodB()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ Assert.Fail("");
+ }
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
+
+ [TestMethod]
+ public async Task UseAssertMethodInAsyncVoidMethod_GlobalUsingPresent_InsertsAfterGlobalUsings()
+ {
+ // C# 10+ same-file 'global using' directives must precede non-global usings.
+ // Verify the new 'using System.Threading.Tasks;' is inserted after the global block,
+ // not before it (which would be a compile error).
+ string code = """
+ global using Microsoft.VisualStudio.TestTools.UnitTesting;
+ using System;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async void TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ var _ = nameof(Console);
+ [|Assert.Fail("")|];
+ }
+ }
+ """;
+
+ string fixedCode = """
+ global using Microsoft.VisualStudio.TestTools.UnitTesting;
+ using System;
+ using System.Threading.Tasks;
+
+ [TestClass]
+ public class MyTestClass
+ {
+ [TestMethod]
+ public async Task TestMethod()
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ var _ = nameof(Console);
+ Assert.Fail("");
+ }
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
+ }
}