diff --git a/src/Analyzers/MSTest.Analyzers/UseProperAssertMethodsAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/UseProperAssertMethodsAnalyzer.cs index 45f4e90b07..65db73ad2a 100644 --- a/src/Analyzers/MSTest.Analyzers/UseProperAssertMethodsAnalyzer.cs +++ b/src/Analyzers/MSTest.Analyzers/UseProperAssertMethodsAnalyzer.cs @@ -302,15 +302,22 @@ private static bool IsIsNotNullPattern(IOperation operation, [NotNullWhen(true)] } // TODO: Recognize 'null == something' (i.e, when null is the left operand) - private static bool IsEqualsNullBinaryOperator(IOperation operation, [NotNullWhen(true)] out SyntaxNode? expressionUnderTest, out ITypeSymbol? typeOfExpressionUnderTest) + private static bool IsEqualsNullBinaryOperator(IOperation operation, INamedTypeSymbol objectTypeSymbol, [NotNullWhen(true)] out SyntaxNode? expressionUnderTest, out ITypeSymbol? typeOfExpressionUnderTest) { if (operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals, RightOperand: { } rightOperand } binaryOperation && - binaryOperation.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator } && rightOperand.WalkDownConversion() is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } }) { - expressionUnderTest = binaryOperation.LeftOperand.Syntax; - typeOfExpressionUnderTest = binaryOperation.LeftOperand.WalkDownConversion().Type; - return true; + // Allow built-in operators or user-defined operators from BCL types + bool isBuiltInOperator = binaryOperation.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator }; + bool isBCLUserDefinedOperator = binaryOperation.OperatorMethod is { MethodKind: MethodKind.UserDefinedOperator } && + IsBCLType(binaryOperation.OperatorMethod.ContainingType, objectTypeSymbol); + + if (isBuiltInOperator || isBCLUserDefinedOperator) + { + expressionUnderTest = binaryOperation.LeftOperand.Syntax; + typeOfExpressionUnderTest = binaryOperation.LeftOperand.WalkDownConversion().Type; + return true; + } } expressionUnderTest = null; @@ -319,15 +326,22 @@ private static bool IsEqualsNullBinaryOperator(IOperation operation, [NotNullWhe } // TODO: Recognize 'null != something' (i.e, when null is the left operand) - private static bool IsNotEqualsNullBinaryOperator(IOperation operation, [NotNullWhen(true)] out SyntaxNode? expressionUnderTest, out ITypeSymbol? typeOfExpressionUnderTest) + private static bool IsNotEqualsNullBinaryOperator(IOperation operation, INamedTypeSymbol objectTypeSymbol, [NotNullWhen(true)] out SyntaxNode? expressionUnderTest, out ITypeSymbol? typeOfExpressionUnderTest) { if (operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.NotEquals, RightOperand: { } rightOperand } binaryOperation && - binaryOperation.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator } && rightOperand.WalkDownConversion() is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } }) { - expressionUnderTest = binaryOperation.LeftOperand.Syntax; - typeOfExpressionUnderTest = binaryOperation.LeftOperand.WalkDownConversion().Type; - return true; + // Allow built-in operators or user-defined operators from BCL types + bool isBuiltInOperator = binaryOperation.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator }; + bool isBCLUserDefinedOperator = binaryOperation.OperatorMethod is { MethodKind: MethodKind.UserDefinedOperator } && + IsBCLType(binaryOperation.OperatorMethod.ContainingType, objectTypeSymbol); + + if (isBuiltInOperator || isBCLUserDefinedOperator) + { + expressionUnderTest = binaryOperation.LeftOperand.Syntax; + typeOfExpressionUnderTest = binaryOperation.LeftOperand.WalkDownConversion().Type; + return true; + } } expressionUnderTest = null; @@ -337,18 +351,19 @@ private static bool IsNotEqualsNullBinaryOperator(IOperation operation, [NotNull private static NullCheckStatus RecognizeNullCheck( IOperation operation, + INamedTypeSymbol objectTypeSymbol, // Note that expressionUnderTest is guaranteed to be non-null when the method returns a value other than NullCheckStatus.Unknown. // Given the current nullability attributes, there is no way to express this. out SyntaxNode? expressionUnderTest, out ITypeSymbol? typeOfExpressionUnderTest) { if (IsIsNullPattern(operation, out expressionUnderTest, out typeOfExpressionUnderTest) || - IsEqualsNullBinaryOperator(operation, out expressionUnderTest, out typeOfExpressionUnderTest)) + IsEqualsNullBinaryOperator(operation, objectTypeSymbol, out expressionUnderTest, out typeOfExpressionUnderTest)) { return NullCheckStatus.IsNull; } else if (IsIsNotNullPattern(operation, out expressionUnderTest, out typeOfExpressionUnderTest) || - IsNotEqualsNullBinaryOperator(operation, out expressionUnderTest, out typeOfExpressionUnderTest)) + IsNotEqualsNullBinaryOperator(operation, objectTypeSymbol, out expressionUnderTest, out typeOfExpressionUnderTest)) { return NullCheckStatus.IsNotNull; } @@ -358,6 +373,7 @@ private static NullCheckStatus RecognizeNullCheck( private static EqualityCheckStatus RecognizeEqualityCheck( IOperation operation, + INamedTypeSymbol objectTypeSymbol, out SyntaxNode? toBecomeExpected, out SyntaxNode? toBecomeActual, out ITypeSymbol? leftType, @@ -371,15 +387,22 @@ private static EqualityCheckStatus RecognizeEqualityCheck( rightType = isPattern1.Value.WalkDownConversion().Type; return EqualityCheckStatus.Equals; } - else if (operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals } binaryOperation1 && - binaryOperation1.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator }) + else if (operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals } binaryOperation1) { - // This is quite arbitrary. We can do extra checks to see which one (if any) looks like a "constant" and make it the expected. - toBecomeExpected = binaryOperation1.RightOperand.Syntax; - toBecomeActual = binaryOperation1.LeftOperand.Syntax; - leftType = binaryOperation1.RightOperand.WalkDownConversion().Type; - rightType = binaryOperation1.LeftOperand.WalkDownConversion().Type; - return EqualityCheckStatus.Equals; + // Allow built-in operators or user-defined operators from BCL types + bool isBuiltInOperator = binaryOperation1.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator }; + bool isBCLUserDefinedOperator = binaryOperation1.OperatorMethod is { MethodKind: MethodKind.UserDefinedOperator } && + IsBCLType(binaryOperation1.OperatorMethod.ContainingType, objectTypeSymbol); + + if (isBuiltInOperator || isBCLUserDefinedOperator) + { + // This is quite arbitrary. We can do extra checks to see which one (if any) looks like a "constant" and make it the expected. + toBecomeExpected = binaryOperation1.RightOperand.Syntax; + toBecomeActual = binaryOperation1.LeftOperand.Syntax; + leftType = binaryOperation1.RightOperand.WalkDownConversion().Type; + rightType = binaryOperation1.LeftOperand.WalkDownConversion().Type; + return EqualityCheckStatus.Equals; + } } else if (operation is IIsPatternOperation { Pattern: INegatedPatternOperation { Pattern: IConstantPatternOperation constantPattern2 } } isPattern2) { @@ -389,15 +412,22 @@ private static EqualityCheckStatus RecognizeEqualityCheck( rightType = isPattern2.Value.WalkDownConversion().Type; return EqualityCheckStatus.NotEquals; } - else if (operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.NotEquals } binaryOperation2 && - binaryOperation2.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator }) + else if (operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.NotEquals } binaryOperation2) { - // This is quite arbitrary. We can do extra checks to see which one (if any) looks like a "constant" and make it the expected. - toBecomeExpected = binaryOperation2.RightOperand.Syntax; - toBecomeActual = binaryOperation2.LeftOperand.Syntax; - leftType = binaryOperation2.RightOperand.WalkDownConversion().Type; - rightType = binaryOperation2.LeftOperand.WalkDownConversion().Type; - return EqualityCheckStatus.NotEquals; + // Allow built-in operators or user-defined operators from BCL types + bool isBuiltInOperator = binaryOperation2.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator }; + bool isBCLUserDefinedOperator = binaryOperation2.OperatorMethod is { MethodKind: MethodKind.UserDefinedOperator } && + IsBCLType(binaryOperation2.OperatorMethod.ContainingType, objectTypeSymbol); + + if (isBuiltInOperator || isBCLUserDefinedOperator) + { + // This is quite arbitrary. We can do extra checks to see which one (if any) looks like a "constant" and make it the expected. + toBecomeExpected = binaryOperation2.RightOperand.Syntax; + toBecomeActual = binaryOperation2.LeftOperand.Syntax; + leftType = binaryOperation2.RightOperand.WalkDownConversion().Type; + rightType = binaryOperation2.LeftOperand.WalkDownConversion().Type; + return EqualityCheckStatus.NotEquals; + } } toBecomeExpected = null; @@ -493,25 +523,41 @@ private static bool IsBCLCollectionType(ITypeSymbol type, INamedTypeSymbol objec type.ContainingAssembly.Identity.HasPublicKey == objectTypeSymbol.ContainingAssembly.Identity.HasPublicKey && type.ContainingAssembly.Identity.PublicKey.SequenceEqual(objectTypeSymbol.ContainingAssembly.Identity.PublicKey); + private static bool IsBCLType(ITypeSymbol? type, INamedTypeSymbol objectTypeSymbol) + // Check if the type is from the BCL by comparing its assembly's public key with the object type's assembly public key. + => type is not null && + type.ContainingAssembly is not null && + // object is coming from BCL and it's expected to always have a public key. + type.ContainingAssembly.Identity.HasPublicKey == objectTypeSymbol.ContainingAssembly.Identity.HasPublicKey && + type.ContainingAssembly.Identity.PublicKey.SequenceEqual(objectTypeSymbol.ContainingAssembly.Identity.PublicKey); + private static ComparisonCheckStatus RecognizeComparisonCheck( IOperation operation, + INamedTypeSymbol objectTypeSymbol, out SyntaxNode? leftExpression, out SyntaxNode? rightExpression) { - if (operation is IBinaryOperation binaryOperation && - binaryOperation.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator }) + if (operation is IBinaryOperation binaryOperation) { - leftExpression = binaryOperation.LeftOperand.Syntax; - rightExpression = binaryOperation.RightOperand.Syntax; + // Allow built-in operators or user-defined operators from BCL types + bool isBuiltInOperator = binaryOperation.OperatorMethod is not { MethodKind: MethodKind.UserDefinedOperator }; + bool isBCLUserDefinedOperator = binaryOperation.OperatorMethod is { MethodKind: MethodKind.UserDefinedOperator } && + IsBCLType(binaryOperation.OperatorMethod.ContainingType, objectTypeSymbol); - return binaryOperation.OperatorKind switch + if (isBuiltInOperator || isBCLUserDefinedOperator) { - BinaryOperatorKind.GreaterThan => ComparisonCheckStatus.GreaterThan, - BinaryOperatorKind.GreaterThanOrEqual => ComparisonCheckStatus.GreaterThanOrEqual, - BinaryOperatorKind.LessThan => ComparisonCheckStatus.LessThan, - BinaryOperatorKind.LessThanOrEqual => ComparisonCheckStatus.LessThanOrEqual, - _ => ComparisonCheckStatus.Unknown, - }; + leftExpression = binaryOperation.LeftOperand.Syntax; + rightExpression = binaryOperation.RightOperand.Syntax; + + return binaryOperation.OperatorKind switch + { + BinaryOperatorKind.GreaterThan => ComparisonCheckStatus.GreaterThan, + BinaryOperatorKind.GreaterThanOrEqual => ComparisonCheckStatus.GreaterThanOrEqual, + BinaryOperatorKind.LessThan => ComparisonCheckStatus.LessThan, + BinaryOperatorKind.LessThanOrEqual => ComparisonCheckStatus.LessThanOrEqual, + _ => ComparisonCheckStatus.Unknown, + }; + } } leftExpression = null; @@ -523,7 +569,7 @@ private static void AnalyzeIsTrueOrIsFalseInvocation(OperationAnalysisContext co { RoslynDebug.Assert(context.Operation is IInvocationOperation, "Expected IInvocationOperation."); - NullCheckStatus nullCheckStatus = RecognizeNullCheck(conditionArgument, out SyntaxNode? expressionUnderTest, out ITypeSymbol? typeOfExpressionUnderTest); + NullCheckStatus nullCheckStatus = RecognizeNullCheck(conditionArgument, objectTypeSymbol, out SyntaxNode? expressionUnderTest, out ITypeSymbol? typeOfExpressionUnderTest); // In this code path, we will be suggesting the use of IsNull/IsNotNull. // These assert methods only have an "object" overload. @@ -625,7 +671,7 @@ private static void AnalyzeIsTrueOrIsFalseInvocation(OperationAnalysisContext co } // Check for comparison patterns: a > b, a >= b, a < b, a <= b - ComparisonCheckStatus comparisonStatus = RecognizeComparisonCheck(conditionArgument, out SyntaxNode? leftExpr, out SyntaxNode? rightExpr); + ComparisonCheckStatus comparisonStatus = RecognizeComparisonCheck(conditionArgument, objectTypeSymbol, out SyntaxNode? leftExpr, out SyntaxNode? rightExpr); if (comparisonStatus != ComparisonCheckStatus.Unknown) { string properAssertMethod = (isTrueInvocation, comparisonStatus) switch @@ -686,7 +732,7 @@ private static void AnalyzeIsTrueOrIsFalseInvocation(OperationAnalysisContext co return; } - EqualityCheckStatus equalityCheckStatus = RecognizeEqualityCheck(conditionArgument, out SyntaxNode? toBecomeExpected, out SyntaxNode? toBecomeActual, out ITypeSymbol? leftType, out ITypeSymbol? rightType); + EqualityCheckStatus equalityCheckStatus = RecognizeEqualityCheck(conditionArgument, objectTypeSymbol, out SyntaxNode? toBecomeExpected, out SyntaxNode? toBecomeActual, out ITypeSymbol? leftType, out ITypeSymbol? rightType); if (equalityCheckStatus != EqualityCheckStatus.Unknown && CanUseTypeAsObject(context.Compilation, leftType) && CanUseTypeAsObject(context.Compilation, rightType)) diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/UseProperAssertMethodsAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/UseProperAssertMethodsAnalyzerTests.cs index deb75869dd..657448724b 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/UseProperAssertMethodsAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/UseProperAssertMethodsAnalyzerTests.cs @@ -112,7 +112,7 @@ public unsafe void MyTestMethod() Assert.IsFalse(y == null); Assert.IsFalse(null == x); Assert.IsFalse(null == y); - + // Assert.IsFalse: not null comparisons Assert.IsFalse(x is not null); Assert.IsFalse(y is not null); @@ -120,13 +120,13 @@ public unsafe void MyTestMethod() Assert.IsFalse(y != null); Assert.IsFalse(null != x); Assert.IsFalse(null != y); - + // Assert.IsFalse: two pointers equality comparisons Assert.IsFalse(x == x); Assert.IsFalse(x == y); Assert.IsFalse(y == x); Assert.IsFalse(y == y); - + // Assert.IsFalse: two pointers inequality comparisons Assert.IsFalse(x != x); Assert.IsFalse(x != y); @@ -2908,4 +2908,240 @@ await VerifyCS.VerifyCodeFixAsync( } #endregion + + #region BCL Types with IComparable Tests + + [TestMethod] + public async Task WhenAssertIsTrueWithTimeSpanComparison() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void MyTestMethod() + { + var ts1 = TimeSpan.Zero; + var ts2 = TimeSpan.FromSeconds(1); + {|#0:Assert.IsTrue(ts2 > ts1)|}; + {|#1:Assert.IsTrue(ts2 >= ts1)|}; + {|#2:Assert.IsTrue(ts1 < ts2)|}; + {|#3:Assert.IsTrue(ts1 <= ts2)|}; + {|#4:Assert.IsTrue(ts1 == ts1)|}; + {|#5:Assert.IsTrue(ts1 != ts2)|}; + } + } + """; + + string fixedCode = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void MyTestMethod() + { + var ts1 = TimeSpan.Zero; + var ts2 = TimeSpan.FromSeconds(1); + Assert.IsGreaterThan(ts1, ts2); + Assert.IsGreaterThanOrEqualTo(ts1, ts2); + Assert.IsLessThan(ts2, ts1); + Assert.IsLessThanOrEqualTo(ts2, ts1); + Assert.AreEqual(ts1, ts1); + Assert.AreNotEqual(ts2, ts1); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync( + code, + [ + // /0/Test0.cs(11,9): info MSTEST0037: Use 'Assert.IsGreaterThan' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(0).WithArguments("IsGreaterThan", "IsTrue"), + // /0/Test0.cs(12,9): info MSTEST0037: Use 'Assert.IsGreaterThanOrEqualTo' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(1).WithArguments("IsGreaterThanOrEqualTo", "IsTrue"), + // /0/Test0.cs(13,9): info MSTEST0037: Use 'Assert.IsLessThan' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(2).WithArguments("IsLessThan", "IsTrue"), + // /0/Test0.cs(14,9): info MSTEST0037: Use 'Assert.IsLessThanOrEqualTo' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(3).WithArguments("IsLessThanOrEqualTo", "IsTrue"), + // /0/Test0.cs(15,9): info MSTEST0037: Use 'Assert.AreEqual' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(4).WithArguments("AreEqual", "IsTrue"), + // /0/Test0.cs(16,9): info MSTEST0037: Use 'Assert.AreNotEqual' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(5).WithArguments("AreNotEqual", "IsTrue"), + ], + fixedCode); + } + + [TestMethod] + public async Task WhenAssertIsTrueWithDateTimeComparison() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void MyTestMethod() + { + var dt1 = DateTime.Today; + var dt2 = DateTime.Today.AddDays(1); + {|#0:Assert.IsTrue(dt2 > dt1)|}; + {|#1:Assert.IsTrue(dt2 >= dt1)|}; + {|#2:Assert.IsTrue(dt1 < dt2)|}; + {|#3:Assert.IsTrue(dt1 <= dt2)|}; + {|#4:Assert.IsTrue(dt1 == dt1)|}; + {|#5:Assert.IsTrue(dt1 != dt2)|}; + } + } + """; + + string fixedCode = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void MyTestMethod() + { + var dt1 = DateTime.Today; + var dt2 = DateTime.Today.AddDays(1); + Assert.IsGreaterThan(dt1, dt2); + Assert.IsGreaterThanOrEqualTo(dt1, dt2); + Assert.IsLessThan(dt2, dt1); + Assert.IsLessThanOrEqualTo(dt2, dt1); + Assert.AreEqual(dt1, dt1); + Assert.AreNotEqual(dt2, dt1); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync( + code, + [ + // /0/Test0.cs(11,9): info MSTEST0037: Use 'Assert.IsGreaterThan' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(0).WithArguments("IsGreaterThan", "IsTrue"), + // /0/Test0.cs(12,9): info MSTEST0037: Use 'Assert.IsGreaterThanOrEqualTo' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(1).WithArguments("IsGreaterThanOrEqualTo", "IsTrue"), + // /0/Test0.cs(13,9): info MSTEST0037: Use 'Assert.IsLessThan' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(2).WithArguments("IsLessThan", "IsTrue"), + // /0/Test0.cs(14,9): info MSTEST0037: Use 'Assert.IsLessThanOrEqualTo' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(3).WithArguments("IsLessThanOrEqualTo", "IsTrue"), + // /0/Test0.cs(15,9): info MSTEST0037: Use 'Assert.AreEqual' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(4).WithArguments("AreEqual", "IsTrue"), + // /0/Test0.cs(16,9): info MSTEST0037: Use 'Assert.AreNotEqual' instead of 'Assert.IsTrue' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(5).WithArguments("AreNotEqual", "IsTrue"), + ], + fixedCode); + } + + [TestMethod] + public async Task WhenAssertIsFalseWithTimeSpanComparison() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void MyTestMethod() + { + var ts1 = TimeSpan.Zero; + var ts2 = TimeSpan.FromSeconds(1); + {|#0:Assert.IsFalse(ts2 > ts1)|}; + {|#1:Assert.IsFalse(ts2 >= ts1)|}; + {|#2:Assert.IsFalse(ts1 < ts2)|}; + {|#3:Assert.IsFalse(ts1 <= ts2)|}; + {|#4:Assert.IsFalse(ts1 == ts1)|}; + {|#5:Assert.IsFalse(ts1 != ts2)|}; + } + } + """; + + string fixedCode = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void MyTestMethod() + { + var ts1 = TimeSpan.Zero; + var ts2 = TimeSpan.FromSeconds(1); + Assert.IsLessThanOrEqualTo(ts1, ts2); + Assert.IsLessThan(ts1, ts2); + Assert.IsGreaterThanOrEqualTo(ts2, ts1); + Assert.IsGreaterThan(ts2, ts1); + Assert.AreNotEqual(ts1, ts1); + Assert.AreEqual(ts2, ts1); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync( + code, + [ + // /0/Test0.cs(11,9): info MSTEST0037: Use 'Assert.IsLessThanOrEqualTo' instead of 'Assert.IsFalse' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(0).WithArguments("IsLessThanOrEqualTo", "IsFalse"), + // /0/Test0.cs(12,9): info MSTEST0037: Use 'Assert.IsLessThan' instead of 'Assert.IsFalse' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(1).WithArguments("IsLessThan", "IsFalse"), + // /0/Test0.cs(13,9): info MSTEST0037: Use 'Assert.IsGreaterThanOrEqualTo' instead of 'Assert.IsFalse' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(2).WithArguments("IsGreaterThanOrEqualTo", "IsFalse"), + // /0/Test0.cs(14,9): info MSTEST0037: Use 'Assert.IsGreaterThan' instead of 'Assert.IsFalse' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(3).WithArguments("IsGreaterThan", "IsFalse"), + // /0/Test0.cs(15,9): info MSTEST0037: Use 'Assert.AreNotEqual' instead of 'Assert.IsFalse' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(4).WithArguments("AreNotEqual", "IsFalse"), + // /0/Test0.cs(16,9): info MSTEST0037: Use 'Assert.AreEqual' instead of 'Assert.IsFalse' + VerifyCS.DiagnosticIgnoringAdditionalLocations().WithLocation(5).WithArguments("AreEqual", "IsFalse"), + ], + fixedCode); + } + + #endregion + + [TestMethod] + public async Task WhenAssertIsTrueWithUserDefinedComparisonOperatorsThenNoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public class MyCustomType + { + public static bool operator >(MyCustomType x, MyCustomType y) => true; + public static bool operator <(MyCustomType x, MyCustomType y) => false; + public static bool operator >=(MyCustomType x, MyCustomType y) => true; + public static bool operator <=(MyCustomType x, MyCustomType y) => false; + } + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void MyTestMethod() + { + var a = new MyCustomType(); + var b = new MyCustomType(); + // These should NOT trigger diagnostics because they're user-defined operators from non-BCL types + Assert.IsTrue(a > b); + Assert.IsTrue(a >= b); + Assert.IsTrue(a < b); + Assert.IsTrue(a <= b); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } }