From 8828937d814690561b2acbb78fc3496f67f61e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 20 Jul 2026 09:44:40 +0200 Subject: [PATCH 1/7] Add outstanding test improver coverage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 28567541-d476-4300-a979-3c1fb89404c2 --- ...ouldContainSingleStatementAnalyzerTests.cs | 65 ++++++++++++++ ...ssertAreSameWithValueTypesAnalyzerTests.cs | 88 +++++++++++++++++++ ...OutRefTestMethodParametersAnalyzerTests.cs | 23 +++++ ...eReferenceNotInitializedSuppressorTests.cs | 49 +++++++++++ 4 files changed, 225 insertions(+) diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AssertThrowsShouldContainSingleStatementAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AssertThrowsShouldContainSingleStatementAnalyzerTests.cs index 26c4c0754b..840d2c9b48 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AssertThrowsShouldContainSingleStatementAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AssertThrowsShouldContainSingleStatementAnalyzerTests.cs @@ -602,4 +602,69 @@ public void MyTestMethod() await VerifyCS.VerifyAnalyzerAsync(code); } + + [TestMethod] + public async Task WhenAssertThrowsReceivesMethodGroup_CSharp_NoDiagnostic() + { + // When a method group (not a lambda) is passed to Assert.Throws, the delegate creation + // target is an IMethodReferenceOperation, not IAnonymousFunctionOperation. + // The analyzer's early-return guard ('delegateCreation.Target is not IAnonymousFunctionOperation') + // fires, so no diagnostic is reported regardless of how many statements the method contains. + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void MyTestMethod() + { + // Method group passed as Action — not a lambda, so the analyzer cannot inspect + // the body and must not fire. + Assert.Throws(DoSomethingMultiple); + Assert.ThrowsExactly(DoSomethingMultiple); + } + + private static void DoSomethingMultiple() + { + Console.WriteLine("one"); + throw new Exception("two"); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenAssertThrowsReceivesNonLambdaDelegate_CSharp_NoDiagnostic() + { + // When a delegate variable (not an inline lambda) is passed to Assert.Throws, the + // delegate creation wraps an IMethodReferenceOperation, not IAnonymousFunctionOperation, + // so the analyzer's early-return guard fires and no diagnostic is emitted. + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void MyTestMethod() + { + Action multiStep = () => + { + Console.WriteLine("one"); + throw new Exception("two"); + }; + + // The delegate is referenced by name, not created inline — no diagnostic expected. + Assert.Throws(multiStep); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidAssertAreSameWithValueTypesAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidAssertAreSameWithValueTypesAnalyzerTests.cs index 8598c73d57..63d6cd4098 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidAssertAreSameWithValueTypesAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidAssertAreSameWithValueTypesAnalyzerTests.cs @@ -386,4 +386,92 @@ public void TestMethod() await VerifyCS.VerifyCodeFixAsync(code, fixedCode); } + + [TestMethod] + public async Task WhenBothArgsAreNullLiterals_NoDiagnostic() + { + // null cast to a reference type has IsValueType == false after WalkDownConversion(). + // Neither arg is a value type, so no diagnostic should fire. + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + Assert.AreSame((object)null, (object)null); + Assert.AreNotSame((object)null, (object)null); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenGenericTypeParameterConstrainedToStruct_Diagnostic() + { + // A generic type parameter constrained to 'struct' has IsValueType == true, + // so the analyzer should report a diagnostic and the fixer should replace the method name. + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() where T : struct + { + T a = default; + T b = default; + [|Assert.AreSame(a, b)|]; + [|Assert.AreNotSame(a, b)|]; + } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() where T : struct + { + T a = default; + T b = default; + Assert.AreEqual(a, b); + Assert.AreNotEqual(a, b); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenGenericTypeParameterWithNoConstraint_NoDiagnostic() + { + // An unconstrained generic type parameter is not a value type (IsValueType == false), + // so the analyzer should not report a diagnostic. + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod(T a, T b) + { + Assert.AreSame(a, b); + Assert.AreNotSame(a, b); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs index 1a6d23bd50..615658d95a 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs @@ -324,4 +324,27 @@ public void TestMethod1(in string s) await VerifyCS.VerifyCodeFixAsync(code, code); } + +#if NET + [TestMethod] + public async Task WhenTestMethodHasRefReadonlyParameter_NoDiagnostic() + { + // 'ref readonly' parameters have RefKind.RefReadOnlyParameter, which is neither + // RefKind.Out nor RefKind.Ref, so the analyzer must not report a diagnostic. + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod1(ref readonly int value) + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } +#endif } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs index ad3daf8bb8..fe48bfb2e9 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs @@ -143,6 +143,55 @@ public class SomeClass await test.RunAsync(); } + [TestMethod] + public async Task TestContextFieldOnTestClass_DiagnosticIsNotSuppressed() + { + string code = @" +#nullable enable + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class SomeClass +{ + public TestContext {|#0:_testContext|}; +} +"; + + var test = new VerifyCS.Test + { + TestCode = code, + }; + + test.ExpectedDiagnostics.Add(DiagnosticResult.CompilerError("CS8618") + .WithLocation(0) + .WithOptions(DiagnosticOptions.IgnoreAdditionalLocations) + .WithArguments("field", "_testContext") + .WithIsSuppressed(false)); + + await test.RunAsync(); + } + + [TestMethod] + public async Task TestContextGetterOnlyPropertyOnTestClass_DiagnosticIsSuppressed() + { + // A TestContext property with only a getter still satisfies 'declaredSymbol is IPropertySymbol', + // so the suppressor must fire and suppress CS8618. + string code = @" +#nullable enable + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class SomeClass +{ + public TestContext {|#0:TestContext|} { get; } +} +"; + + await VerifySingleSuppressionAsync(code, isSuppressed: true); + } + private Task VerifySingleSuppressionAsync(string source, bool isSuppressed) => VerifyDiagnosticsAsync(source, [(0, isSuppressed)]); From 5fa8b83f567467396ab4b5be1ed6447c73e49976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 20 Jul 2026 10:14:52 +0200 Subject: [PATCH 2/7] Address analyzer test review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 28567541-d476-4300-a979-3c1fb89404c2 --- .../AvoidOutRefTestMethodParametersFixer.cs | 20 ++++++++---------- ...AvoidOutRefTestMethodParametersAnalyzer.cs | 11 +++++++++- ...ouldContainSingleStatementAnalyzerTests.cs | 5 ++--- ...OutRefTestMethodParametersAnalyzerTests.cs | 21 ++++++++++++++----- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidOutRefTestMethodParametersFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidOutRefTestMethodParametersFixer.cs index 53f7d1005e..923c766e71 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidOutRefTestMethodParametersFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidOutRefTestMethodParametersFixer.cs @@ -61,20 +61,18 @@ private static async Task RemoveOutRefModifiersAsync(Document document DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); foreach (ParameterSyntax parameter in methodDeclaration.ParameterList.Parameters) { - int indexToRemove = parameter.Modifiers.IndexOf(SyntaxKind.OutKeyword); - if (indexToRemove < 0) + if (!parameter.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.OutKeyword) || modifier.IsKind(SyntaxKind.RefKeyword))) { - indexToRemove = parameter.Modifiers.IndexOf(SyntaxKind.RefKeyword); + continue; } - if (indexToRemove >= 0) - { - editor.ReplaceNode(parameter, (node, _) => - { - var parameter = (ParameterSyntax)node; - return parameter.WithModifiers(parameter.Modifiers.RemoveAt(indexToRemove)).WithLeadingTrivia(parameter.GetLeadingTrivia()); - }); - } + SyntaxTokenList filteredModifiers = SyntaxFactory.TokenList( + parameter.Modifiers.Where(modifier => + !modifier.IsKind(SyntaxKind.OutKeyword) + && !modifier.IsKind(SyntaxKind.RefKeyword) + && !modifier.IsKind(SyntaxKind.ReadOnlyKeyword))); + + editor.ReplaceNode(parameter, parameter.WithModifiers(filteredModifiers).WithLeadingTrivia(parameter.GetLeadingTrivia())); } return editor.GetChangedDocument(); diff --git a/src/Analyzers/MSTest.Analyzers/AvoidOutRefTestMethodParametersAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/AvoidOutRefTestMethodParametersAnalyzer.cs index 2011885971..63cc67c6a9 100644 --- a/src/Analyzers/MSTest.Analyzers/AvoidOutRefTestMethodParametersAnalyzer.cs +++ b/src/Analyzers/MSTest.Analyzers/AvoidOutRefTestMethodParametersAnalyzer.cs @@ -61,9 +61,18 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbo } // Check for out/ref parameters - if (methodSymbol.Parameters.Any(p => p.RefKind is RefKind.Out or RefKind.Ref)) + if (methodSymbol.Parameters.Any(parameter => IsOutOrRefParameter(parameter, context.CancellationToken))) { context.ReportDiagnostic(methodSymbol.CreateDiagnostic(AvoidOutRefParametersRule, methodSymbol.Name)); } } + + private static bool IsOutOrRefParameter(IParameterSymbol parameter, CancellationToken cancellationToken) + => parameter.RefKind is RefKind.Out or RefKind.Ref + || parameter.DeclaringSyntaxReferences.Any(reference => + { + SyntaxNode parameterSyntax = reference.GetSyntax(cancellationToken); + return parameterSyntax.ChildTokens().Any(token => token.ValueText is "ref") + && parameterSyntax.ChildTokens().Any(token => token.ValueText is "readonly"); + }); } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AssertThrowsShouldContainSingleStatementAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AssertThrowsShouldContainSingleStatementAnalyzerTests.cs index 840d2c9b48..f13fedaa60 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AssertThrowsShouldContainSingleStatementAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AssertThrowsShouldContainSingleStatementAnalyzerTests.cs @@ -640,9 +640,8 @@ private static void DoSomethingMultiple() [TestMethod] public async Task WhenAssertThrowsReceivesNonLambdaDelegate_CSharp_NoDiagnostic() { - // When a delegate variable (not an inline lambda) is passed to Assert.Throws, the - // delegate creation wraps an IMethodReferenceOperation, not IAnonymousFunctionOperation, - // so the analyzer's early-return guard fires and no diagnostic is emitted. + // An existing delegate local is an ILocalReferenceOperation, not an + // IDelegateCreationOperation, so the analyzer's early-return guard fires. string code = """ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs index 615658d95a..16be074a24 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs @@ -327,10 +327,8 @@ public void TestMethod1(in string s) #if NET [TestMethod] - public async Task WhenTestMethodHasRefReadonlyParameter_NoDiagnostic() + public async Task WhenTestMethodHasRefReadonlyParameter_Diagnostic() { - // 'ref readonly' parameters have RefKind.RefReadOnlyParameter, which is neither - // RefKind.Out nor RefKind.Ref, so the analyzer must not report a diagnostic. string code = """ using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -338,13 +336,26 @@ public async Task WhenTestMethodHasRefReadonlyParameter_NoDiagnostic() public class MyTestClass { [TestMethod] - public void TestMethod1(ref readonly int value) + public void [|TestMethod1|](ref readonly int value) { } } """; - await VerifyCS.VerifyCodeFixAsync(code, code); + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod1(int value) + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); } #endif } From 209673a8cc61018b0bac54d41e024350796c9e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 20 Jul 2026 10:22:04 +0200 Subject: [PATCH 3/7] Address suppressor review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 28567541-d476-4300-a979-3c1fb89404c2 --- .../NonNullableReferenceNotInitializedSuppressor.cs | 1 + .../AvoidAssertAreSameWithValueTypesAnalyzerTests.cs | 8 ++++---- .../NonNullableReferenceNotInitializedSuppressorTests.cs | 6 ++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers/NonNullableReferenceNotInitializedSuppressor.cs b/src/Analyzers/MSTest.Analyzers/NonNullableReferenceNotInitializedSuppressor.cs index 201c1dd520..cd326e85ae 100644 --- a/src/Analyzers/MSTest.Analyzers/NonNullableReferenceNotInitializedSuppressor.cs +++ b/src/Analyzers/MSTest.Analyzers/NonNullableReferenceNotInitializedSuppressor.cs @@ -65,6 +65,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) if (declaredSymbol is IPropertySymbol property && string.Equals(property.Name, "TestContext", StringComparison.Ordinal) && SymbolEqualityComparer.Default.Equals(testContextSymbol, property.GetMethod?.ReturnType) + && property.SetMethod is not null && property.ContainingType.GetAttributes().Any(attr => attr.AttributeClass.Inherits(testClassAttributeSymbol))) { context.ReportSuppression(Suppression.Create(Rule, diagnostic)); diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidAssertAreSameWithValueTypesAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidAssertAreSameWithValueTypesAnalyzerTests.cs index 63d6cd4098..d4d55dd9b3 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidAssertAreSameWithValueTypesAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidAssertAreSameWithValueTypesAnalyzerTests.cs @@ -390,8 +390,8 @@ public void TestMethod() [TestMethod] public async Task WhenBothArgsAreNullLiterals_NoDiagnostic() { - // null cast to a reference type has IsValueType == false after WalkDownConversion(). - // Neither arg is a value type, so no diagnostic should fire. + // WalkDownConversion() removes the object conversion, leaving an untyped null literal. + // The null-propagating IsValueType check therefore does not report a diagnostic. string code = """ using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -455,8 +455,8 @@ public void TestMethod() where T : struct [TestMethod] public async Task WhenGenericTypeParameterWithNoConstraint_NoDiagnostic() { - // An unconstrained generic type parameter is not a value type (IsValueType == false), - // so the analyzer should not report a diagnostic. + // An unconstrained generic type parameter is not known to be a value type at analysis time, + // so the analyzer should not report a diagnostic even though T can be instantiated with one. string code = """ using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs index fe48bfb2e9..189c5ba656 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs @@ -173,10 +173,8 @@ public class SomeClass } [TestMethod] - public async Task TestContextGetterOnlyPropertyOnTestClass_DiagnosticIsSuppressed() + public async Task TestContextGetterOnlyPropertyOnTestClass_DiagnosticIsNotSuppressed() { - // A TestContext property with only a getter still satisfies 'declaredSymbol is IPropertySymbol', - // so the suppressor must fire and suppress CS8618. string code = @" #nullable enable @@ -189,7 +187,7 @@ public class SomeClass } "; - await VerifySingleSuppressionAsync(code, isSuppressed: true); + await VerifySingleSuppressionAsync(code, isSuppressed: false); } private Task VerifySingleSuppressionAsync(string source, bool isSuppressed) From a7514e13b97372f584c9358858f2daf5faa25720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 20 Jul 2026 10:29:47 +0200 Subject: [PATCH 4/7] Document getter-only suppression expectation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 28567541-d476-4300-a979-3c1fb89404c2 --- .../NonNullableReferenceNotInitializedSuppressorTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs index 189c5ba656..a811d7fd98 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs @@ -175,6 +175,7 @@ public class SomeClass [TestMethod] public async Task TestContextGetterOnlyPropertyOnTestClass_DiagnosticIsNotSuppressed() { + // MSTest cannot assign a getter-only property, so CS8618 must remain visible. string code = @" #nullable enable From e3bc8e8bca630a700a012ca13788b73856ee4d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 20 Jul 2026 10:35:02 +0200 Subject: [PATCH 5/7] Avoid escaped keyword false positives Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 28567541-d476-4300-a979-3c1fb89404c2 --- ...AvoidOutRefTestMethodParametersAnalyzer.cs | 4 ++-- ...OutRefTestMethodParametersAnalyzerTests.cs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers/AvoidOutRefTestMethodParametersAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/AvoidOutRefTestMethodParametersAnalyzer.cs index 63cc67c6a9..899a1a333c 100644 --- a/src/Analyzers/MSTest.Analyzers/AvoidOutRefTestMethodParametersAnalyzer.cs +++ b/src/Analyzers/MSTest.Analyzers/AvoidOutRefTestMethodParametersAnalyzer.cs @@ -72,7 +72,7 @@ private static bool IsOutOrRefParameter(IParameterSymbol parameter, Cancellation || parameter.DeclaringSyntaxReferences.Any(reference => { SyntaxNode parameterSyntax = reference.GetSyntax(cancellationToken); - return parameterSyntax.ChildTokens().Any(token => token.ValueText is "ref") - && parameterSyntax.ChildTokens().Any(token => token.ValueText is "readonly"); + return parameterSyntax.ChildTokens().Any(token => token.Text is "ref") + && parameterSyntax.ChildTokens().Any(token => token.Text is "readonly"); }); } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs index 16be074a24..0a8c43c0e5 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs @@ -358,4 +358,27 @@ public void TestMethod1(int value) await VerifyCS.VerifyCodeFixAsync(code, fixedCode); } #endif + + [TestMethod] + public async Task WhenParameterTypeAndNameAreEscapedKeywords_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public sealed class @ref + { + } + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod1(@ref @readonly) + { + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } } From add742b199c0971d56b8a2f8bc5dfccf969b770a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 20 Jul 2026 10:41:03 +0200 Subject: [PATCH 6/7] Limit TestContext suppression to public properties Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 28567541-d476-4300-a979-3c1fb89404c2 --- ...ullableReferenceNotInitializedSuppressor.cs | 1 + ...leReferenceNotInitializedSuppressorTests.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Analyzers/MSTest.Analyzers/NonNullableReferenceNotInitializedSuppressor.cs b/src/Analyzers/MSTest.Analyzers/NonNullableReferenceNotInitializedSuppressor.cs index cd326e85ae..c82681fcea 100644 --- a/src/Analyzers/MSTest.Analyzers/NonNullableReferenceNotInitializedSuppressor.cs +++ b/src/Analyzers/MSTest.Analyzers/NonNullableReferenceNotInitializedSuppressor.cs @@ -65,6 +65,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) if (declaredSymbol is IPropertySymbol property && string.Equals(property.Name, "TestContext", StringComparison.Ordinal) && SymbolEqualityComparer.Default.Equals(testContextSymbol, property.GetMethod?.ReturnType) + && property.DeclaredAccessibility == Accessibility.Public && property.SetMethod is not null && property.ContainingType.GetAttributes().Any(attr => attr.AttributeClass.Inherits(testClassAttributeSymbol))) { diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs index a811d7fd98..129920c678 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/NonNullableReferenceNotInitializedSuppressorTests.cs @@ -191,6 +191,24 @@ public class SomeClass await VerifySingleSuppressionAsync(code, isSuppressed: false); } + [TestMethod] + public async Task PrivateTestContextPropertyOnTestClass_DiagnosticIsNotSuppressed() + { + string code = @" +#nullable enable + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class SomeClass +{ + private TestContext {|#0:TestContext|} { get; set; } +} +"; + + await VerifySingleSuppressionAsync(code, isSuppressed: false); + } + private Task VerifySingleSuppressionAsync(string source, bool isSuppressed) => VerifyDiagnosticsAsync(source, [(0, isSuppressed)]); From 6f136063af0fdf23df7c85c06a6c7ac2162ac756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 20 Jul 2026 10:49:38 +0200 Subject: [PATCH 7/7] Preserve ref modifier comments in code fix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 28567541-d476-4300-a979-3c1fb89404c2 --- .../AvoidOutRefTestMethodParametersFixer.cs | 37 ++++++++++++++++--- ...OutRefTestMethodParametersAnalyzerTests.cs | 32 ++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidOutRefTestMethodParametersFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidOutRefTestMethodParametersFixer.cs index 923c766e71..e4c99eb992 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidOutRefTestMethodParametersFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/AvoidOutRefTestMethodParametersFixer.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using MSTest.Analyzers.Helpers; @@ -61,18 +62,42 @@ private static async Task RemoveOutRefModifiersAsync(Document document DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); foreach (ParameterSyntax parameter in methodDeclaration.ParameterList.Parameters) { - if (!parameter.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.OutKeyword) || modifier.IsKind(SyntaxKind.RefKeyword))) + SyntaxToken[] removedModifiers = parameter.Modifiers + .Where(modifier => modifier.IsKind(SyntaxKind.OutKeyword) + || modifier.IsKind(SyntaxKind.RefKeyword) + || modifier.IsKind(SyntaxKind.ReadOnlyKeyword)) + .ToArray(); + if (!removedModifiers.Any(modifier => modifier.IsKind(SyntaxKind.OutKeyword) || modifier.IsKind(SyntaxKind.RefKeyword))) { continue; } SyntaxTokenList filteredModifiers = SyntaxFactory.TokenList( - parameter.Modifiers.Where(modifier => - !modifier.IsKind(SyntaxKind.OutKeyword) - && !modifier.IsKind(SyntaxKind.RefKeyword) - && !modifier.IsKind(SyntaxKind.ReadOnlyKeyword))); + parameter.Modifiers.Except(removedModifiers)); - editor.ReplaceNode(parameter, parameter.WithModifiers(filteredModifiers).WithLeadingTrivia(parameter.GetLeadingTrivia())); + ParameterSyntax updatedParameter = parameter.WithModifiers(filteredModifiers); + SyntaxTrivia[] removedTrivia = removedModifiers + .SelectMany(modifier => modifier.LeadingTrivia.Concat(modifier.TrailingTrivia)) + .ToArray(); + SyntaxTrivia[] preservedTrivia = removedTrivia + .SkipWhile(trivia => trivia.IsKind(SyntaxKind.WhitespaceTrivia)) + .Reverse() + .SkipWhile(trivia => trivia.IsKind(SyntaxKind.WhitespaceTrivia)) + .Reverse() + .ToArray(); + IEnumerable typeLeadingTrivia = preservedTrivia.Length > 0 + && !preservedTrivia[^1].IsKind(SyntaxKind.EndOfLineTrivia) + ? preservedTrivia.Append(SyntaxFactory.Space) + : preservedTrivia; + + updatedParameter = parameter.Type is { } parameterType + && preservedTrivia.Any(trivia => !trivia.IsKind(SyntaxKind.EndOfLineTrivia)) + ? updatedParameter.WithType(parameterType.WithLeadingTrivia(typeLeadingTrivia.Concat(parameterType.GetLeadingTrivia()))) + : updatedParameter.WithLeadingTrivia(parameter.GetLeadingTrivia()); + + editor.ReplaceNode( + parameter, + updatedParameter.WithAdditionalAnnotations(Formatter.Annotation)); } return editor.GetChangedDocument(); diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs index 0a8c43c0e5..de90c091cd 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AvoidOutRefTestMethodParametersAnalyzerTests.cs @@ -357,6 +357,38 @@ public void TestMethod1(int value) await VerifyCS.VerifyCodeFixAsync(code, fixedCode); } + + [TestMethod] + public async Task WhenRefReadonlyModifiersContainComment_CodeFixPreservesComment() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void [|TestMethod1|](ref /* rationale */ readonly int value) + { + } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod1(/* rationale */ int value) + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } #endif [TestMethod]