From 43b8fcfbd4e638a4b0ddc2d9822f3add4bf05307 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:51:19 +0000 Subject: [PATCH 1/6] Initial plan From 7f977f622424e0fbebfaca45c366f1dd99f95492 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:59:38 +0000 Subject: [PATCH 2/6] Fix MSTest0008 fixer to preserve comments in method body Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- .../Helpers/FixtureMethodFixer.cs | 118 +++++++++++++----- ...estInitializeShouldBeValidAnalyzerTests.cs | 45 +++++++ 2 files changed, 129 insertions(+), 34 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs index 2a77f45a26..77b3d38bfd 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs @@ -5,19 +5,24 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; namespace MSTest.Analyzers.Helpers; internal static class FixtureMethodFixer { - private const SyntaxNode? VoidReturnTypeNode = null; - public static async Task FixSignatureAsync(Document document, SyntaxNode root, SyntaxNode node, bool isParameterLess, bool shouldBeStatic, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + // Cast to MethodDeclarationSyntax to preserve method body and trivia + if (node is not MethodDeclarationSyntax methodDeclaration) + { + return document.Project.Solution; + } + SemanticModel? semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var methodSymbol = (IMethodSymbol?)semanticModel.GetDeclaredSymbol(node, cancellationToken); @@ -27,66 +32,111 @@ public static async Task FixSignatureAsync(Document document, SyntaxNo } var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(semanticModel.Compilation); - var syntaxGenerator = SyntaxGenerator.GetGenerator(document); - SyntaxNode fixedMethodDeclarationNode = syntaxGenerator.MethodDeclaration( - methodSymbol.Name, - GetParameters(syntaxGenerator, isParameterLess, wellKnownTypeProvider), - typeParameters: null, - GetReturnType(syntaxGenerator, methodSymbol, wellKnownTypeProvider), - Accessibility.Public, - GetModifiers(methodSymbol, shouldBeStatic), - GetStatements(node, syntaxGenerator)); + // Start with the original method declaration to preserve trivia and body + MethodDeclarationSyntax fixedMethodDeclaration = methodDeclaration; - // Copy the attributes from the old method to the new method. - fixedMethodDeclarationNode = syntaxGenerator.AddAttributes(fixedMethodDeclarationNode, syntaxGenerator.GetAttributes(node)); + // Update parameters + ParameterListSyntax newParameterList = GetParameterList(isParameterLess, wellKnownTypeProvider); + fixedMethodDeclaration = fixedMethodDeclaration.WithParameterList(newParameterList); - return document.WithSyntaxRoot(root.ReplaceNode(node, fixedMethodDeclarationNode)).Project.Solution; - } + // Update return type + TypeSyntax? newReturnType = GetReturnType(methodSymbol, wellKnownTypeProvider); + if (newReturnType is not null) + { + fixedMethodDeclaration = fixedMethodDeclaration.WithReturnType(newReturnType); + } + + // Update modifiers (accessibility and static) + SyntaxTokenList newModifiers = GetModifiers(methodDeclaration, methodSymbol, shouldBeStatic); + fixedMethodDeclaration = fixedMethodDeclaration.WithModifiers(newModifiers); + + // Remove type parameters if any + if (fixedMethodDeclaration.TypeParameterList is not null) + { + fixedMethodDeclaration = fixedMethodDeclaration.WithTypeParameterList(null); + } + + // Remove return and yield return statements from body if needed + if (fixedMethodDeclaration.Body is not null) + { + SyntaxList statements = fixedMethodDeclaration.Body.Statements; + IEnumerable filteredStatements = statements + .Where(x => !x.IsKind(SyntaxKind.ReturnStatement) && !x.IsKind(SyntaxKind.YieldReturnStatement)); + + if (statements.Count != filteredStatements.Count()) + { + fixedMethodDeclaration = fixedMethodDeclaration.WithBody( + fixedMethodDeclaration.Body.WithStatements(SyntaxFactory.List(filteredStatements))); + } + } - private static IEnumerable GetStatements(SyntaxNode node, SyntaxGenerator syntaxGenerator) - => syntaxGenerator.GetStatements(node) - .Where(x => !x.IsKind(SyntaxKind.ReturnStatement) && !x.IsKind(SyntaxKind.YieldReturnStatement)); + return document.WithSyntaxRoot(root.ReplaceNode(node, fixedMethodDeclaration)).Project.Solution; + } - private static DeclarationModifiers GetModifiers(IMethodSymbol methodSymbol, bool shouldBeStatic) + private static SyntaxTokenList GetModifiers(MethodDeclarationSyntax methodDeclaration, IMethodSymbol methodSymbol, bool shouldBeStatic) { - DeclarationModifiers newModifiers = methodSymbol.IsAsync - ? DeclarationModifiers.Async - : DeclarationModifiers.None; + // Start with existing modifiers + SyntaxTokenList modifiers = methodDeclaration.Modifiers; + + // Remove all accessibility modifiers + modifiers = SyntaxFactory.TokenList(modifiers.Where(m => + !m.IsKind(SyntaxKind.PublicKeyword) && + !m.IsKind(SyntaxKind.PrivateKeyword) && + !m.IsKind(SyntaxKind.ProtectedKeyword) && + !m.IsKind(SyntaxKind.InternalKeyword))); + + // Remove abstract modifier if present + modifiers = SyntaxFactory.TokenList(modifiers.Where(m => !m.IsKind(SyntaxKind.AbstractKeyword))); + + // Handle static modifier + bool hasStaticModifier = modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)); + if (shouldBeStatic && !hasStaticModifier) + { + modifiers = modifiers.Add(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); + } + else if (!shouldBeStatic && hasStaticModifier) + { + modifiers = SyntaxFactory.TokenList(modifiers.Where(m => !m.IsKind(SyntaxKind.StaticKeyword))); + } - return newModifiers.WithIsStatic(shouldBeStatic); + // Add public at the beginning + modifiers = SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword)).AddRange(modifiers); + + return modifiers; } - private static SyntaxNode? GetReturnType(SyntaxGenerator syntaxGenerator, IMethodSymbol methodSymbol, WellKnownTypeProvider wellKnownTypeProvider) + private static TypeSyntax? GetReturnType(IMethodSymbol methodSymbol, WellKnownTypeProvider wellKnownTypeProvider) { if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType.OriginalDefinition, wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask1))) { - return syntaxGenerator.IdentifierName("ValueTask"); + return SyntaxFactory.IdentifierName("ValueTask"); } if (methodSymbol.IsAsync || SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType.OriginalDefinition, wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask1))) { - return syntaxGenerator.IdentifierName("Task"); + return SyntaxFactory.IdentifierName("Task"); } - // For all other cases return void. - return VoidReturnTypeNode; + // For void, return a predefined void type + return SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)); } - private static IEnumerable GetParameters(SyntaxGenerator syntaxGenerator, bool isParameterLess, - WellKnownTypeProvider wellKnownTypeProvider) + private static ParameterListSyntax GetParameterList(bool isParameterLess, WellKnownTypeProvider wellKnownTypeProvider) { if (isParameterLess || !wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName( WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestContext, out INamedTypeSymbol? testContextTypeSymbol)) { - return []; + return SyntaxFactory.ParameterList(); } - SyntaxNode testContextType = syntaxGenerator.TypeExpression(testContextTypeSymbol); - SyntaxNode testContextParameter = syntaxGenerator.ParameterDeclaration("testContext", testContextType); - return [testContextParameter]; + TypeSyntax testContextType = SyntaxFactory.ParseTypeName(testContextTypeSymbol.ToDisplayString()); + ParameterSyntax testContextParameter = SyntaxFactory.Parameter(SyntaxFactory.Identifier("testContext")) + .WithType(testContextType); + + return SyntaxFactory.ParameterList(SyntaxFactory.SingletonSeparatedList(testContextParameter)); } } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/TestInitializeShouldBeValidAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/TestInitializeShouldBeValidAnalyzerTests.cs index 05367fc67d..5ee47216d8 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/TestInitializeShouldBeValidAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/TestInitializeShouldBeValidAnalyzerTests.cs @@ -478,6 +478,51 @@ await VerifyCS.VerifyCodeFixAsync( fixedCode); } + [TestMethod] + public async Task WhenTestInitializeHasComments_CommentsArePreserved() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestInitialize] + public void {|#0:TestSetup|}(TestContext tc) + { + SomeCode(); + + // Some comments; + } + + private void SomeCode() { } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestInitialize] + public void TestSetup() + { + SomeCode(); + + // Some comments; + } + + private void SomeCode() { } + } + """; + + await VerifyCS.VerifyCodeFixAsync( + code, + VerifyCS.Diagnostic().WithLocation(0).WithArguments("TestSetup"), + fixedCode); + } + [TestMethod] public async Task WhenTestInitializeIsNotOnClass_Diagnostic() { From 4f0f45bbf00c18e93878d70c796c5b2abf917219 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:01:43 +0000 Subject: [PATCH 3/6] Add tests to verify comments are preserved for TestCleanup and ClassInitialize fixers Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- ...assInitializeShouldBeValidAnalyzerTests.cs | 47 +++++++++++++++++++ .../TestCleanupShouldBeValidAnalyzerTests.cs | 47 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/ClassInitializeShouldBeValidAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/ClassInitializeShouldBeValidAnalyzerTests.cs index 9e279b7ac4..7ee4d95cad 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/ClassInitializeShouldBeValidAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/ClassInitializeShouldBeValidAnalyzerTests.cs @@ -711,4 +711,51 @@ public static void ClassInitialize(TestContext testContext) await VerifyCS.VerifyAnalyzerAsync(code); } + + [TestMethod] + public async Task WhenClassInitializeHasComments_CommentsArePreserved() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [ClassInitialize] + internal static void {|#0:ClassInitialize|}(TestContext testContext) + { + InitializeClass(); + + // Class initialization comments; + // Setup code here + } + + private static void InitializeClass() { } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [ClassInitialize] + public static void ClassInitialize(TestContext testContext) + { + InitializeClass(); + + // Class initialization comments; + // Setup code here + } + + private static void InitializeClass() { } + } + """; + + await VerifyCS.VerifyCodeFixAsync( + code, + VerifyCS.Diagnostic().WithLocation(0).WithArguments("ClassInitialize"), + fixedCode); + } } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/TestCleanupShouldBeValidAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/TestCleanupShouldBeValidAnalyzerTests.cs index aede7da0fb..fa3e57baf6 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/TestCleanupShouldBeValidAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/TestCleanupShouldBeValidAnalyzerTests.cs @@ -552,4 +552,51 @@ public void TestCleanup() await VerifyCS.VerifyAnalyzerAsync(code); } + + [TestMethod] + public async Task WhenTestCleanupHasComments_CommentsArePreserved() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestCleanup] + internal void {|#0:TestCleanup|}() + { + CleanupCode(); + + // Cleanup comments; + // More comments + } + + private void CleanupCode() { } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestCleanup] + public void TestCleanup() + { + CleanupCode(); + + // Cleanup comments; + // More comments + } + + private void CleanupCode() { } + } + """; + + await VerifyCS.VerifyCodeFixAsync( + code, + VerifyCS.Diagnostic().WithLocation(0).WithArguments("TestCleanup"), + fixedCode); + } } From 6ef90fd86a69edd6bcb903e163c4bfe017d6a08b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 06:06:10 +0000 Subject: [PATCH 4/6] Remove unused using Microsoft.CodeAnalysis.Editing Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- .../MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs index 77b3d38bfd..acaf0ead11 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs @@ -6,7 +6,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; namespace MSTest.Analyzers.Helpers; From b324f5750d0754e91856712c0e95aeae2ed9a82c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:03:00 +0000 Subject: [PATCH 5/6] Fix modifier ordering and parameter type formatting issues - Ensure modifiers are in correct order: public static async (not public async static) - Use simple type name "TestContext" instead of fully qualified name - Fixes failing tests in AssemblyCleanup and AssemblyInitialize analyzers Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- .../Helpers/FixtureMethodFixer.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs index acaf0ead11..7154a0f48b 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs @@ -78,29 +78,27 @@ private static SyntaxTokenList GetModifiers(MethodDeclarationSyntax methodDeclar // Start with existing modifiers SyntaxTokenList modifiers = methodDeclaration.Modifiers; - // Remove all accessibility modifiers + // Remove all accessibility modifiers and abstract modifier modifiers = SyntaxFactory.TokenList(modifiers.Where(m => !m.IsKind(SyntaxKind.PublicKeyword) && !m.IsKind(SyntaxKind.PrivateKeyword) && !m.IsKind(SyntaxKind.ProtectedKeyword) && - !m.IsKind(SyntaxKind.InternalKeyword))); - - // Remove abstract modifier if present - modifiers = SyntaxFactory.TokenList(modifiers.Where(m => !m.IsKind(SyntaxKind.AbstractKeyword))); + !m.IsKind(SyntaxKind.InternalKeyword) && + !m.IsKind(SyntaxKind.AbstractKeyword))); // Handle static modifier bool hasStaticModifier = modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)); if (shouldBeStatic && !hasStaticModifier) { - modifiers = modifiers.Add(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); + modifiers = modifiers.Insert(0, SyntaxFactory.Token(SyntaxKind.StaticKeyword)); } else if (!shouldBeStatic && hasStaticModifier) { modifiers = SyntaxFactory.TokenList(modifiers.Where(m => !m.IsKind(SyntaxKind.StaticKeyword))); } - // Add public at the beginning - modifiers = SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword)).AddRange(modifiers); + // Add public at the beginning (before static if present) + modifiers = modifiers.Insert(0, SyntaxFactory.Token(SyntaxKind.PublicKeyword)); return modifiers; } @@ -132,7 +130,8 @@ private static ParameterListSyntax GetParameterList(bool isParameterLess, WellKn return SyntaxFactory.ParameterList(); } - TypeSyntax testContextType = SyntaxFactory.ParseTypeName(testContextTypeSymbol.ToDisplayString()); + // Use simple name "TestContext" instead of fully qualified name + TypeSyntax testContextType = SyntaxFactory.IdentifierName("TestContext"); ParameterSyntax testContextParameter = SyntaxFactory.Parameter(SyntaxFactory.Identifier("testContext")) .WithType(testContextType); From 7fabcc2232fddf68a4b3ee8d10ac06ab09e6ed61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:01:49 +0000 Subject: [PATCH 6/6] Fix abstract method handling to add empty body When fixing abstract methods, ensure they get a proper body {} instead of remaining as abstract declarations with semicolons. This fixes failing tests for TestInitialize and TestCleanup abstract methods. Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- .../Helpers/FixtureMethodFixer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs index 7154a0f48b..6f640abe2d 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/Helpers/FixtureMethodFixer.cs @@ -56,9 +56,16 @@ public static async Task FixSignatureAsync(Document document, SyntaxNo fixedMethodDeclaration = fixedMethodDeclaration.WithTypeParameterList(null); } - // Remove return and yield return statements from body if needed - if (fixedMethodDeclaration.Body is not null) + // If the method is abstract (no body), add an empty body + if (fixedMethodDeclaration.Body is null) { + fixedMethodDeclaration = fixedMethodDeclaration + .WithBody(SyntaxFactory.Block()) + .WithSemicolonToken(default); + } + else + { + // Remove return and yield return statements from body if needed SyntaxList statements = fixedMethodDeclaration.Body.Statements; IEnumerable filteredStatements = statements .Where(x => !x.IsKind(SyntaxKind.ReturnStatement) && !x.IsKind(SyntaxKind.YieldReturnStatement));