From f93b9ccc6c19d75f360af793a493d7133a63a757 Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Mon, 11 Mar 2024 15:00:58 +0800 Subject: [PATCH] NUnit2010: Add support for detecting use of 'is' pattern inside 'Assert.That' --- .../EqualConstraintUsageAnalyzerTests.cs | 20 ++++ .../EqualConstraintUsageCodeFixTests.cs | 70 ++++++++++++ .../EqualConstraintUsageAnalyzer.cs | 6 + .../EqualConstraintUsageCodeFix.cs | 105 +++++++++++++++++- 4 files changed, 199 insertions(+), 2 deletions(-) diff --git a/src/nunit.analyzers.tests/ConstraintsUsage/EqualConstraintUsageAnalyzerTests.cs b/src/nunit.analyzers.tests/ConstraintsUsage/EqualConstraintUsageAnalyzerTests.cs index df963251..41741e9a 100644 --- a/src/nunit.analyzers.tests/ConstraintsUsage/EqualConstraintUsageAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/ConstraintsUsage/EqualConstraintUsageAnalyzerTests.cs @@ -41,6 +41,26 @@ public void AnalyzeWhenNotEqualsOperatorUsed() RoslynAssert.Diagnostics(analyzer, isNotEqualToDiagnostic, testCode); } + [Test] + public void AnalyzeWhenIsOperatorUsed() + { + var testCode = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(↓actual is ""abc"");"); + + RoslynAssert.Diagnostics(analyzer, isEqualToDiagnostic, testCode); + } + + [Test] + public void AnalyzeWhenIsNotOperatorUsed() + { + var testCode = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(↓actual is not ""bcd"");"); + + RoslynAssert.Diagnostics(analyzer, isNotEqualToDiagnostic, testCode); + } + [Test] public void AnalyzeWhenEqualsInstanceMethodUsed() { diff --git a/src/nunit.analyzers.tests/ConstraintsUsage/EqualConstraintUsageCodeFixTests.cs b/src/nunit.analyzers.tests/ConstraintsUsage/EqualConstraintUsageCodeFixTests.cs index e9db73a5..9b6fc0be 100644 --- a/src/nunit.analyzers.tests/ConstraintsUsage/EqualConstraintUsageCodeFixTests.cs +++ b/src/nunit.analyzers.tests/ConstraintsUsage/EqualConstraintUsageCodeFixTests.cs @@ -41,6 +41,76 @@ public void FixesNotEqualsOperator() RoslynAssert.CodeFix(analyzer, fix, equalConstraintDiagnostic, code, fixedCode); } + [Test] + public void FixesIsOperator() + { + var code = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(actual is ""abc"");"); + + var fixedCode = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(actual, Is.EqualTo(""abc""));"); + + RoslynAssert.CodeFix(analyzer, fix, equalConstraintDiagnostic, code, fixedCode); + } + + [Test] + public void FixesIsNotOperator() + { + var code = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(actual is not ""abc"");"); + + var fixedCode = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(actual, Is.Not.EqualTo(""abc""));"); + + RoslynAssert.CodeFix(analyzer, fix, equalConstraintDiagnostic, code, fixedCode); + } + + [Test] + public void FixesComplexIsOperator() + { + var code = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(actual is ""abc"" or ""def"");"); + + var fixedCode = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(actual, Is.EqualTo(""abc"").Or.EqualTo(""def""));"); + + RoslynAssert.CodeFix(analyzer, fix, equalConstraintDiagnostic, code, fixedCode); + } + + [Test] + public void FixesComplexIsNotOperator() + { + var code = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(actual is not ""abc"" and not ""def"");"); + + var fixedCode = TestUtility.WrapInTestMethod(@" + var actual = ""abc""; + Assert.That(actual, Is.Not.EqualTo(""abc"").And.Not.EqualTo(""def""));"); + + RoslynAssert.CodeFix(analyzer, fix, equalConstraintDiagnostic, code, fixedCode); + } + + [Test] + public void FixesComplexRelationalIsOperator() + { + var code = TestUtility.WrapInTestMethod(@" + double actual = 1.234; + Assert.That(actual is > 1 and <= 2 or 3 or > 4);"); + + var fixedCode = TestUtility.WrapInTestMethod(@" + double actual = 1.234; + Assert.That(actual, Is.GreaterThan(1).And.LessThanOrEqualTo(2).Or.EqualTo(3).Or.GreaterThan(4));"); + + RoslynAssert.CodeFix(analyzer, fix, equalConstraintDiagnostic, code, fixedCode); + } + [Test] public void FixesEqualsInstanceMethod() { diff --git a/src/nunit.analyzers/ConstraintUsage/EqualConstraintUsageAnalyzer.cs b/src/nunit.analyzers/ConstraintUsage/EqualConstraintUsageAnalyzer.cs index cf5769be..f1118a8d 100644 --- a/src/nunit.analyzers/ConstraintUsage/EqualConstraintUsageAnalyzer.cs +++ b/src/nunit.analyzers/ConstraintUsage/EqualConstraintUsageAnalyzer.cs @@ -42,6 +42,12 @@ public class EqualConstraintUsageAnalyzer : BaseConditionConstraintAnalyzer shouldReport = true; negated = !negated; } + else if (actual is IIsPatternOperation isPatternOperation) + { + shouldReport = true; + if (isPatternOperation.Pattern is INegatedPatternOperation) + negated = true; + } if (shouldReport) { diff --git a/src/nunit.analyzers/ConstraintUsage/EqualConstraintUsageCodeFix.cs b/src/nunit.analyzers/ConstraintUsage/EqualConstraintUsageCodeFix.cs index 36c233c6..cc059520 100644 --- a/src/nunit.analyzers/ConstraintUsage/EqualConstraintUsageCodeFix.cs +++ b/src/nunit.analyzers/ConstraintUsage/EqualConstraintUsageCodeFix.cs @@ -17,17 +17,38 @@ public class EqualConstraintUsageCodeFix : BaseConditionConstraintCodeFix protected override (ExpressionSyntax? actual, ExpressionSyntax? constraintExpression) GetActualAndConstraintExpression(ExpressionSyntax conditionNode, string suggestedConstraintString) { var (actual, expected) = GetActualExpected(conditionNode); - var constraintExpression = GetConstraintExpression(suggestedConstraintString, expected); + + InvocationExpressionSyntax? constraintExpression; + + if (expected is ExpressionSyntax expression) + { + constraintExpression = GetConstraintExpression(suggestedConstraintString, expression); + } + else if (expected is PatternSyntax pattern) + { + constraintExpression = this.ConvertPattern( + SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIs), + pattern); + } + else + { + constraintExpression = null; + } + return (actual, constraintExpression); } - private static (ExpressionSyntax? actual, ExpressionSyntax? expected) GetActualExpected(SyntaxNode conditionNode) + private static (ExpressionSyntax? actual, ExpressionOrPatternSyntax? expected) GetActualExpected(SyntaxNode conditionNode) { if (conditionNode is BinaryExpressionSyntax binaryExpression && (binaryExpression.IsKind(SyntaxKind.EqualsExpression) || binaryExpression.IsKind(SyntaxKind.NotEqualsExpression))) { return (binaryExpression.Left, binaryExpression.Right); } + else if (conditionNode is IsPatternExpressionSyntax isPatternExpression) + { + return (isPatternExpression.Expression, isPatternExpression.Pattern); + } else { if (conditionNode is PrefixUnaryExpressionSyntax prefixUnary @@ -58,5 +79,85 @@ private static (ExpressionSyntax? actual, ExpressionSyntax? expected) GetActualE return (null, null); } + + /// + /// Converts an 'is' pattern to a corresponding nunit EqualTo invocation. + /// + /// + /// We support: + /// constant-pattern, + /// relational-pattern: <, <=, >, >=. + /// not supported-pattern, + /// supported-pattern or supported-pattern, + /// supported-pattern and supported-pattern. + /// + private InvocationExpressionSyntax? ConvertPattern(ExpressionSyntax member, PatternSyntax pattern) + { + if (pattern is ConstantPatternSyntax constantPattern) + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + member, + SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIsEqualTo)), + SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(constantPattern.Expression)))); + } + else if (pattern is RelationalPatternSyntax relationalPattern) + { + string? identifier = relationalPattern.OperatorToken.Kind() switch + { + SyntaxKind.LessThanToken => NUnitFrameworkConstants.NameOfIsLessThan, + SyntaxKind.LessThanEqualsToken => NUnitFrameworkConstants.NameOfIsLessThanOrEqualTo, + SyntaxKind.GreaterThanToken => NUnitFrameworkConstants.NameOfIsGreaterThan, + SyntaxKind.GreaterThanEqualsToken => NUnitFrameworkConstants.NameOfIsGreaterThanOrEqualTo, + _ => null, + }; + + if (identifier is not null) + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + member, + SyntaxFactory.IdentifierName(identifier)), + SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(relationalPattern.Expression)))); + } + } + else if (pattern is UnaryPatternSyntax unaryPattern && unaryPattern.IsKind(SyntaxKind.NotPattern)) + { + return this.ConvertPattern( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + member, + SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIsNot)), + unaryPattern.Pattern); + } + else if (pattern is BinaryPatternSyntax binaryPattern) + { + string? constraint = binaryPattern.Kind() switch + { + SyntaxKind.OrPattern => NUnitFrameworkConstants.NameOfConstraintExpressionOr, + SyntaxKind.AndPattern => NUnitFrameworkConstants.NameOfConstraintExpressionAnd, + _ => null, + }; + + if (constraint is not null) + { + InvocationExpressionSyntax? leftExpression = this.ConvertPattern(member, binaryPattern.Left); + + if (leftExpression is not null) + { + return this.ConvertPattern( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + leftExpression, + SyntaxFactory.IdentifierName(constraint)), + binaryPattern.Right); + } + } + } + + return null; + } } }