From 66a48bf4bbf68750c3a6ca5c6da28098c33cadf4 Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 11 Jun 2026 11:13:46 +0100 Subject: [PATCH] Add CollectionIsEmptyRefactor (java:S1155) Implements SonarQube rule java:S1155 - rewrites size comparisons on Collection types to use isEmpty()/!isEmpty() instead. Classes placed in the sonar.s1155 package convention: - CollectionIsEmptyRefactor (main operation) - TestCollectionIsEmptyRefactor and test fixtures Co-Authored-By: Claude Sonnet 4.6 --- .../s1155/CollectionIsEmptyRefactor.java | 168 ++++++++++++++++++ .../sonar/s1155/CollectionIsEmptyExample.java | 27 +++ .../s1155/CollectionIsEmptyExampleAfter.java | 27 +++ ...CollectionIsEmptyNonCollectionExample.java | 26 +++ ...ctionIsEmptyNonCollectionExampleAfter.java | 26 +++ .../sonar/s1155/NonCollectionWithSize.java | 6 + .../s1155/TestCollectionIsEmptyRefactor.java | 30 ++++ 7 files changed, 310 insertions(+) create mode 100644 astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyRefactor.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyNonCollectionExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyNonCollectionExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/NonCollectionWithSize.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/TestCollectionIsEmptyRefactor.java diff --git a/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyRefactor.java b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyRefactor.java new file mode 100644 index 0000000..6367493 --- /dev/null +++ b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyRefactor.java @@ -0,0 +1,168 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s1155; + +import java.io.IOException; + +import org.alfasoftware.astra.core.utils.ASTOperation; +import org.eclipse.jdt.core.dom.AST; +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.ITypeBinding; +import org.eclipse.jdt.core.dom.InfixExpression; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.NumberLiteral; +import org.eclipse.jdt.core.dom.PrefixExpression; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.text.edits.MalformedTreeException; + +/** + * Implements SonarQube rule java:S1155 — {@code Collection.isEmpty()} should be used to test for emptiness. + * + *

Replaces {@code size()} comparisons against 0 or 1 with the semantically equivalent + * {@code isEmpty()} call. Both operand orderings are supported. + * + *

+ *   col.size() == 0   →  col.isEmpty()
+ *   col.size() != 0   →  !col.isEmpty()
+ *   col.size() > 0    →  !col.isEmpty()
+ *   col.size() >= 1   →  !col.isEmpty()
+ *   col.size() < 1    →  col.isEmpty()
+ *   col.size() <= 0   →  col.isEmpty()
+ *   0 == col.size()   →  col.isEmpty()
+ *   0 != col.size()   →  !col.isEmpty()
+ * 
+ * + *

Binding resolution is used to confirm the receiver is a {@code java.util.Collection} subtype + * before rewriting. Expressions where the type cannot be resolved are left unchanged. + */ +public class CollectionIsEmptyRefactor implements ASTOperation { + + private static final String COLLECTION_FQN = "java.util.Collection"; + + @Override + public void run(CompilationUnit compilationUnit, ASTNode node, ASTRewrite rewriter) + throws IOException, MalformedTreeException, BadLocationException { + if (!(node instanceof InfixExpression)) { + return; + } + InfixExpression infix = (InfixExpression) node; + + // Extended operands occur on chains like a + b + c; comparisons never have them. + // Guard anyway to keep the logic below simple. + if (!infix.extendedOperands().isEmpty()) { + return; + } + + Expression left = infix.getLeftOperand(); + Expression right = infix.getRightOperand(); + InfixExpression.Operator op = infix.getOperator(); + + // size() on left, literal on right + if (isSizeInvocation(left) && isZeroOrOne(right)) { + Boolean negate = shouldNegate(op, literalValue(right), false); + if (negate != null && isCollectionReceiver((MethodInvocation) left)) { + rewrite(rewriter, infix, (MethodInvocation) left, negate); + } + return; + } + + // literal on left, size() on right (reversed operand ordering) + if (isZeroOrOne(left) && isSizeInvocation(right)) { + Boolean negate = shouldNegate(op, literalValue(left), true); + if (negate != null && isCollectionReceiver((MethodInvocation) right)) { + rewrite(rewriter, infix, (MethodInvocation) right, negate); + } + } + } + + + private boolean isSizeInvocation(Expression expr) { + if (!(expr instanceof MethodInvocation)) return false; + MethodInvocation mi = (MethodInvocation) expr; + return "size".equals(mi.getName().getIdentifier()) + && mi.arguments().isEmpty() + && mi.getExpression() != null; + } + + + private boolean isZeroOrOne(Expression expr) { + if (!(expr instanceof NumberLiteral)) return false; + String token = ((NumberLiteral) expr).getToken(); + return "0".equals(token) || "1".equals(token); + } + + + private int literalValue(Expression expr) { + return Integer.parseInt(((NumberLiteral) expr).getToken()); + } + + + /** + * Returns {@code true} when the replacement should be {@code !isEmpty()}, {@code false} for + * {@code isEmpty()}, or {@code null} when the operator/literal combination is not a recognised + * S1155 pattern. + * + * @param op the infix operator + * @param value the numeric literal value (0 or 1) + * @param sizeOnRight whether {@code size()} is the right operand (reversed ordering) + */ + private Boolean shouldNegate(InfixExpression.Operator op, int value, boolean sizeOnRight) { + InfixExpression.Operator normalized = sizeOnRight ? flip(op) : op; + + if (normalized == InfixExpression.Operator.EQUALS && value == 0) return false; + if (normalized == InfixExpression.Operator.NOT_EQUALS && value == 0) return true; + if (normalized == InfixExpression.Operator.GREATER && value == 0) return true; + if (normalized == InfixExpression.Operator.GREATER_EQUALS && value == 1) return true; + if (normalized == InfixExpression.Operator.LESS && value == 1) return false; + if (normalized == InfixExpression.Operator.LESS_EQUALS && value == 0) return false; + return null; + } + + + private InfixExpression.Operator flip(InfixExpression.Operator op) { + if (op == InfixExpression.Operator.GREATER) return InfixExpression.Operator.LESS; + if (op == InfixExpression.Operator.LESS) return InfixExpression.Operator.GREATER; + if (op == InfixExpression.Operator.GREATER_EQUALS) return InfixExpression.Operator.LESS_EQUALS; + if (op == InfixExpression.Operator.LESS_EQUALS) return InfixExpression.Operator.GREATER_EQUALS; + return op; // == and != are symmetric + } + + + private boolean isCollectionReceiver(MethodInvocation sizeCall) { + Expression receiver = sizeCall.getExpression(); + ITypeBinding binding = receiver.resolveTypeBinding(); + if (binding == null || binding.isRecovered()) { + return false; + } + return implementsCollection(binding); + } + + + private boolean implementsCollection(ITypeBinding binding) { + if (binding == null) return false; + if (COLLECTION_FQN.equals(binding.getErasure().getQualifiedName())) return true; + for (ITypeBinding iface : binding.getInterfaces()) { + if (implementsCollection(iface)) return true; + } + return implementsCollection(binding.getSuperclass()); + } + + + private void rewrite(ASTRewrite rewriter, InfixExpression infix, MethodInvocation sizeCall, boolean negate) { + AST ast = infix.getAST(); + + MethodInvocation isEmptyCall = ast.newMethodInvocation(); + isEmptyCall.setName(ast.newSimpleName("isEmpty")); + isEmptyCall.setExpression((Expression) ASTNode.copySubtree(ast, sizeCall.getExpression())); + + if (negate) { + PrefixExpression prefix = ast.newPrefixExpression(); + prefix.setOperator(PrefixExpression.Operator.NOT); + prefix.setOperand(isEmptyCall); + rewriter.replace(infix, prefix, null); + } else { + rewriter.replace(infix, isEmptyCall, null); + } + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyExample.java new file mode 100644 index 0000000..f789cdb --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyExample.java @@ -0,0 +1,27 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s1155; + +import java.util.ArrayList; +import java.util.List; + +public class CollectionIsEmptyExample { + + private final List list = new ArrayList<>(); + + void sizeOnLeft() { + boolean a = list.size() == 0; + boolean b = list.size() != 0; + boolean c = list.size() > 0; + boolean d = list.size() >= 1; + boolean e = list.size() < 1; + boolean f = list.size() <= 0; + } + + void reversedOperands() { + boolean a = 0 == list.size(); + boolean b = 0 != list.size(); + boolean c = 0 < list.size(); + boolean d = 1 <= list.size(); + boolean e = 1 > list.size(); + boolean f = 0 >= list.size(); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyExampleAfter.java new file mode 100644 index 0000000..27a3fb2 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyExampleAfter.java @@ -0,0 +1,27 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s1155; + +import java.util.ArrayList; +import java.util.List; + +public class CollectionIsEmptyExampleAfter { + + private final List list = new ArrayList<>(); + + void sizeOnLeft() { + boolean a = list.isEmpty(); + boolean b = ! list.isEmpty(); + boolean c = ! list.isEmpty(); + boolean d = ! list.isEmpty(); + boolean e = list.isEmpty(); + boolean f = list.isEmpty(); + } + + void reversedOperands() { + boolean a = list.isEmpty(); + boolean b = ! list.isEmpty(); + boolean c = ! list.isEmpty(); + boolean d = ! list.isEmpty(); + boolean e = list.isEmpty(); + boolean f = list.isEmpty(); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyNonCollectionExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyNonCollectionExample.java new file mode 100644 index 0000000..b4c9dcf --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyNonCollectionExample.java @@ -0,0 +1,26 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s1155; + +import java.util.ArrayList; +import java.util.List; + +public class CollectionIsEmptyNonCollectionExample { + + private final NonCollectionWithSize custom = new NonCollectionWithSize(); + private final List list = new ArrayList<>(); + + void nonCollectionShouldNotChange() { + boolean a = custom.size() == 0; + boolean b = custom.size() != 0; + boolean c = custom.size() > 0; + } + + void alreadyUsingIsEmpty() { + boolean a = list.isEmpty(); + boolean b = !list.isEmpty(); + } + + void unsupportedLiteralShouldNotChange() { + boolean a = list.size() == 2; + boolean b = list.size() > 5; + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyNonCollectionExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyNonCollectionExampleAfter.java new file mode 100644 index 0000000..4c7d095 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/CollectionIsEmptyNonCollectionExampleAfter.java @@ -0,0 +1,26 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s1155; + +import java.util.ArrayList; +import java.util.List; + +public class CollectionIsEmptyNonCollectionExampleAfter { + + private final NonCollectionWithSize custom = new NonCollectionWithSize(); + private final List list = new ArrayList<>(); + + void nonCollectionShouldNotChange() { + boolean a = custom.size() == 0; + boolean b = custom.size() != 0; + boolean c = custom.size() > 0; + } + + void alreadyUsingIsEmpty() { + boolean a = list.isEmpty(); + boolean b = !list.isEmpty(); + } + + void unsupportedLiteralShouldNotChange() { + boolean a = list.size() == 2; + boolean b = list.size() > 5; + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/NonCollectionWithSize.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/NonCollectionWithSize.java new file mode 100644 index 0000000..3480db6 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/NonCollectionWithSize.java @@ -0,0 +1,6 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s1155; + +/** Helper type used by tests — has {@code size()} but does NOT implement {@code java.util.Collection}. */ +class NonCollectionWithSize { + public int size() { return 0; } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/TestCollectionIsEmptyRefactor.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/TestCollectionIsEmptyRefactor.java new file mode 100644 index 0000000..b4382d2 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s1155/TestCollectionIsEmptyRefactor.java @@ -0,0 +1,30 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s1155; + +import java.util.Collections; + +import org.alfasoftware.astra.core.refactoring.AbstractRefactorTest; +import org.alfasoftware.astra.core.refactoring.operations.sonar.s1155.CollectionIsEmptyRefactor; +import org.junit.Test; + +public class TestCollectionIsEmptyRefactor extends AbstractRefactorTest { + + /** + * All six size()-comparison forms in both normal and reversed operand order + * are replaced with isEmpty() or !isEmpty() as appropriate. + */ + @Test + public void testAllForms() { + assertRefactor(CollectionIsEmptyExample.class, + Collections.singleton(new CollectionIsEmptyRefactor())); + } + + /** + * Non-Collection.size() calls, already-correct isEmpty() calls, and + * size() comparisons against literals other than 0/1 are left unchanged. + */ + @Test + public void testNoOpCases() { + assertRefactor(CollectionIsEmptyNonCollectionExample.class, + Collections.singleton(new CollectionIsEmptyRefactor())); + } +}