From 1348b60f48b040daee6c7b7e693e62bc38bda20f Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 11:47:33 +0200 Subject: [PATCH 1/9] SONARJAVA-6780: Implement S9354: Comparable.compareTo() and Comparator.compare() should not use subtraction on numerical fields --- ...gerSubtractionInComparisonCheckSample.java | 276 ++++++++++++++++++ .../IntegerSubtractionInComparisonCheck.java | 139 +++++++++ ...tegerSubtractionInComparisonCheckTest.java | 43 +++ .../org/sonar/l10n/java/rules/java/S9354.html | 104 +++++++ .../org/sonar/l10n/java/rules/java/S9354.json | 23 ++ .../main/resources/profiles/Sonar_way/S9354 | 0 6 files changed, 585 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9354 diff --git a/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java new file mode 100644 index 00000000000..f06de5d25e7 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java @@ -0,0 +1,276 @@ +package checks; + +import java.io.File; +import java.util.Comparator; +import java.util.List; +import java.util.function.IntSupplier; + +class IntegerSubtractionInComparisonCheckSample { + + static class TimestampedEvent implements Comparable { + private long timestamp; + + @Override + public int compareTo(TimestampedEvent other) { + return (int) (this.timestamp - other.timestamp); // Noncompliant {{Subtracting numeric values in compareTo can overflow; use Long.compare instead.}} + } + } + + static class IntHolder implements Comparable { + private int value; + + @Override + public int compareTo(IntHolder other) { + return this.value - other.value; // Noncompliant {{Subtracting numeric values in compareTo can overflow; use Integer.compare instead.}} + } + } + + static class BoxedIntHolder implements Comparable { + private Integer value; + + @Override + public int compareTo(BoxedIntHolder other) { + return this.value - other.value; // Noncompliant {{Subtracting numeric values in compareTo can overflow; use Integer.compare instead.}} + } + } + + static class BoxedLongHolder implements Comparable { + private Long value; + + @Override + public int compareTo(BoxedLongHolder other) { + return (int) (this.value - other.value); // Noncompliant {{Subtracting numeric values in compareTo can overflow; use Long.compare instead.}} + } + } + + static class MixedOperands implements Comparable { + private long longValue; + private int intValue; + + @Override + public int compareTo(MixedOperands other) { + return (int) (this.longValue - other.intValue); // Noncompliant {{Subtracting numeric values in compareTo can overflow; use Long.compare instead.}} + } + } + + static class IntermediateDiff implements Comparable { + private int age; + + @Override + public int compareTo(IntermediateDiff other) { + int diff = this.age - other.age; // Noncompliant {{Subtracting numeric values in compareTo can overflow; use Integer.compare instead.}} + if (diff != 0) { + return diff; + } + return 0; + } + } + + static class HashCodeCompare implements Comparable { + @Override + public int compareTo(HashCodeCompare other) { + return this.hashCode() - other.hashCode(); // Noncompliant {{Subtracting numeric values in compareTo can overflow; use Integer.compare instead.}} + } + } + + static class AgeComparator implements Comparator { + @Override + public int compare(IntHolder left, IntHolder right) { + return left.value - right.value; // Noncompliant {{Subtracting numeric values in compare can overflow; use Integer.compare instead.}} + } + } + + static class LongArrayComparator implements Comparator { + @Override + public int compare(long[] a, long[] b) { + return (int) (a[0] - b[0]); // Noncompliant {{Subtracting numeric values in compare can overflow; use Long.compare instead.}} + } + } + + static final Comparator COMPARATOR_UNBOXED_INT_CAST = new Comparator() { + @Override + public int compare(Number n1, Number n2) { + return (int) (n1.longValue() - n2.longValue()); // Noncompliant {{Subtracting numeric values in compare can overflow; use Long.compare instead.}} + } + }; + + static final Comparator COMPARATOR_BOXED_INT_CAST = new Comparator() { + @Override + public int compare(Long n1, Long n2) { + return (int) (n1 - n2); // Noncompliant {{Subtracting numeric values in compare can overflow; use Long.compare instead.}} + } + }; + + static final Comparator COMPARATOR_FILE_INT_CAST = new Comparator() { + @Override + public int compare(File lhs, File rhs) { + return (int) (rhs.lastModified() - lhs.lastModified()); // Noncompliant {{Subtracting numeric values in compare can overflow; use Long.compare instead.}} + } + }; + + void lambdaSubtraction(List list, List events) { + list.sort((a, b) -> a.value - b.value); // Noncompliant {{Subtracting numeric values in compare can overflow; use Integer.compare instead.}} + events.sort((left, right) -> (int) (left.timestamp - right.timestamp)); // Noncompliant {{Subtracting numeric values in compare can overflow; use Long.compare instead.}} + } + + static class CorrectLongCompareTo implements Comparable { + private long timestamp; + + @Override + public int compareTo(CorrectLongCompareTo other) { + return Long.compare(this.timestamp, other.timestamp); // Compliant + } + } + + static class CorrectIntCompareTo implements Comparable { + private int value; + + @Override + public int compareTo(CorrectIntCompareTo other) { + return Integer.compare(this.value, other.value); // Compliant + } + } + + static class CorrectBoxedLongCompareTo implements Comparable { + private Long value; + + @Override + public int compareTo(CorrectBoxedLongCompareTo other) { + return value.compareTo(other.value); // Compliant + } + } + + static class IntegerRelational implements Comparable { + private int value; + + @Override + public int compareTo(IntegerRelational other) { + if (this.value < other.value) { // Compliant + return -1; + } + return this.value > other.value ? 1 : 0; // Compliant + } + } + + static class FloatingPointCompareTo implements Comparable { + private double latitude; + + @Override + public int compareTo(FloatingPointCompareTo other) { + return (int) (this.latitude - other.latitude); // Compliant - handled by S9148 + } + } + + static class ByteCompareTo implements Comparable { + private byte value; + + @Override + public int compareTo(ByteCompareTo other) { + return this.value - other.value; // Compliant - difference fits in int + } + } + + static class ShortCompareTo implements Comparable { + private short value; + + @Override + public int compareTo(ShortCompareTo other) { + return this.value - other.value; // Compliant - difference fits in int + } + } + + static class CharCompareTo implements Comparable { + private char value; + + @Override + public int compareTo(CharCompareTo other) { + return this.value - other.value; // Compliant - difference fits in int + } + } + + static class BoxedShortCompareTo implements Comparable { + private Short value; + + @Override + public int compareTo(BoxedShortCompareTo other) { + return this.value - other.value; // Compliant - difference fits in int + } + } + + int subtract(int a, int b) { + return a - b; // Compliant - not in a comparison method + } + + static class DoubleUtils { + int compare(int a, int b) { + return a - b; // Compliant - not in a Comparator + } + } + + static class NonComparableTest { + private final long value = 0; + + public int compareTo(NonComparableTest other) { + return (int) (this.value - other.value); // Compliant - class is not Comparable + } + } + + static final Object COMPARATOR_LIKE_INT_CAST = new Object() { + public int compare(Long n1, Long n2) { + return (int) (n1 - n2); // Compliant - not a Comparator + } + }; + + static class LookAlikeMethods { + double compareTo(Object other) { + return other.hashCode() - 1; // Compliant - does not return an int + } + + int compareTo(Object a, Object b) { + return a.hashCode() - b.hashCode(); // Compliant - compareTo takes exactly one parameter + } + + int compare(IntHolder a) { + return a.value - 1; // Compliant - compare takes exactly two parameters + } + } + + static class NestedLambda implements Comparable { + private int value; + + @Override + public int compareTo(NestedLambda other) { + IntSupplier difference = () -> this.value - other.value; // Compliant - not a Comparator + return Integer.compare(difference.getAsInt(), 0); // Compliant + } + } + + static class LocalClassInCompareTo implements Comparable { + private int value; + + @Override + public int compareTo(LocalClassInCompareTo other) { + class Difference { + int between(int a, int b) { + return a - b; // Compliant - not in a comparison method + } + } + return Integer.compare(new Difference().between(this.value, other.value), 0); // Compliant + } + } + + interface CustomComparable { + int compareTo(T other); // Compliant - abstract method, no body + } + + abstract static class AbstractIntComparator implements Comparator { + @Override + public abstract int compare(Integer a, Integer b); // Compliant - abstract method, no body + } + + void lambdaCorrect(List list, List events) { + list.sort((a, b) -> Integer.compare(a.value, b.value)); // Compliant + events.sort((left, right) -> Long.compare(left.timestamp, right.timestamp)); // Compliant + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java new file mode 100644 index 00000000000..c1c5314e543 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java @@ -0,0 +1,139 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import java.util.Arrays; +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.BinaryExpressionTree; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.LambdaExpressionTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S9354") +public class IntegerSubtractionInComparisonCheck extends IssuableSubscriptionVisitor { + + private static final String MESSAGE = "Subtracting numeric values in %s can overflow; use %s instead."; + + private static final MethodMatchers COMPARE_METHODS = MethodMatchers.or( + MethodMatchers.create() + .ofSubTypes("java.lang.Comparable") + .names("compareTo") + .addParametersMatcher(MethodMatchers.ANY) + .build(), + MethodMatchers.create() + .ofSubTypes("java.util.Comparator") + .names("compare") + .addParametersMatcher(MethodMatchers.ANY, MethodMatchers.ANY) + .build()); + + @Override + public List nodesToVisit() { + return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); + } + + @Override + public void visitNode(Tree tree) { + if (context.getSemanticModel() == null) { + return; + } + if (tree.is(Tree.Kind.METHOD)) { + MethodTree methodTree = (MethodTree) tree; + if (COMPARE_METHODS.matches(methodTree) && methodTree.block() != null) { + methodTree.block().accept(new SubtractionInComparisonVisitor(methodTree.simpleName().name())); + } + } else { + LambdaExpressionTree lambda = (LambdaExpressionTree) tree; + if (lambda.symbolType().isSubtypeOf("java.util.Comparator")) { + lambda.body().accept(new SubtractionInComparisonVisitor("compare")); + } + } + } + + private class SubtractionInComparisonVisitor extends BaseTreeVisitor { + + private final String enclosingMethodName; + + private SubtractionInComparisonVisitor(String enclosingMethodName) { + this.enclosingMethodName = enclosingMethodName; + } + + @Override + public void visitBinaryExpression(BinaryExpressionTree tree) { + if (tree.is(Tree.Kind.MINUS)) { + String replacement = replacementFor(tree); + if (replacement != null) { + reportIssue(tree.operatorToken(), String.format(MESSAGE, enclosingMethodName, replacement)); + } + } + super.visitBinaryExpression(tree); + } + + @Override + public void visitClass(ClassTree tree) { + // Do not visit inner classes + } + + @Override + public void visitLambdaExpression(LambdaExpressionTree tree) { + // Do not visit nested lambdas + } + } + + private static String replacementFor(BinaryExpressionTree tree) { + Type left = primitiveOrSelf(tree.leftOperand()); + Type right = primitiveOrSelf(tree.rightOperand()); + if (left.isUnknown() || right.isUnknown() || isFloating(left) || isFloating(right)) { + return null; + } + if (isLong(left) || isLong(right)) { + return "Long.compare"; + } + if (isInt(left) || isInt(right)) { + return "Integer.compare"; + } + return null; + } + + private static Type primitiveOrSelf(ExpressionTree tree) { + Type type = tree.symbolType(); + if (type.isPrimitive()) { + return type; + } + Type primitive = type.primitiveType(); + return primitive != null ? primitive : type; + } + + private static boolean isFloating(Type type) { + return type.isPrimitive(Type.Primitives.FLOAT) || type.isPrimitive(Type.Primitives.DOUBLE); + } + + private static boolean isLong(Type type) { + return type.isPrimitive(Type.Primitives.LONG); + } + + private static boolean isInt(Type type) { + return type.isPrimitive(Type.Primitives.INT); + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheckTest.java new file mode 100644 index 00000000000..d11b3e54905 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheckTest.java @@ -0,0 +1,43 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class IntegerSubtractionInComparisonCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/IntegerSubtractionInComparisonCheckSample.java")) + .withCheck(new IntegerSubtractionInComparisonCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/IntegerSubtractionInComparisonCheckSample.java")) + .withCheck(new IntegerSubtractionInComparisonCheck()) + .withoutSemantic() + .verifyNoIssues(); + } + +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.html new file mode 100644 index 00000000000..9bc09eb5407 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.html @@ -0,0 +1,104 @@ +

This rule raises an issue when compareTo() or compare() orders values by subtracting integers. Integer subtraction can +overflow and reverse the comparison result.

+

Why is this an issue?

+

Comparable.compareTo() and Comparator.compare() must return a negative integer, zero, or a positive integer as the first +value is less than, equal to, or greater than the second. The magnitude of that result does not matter, only its sign.

+

A common shortcut is to return the difference of two numeric fields:

+
+return this.value - other.value;
+
+

Java integer operators wrap on overflow instead of throwing. When the true difference is larger than Integer.MAX_VALUE or smaller than +Integer.MIN_VALUE, the wrapped result can have the wrong sign. For example, (int) (1_000_000_000L - (-1_500_000_000L)) is +negative even though the first value is greater.

+

The same problem appears when a long difference is narrowed to int:

+
+return (int) (this.timestamp - other.timestamp);
+
+

That broken ordering violates the comparison contract. Sorting with Arrays.sort() or Collections.sort() can throw +IllegalArgumentException: Comparison method violates its general contract!. Ordered collections such as TreeSet and +TreeMap can become corrupted.

+

Use Integer.compare() for int values and Long.compare() for long values. Those methods compare +without computing a difference that can overflow.

+

This rule does not flag floating-point subtraction in ordering methods. See {rule:java:S9148}.

+

How to fix it

+

Replace the subtraction with Integer.compare() or Long.compare(), matching the operand type.

+

Code examples

+

Noncompliant code example

+
+class TimestampedEvent implements Comparable<TimestampedEvent> {
+  private long timestamp;
+
+  @Override
+  public int compareTo(TimestampedEvent other) {
+    return (int) (this.timestamp - other.timestamp); // Noncompliant
+  }
+}
+
+

Compliant solution

+
+class TimestampedEvent implements Comparable<TimestampedEvent> {
+  private long timestamp;
+
+  @Override
+  public int compareTo(TimestampedEvent other) {
+    return Long.compare(this.timestamp, other.timestamp);
+  }
+}
+
+

Noncompliant code example

+
+import java.util.Comparator;
+
+class AgeComparator implements Comparator<Person> {
+  @Override
+  public int compare(Person left, Person right) {
+    return left.age - right.age; // Noncompliant
+  }
+}
+
+

Compliant solution

+
+import java.util.Comparator;
+
+class AgeComparator implements Comparator<Person> {
+  @Override
+  public int compare(Person left, Person right) {
+    return Integer.compare(left.age, right.age);
+  }
+}
+
+

Noncompliant code example

+
+import java.util.List;
+
+void sortById(List<Event> events) {
+  events.sort((left, right) -> (int) (left.id - right.id)); // Noncompliant
+}
+
+

Compliant solution

+
+import java.util.List;
+
+void sortById(List<Event> events) {
+  events.sort((left, right) -> Long.compare(left.id, right.id));
+}
+
+

Resources

+

Documentation

+ +

Related rules

+
    +
  • {rule:java:S9148} - "Float.compare" or "Double.compare" should be used for floating-point comparisons
  • +
+ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.json new file mode 100644 index 00000000000..db6f076798a --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.json @@ -0,0 +1,23 @@ +{ + "title": "\"Comparable.compareTo()\" and \"Comparator.compare()\" should not use subtraction on numerical fields", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "pitfall" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9354", + "sqKey": "S9354", + "scope": "All", + "quickfix": "targeted", + "code": { + "impacts": { + "RELIABILITY": "HIGH" + }, + "attribute": "LOGICAL" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9354 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9354 new file mode 100644 index 00000000000..e69de29bb2d From 50af88dfc7e41447b5d45d78556cd6a78fc8e5f6 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 12:00:47 +0200 Subject: [PATCH 2/9] Report S9354 only when subtraction is the comparison result --- ...gerSubtractionInComparisonCheckSample.java | 20 +++++++- .../IntegerSubtractionInComparisonCheck.java | 51 +++++++++++++++---- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java index f06de5d25e7..3eeb715417c 100644 --- a/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java @@ -13,6 +13,7 @@ static class TimestampedEvent implements Comparable { @Override public int compareTo(TimestampedEvent other) { return (int) (this.timestamp - other.timestamp); // Noncompliant {{Subtracting numeric values in compareTo can overflow; use Long.compare instead.}} +// ^ } } @@ -53,14 +54,29 @@ public int compareTo(MixedOperands other) { } } + static class IndexArithmetic implements Comparable { + private int[] parts; + + @Override + public int compareTo(IndexArithmetic other) { + for (int i = 0; i < parts.length - 1; i++) { // Compliant - subtraction is not the comparison result + int cmp = Integer.compare(this.parts[i], other.parts[i]); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + } + static class IntermediateDiff implements Comparable { private int age; @Override public int compareTo(IntermediateDiff other) { - int diff = this.age - other.age; // Noncompliant {{Subtracting numeric values in compareTo can overflow; use Integer.compare instead.}} + int diff = this.age - other.age; // Compliant - only a returned subtraction is reported if (diff != 0) { - return diff; + return Integer.compare(this.age, other.age); } return 0; } diff --git a/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java index c1c5314e543..815910a1b79 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.List; import org.sonar.check.Rule; +import org.sonar.java.model.ExpressionUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.semantic.Type; @@ -28,7 +29,9 @@ import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.LambdaExpressionTree; import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.ReturnStatementTree; import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.TypeCastTree; @Rule(key = "S9354") public class IntegerSubtractionInComparisonCheck extends IssuableSubscriptionVisitor { @@ -60,33 +63,47 @@ public void visitNode(Tree tree) { if (tree.is(Tree.Kind.METHOD)) { MethodTree methodTree = (MethodTree) tree; if (COMPARE_METHODS.matches(methodTree) && methodTree.block() != null) { - methodTree.block().accept(new SubtractionInComparisonVisitor(methodTree.simpleName().name())); + methodTree.block().accept(new ComparisonResultVisitor(methodTree.simpleName().name())); } } else { LambdaExpressionTree lambda = (LambdaExpressionTree) tree; if (lambda.symbolType().isSubtypeOf("java.util.Comparator")) { - lambda.body().accept(new SubtractionInComparisonVisitor("compare")); + Tree body = lambda.body(); + ComparisonResultVisitor visitor = new ComparisonResultVisitor("compare"); + if (body.is(Tree.Kind.BLOCK)) { + body.accept(visitor); + } else { + visitor.checkComparisonResult((ExpressionTree) body); + } } } } - private class SubtractionInComparisonVisitor extends BaseTreeVisitor { + private class ComparisonResultVisitor extends BaseTreeVisitor { private final String enclosingMethodName; - private SubtractionInComparisonVisitor(String enclosingMethodName) { + private ComparisonResultVisitor(String enclosingMethodName) { this.enclosingMethodName = enclosingMethodName; } @Override - public void visitBinaryExpression(BinaryExpressionTree tree) { - if (tree.is(Tree.Kind.MINUS)) { - String replacement = replacementFor(tree); - if (replacement != null) { - reportIssue(tree.operatorToken(), String.format(MESSAGE, enclosingMethodName, replacement)); - } + public void visitReturnStatement(ReturnStatementTree tree) { + ExpressionTree expression = tree.expression(); + if (expression != null) { + checkComparisonResult(expression); + } + } + + private void checkComparisonResult(ExpressionTree expression) { + ExpressionTree unwrapped = skipParenthesesAndIntCasts(expression); + if (!unwrapped.is(Tree.Kind.MINUS)) { + return; + } + String replacement = replacementFor((BinaryExpressionTree) unwrapped); + if (replacement != null) { + reportIssue(((BinaryExpressionTree) unwrapped).operatorToken(), String.format(MESSAGE, enclosingMethodName, replacement)); } - super.visitBinaryExpression(tree); } @Override @@ -100,6 +117,18 @@ public void visitLambdaExpression(LambdaExpressionTree tree) { } } + private static ExpressionTree skipParenthesesAndIntCasts(ExpressionTree expression) { + ExpressionTree current = ExpressionUtils.skipParentheses(expression); + while (current.is(Tree.Kind.TYPE_CAST)) { + TypeCastTree cast = (TypeCastTree) current; + if (!cast.type().symbolType().isPrimitive(Type.Primitives.INT)) { + return current; + } + current = ExpressionUtils.skipParentheses(cast.expression()); + } + return current; + } + private static String replacementFor(BinaryExpressionTree tree) { Type left = primitiveOrSelf(tree.leftOperand()); Type right = primitiveOrSelf(tree.rightOperand()); From 3f657c0fe4a287543def94a9327964501ca48d17 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:14:43 +0200 Subject: [PATCH 3/9] Update ruling results for PR #5959 (#5961) Co-authored-by: github-actions[bot] --- .../resources/eclipse-jetty/java-S9354.json | 10 +++++ .../src/test/resources/guava/java-S9354.json | 40 +++++++++++++++++++ .../resources/sonar-server/java-S9354.json | 9 +++++ 3 files changed, 59 insertions(+) create mode 100644 its/ruling/src/test/resources/eclipse-jetty/java-S9354.json create mode 100644 its/ruling/src/test/resources/guava/java-S9354.json create mode 100644 its/ruling/src/test/resources/sonar-server/java-S9354.json diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S9354.json b/its/ruling/src/test/resources/eclipse-jetty/java-S9354.json new file mode 100644 index 00000000000..7be73e7eac9 --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S9354.json @@ -0,0 +1,10 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/AbstractPathSpec.java": [ +29, +34 +], +"org.eclipse.jetty:jetty-project:jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java": [ +119, +119 +] +} diff --git a/its/ruling/src/test/resources/guava/java-S9354.json b/its/ruling/src/test/resources/guava/java-S9354.json new file mode 100644 index 00000000000..5bd8aea52dc --- /dev/null +++ b/its/ruling/src/test/resources/guava/java-S9354.json @@ -0,0 +1,40 @@ +{ +"com.google.guava:guava:src/com/google/common/collect/ExplicitOrdering.java": [ +41 +], +"com.google.guava:guava:src/com/google/common/primitives/Booleans.java": [ +297 +], +"com.google.guava:guava:src/com/google/common/primitives/Chars.java": [ +414 +], +"com.google.guava:guava:src/com/google/common/primitives/Doubles.java": [ +401 +], +"com.google.guava:guava:src/com/google/common/primitives/Floats.java": [ +397 +], +"com.google.guava:guava:src/com/google/common/primitives/Ints.java": [ +462 +], +"com.google.guava:guava:src/com/google/common/primitives/Longs.java": [ +498 +], +"com.google.guava:guava:src/com/google/common/primitives/Shorts.java": [ +461 +], +"com.google.guava:guava:src/com/google/common/primitives/SignedBytes.java": [ +202 +], +"com.google.guava:guava:src/com/google/common/primitives/UnsignedBytes.java": [ +409, +420, +436 +], +"com.google.guava:guava:src/com/google/common/primitives/UnsignedInts.java": [ +176 +], +"com.google.guava:guava:src/com/google/common/primitives/UnsignedLongs.java": [ +177 +] +} diff --git a/its/ruling/src/test/resources/sonar-server/java-S9354.json b/its/ruling/src/test/resources/sonar-server/java-S9354.json new file mode 100644 index 00000000000..e6ce043e109 --- /dev/null +++ b/its/ruling/src/test/resources/sonar-server/java-S9354.json @@ -0,0 +1,9 @@ +{ +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/duplication/Duplication.java": [ +119 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/duplication/TextBlock.java": [ +59, +61 +] +} From f1cfc2bc58142d25048e47555fbc5e7586fb5317 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:26:46 +0200 Subject: [PATCH 4/9] Update ruling results for PR #5959 (#5963) Co-authored-by: github-actions[bot] --- its/ruling/src/test/resources/sonar-server/java-S9354.json | 1 - 1 file changed, 1 deletion(-) diff --git a/its/ruling/src/test/resources/sonar-server/java-S9354.json b/its/ruling/src/test/resources/sonar-server/java-S9354.json index e6ce043e109..81d1a692495 100644 --- a/its/ruling/src/test/resources/sonar-server/java-S9354.json +++ b/its/ruling/src/test/resources/sonar-server/java-S9354.json @@ -3,7 +3,6 @@ 119 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/duplication/TextBlock.java": [ -59, 61 ] } From c7a8e12fdc4562ea262c6c46401a0be7cab12d1e Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 13:48:18 +0200 Subject: [PATCH 5/9] fix-ci: drop stale eclipse-jetty S9354 ruling expectations Ruling QA failed because java-S9354.json still expected issues from the first visitor that flagged every minus in compareTo. After reporting only when subtraction is the comparison result, AbstractPathSpec's intermediate diff and XmlConfiguration's index arithmetic are compliant. --- .../src/test/resources/eclipse-jetty/java-S9354.json | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 its/ruling/src/test/resources/eclipse-jetty/java-S9354.json diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S9354.json b/its/ruling/src/test/resources/eclipse-jetty/java-S9354.json deleted file mode 100644 index 7be73e7eac9..00000000000 --- a/its/ruling/src/test/resources/eclipse-jetty/java-S9354.json +++ /dev/null @@ -1,10 +0,0 @@ -{ -"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/AbstractPathSpec.java": [ -29, -34 -], -"org.eclipse.jetty:jetty-project:jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java": [ -119, -119 -] -} From e7a669b1efab3749a2017fcd1ed5a1a61dc357b5 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 14:08:49 +0200 Subject: [PATCH 6/9] fix-ci: share comparison-method visitor to pass the quality gate SonarQube Code Analysis failed on 12.7% duplication with S9148 and 89.6% coverage on new code. Extract the shared Comparable/Comparator dispatch and add a block-lambda case so S9354 is no longer a near-copy of S9148. --- ...gerSubtractionInComparisonCheckSample.java | 12 +++ .../checks/AbstractComparisonMethodCheck.java | 84 +++++++++++++++++++ .../checks/FloatingPointComparisonCheck.java | 53 ++---------- .../IntegerSubtractionInComparisonCheck.java | 61 +++----------- 4 files changed, 112 insertions(+), 98 deletions(-) create mode 100644 java-checks/src/main/java/org/sonar/java/checks/AbstractComparisonMethodCheck.java diff --git a/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java index 3eeb715417c..dc7a7f46c3d 100644 --- a/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/IntegerSubtractionInComparisonCheckSample.java @@ -89,6 +89,15 @@ public int compareTo(HashCodeCompare other) { } } + static class NonIntCastCompareTo implements Comparable { + private int value; + + @Override + public int compareTo(NonIntCastCompareTo other) { + return (int) (short) (this.value - other.value); // Compliant - the subtraction is wrapped in a non-int cast + } + } + static class AgeComparator implements Comparator { @Override public int compare(IntHolder left, IntHolder right) { @@ -127,6 +136,9 @@ public int compare(File lhs, File rhs) { void lambdaSubtraction(List list, List events) { list.sort((a, b) -> a.value - b.value); // Noncompliant {{Subtracting numeric values in compare can overflow; use Integer.compare instead.}} events.sort((left, right) -> (int) (left.timestamp - right.timestamp)); // Noncompliant {{Subtracting numeric values in compare can overflow; use Long.compare instead.}} + list.sort((a, b) -> { + return a.value - b.value; // Noncompliant {{Subtracting numeric values in compare can overflow; use Integer.compare instead.}} + }); } static class CorrectLongCompareTo implements Comparable { diff --git a/java-checks/src/main/java/org/sonar/java/checks/AbstractComparisonMethodCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AbstractComparisonMethodCheck.java new file mode 100644 index 00000000000..748397edc9e --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/AbstractComparisonMethodCheck.java @@ -0,0 +1,84 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import java.util.Arrays; +import java.util.List; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.LambdaExpressionTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; + +/** + * Shared entry point for checks that inspect {@code Comparable.compareTo} and {@code Comparator.compare}. + */ +abstract class AbstractComparisonMethodCheck extends IssuableSubscriptionVisitor { + + private static final MethodMatchers COMPARE_METHODS = MethodMatchers.or( + MethodMatchers.create() + .ofSubTypes("java.lang.Comparable") + .names("compareTo") + .addParametersMatcher(MethodMatchers.ANY) + .build(), + MethodMatchers.create() + .ofSubTypes("java.util.Comparator") + .names("compare") + .addParametersMatcher(MethodMatchers.ANY, MethodMatchers.ANY) + .build()); + + @Override + public List nodesToVisit() { + return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); + } + + @Override + public void visitNode(Tree tree) { + if (context.getSemanticModel() == null) { + return; + } + if (tree.is(Tree.Kind.METHOD)) { + MethodTree methodTree = (MethodTree) tree; + if (COMPARE_METHODS.matches(methodTree) && methodTree.block() != null) { + visitComparisonMethod(methodTree); + } + } else { + LambdaExpressionTree lambda = (LambdaExpressionTree) tree; + if (lambda.symbolType().isSubtypeOf("java.util.Comparator")) { + visitComparatorLambda(lambda); + } + } + } + + abstract void visitComparisonMethod(MethodTree methodTree); + + abstract void visitComparatorLambda(LambdaExpressionTree lambda); + + abstract static class IgnoreNestedTypesVisitor extends BaseTreeVisitor { + @Override + public void visitClass(ClassTree tree) { + // Do not visit inner classes + } + + @Override + public void visitLambdaExpression(LambdaExpressionTree tree) { + // Do not visit nested lambdas + } + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java b/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java index 236eee80506..872eff15f6e 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java @@ -16,58 +16,27 @@ */ package org.sonar.java.checks; -import java.util.Arrays; -import java.util.List; import org.sonar.check.Rule; -import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; -import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.semantic.Type; -import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; -import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.LambdaExpressionTree; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; @Rule(key = "S9148") -public class FloatingPointComparisonCheck extends IssuableSubscriptionVisitor { +public class FloatingPointComparisonCheck extends AbstractComparisonMethodCheck { private static final String MESSAGE = "Use \"Double.compare\" or \"Float.compare\" to compare floating-point values."; - private static final MethodMatchers COMPARE_METHODS = MethodMatchers.or( - MethodMatchers.create() - .ofSubTypes("java.lang.Comparable") - .names("compareTo") - .addParametersMatcher(MethodMatchers.ANY) - .build(), - MethodMatchers.create() - .ofSubTypes("java.util.Comparator") - .names("compare") - .addParametersMatcher(MethodMatchers.ANY, MethodMatchers.ANY) - .build()); - @Override - public List nodesToVisit() { - return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); + void visitComparisonMethod(MethodTree methodTree) { + methodTree.block().accept(new FloatingPointComparisonVisitor()); } @Override - public void visitNode(Tree tree) { - if (context.getSemanticModel() == null) { - return; - } - if (tree.is(Tree.Kind.METHOD)) { - MethodTree methodTree = (MethodTree) tree; - if (COMPARE_METHODS.matches(methodTree) && methodTree.block() != null) { - methodTree.block().accept(new FloatingPointComparisonVisitor()); - } - } else { - LambdaExpressionTree lambda = (LambdaExpressionTree) tree; - if (lambda.symbolType().isSubtypeOf("java.util.Comparator")) { - lambda.body().accept(new FloatingPointComparisonVisitor()); - } - } + void visitComparatorLambda(LambdaExpressionTree lambda) { + lambda.body().accept(new FloatingPointComparisonVisitor()); } private static boolean hasFloatingType(ExpressionTree tree) { @@ -75,7 +44,7 @@ private static boolean hasFloatingType(ExpressionTree tree) { || tree.symbolType().isPrimitive(Type.Primitives.DOUBLE); } - private class FloatingPointComparisonVisitor extends BaseTreeVisitor { + private class FloatingPointComparisonVisitor extends IgnoreNestedTypesVisitor { @Override public void visitBinaryExpression(BinaryExpressionTree tree) { @@ -90,15 +59,5 @@ && hasFloatingOperand(tree)) { private boolean hasFloatingOperand(BinaryExpressionTree tree) { return hasFloatingType(tree.leftOperand()) || hasFloatingType(tree.rightOperand()); } - - @Override - public void visitClass(ClassTree tree) { - // Do not visit inner classes - } - - @Override - public void visitLambdaExpression(LambdaExpressionTree tree) { - // Do not visit nested lambdas - } } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java index 815910a1b79..334610fc36c 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java @@ -16,16 +16,10 @@ */ package org.sonar.java.checks; -import java.util.Arrays; -import java.util.List; import org.sonar.check.Rule; import org.sonar.java.model.ExpressionUtils; -import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; -import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.semantic.Type; -import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; -import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.LambdaExpressionTree; import org.sonar.plugins.java.api.tree.MethodTree; @@ -34,52 +28,27 @@ import org.sonar.plugins.java.api.tree.TypeCastTree; @Rule(key = "S9354") -public class IntegerSubtractionInComparisonCheck extends IssuableSubscriptionVisitor { +public class IntegerSubtractionInComparisonCheck extends AbstractComparisonMethodCheck { private static final String MESSAGE = "Subtracting numeric values in %s can overflow; use %s instead."; - private static final MethodMatchers COMPARE_METHODS = MethodMatchers.or( - MethodMatchers.create() - .ofSubTypes("java.lang.Comparable") - .names("compareTo") - .addParametersMatcher(MethodMatchers.ANY) - .build(), - MethodMatchers.create() - .ofSubTypes("java.util.Comparator") - .names("compare") - .addParametersMatcher(MethodMatchers.ANY, MethodMatchers.ANY) - .build()); - @Override - public List nodesToVisit() { - return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); + void visitComparisonMethod(MethodTree methodTree) { + methodTree.block().accept(new ComparisonResultVisitor(methodTree.simpleName().name())); } @Override - public void visitNode(Tree tree) { - if (context.getSemanticModel() == null) { - return; - } - if (tree.is(Tree.Kind.METHOD)) { - MethodTree methodTree = (MethodTree) tree; - if (COMPARE_METHODS.matches(methodTree) && methodTree.block() != null) { - methodTree.block().accept(new ComparisonResultVisitor(methodTree.simpleName().name())); - } + void visitComparatorLambda(LambdaExpressionTree lambda) { + Tree body = lambda.body(); + ComparisonResultVisitor visitor = new ComparisonResultVisitor("compare"); + if (body.is(Tree.Kind.BLOCK)) { + body.accept(visitor); } else { - LambdaExpressionTree lambda = (LambdaExpressionTree) tree; - if (lambda.symbolType().isSubtypeOf("java.util.Comparator")) { - Tree body = lambda.body(); - ComparisonResultVisitor visitor = new ComparisonResultVisitor("compare"); - if (body.is(Tree.Kind.BLOCK)) { - body.accept(visitor); - } else { - visitor.checkComparisonResult((ExpressionTree) body); - } - } + visitor.checkComparisonResult((ExpressionTree) body); } } - private class ComparisonResultVisitor extends BaseTreeVisitor { + private class ComparisonResultVisitor extends IgnoreNestedTypesVisitor { private final String enclosingMethodName; @@ -105,16 +74,6 @@ private void checkComparisonResult(ExpressionTree expression) { reportIssue(((BinaryExpressionTree) unwrapped).operatorToken(), String.format(MESSAGE, enclosingMethodName, replacement)); } } - - @Override - public void visitClass(ClassTree tree) { - // Do not visit inner classes - } - - @Override - public void visitLambdaExpression(LambdaExpressionTree tree) { - // Do not visit nested lambdas - } } private static ExpressionTree skipParenthesesAndIntCasts(ExpressionTree expression) { From cd96dbad9ce1e61a1239400b912aad77665ee8c0 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 14:27:39 +0200 Subject: [PATCH 7/9] fix-ci: exclude AbstractComparisonMethodCheck from the generated check list count GeneratedCheckListTest.count failed because every *Check.java is expected to be a registered rule. The new shared comparison-method base is abstract and has no @Rule, matching the existing AbstractRegexCheck blacklist. --- .../java/org/sonar/plugins/java/GeneratedCheckListTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java index 7e982e7672a..a06517f17a3 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java @@ -50,7 +50,8 @@ class GeneratedCheckListTest { "AbstractXPathBasedCheck.java", "AbstractWebXmlXPathBasedCheck.java", "AbstractRedosCheck.java", - "AbstractRegexCheck.java"); + "AbstractRegexCheck.java", + "AbstractComparisonMethodCheck.java"); /** * Enforces that each check declared in list. From 66c7c48b2d7354aeb8559dbdfff77e07448631bf Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 16:23:26 +0200 Subject: [PATCH 8/9] Address review: drop shared abstract check and align S9354 impact. S9148 and S9354 now share only ComparisonMethodUtils instead of an abstract check class. GeneratedCheckListTest no longer needs a blacklist entry, and RELIABILITY is MEDIUM to match Major severity. --- .../checks/AbstractComparisonMethodCheck.java | 84 ------------------- .../checks/FloatingPointComparisonCheck.java | 41 +++++++-- .../IntegerSubtractionInComparisonCheck.java | 49 ++++++++--- .../checks/helpers/ComparisonMethodUtils.java | 47 +++++++++++ .../org/sonar/l10n/java/rules/java/S9354.json | 2 +- .../plugins/java/GeneratedCheckListTest.java | 3 +- 6 files changed, 123 insertions(+), 103 deletions(-) delete mode 100644 java-checks/src/main/java/org/sonar/java/checks/AbstractComparisonMethodCheck.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/helpers/ComparisonMethodUtils.java diff --git a/java-checks/src/main/java/org/sonar/java/checks/AbstractComparisonMethodCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AbstractComparisonMethodCheck.java deleted file mode 100644 index 748397edc9e..00000000000 --- a/java-checks/src/main/java/org/sonar/java/checks/AbstractComparisonMethodCheck.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SonarQube Java - * Copyright (C) SonarSource Sàrl - * mailto:info AT sonarsource DOT com - * - * You can redistribute and/or modify this program under the terms of - * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the Sonar Source-Available License for more details. - * - * You should have received a copy of the Sonar Source-Available License - * along with this program; if not, see https://sonarsource.com/license/ssal/ - */ -package org.sonar.java.checks; - -import java.util.Arrays; -import java.util.List; -import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; -import org.sonar.plugins.java.api.semantic.MethodMatchers; -import org.sonar.plugins.java.api.tree.BaseTreeVisitor; -import org.sonar.plugins.java.api.tree.ClassTree; -import org.sonar.plugins.java.api.tree.LambdaExpressionTree; -import org.sonar.plugins.java.api.tree.MethodTree; -import org.sonar.plugins.java.api.tree.Tree; - -/** - * Shared entry point for checks that inspect {@code Comparable.compareTo} and {@code Comparator.compare}. - */ -abstract class AbstractComparisonMethodCheck extends IssuableSubscriptionVisitor { - - private static final MethodMatchers COMPARE_METHODS = MethodMatchers.or( - MethodMatchers.create() - .ofSubTypes("java.lang.Comparable") - .names("compareTo") - .addParametersMatcher(MethodMatchers.ANY) - .build(), - MethodMatchers.create() - .ofSubTypes("java.util.Comparator") - .names("compare") - .addParametersMatcher(MethodMatchers.ANY, MethodMatchers.ANY) - .build()); - - @Override - public List nodesToVisit() { - return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); - } - - @Override - public void visitNode(Tree tree) { - if (context.getSemanticModel() == null) { - return; - } - if (tree.is(Tree.Kind.METHOD)) { - MethodTree methodTree = (MethodTree) tree; - if (COMPARE_METHODS.matches(methodTree) && methodTree.block() != null) { - visitComparisonMethod(methodTree); - } - } else { - LambdaExpressionTree lambda = (LambdaExpressionTree) tree; - if (lambda.symbolType().isSubtypeOf("java.util.Comparator")) { - visitComparatorLambda(lambda); - } - } - } - - abstract void visitComparisonMethod(MethodTree methodTree); - - abstract void visitComparatorLambda(LambdaExpressionTree lambda); - - abstract static class IgnoreNestedTypesVisitor extends BaseTreeVisitor { - @Override - public void visitClass(ClassTree tree) { - // Do not visit inner classes - } - - @Override - public void visitLambdaExpression(LambdaExpressionTree tree) { - // Do not visit nested lambdas - } - } -} diff --git a/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java b/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java index 872eff15f6e..846d0bfd4f9 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java @@ -16,27 +16,46 @@ */ package org.sonar.java.checks; +import java.util.Arrays; +import java.util.List; import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.ComparisonMethodUtils; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; +import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.LambdaExpressionTree; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; @Rule(key = "S9148") -public class FloatingPointComparisonCheck extends AbstractComparisonMethodCheck { +public class FloatingPointComparisonCheck extends IssuableSubscriptionVisitor { private static final String MESSAGE = "Use \"Double.compare\" or \"Float.compare\" to compare floating-point values."; @Override - void visitComparisonMethod(MethodTree methodTree) { - methodTree.block().accept(new FloatingPointComparisonVisitor()); + public List nodesToVisit() { + return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); } @Override - void visitComparatorLambda(LambdaExpressionTree lambda) { - lambda.body().accept(new FloatingPointComparisonVisitor()); + public void visitNode(Tree tree) { + if (context.getSemanticModel() == null) { + return; + } + if (tree.is(Tree.Kind.METHOD)) { + MethodTree methodTree = (MethodTree) tree; + if (ComparisonMethodUtils.isCompareMethod(methodTree)) { + methodTree.block().accept(new FloatingPointComparisonVisitor()); + } + } else { + LambdaExpressionTree lambda = (LambdaExpressionTree) tree; + if (ComparisonMethodUtils.isComparatorLambda(lambda)) { + lambda.body().accept(new FloatingPointComparisonVisitor()); + } + } } private static boolean hasFloatingType(ExpressionTree tree) { @@ -44,7 +63,7 @@ private static boolean hasFloatingType(ExpressionTree tree) { || tree.symbolType().isPrimitive(Type.Primitives.DOUBLE); } - private class FloatingPointComparisonVisitor extends IgnoreNestedTypesVisitor { + private class FloatingPointComparisonVisitor extends BaseTreeVisitor { @Override public void visitBinaryExpression(BinaryExpressionTree tree) { @@ -59,5 +78,15 @@ && hasFloatingOperand(tree)) { private boolean hasFloatingOperand(BinaryExpressionTree tree) { return hasFloatingType(tree.leftOperand()) || hasFloatingType(tree.rightOperand()); } + + @Override + public void visitClass(ClassTree tree) { + // Do not visit inner classes + } + + @Override + public void visitLambdaExpression(LambdaExpressionTree tree) { + // Do not visit nested lambdas + } } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java index 334610fc36c..311f6d3d9a9 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java @@ -16,10 +16,16 @@ */ package org.sonar.java.checks; +import java.util.Arrays; +import java.util.List; import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.ComparisonMethodUtils; import org.sonar.java.model.ExpressionUtils; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; +import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.LambdaExpressionTree; import org.sonar.plugins.java.api.tree.MethodTree; @@ -28,27 +34,40 @@ import org.sonar.plugins.java.api.tree.TypeCastTree; @Rule(key = "S9354") -public class IntegerSubtractionInComparisonCheck extends AbstractComparisonMethodCheck { +public class IntegerSubtractionInComparisonCheck extends IssuableSubscriptionVisitor { private static final String MESSAGE = "Subtracting numeric values in %s can overflow; use %s instead."; @Override - void visitComparisonMethod(MethodTree methodTree) { - methodTree.block().accept(new ComparisonResultVisitor(methodTree.simpleName().name())); + public List nodesToVisit() { + return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); } @Override - void visitComparatorLambda(LambdaExpressionTree lambda) { - Tree body = lambda.body(); - ComparisonResultVisitor visitor = new ComparisonResultVisitor("compare"); - if (body.is(Tree.Kind.BLOCK)) { - body.accept(visitor); + public void visitNode(Tree tree) { + if (context.getSemanticModel() == null) { + return; + } + if (tree.is(Tree.Kind.METHOD)) { + MethodTree methodTree = (MethodTree) tree; + if (ComparisonMethodUtils.isCompareMethod(methodTree)) { + methodTree.block().accept(new ComparisonResultVisitor(methodTree.simpleName().name())); + } } else { - visitor.checkComparisonResult((ExpressionTree) body); + LambdaExpressionTree lambda = (LambdaExpressionTree) tree; + if (ComparisonMethodUtils.isComparatorLambda(lambda)) { + Tree body = lambda.body(); + ComparisonResultVisitor visitor = new ComparisonResultVisitor("compare"); + if (body.is(Tree.Kind.BLOCK)) { + body.accept(visitor); + } else { + visitor.checkComparisonResult((ExpressionTree) body); + } + } } } - private class ComparisonResultVisitor extends IgnoreNestedTypesVisitor { + private class ComparisonResultVisitor extends BaseTreeVisitor { private final String enclosingMethodName; @@ -74,6 +93,16 @@ private void checkComparisonResult(ExpressionTree expression) { reportIssue(((BinaryExpressionTree) unwrapped).operatorToken(), String.format(MESSAGE, enclosingMethodName, replacement)); } } + + @Override + public void visitClass(ClassTree tree) { + // Do not visit inner classes + } + + @Override + public void visitLambdaExpression(LambdaExpressionTree tree) { + // Do not visit nested lambdas + } } private static ExpressionTree skipParenthesesAndIntCasts(ExpressionTree expression) { diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/ComparisonMethodUtils.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/ComparisonMethodUtils.java new file mode 100644 index 00000000000..8fdb78af024 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/ComparisonMethodUtils.java @@ -0,0 +1,47 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.helpers; + +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.LambdaExpressionTree; +import org.sonar.plugins.java.api.tree.MethodTree; + +public final class ComparisonMethodUtils { + + private static final MethodMatchers COMPARE_METHODS = MethodMatchers.or( + MethodMatchers.create() + .ofSubTypes("java.lang.Comparable") + .names("compareTo") + .addParametersMatcher(MethodMatchers.ANY) + .build(), + MethodMatchers.create() + .ofSubTypes("java.util.Comparator") + .names("compare") + .addParametersMatcher(MethodMatchers.ANY, MethodMatchers.ANY) + .build()); + + private ComparisonMethodUtils() { + } + + public static boolean isCompareMethod(MethodTree methodTree) { + return methodTree.block() != null && COMPARE_METHODS.matches(methodTree); + } + + public static boolean isComparatorLambda(LambdaExpressionTree lambda) { + return lambda.symbolType().isSubtypeOf("java.util.Comparator"); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.json index db6f076798a..08a41d76adf 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9354.json @@ -16,7 +16,7 @@ "quickfix": "targeted", "code": { "impacts": { - "RELIABILITY": "HIGH" + "RELIABILITY": "MEDIUM" }, "attribute": "LOGICAL" } diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java index a06517f17a3..7e982e7672a 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java @@ -50,8 +50,7 @@ class GeneratedCheckListTest { "AbstractXPathBasedCheck.java", "AbstractWebXmlXPathBasedCheck.java", "AbstractRedosCheck.java", - "AbstractRegexCheck.java", - "AbstractComparisonMethodCheck.java"); + "AbstractRegexCheck.java"); /** * Enforces that each check declared in list. From f7c914af56deda695f68b6d29e4992e88f9a18a9 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 16:50:24 +0200 Subject: [PATCH 9/9] fix-ci: extract comparison dispatch into utilities to cut duplication The quality gate failed on 6.3% duplicated new code (limit 3%) because S9148 and S9354 still shared the same visitNode shape after dropping the abstract check class. ComparisonMethodUtils now owns that dispatch. --- .../checks/FloatingPointComparisonCheck.java | 36 +++------------- .../IntegerSubtractionInComparisonCheck.java | 36 +++------------- .../checks/helpers/ComparisonMethodUtils.java | 41 +++++++++++++++++++ 3 files changed, 52 insertions(+), 61 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java b/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java index 846d0bfd4f9..2d597c3559e 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/FloatingPointComparisonCheck.java @@ -16,18 +16,13 @@ */ package org.sonar.java.checks; -import java.util.Arrays; import java.util.List; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.ComparisonMethodUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.semantic.Type; -import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; -import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.ExpressionTree; -import org.sonar.plugins.java.api.tree.LambdaExpressionTree; -import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; @Rule(key = "S9148") @@ -37,25 +32,14 @@ public class FloatingPointComparisonCheck extends IssuableSubscriptionVisitor { @Override public List nodesToVisit() { - return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); + return ComparisonMethodUtils.nodesToVisit(); } @Override public void visitNode(Tree tree) { - if (context.getSemanticModel() == null) { - return; - } - if (tree.is(Tree.Kind.METHOD)) { - MethodTree methodTree = (MethodTree) tree; - if (ComparisonMethodUtils.isCompareMethod(methodTree)) { - methodTree.block().accept(new FloatingPointComparisonVisitor()); - } - } else { - LambdaExpressionTree lambda = (LambdaExpressionTree) tree; - if (ComparisonMethodUtils.isComparatorLambda(lambda)) { - lambda.body().accept(new FloatingPointComparisonVisitor()); - } - } + ComparisonMethodUtils.visitComparisonNode(context, tree, + methodTree -> methodTree.block().accept(new FloatingPointComparisonVisitor()), + lambda -> lambda.body().accept(new FloatingPointComparisonVisitor())); } private static boolean hasFloatingType(ExpressionTree tree) { @@ -63,7 +47,7 @@ private static boolean hasFloatingType(ExpressionTree tree) { || tree.symbolType().isPrimitive(Type.Primitives.DOUBLE); } - private class FloatingPointComparisonVisitor extends BaseTreeVisitor { + private class FloatingPointComparisonVisitor extends ComparisonMethodUtils.SkipNestedTypesVisitor { @Override public void visitBinaryExpression(BinaryExpressionTree tree) { @@ -78,15 +62,5 @@ && hasFloatingOperand(tree)) { private boolean hasFloatingOperand(BinaryExpressionTree tree) { return hasFloatingType(tree.leftOperand()) || hasFloatingType(tree.rightOperand()); } - - @Override - public void visitClass(ClassTree tree) { - // Do not visit inner classes - } - - @Override - public void visitLambdaExpression(LambdaExpressionTree tree) { - // Do not visit nested lambdas - } } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java index 311f6d3d9a9..951742668a8 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IntegerSubtractionInComparisonCheck.java @@ -16,19 +16,14 @@ */ package org.sonar.java.checks; -import java.util.Arrays; import java.util.List; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.ComparisonMethodUtils; import org.sonar.java.model.ExpressionUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.semantic.Type; -import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; -import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.ExpressionTree; -import org.sonar.plugins.java.api.tree.LambdaExpressionTree; -import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.ReturnStatementTree; import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.TypeCastTree; @@ -40,22 +35,14 @@ public class IntegerSubtractionInComparisonCheck extends IssuableSubscriptionVis @Override public List nodesToVisit() { - return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); + return ComparisonMethodUtils.nodesToVisit(); } @Override public void visitNode(Tree tree) { - if (context.getSemanticModel() == null) { - return; - } - if (tree.is(Tree.Kind.METHOD)) { - MethodTree methodTree = (MethodTree) tree; - if (ComparisonMethodUtils.isCompareMethod(methodTree)) { - methodTree.block().accept(new ComparisonResultVisitor(methodTree.simpleName().name())); - } - } else { - LambdaExpressionTree lambda = (LambdaExpressionTree) tree; - if (ComparisonMethodUtils.isComparatorLambda(lambda)) { + ComparisonMethodUtils.visitComparisonNode(context, tree, + methodTree -> methodTree.block().accept(new ComparisonResultVisitor(methodTree.simpleName().name())), + lambda -> { Tree body = lambda.body(); ComparisonResultVisitor visitor = new ComparisonResultVisitor("compare"); if (body.is(Tree.Kind.BLOCK)) { @@ -63,11 +50,10 @@ public void visitNode(Tree tree) { } else { visitor.checkComparisonResult((ExpressionTree) body); } - } - } + }); } - private class ComparisonResultVisitor extends BaseTreeVisitor { + private class ComparisonResultVisitor extends ComparisonMethodUtils.SkipNestedTypesVisitor { private final String enclosingMethodName; @@ -93,16 +79,6 @@ private void checkComparisonResult(ExpressionTree expression) { reportIssue(((BinaryExpressionTree) unwrapped).operatorToken(), String.format(MESSAGE, enclosingMethodName, replacement)); } } - - @Override - public void visitClass(ClassTree tree) { - // Do not visit inner classes - } - - @Override - public void visitLambdaExpression(LambdaExpressionTree tree) { - // Do not visit nested lambdas - } } private static ExpressionTree skipParenthesesAndIntCasts(ExpressionTree expression) { diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/ComparisonMethodUtils.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/ComparisonMethodUtils.java index 8fdb78af024..4716de263e2 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/helpers/ComparisonMethodUtils.java +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/ComparisonMethodUtils.java @@ -16,9 +16,16 @@ */ package org.sonar.java.checks.helpers; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.LambdaExpressionTree; import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; public final class ComparisonMethodUtils { @@ -37,6 +44,28 @@ public final class ComparisonMethodUtils { private ComparisonMethodUtils() { } + public static List nodesToVisit() { + return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION); + } + + public static void visitComparisonNode(JavaFileScannerContext context, Tree tree, + Consumer onCompareMethod, Consumer onComparatorLambda) { + if (context.getSemanticModel() == null) { + return; + } + if (tree.is(Tree.Kind.METHOD)) { + MethodTree methodTree = (MethodTree) tree; + if (isCompareMethod(methodTree)) { + onCompareMethod.accept(methodTree); + } + } else { + LambdaExpressionTree lambda = (LambdaExpressionTree) tree; + if (isComparatorLambda(lambda)) { + onComparatorLambda.accept(lambda); + } + } + } + public static boolean isCompareMethod(MethodTree methodTree) { return methodTree.block() != null && COMPARE_METHODS.matches(methodTree); } @@ -44,4 +73,16 @@ public static boolean isCompareMethod(MethodTree methodTree) { public static boolean isComparatorLambda(LambdaExpressionTree lambda) { return lambda.symbolType().isSubtypeOf("java.util.Comparator"); } + + public static class SkipNestedTypesVisitor extends BaseTreeVisitor { + @Override + public void visitClass(ClassTree tree) { + // Do not visit inner classes + } + + @Override + public void visitLambdaExpression(LambdaExpressionTree tree) { + // Do not visit nested lambdas + } + } }