From 7a9cabcb59d6de0a25fb407e97616470da4c0088 Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 11 Jun 2026 13:21:11 +0100 Subject: [PATCH] Add AssertTrueInsteadOfDedicatedAssertOperation (java:S5785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements SonarQube rule java:S5785 "JUnit assertTrue/assertFalse should be simplified to its dedicated assertion". Mirrors the SonarJava detection logic: - assertTrue(a == null) → assertNull(a) - assertTrue(a != null) → assertNotNull(a) - assertTrue(a == b) [prim] → assertEquals(a, b) - assertTrue(a != b) [prim] → assertNotEquals(a, b) - assertTrue(a == b) [obj] → assertSame(a, b) - assertTrue(a != b) [obj] → assertNotSame(a, b) - assertTrue(a.equals(b)) → assertEquals(a, b) - assertTrue(Objects.equals(a,b)) → assertEquals(a, b) - assertTrue(!expr) → complement of assertTrue(expr) - assertFalse(...) → complement of the above Supports JUnit 4 (org.junit.Assert, junit.framework.Assert/TestCase) and JUnit 5 (org.junit.jupiter.api.Assertions), with message argument preserved in its original position (first for JUnit 4, last for JUnit 5). Static wildcard imports are handled without any additional import management. Four test cases: JUnit 4 qualified calls, JUnit 5 qualified calls (via .txt source file), wildcard static imports, and noop cases. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 + ...TrueInsteadOfDedicatedAssertOperation.java | 334 ++++++++++++++++++ .../sonar/s5785/AssertTrueJUnit4Example.java | 92 +++++ .../s5785/AssertTrueJUnit4ExampleAfter.java | 91 +++++ .../sonar/s5785/AssertTrueJUnit5Example.txt | 68 ++++ .../s5785/AssertTrueJUnit5ExampleAfter.txt | 67 ++++ .../sonar/s5785/AssertTrueNoopExample.java | 36 ++ .../s5785/AssertTrueNoopExampleAfter.java | 36 ++ .../s5785/AssertTrueStaticImportExample.java | 23 ++ .../AssertTrueStaticImportExampleAfter.java | 23 ++ ...TrueInsteadOfDedicatedAssertOperation.java | 68 ++++ 11 files changed, 842 insertions(+) create mode 100644 astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueInsteadOfDedicatedAssertOperation.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit4Example.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit4ExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit5Example.txt create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit5ExampleAfter.txt create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueNoopExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueNoopExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueStaticImportExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueStaticImportExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/TestAssertTrueInsteadOfDedicatedAssertOperation.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e914fec..c9e676d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [Unreleased] +### Added +* Added `AssertTrueInsteadOfDedicatedAssertOperation` implementing SonarQube rule java:S5785 — rewrites `assertTrue`/`assertFalse` calls to dedicated assertion methods (`assertNull`, `assertNotNull`, `assertEquals`, `assertNotEquals`, `assertSame`, `assertNotSame`) for JUnit 4 and JUnit 5 + ## [2.7.0] - 2026-04-28 ### Changed * Updated Java compatibility to 17 (#139) in https://github.com/alfasoftware/astra/pull/140 diff --git a/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueInsteadOfDedicatedAssertOperation.java b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueInsteadOfDedicatedAssertOperation.java new file mode 100644 index 0000000..a0c2360 --- /dev/null +++ b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueInsteadOfDedicatedAssertOperation.java @@ -0,0 +1,334 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.alfasoftware.astra.core.utils.ASTOperation; +import org.alfasoftware.astra.core.utils.AstraUtils; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.InfixExpression; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.NullLiteral; +import org.eclipse.jdt.core.dom.PrefixExpression; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.text.edits.MalformedTreeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Refactoring operation implementing SonarQube rule java:S5785 + * "JUnit assertTrue/assertFalse should be simplified to its dedicated assertion". + * + *

Detects calls to {@code assertTrue} and {@code assertFalse} from JUnit 4 and JUnit 5 + * assertion classes where the boolean argument can be expressed more precisely with a dedicated + * assertion method, and rewrites them accordingly. + * + *

Supported assertion classes: + *

+ * + *

Transformations (applies symmetrically for {@code assertTrue}/{@code assertFalse}, + * with {@code assertFalse} using the complement assertion): + *

+ * + *

Message arguments are preserved in their original position (first for JUnit 4, last for JUnit 5). + * Static imports are updated when the method is invoked without a qualifier. + */ +public class AssertTrueInsteadOfDedicatedAssertOperation implements ASTOperation { + + private static final Logger log = LoggerFactory.getLogger(AssertTrueInsteadOfDedicatedAssertOperation.class); + + static final Set ASSERTION_CLASS_FQNS = Set.of( + "org.junit.Assert", + "junit.framework.Assert", + "junit.framework.TestCase", + "org.junit.jupiter.api.Assertions"); + + private static final Set JUNIT4_CLASS_FQNS = Set.of( + "org.junit.Assert", + "junit.framework.Assert", + "junit.framework.TestCase"); + + private static final String OBJECTS_FQN = "java.util.Objects"; + + private enum Assertion { + NULL("assertNull"), + NOT_NULL("assertNotNull"), + SAME("assertSame"), + NOT_SAME("assertNotSame"), + EQUALS("assertEquals"), + NOT_EQUALS("assertNotEquals"); + + final String methodName; + + Assertion(String methodName) { + this.methodName = methodName; + } + + Assertion complement() { + switch (this) { + case NULL: return NOT_NULL; + case NOT_NULL: return NULL; + case SAME: return NOT_SAME; + case NOT_SAME: return SAME; + case EQUALS: return NOT_EQUALS; + case NOT_EQUALS: return EQUALS; + default: throw new IllegalStateException("Unknown assertion: " + this); + } + } + } + + private static final class AnalysisResult { + final Assertion assertion; + final List newArgs; + + AnalysisResult(Assertion assertion, List newArgs) { + this.assertion = assertion; + this.newArgs = newArgs; + } + } + + @Override + public void run(CompilationUnit compilationUnit, ASTNode node, ASTRewrite rewriter) + throws IOException, MalformedTreeException, BadLocationException { + + if (!(node instanceof MethodInvocation)) { + return; + } + MethodInvocation mi = (MethodInvocation) node; + + String methodName = mi.getName().getIdentifier(); + if (!"assertTrue".equals(methodName) && !"assertFalse".equals(methodName)) { + return; + } + + IMethodBinding binding = mi.resolveMethodBinding(); + if (binding == null) { + log.debug("Could not resolve binding for {} in [{}] - skipping", + methodName, AstraUtils.getNameForCompilationUnit(compilationUnit)); + return; + } + ITypeBinding declaringClass = binding.getDeclaringClass(); + if (declaringClass == null) { + return; + } + String declaringFqn = declaringClass.getQualifiedName(); + if (!ASSERTION_CLASS_FQNS.contains(declaringFqn)) { + return; + } + + boolean isJunit4 = JUNIT4_CLASS_FQNS.contains(declaringFqn); + boolean isAssertFalse = "assertFalse".equals(methodName); + + @SuppressWarnings("unchecked") + List args = mi.arguments(); + + // Find the first boolean-typed argument (mirrors SonarJava's detection) + Expression boolArg = null; + for (Expression arg : args) { + ITypeBinding tb = arg.resolveTypeBinding(); + if (tb != null && tb.isPrimitive() && "boolean".equals(tb.getName())) { + boolArg = arg; + break; + } + } + if (boolArg == null) { + return; + } + + Optional resultOpt = analyze(boolArg, isAssertFalse); + if (!resultOpt.isPresent()) { + return; + } + AnalysisResult result = resultOpt.get(); + + log.info("Rewriting {}.{} to {} in [{}]", + declaringClass.getName(), methodName, result.assertion.methodName, + AstraUtils.getNameForCompilationUnit(compilationUnit)); + + // Change the method name + rewriter.set(mi.getName(), SimpleName.IDENTIFIER_PROPERTY, result.assertion.methodName, null); + + // If the method has no expression qualifier and is static, it was statically imported; + // add a static import for the replacement method. + if (mi.getExpression() == null && Modifier.isStatic(binding.getModifiers())) { + addStaticImportIfNeeded(compilationUnit, rewriter, declaringFqn, result.assertion.methodName); + } + + // Replace the boolean arg with the new arg(s) in-place, preserving any message arg. + // Using replace + insertAfter relative to the original boolArg position means the + // message arg (if present) stays wherever it already is in the list. + ListRewrite lrw = rewriter.getListRewrite(mi, MethodInvocation.ARGUMENTS_PROPERTY); + List newArgs = result.newArgs; + + ASTNode firstCopy = rewriter.createCopyTarget(newArgs.get(0)); + lrw.replace(boolArg, firstCopy, null); + for (int i = 1; i < newArgs.size(); i++) { + ASTNode copy = rewriter.createCopyTarget(newArgs.get(i)); + lrw.insertAfter(copy, boolArg, null); + } + } + + private static void addStaticImportIfNeeded( + CompilationUnit compilationUnit, ASTRewrite rewriter, + String declaringFqn, String newMethodName) { + + String fullImport = declaringFqn + "." + newMethodName; + for (ImportDeclaration imp : AstraUtils.getImportDeclarations(compilationUnit)) { + if (!imp.isStatic()) { + continue; + } + String name = imp.getName().getFullyQualifiedName(); + // On-demand (wildcard) static import already covers the new method + if (imp.isOnDemand() && declaringFqn.equals(name)) { + return; + } + // Exact import already present + if (!imp.isOnDemand() && fullImport.equals(name)) { + return; + } + } + AstraUtils.addStaticImport(compilationUnit, fullImport, rewriter); + } + + /** + * Determines the replacement assertion and the new arguments for the given boolean expression. + * The {@code inverted} flag starts as {@code false} for {@code assertTrue} and {@code true} + * for {@code assertFalse}, and is flipped each time a logical-complement ({@code !}) is unwrapped. + */ + private static Optional analyze(Expression expr, boolean inverted) { + + // Logical complement: !(inner) → recurse with flipped polarity + if (expr instanceof PrefixExpression) { + PrefixExpression prefix = (PrefixExpression) expr; + if (PrefixExpression.Operator.NOT.equals(prefix.getOperator())) { + return analyze(prefix.getOperand(), !inverted); + } + } + + // == and != operators + if (expr instanceof InfixExpression) { + InfixExpression infix = (InfixExpression) expr; + if (InfixExpression.Operator.EQUALS.equals(infix.getOperator())) { + return analyzeEquality(infix, false, inverted); + } + if (InfixExpression.Operator.NOT_EQUALS.equals(infix.getOperator())) { + return analyzeEquality(infix, true, inverted); + } + } + + // a.equals(b) or Objects.equals(a, b) + if (expr instanceof MethodInvocation) { + MethodInvocation innerMi = (MethodInvocation) expr; + if (isEqualsMethod(innerMi)) { + Assertion base = inverted ? Assertion.NOT_EQUALS : Assertion.EQUALS; + List newArgs; + @SuppressWarnings("unchecked") + List innerArgs = innerMi.arguments(); + IMethodBinding innerBinding = innerMi.resolveMethodBinding(); + ITypeBinding innerDeclaring = innerBinding != null ? innerBinding.getDeclaringClass() : null; + if (innerDeclaring != null && OBJECTS_FQN.equals(innerDeclaring.getQualifiedName())) { + // Objects.equals(a, b): carry both arguments directly (whether qualified or statically imported) + newArgs = List.copyOf(innerArgs); + } else if (innerMi.getExpression() != null) { + // a.equals(b): receiver is expected, first argument is actual + newArgs = List.of(innerMi.getExpression(), innerArgs.get(0)); + } else { + return Optional.empty(); + } + return Optional.of(new AnalysisResult(base, newArgs)); + } + } + + return Optional.empty(); + } + + private static Optional analyzeEquality( + InfixExpression infix, boolean isNotEquals, boolean inverted) { + + Expression left = infix.getLeftOperand(); + Expression right = infix.getRightOperand(); + + // Null check: a == null or null == a + if (left instanceof NullLiteral) { + Assertion base = isNotEquals ? Assertion.NOT_NULL : Assertion.NULL; + Assertion chosen = inverted ? base.complement() : base; + return Optional.of(new AnalysisResult(chosen, List.of(right))); + } + if (right instanceof NullLiteral) { + Assertion base = isNotEquals ? Assertion.NOT_NULL : Assertion.NULL; + Assertion chosen = inverted ? base.complement() : base; + return Optional.of(new AnalysisResult(chosen, List.of(left))); + } + + // Primitive or reference comparison — requires type resolution + ITypeBinding leftType = left.resolveTypeBinding(); + ITypeBinding rightType = right.resolveTypeBinding(); + if (leftType == null || rightType == null) { + return Optional.empty(); + } + + Assertion base; + if (leftType.isPrimitive() || rightType.isPrimitive()) { + base = isNotEquals ? Assertion.NOT_EQUALS : Assertion.EQUALS; + } else { + base = isNotEquals ? Assertion.NOT_SAME : Assertion.SAME; + } + Assertion chosen = inverted ? base.complement() : base; + return Optional.of(new AnalysisResult(chosen, List.of(left, right))); + } + + private static boolean isEqualsMethod(MethodInvocation mi) { + if (!"equals".equals(mi.getName().getIdentifier())) { + return false; + } + IMethodBinding binding = mi.resolveMethodBinding(); + if (binding == null) { + return false; + } + ITypeBinding declaring = binding.getDeclaringClass(); + if (declaring == null) { + return false; + } + @SuppressWarnings("unchecked") + List innerArgs = mi.arguments(); + + // Objects.equals(a, b) — static, 2-argument form + if (OBJECTS_FQN.equals(declaring.getQualifiedName()) && innerArgs.size() == 2) { + return true; + } + + // a.equals(b) — instance method with a single Object parameter + if (innerArgs.size() == 1 && mi.getExpression() != null) { + ITypeBinding[] params = binding.getParameterTypes(); + return params.length == 1 && "java.lang.Object".equals(params[0].getQualifiedName()); + } + + return false; + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit4Example.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit4Example.java new file mode 100644 index 0000000..cafaa9b --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit4Example.java @@ -0,0 +1,92 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import java.util.Objects; +import org.junit.Assert; + +public class AssertTrueJUnit4Example { + + // assertTrue(a == null) → assertNull(a) + void trueNullCheckRight(Object a) { + Assert.assertTrue(a == null); + } + + // assertTrue(null == a) → assertNull(a) [null on the left] + void trueNullCheckLeft(Object a) { + Assert.assertTrue(null == a); + } + + // assertTrue(a != null) → assertNotNull(a) + void trueNotNullCheck(Object a) { + Assert.assertTrue(a != null); + } + + // assertFalse(a == null) → assertNotNull(a) + void falseNullCheck(Object a) { + Assert.assertFalse(a == null); + } + + // assertFalse(a != null) → assertNull(a) + void falseNotNullCheck(Object a) { + Assert.assertFalse(a != null); + } + + // assertTrue(a.equals(b)) → assertEquals(a, b) + void trueEqualsMethod(String a, String b) { + Assert.assertTrue(a.equals(b)); + } + + // assertFalse(a.equals(b)) → assertNotEquals(a, b) + void falseEqualsMethod(String a, String b) { + Assert.assertFalse(a.equals(b)); + } + + // assertTrue(Objects.equals(a, b)) → assertEquals(a, b) + void trueObjectsEquals(Object a, Object b) { + Assert.assertTrue(Objects.equals(a, b)); + } + + // assertTrue(a == b) with primitives → assertEquals(a, b) + void truePrimitiveEquals(int a, int b) { + Assert.assertTrue(a == b); + } + + // assertTrue(a != b) with primitives → assertNotEquals(a, b) + void truePrimitiveNotEquals(int a, int b) { + Assert.assertTrue(a != b); + } + + // assertTrue(a == b) with objects (reference equality) → assertSame(a, b) + void trueObjectSame(Object a, Object b) { + Assert.assertTrue(a == b); + } + + // assertTrue(a != b) with objects (reference inequality) → assertNotSame(a, b) + void trueObjectNotSame(Object a, Object b) { + Assert.assertTrue(a != b); + } + + // assertTrue(!a.equals(b)) → assertNotEquals(a, b) [NOT unwrapping] + void trueNegatedEquals(String a, String b) { + Assert.assertTrue(!a.equals(b)); + } + + // assertFalse(!a.equals(b)) → assertEquals(a, b) [double inversion] + void falseNegatedEquals(String a, String b) { + Assert.assertFalse(!a.equals(b)); + } + + // assertTrue with message (JUnit 4: message is first arg) → assertNull with message first + void trueWithMessage(Object a) { + Assert.assertTrue("should be null", a == null); + } + + // assertTrue equals with message → assertEquals with message first + void trueEqualsWithMessage(String a, String b) { + Assert.assertTrue("should be equal", a.equals(b)); + } + + // assertFalse equals with message → assertNotEquals with message first + void falseEqualsWithMessage(String a, String b) { + Assert.assertFalse("should not be equal", a.equals(b)); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit4ExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit4ExampleAfter.java new file mode 100644 index 0000000..b07854f --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit4ExampleAfter.java @@ -0,0 +1,91 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import org.junit.Assert; + +public class AssertTrueJUnit4ExampleAfter { + + // assertTrue(a == null) → assertNull(a) + void trueNullCheckRight(Object a) { + Assert.assertNull(a); + } + + // assertTrue(null == a) → assertNull(a) [null on the left] + void trueNullCheckLeft(Object a) { + Assert.assertNull(a); + } + + // assertTrue(a != null) → assertNotNull(a) + void trueNotNullCheck(Object a) { + Assert.assertNotNull(a); + } + + // assertFalse(a == null) → assertNotNull(a) + void falseNullCheck(Object a) { + Assert.assertNotNull(a); + } + + // assertFalse(a != null) → assertNull(a) + void falseNotNullCheck(Object a) { + Assert.assertNull(a); + } + + // assertTrue(a.equals(b)) → assertEquals(a, b) + void trueEqualsMethod(String a, String b) { + Assert.assertEquals(a, b); + } + + // assertFalse(a.equals(b)) → assertNotEquals(a, b) + void falseEqualsMethod(String a, String b) { + Assert.assertNotEquals(a, b); + } + + // assertTrue(Objects.equals(a, b)) → assertEquals(a, b) + void trueObjectsEquals(Object a, Object b) { + Assert.assertEquals(a, b); + } + + // assertTrue(a == b) with primitives → assertEquals(a, b) + void truePrimitiveEquals(int a, int b) { + Assert.assertEquals(a, b); + } + + // assertTrue(a != b) with primitives → assertNotEquals(a, b) + void truePrimitiveNotEquals(int a, int b) { + Assert.assertNotEquals(a, b); + } + + // assertTrue(a == b) with objects (reference equality) → assertSame(a, b) + void trueObjectSame(Object a, Object b) { + Assert.assertSame(a, b); + } + + // assertTrue(a != b) with objects (reference inequality) → assertNotSame(a, b) + void trueObjectNotSame(Object a, Object b) { + Assert.assertNotSame(a, b); + } + + // assertTrue(!a.equals(b)) → assertNotEquals(a, b) [NOT unwrapping] + void trueNegatedEquals(String a, String b) { + Assert.assertNotEquals(a, b); + } + + // assertFalse(!a.equals(b)) → assertEquals(a, b) [double inversion] + void falseNegatedEquals(String a, String b) { + Assert.assertEquals(a, b); + } + + // assertTrue with message (JUnit 4: message is first arg) → assertNull with message first + void trueWithMessage(Object a) { + Assert.assertNull("should be null", a); + } + + // assertTrue equals with message → assertEquals with message first + void trueEqualsWithMessage(String a, String b) { + Assert.assertEquals("should be equal", a, b); + } + + // assertFalse equals with message → assertNotEquals with message first + void falseEqualsWithMessage(String a, String b) { + Assert.assertNotEquals("should not be equal", a, b); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit5Example.txt b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit5Example.txt new file mode 100644 index 0000000..3e0c97d --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit5Example.txt @@ -0,0 +1,68 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import java.util.Objects; +import org.junit.jupiter.api.Assertions; + +public class AssertTrueJUnit5Example { + + // assertTrue(a == null) → assertNull(a) + void trueNullCheck(Object a) { + Assertions.assertTrue(a == null); + } + + // assertTrue(a != null) → assertNotNull(a) + void trueNotNullCheck(Object a) { + Assertions.assertTrue(a != null); + } + + // assertFalse(a == null) → assertNotNull(a) + void falseNullCheck(Object a) { + Assertions.assertFalse(a == null); + } + + // assertTrue(a.equals(b)) → assertEquals(a, b) + void trueEqualsMethod(String a, String b) { + Assertions.assertTrue(a.equals(b)); + } + + // assertFalse(a.equals(b)) → assertNotEquals(a, b) + void falseEqualsMethod(String a, String b) { + Assertions.assertFalse(a.equals(b)); + } + + // assertTrue(Objects.equals(a, b)) → assertEquals(a, b) + void trueObjectsEquals(Object a, Object b) { + Assertions.assertTrue(Objects.equals(a, b)); + } + + // assertTrue(a == b) with primitives → assertEquals(a, b) + void truePrimitiveEquals(int a, int b) { + Assertions.assertTrue(a == b); + } + + // assertTrue(a == b) with objects → assertSame(a, b) + void trueObjectSame(Object a, Object b) { + Assertions.assertTrue(a == b); + } + + // assertTrue(!a.equals(b)) → assertNotEquals(a, b) + void trueNegatedEquals(String a, String b) { + Assertions.assertTrue(!a.equals(b)); + } + + // JUnit 5: message is the LAST argument + // assertTrue(a == null, "msg") → assertNull(a, "msg") + void trueWithMessage(Object a) { + Assertions.assertTrue(a == null, "should be null"); + } + + // assertTrue(a.equals(b), "msg") → assertEquals(a, b, "msg") + void trueEqualsWithMessage(String a, String b) { + Assertions.assertTrue(a.equals(b), "should be equal"); + } + + // assertFalse(a.equals(b), "msg") → assertNotEquals(a, b, "msg") + void falseEqualsWithMessage(String a, String b) { + Assertions.assertFalse(a.equals(b), "should not be equal"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit5ExampleAfter.txt b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit5ExampleAfter.txt new file mode 100644 index 0000000..7f75719 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueJUnit5ExampleAfter.txt @@ -0,0 +1,67 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import org.junit.jupiter.api.Assertions; + +public class AssertTrueJUnit5ExampleAfter { + + // assertTrue(a == null) → assertNull(a) + void trueNullCheck(Object a) { + Assertions.assertNull(a); + } + + // assertTrue(a != null) → assertNotNull(a) + void trueNotNullCheck(Object a) { + Assertions.assertNotNull(a); + } + + // assertFalse(a == null) → assertNotNull(a) + void falseNullCheck(Object a) { + Assertions.assertNotNull(a); + } + + // assertTrue(a.equals(b)) → assertEquals(a, b) + void trueEqualsMethod(String a, String b) { + Assertions.assertEquals(a, b); + } + + // assertFalse(a.equals(b)) → assertNotEquals(a, b) + void falseEqualsMethod(String a, String b) { + Assertions.assertNotEquals(a, b); + } + + // assertTrue(Objects.equals(a, b)) → assertEquals(a, b) + void trueObjectsEquals(Object a, Object b) { + Assertions.assertEquals(a, b); + } + + // assertTrue(a == b) with primitives → assertEquals(a, b) + void truePrimitiveEquals(int a, int b) { + Assertions.assertEquals(a, b); + } + + // assertTrue(a == b) with objects → assertSame(a, b) + void trueObjectSame(Object a, Object b) { + Assertions.assertSame(a, b); + } + + // assertTrue(!a.equals(b)) → assertNotEquals(a, b) + void trueNegatedEquals(String a, String b) { + Assertions.assertNotEquals(a, b); + } + + // JUnit 5: message is the LAST argument + // assertTrue(a == null, "msg") → assertNull(a, "msg") + void trueWithMessage(Object a) { + Assertions.assertNull(a, "should be null"); + } + + // assertTrue(a.equals(b), "msg") → assertEquals(a, b, "msg") + void trueEqualsWithMessage(String a, String b) { + Assertions.assertEquals(a, b, "should be equal"); + } + + // assertFalse(a.equals(b), "msg") → assertNotEquals(a, b, "msg") + void falseEqualsWithMessage(String a, String b) { + Assertions.assertNotEquals(a, b, "should not be equal"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueNoopExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueNoopExample.java new file mode 100644 index 0000000..b379eab --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueNoopExample.java @@ -0,0 +1,36 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import org.junit.Assert; + +public class AssertTrueNoopExample { + + // Plain boolean variable — no dedicated assertion can be determined + void plainBoolean(boolean flag) { + Assert.assertTrue(flag); + } + + // Non-equals method call — not covered by the rule + void nonEqualsMethod(String s) { + Assert.assertTrue(s.isEmpty()); + } + + // Compound boolean expression (&&) — no dedicated assertion + void compoundAnd(boolean a, boolean b) { + Assert.assertTrue(a && b); + } + + // Compound boolean expression (||) — no dedicated assertion + void compoundOr(boolean a, boolean b) { + Assert.assertTrue(a || b); + } + + // assertFalse with plain boolean — no simplification + void plainBooleanFalse(boolean flag) { + Assert.assertFalse(flag); + } + + // assertFalse with non-equals method — no simplification + void nonEqualsMethodFalse(String s) { + Assert.assertFalse(s.isEmpty()); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueNoopExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueNoopExampleAfter.java new file mode 100644 index 0000000..23c9263 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueNoopExampleAfter.java @@ -0,0 +1,36 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import org.junit.Assert; + +public class AssertTrueNoopExampleAfter { + + // Plain boolean variable — no dedicated assertion can be determined + void plainBoolean(boolean flag) { + Assert.assertTrue(flag); + } + + // Non-equals method call — not covered by the rule + void nonEqualsMethod(String s) { + Assert.assertTrue(s.isEmpty()); + } + + // Compound boolean expression (&&) — no dedicated assertion + void compoundAnd(boolean a, boolean b) { + Assert.assertTrue(a && b); + } + + // Compound boolean expression (||) — no dedicated assertion + void compoundOr(boolean a, boolean b) { + Assert.assertTrue(a || b); + } + + // assertFalse with plain boolean — no simplification + void plainBooleanFalse(boolean flag) { + Assert.assertFalse(flag); + } + + // assertFalse with non-equals method — no simplification + void nonEqualsMethodFalse(String s) { + Assert.assertFalse(s.isEmpty()); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueStaticImportExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueStaticImportExample.java new file mode 100644 index 0000000..d12ec23 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueStaticImportExample.java @@ -0,0 +1,23 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import static org.junit.Assert.*; + +public class AssertTrueStaticImportExample { + + // Wildcard static import — no new import needed; just rename and rewrite args + void trueNullCheck(Object a) { + assertTrue(a == null); + } + + void falseNullCheck(Object a) { + assertFalse(a == null); + } + + void trueEqualsMethod(String a, String b) { + assertTrue(a.equals(b)); + } + + void truePrimitiveEquals(int a, int b) { + assertTrue(a == b); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueStaticImportExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueStaticImportExampleAfter.java new file mode 100644 index 0000000..0aa4c30 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/AssertTrueStaticImportExampleAfter.java @@ -0,0 +1,23 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import static org.junit.Assert.*; + +public class AssertTrueStaticImportExampleAfter { + + // Wildcard static import — no new import needed; just rename and rewrite args + void trueNullCheck(Object a) { + assertNull(a); + } + + void falseNullCheck(Object a) { + assertNotNull(a); + } + + void trueEqualsMethod(String a, String b) { + assertEquals(a, b); + } + + void truePrimitiveEquals(int a, int b) { + assertEquals(a, b); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/TestAssertTrueInsteadOfDedicatedAssertOperation.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/TestAssertTrueInsteadOfDedicatedAssertOperation.java new file mode 100644 index 0000000..920dd50 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s5785/TestAssertTrueInsteadOfDedicatedAssertOperation.java @@ -0,0 +1,68 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s5785; + +import java.nio.file.Paths; +import java.util.Set; + +import org.alfasoftware.astra.core.refactoring.AbstractRefactorTest; +import org.junit.Test; + +public class TestAssertTrueInsteadOfDedicatedAssertOperation extends AbstractRefactorTest { + + private static final String M2 = + Paths.get(System.getProperty("user.home"), ".m2", "repository").toString(); + + private static final String[] JUNIT4_CLASSPATH = { + Paths.get(M2, "junit", "junit", "4.13.2", "junit-4.13.2.jar").toString() + }; + + private static final String[] JUNIT5_CLASSPATH = { + Paths.get(M2, "org", "junit", "jupiter", "junit-jupiter-api", "5.10.2", "junit-jupiter-api-5.10.2.jar").toString(), + Paths.get(M2, "org", "opentest4j", "opentest4j", "1.3.0", "opentest4j-1.3.0.jar").toString() + }; + + private static final Set OPERATION = + Set.of(new AssertTrueInsteadOfDedicatedAssertOperation()); + + /** + * Core rewrites using qualified {@code Assert.*} calls (JUnit 4): + * null checks, equals method, Objects.equals, primitive comparison, + * object reference comparison, logical negation, and message argument. + */ + @Test + public void testJUnit4QualifiedCalls() { + assertRefactorWithClassPath(AssertTrueJUnit4Example.class, OPERATION, JUNIT4_CLASSPATH); + } + + /** + * Core rewrites using qualified {@code Assertions.*} calls (JUnit 5), + * including the JUnit-5 message-last convention. + * Uses a .txt source file because junit-jupiter-api is not a Maven test compile dependency. + */ + @Test + public void testJUnit5QualifiedCalls() { + assertRefactorWithSourcesAndClassPathAndTextFileExamples( + "org.alfasoftware.astra.core.refactoring.operations.sonar.s5785.AssertTrueJUnit5Example", + "AssertTrueJUnit5Example", + OPERATION, + new String[]{TEST_SOURCE}, + JUNIT5_CLASSPATH); + } + + /** + * Statically-imported calls with a wildcard import — method name and args are rewritten, + * no import changes needed. + */ + @Test + public void testStaticWildcardImport() { + assertRefactorWithClassPath(AssertTrueStaticImportExample.class, OPERATION, JUNIT4_CLASSPATH); + } + + /** + * Cases that must NOT be rewritten: + * plain boolean variables, non-equals method calls, compound boolean expressions. + */ + @Test + public void testNoopCases() { + assertRefactorWithClassPath(AssertTrueNoopExample.class, OPERATION, JUNIT4_CLASSPATH); + } +}