From 8963d09958b205c5b8a6b86a3111a8dc58b45e11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:49:16 +0000 Subject: [PATCH 1/4] Initial plan From 134bbf1611a41d57a8f835f396b1d82027c6b091 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:58:59 +0000 Subject: [PATCH 2/4] Fix MSTEST0017 false positive with user-defined conversion operators Agent-Logs-Url: https://github.com/microsoft/testfx/sessions/fca5e0e9-838b-4c3f-94b2-fc6059f60bbe Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- ...rgsShouldBePassedInCorrectOrderAnalyzer.cs | 7 +++- ...ouldBePassedInCorrectOrderAnalyzerTests.cs | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Analyzers/MSTest.Analyzers/AssertionArgsShouldBePassedInCorrectOrderAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/AssertionArgsShouldBePassedInCorrectOrderAnalyzer.cs index decc63b28c..cb89c88eae 100644 --- a/src/Analyzers/MSTest.Analyzers/AssertionArgsShouldBePassedInCorrectOrderAnalyzer.cs +++ b/src/Analyzers/MSTest.Analyzers/AssertionArgsShouldBePassedInCorrectOrderAnalyzer.cs @@ -61,7 +61,12 @@ public override void Initialize(AnalysisContext context) private static bool IsConstant(IArgumentOperation argumentOperation) { - IOperation operation = argumentOperation.Value.WalkDownConversion(); + IOperation operation = argumentOperation.Value; + while (operation is IConversionOperation conversionOperation && !conversionOperation.Conversion.IsUserDefined) + { + operation = conversionOperation.Operand; + } + return operation.ConstantValue.HasValue || operation.Kind == OperationKind.TypeOf; } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AssertionArgsShouldBePassedInCorrectOrderAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AssertionArgsShouldBePassedInCorrectOrderAnalyzerTests.cs index 028edbe6bf..6ff8bc1620 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AssertionArgsShouldBePassedInCorrectOrderAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AssertionArgsShouldBePassedInCorrectOrderAnalyzerTests.cs @@ -988,6 +988,39 @@ public void Compliant() await VerifyCS.VerifyCodeFixAsync(code, code); } + [TestMethod] + public async Task UserDefinedConversionOperator_ShouldNotFlag() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + + [TestClass] + public class MyTestClass + { + private sealed class Foo : IEquatable + { + private readonly string _value; + public Foo(string value) { _value = value; } + public static explicit operator Foo(string s) => new Foo(s); + public override bool Equals(object? obj) => Equals(obj as Foo); + public bool Equals(Foo? other) => other is not null && _value.Equals(other._value); + public override int GetHashCode() => HashCode.Combine(_value); + } + + [TestMethod] + public void Compliant() + { + // User-defined conversion operator should not be treated as a constant + Assert.AreEqual(new Foo("Hello"), (Foo)"Hello"); + Assert.AreNotEqual(new Foo("Hello"), (Foo)"World"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } + [TestMethod] public async Task WhenUsingLiterals_MultiLineWithDifferentIndentation() { From 75297ab59055ec3834b255dd2fecce237ca7fca1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:00:16 +0000 Subject: [PATCH 3/4] Refactor: extract WalkDownBuiltInConversion helper method for user-defined conversion check Agent-Logs-Url: https://github.com/microsoft/testfx/sessions/fca5e0e9-838b-4c3f-94b2-fc6059f60bbe Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- ...ArgsShouldBePassedInCorrectOrderAnalyzer.cs | 7 +------ .../IOperationExtensions.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers/AssertionArgsShouldBePassedInCorrectOrderAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/AssertionArgsShouldBePassedInCorrectOrderAnalyzer.cs index cb89c88eae..098ec95ac5 100644 --- a/src/Analyzers/MSTest.Analyzers/AssertionArgsShouldBePassedInCorrectOrderAnalyzer.cs +++ b/src/Analyzers/MSTest.Analyzers/AssertionArgsShouldBePassedInCorrectOrderAnalyzer.cs @@ -61,12 +61,7 @@ public override void Initialize(AnalysisContext context) private static bool IsConstant(IArgumentOperation argumentOperation) { - IOperation operation = argumentOperation.Value; - while (operation is IConversionOperation conversionOperation && !conversionOperation.Conversion.IsUserDefined) - { - operation = conversionOperation.Operand; - } - + IOperation operation = argumentOperation.Value.WalkDownBuiltInConversion(); return operation.ConstantValue.HasValue || operation.Kind == OperationKind.TypeOf; } diff --git a/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IOperationExtensions.cs b/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IOperationExtensions.cs index e3ec94bd4c..e1b623f79a 100644 --- a/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IOperationExtensions.cs +++ b/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IOperationExtensions.cs @@ -37,4 +37,22 @@ public static IOperation WalkDownConversion(this IOperation operation) return operation; } + + /// + /// Walks down consecutive non-user-defined conversion operations until an operand is reached that isn't a + /// non-user-defined conversion operation. + /// + /// The starting operation. + /// + /// The inner non conversion operation, or the starting operation if it wasn't a non-user-defined conversion operation. + /// + public static IOperation WalkDownBuiltInConversion(this IOperation operation) + { + while (operation is IConversionOperation conversionOperation && !conversionOperation.Conversion.IsUserDefined) + { + operation = conversionOperation.Operand; + } + + return operation; + } } From 2d9ab6874c25e8a634bb80a3106bec85fec026c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 30 Mar 2026 18:27:57 +0200 Subject: [PATCH 4/4] Fixes --- .../IOperationExtensions.cs | 7 +- ...ouldBePassedInCorrectOrderAnalyzerTests.cs | 106 +++++++++++++++++- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IOperationExtensions.cs b/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IOperationExtensions.cs index e1b623f79a..11812837e4 100644 --- a/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IOperationExtensions.cs +++ b/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/IOperationExtensions.cs @@ -39,12 +39,13 @@ public static IOperation WalkDownConversion(this IOperation operation) } /// - /// Walks down consecutive non-user-defined conversion operations until an operand is reached that isn't a - /// non-user-defined conversion operation. + /// Walks down consecutive built-in conversion operations, stopping at user-defined + /// conversions or non-conversion operands. /// /// The starting operation. /// - /// The inner non conversion operation, or the starting operation if it wasn't a non-user-defined conversion operation. + /// The first operand that is either a user-defined conversion or not a conversion at all, + /// or the starting operation if it was already one of those. /// public static IOperation WalkDownBuiltInConversion(this IOperation operation) { diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/AssertionArgsShouldBePassedInCorrectOrderAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/AssertionArgsShouldBePassedInCorrectOrderAnalyzerTests.cs index 6ff8bc1620..a5bdc4870d 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/AssertionArgsShouldBePassedInCorrectOrderAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/AssertionArgsShouldBePassedInCorrectOrderAnalyzerTests.cs @@ -989,9 +989,10 @@ public void Compliant() } [TestMethod] - public async Task UserDefinedConversionOperator_ShouldNotFlag() + public async Task UserDefinedExplicitConversionOperator_ShouldNotFlag() { string code = """ + #nullable enable using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -1021,6 +1022,109 @@ public void Compliant() await VerifyCS.VerifyCodeFixAsync(code, code); } + [TestMethod] + public async Task UserDefinedImplicitConversionOperator_ShouldNotFlag() + { + string code = """ + #nullable enable + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + + [TestClass] + public class MyTestClass + { + private sealed class Bar : IEquatable + { + private readonly string _value; + public Bar(string value) { _value = value; } + public static implicit operator Bar(string s) => new Bar(s); + public override bool Equals(object? obj) => Equals(obj as Bar); + public bool Equals(Bar? other) => other is not null && _value.Equals(other._value); + public override int GetHashCode() => HashCode.Combine(_value); + } + + [TestMethod] + public void Compliant() + { + // Implicit user-defined conversion should not be treated as a constant + Bar b = "Hello"; + Assert.AreEqual(new Bar("Hello"), b); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } + + [TestMethod] + public async Task BuiltInConversionWrappingUserDefined_ShouldNotFlag() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + + [TestClass] + public class MyTestClass + { + private sealed class Foo + { + private readonly string _value; + public Foo(string value) { _value = value; } + public static explicit operator Foo(string s) => new Foo(s); + } + + [TestMethod] + public void Compliant() + { + // A built-in conversion wrapping a user-defined conversion should still stop + Assert.AreEqual(new Foo("Hello"), (object)(Foo)"Hello"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } + + [TestMethod] + public async Task BuiltInCastWithLiteral_ShouldStillFlag() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void NonCompliant() + { + long x = 42; + + // Built-in cast on a literal should still be walked through and flagged + [|Assert.AreEqual(x, (long)42)|]; + } + } + """; + + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void NonCompliant() + { + long x = 42; + + // Built-in cast on a literal should still be walked through and flagged + Assert.AreEqual((long)42, x); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + [TestMethod] public async Task WhenUsingLiterals_MultiLineWithDifferentIndentation() {