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); + } }