diff --git a/java-checks-test-sources/default/src/main/java/checks/BigDecimalEqualsCheckGuavaSample.java b/java-checks-test-sources/default/src/main/java/checks/BigDecimalEqualsCheckGuavaSample.java new file mode 100644 index 00000000000..2246839b5d9 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/BigDecimalEqualsCheckGuavaSample.java @@ -0,0 +1,34 @@ +package checks; + +import java.math.BigDecimal; + +class BigDecimalEqualsCheckGuavaSample { + + void method(BigDecimal a, BigDecimal b, Object o, String s) { + boolean res; + + res = com.google.common.base.Objects.equal(a, b); // Noncompliant [["BigDecimal.equals()" compares scale as well as value; use "compareTo() == 0" for numerical comparison.]] +// ^^^^^ + res = com.google.common.base.Objects.equal(a, o); // Noncompliant + res = com.google.common.base.Objects.equal(o, a); // Noncompliant + + // Compliant + res = com.google.common.base.Objects.equal(s, "hello"); + } + + static class AccountWithGuavaEquals { + private BigDecimal balance; + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof AccountWithGuavaEquals other)) return false; + return com.google.common.base.Objects.equal(balance, other.balance); // Compliant: inside equals method override + } + + @Override + public int hashCode() { + return com.google.common.base.Objects.hashCode(balance); + } + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/BigDecimalEqualsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/BigDecimalEqualsCheckSample.java new file mode 100644 index 00000000000..7d66cb41507 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/BigDecimalEqualsCheckSample.java @@ -0,0 +1,72 @@ +package checks; + +import java.math.BigDecimal; +import java.util.Objects; + +class BigDecimalEqualsCheckSample { + + void method(BigDecimal a, BigDecimal b, Object o, String s) { + boolean res; + + res = a.equals(b); // Noncompliant [["BigDecimal.equals()" compares scale as well as value; use "compareTo() == 0" for numerical comparison.]] +// ^^^^^^ + res = !a.equals(b); // Noncompliant +// ^^^^^^ + res = a.equals(o); // Noncompliant + + res = Objects.equals(a, b); // Noncompliant +// ^^^^^^ + res = Objects.equals(a, o); // Noncompliant + res = Objects.equals(o, a); // Noncompliant + + // Compliant + res = a.compareTo(b) == 0; + res = a.compareTo(b) != 0; + res = o.equals(a); + res = s.equals(a); + res = s.equals("hello"); + res = Objects.equals(s, "hello"); + } + + static class Account { + private BigDecimal balance; + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Account other)) return false; + return balance != null && balance.equals(other.balance); // Compliant: inside equals method override + } + + @Override + public int hashCode() { + return Objects.hashCode(balance); + } + } + + static class AccountWithStaticEquals { + private BigDecimal balance; + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof AccountWithStaticEquals other)) return false; + return Objects.equals(balance, other.balance); // Compliant: inside equals method override + } + + @Override + public int hashCode() { + return Objects.hashCode(balance); + } + } + + static class MyBigDecimal extends BigDecimal { + public MyBigDecimal(String val) { + super(val); + } + + void testCustom(MyBigDecimal other) { + boolean r = this.equals(other); // Noncompliant + } + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/BigDecimalEqualsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/BigDecimalEqualsCheck.java new file mode 100644 index 00000000000..5845603d28a --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/BigDecimalEqualsCheck.java @@ -0,0 +1,88 @@ +/* + * 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.Collections; +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.MethodTreeUtils; +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.Arguments; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S9351") +public class BigDecimalEqualsCheck extends IssuableSubscriptionVisitor { + + private static final String MESSAGE = "\"BigDecimal.equals()\" compares scale as well as value; use \"compareTo() == 0\" for numerical comparison."; + private static final String BIG_DECIMAL = "java.math.BigDecimal"; + private static final String JAVA_LANG_OBJECT = "java.lang.Object"; + + private static final MethodMatchers INSTANCE_EQUALS = MethodMatchers.create() + .ofSubTypes(BIG_DECIMAL) + .names("equals") + .addParametersMatcher(JAVA_LANG_OBJECT) + .build(); + + private static final MethodMatchers STATIC_EQUALS = MethodMatchers.create() + .ofTypes("java.util.Objects", "com.google.common.base.Objects") + .names("equals", "equal") + .addParametersMatcher(JAVA_LANG_OBJECT, JAVA_LANG_OBJECT) + .build(); + + @Override + public List nodesToVisit() { + return Collections.singletonList(Tree.Kind.METHOD_INVOCATION); + } + + @Override + public void visitNode(Tree tree) { + MethodInvocationTree mit = (MethodInvocationTree) tree; + if (isInsideEqualsMethod(mit)) { + return; + } + if (INSTANCE_EQUALS.matches(mit)) { + reportIssue(ExpressionUtils.methodName(mit), MESSAGE); + } else if (STATIC_EQUALS.matches(mit)) { + Arguments arguments = mit.arguments(); + Type firstType = arguments.get(0).symbolType(); + Type secondType = arguments.get(1).symbolType(); + if (isBigDecimal(firstType) || isBigDecimal(secondType)) { + reportIssue(ExpressionUtils.methodName(mit), MESSAGE); + } + } + } + + private static boolean isBigDecimal(Type type) { + return !type.isUnknown() && type.isSubtypeOf(BIG_DECIMAL); + } + + private static boolean isInsideEqualsMethod(Tree tree) { + Tree parent = tree.parent(); + while (parent != null && !parent.is(Tree.Kind.CLASS, Tree.Kind.RECORD, Tree.Kind.INTERFACE, Tree.Kind.ENUM)) { + if (parent.is(Tree.Kind.METHOD)) { + return MethodTreeUtils.isEqualsMethod((MethodTree) parent); + } + parent = parent.parent(); + } + return false; + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/BigDecimalEqualsCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/BigDecimalEqualsCheckTest.java new file mode 100644 index 00000000000..8ac02419b2c --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/BigDecimalEqualsCheckTest.java @@ -0,0 +1,59 @@ +/* + * 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 BigDecimalEqualsCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/BigDecimalEqualsCheckSample.java")) + .withCheck(new BigDecimalEqualsCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/BigDecimalEqualsCheckSample.java")) + .withCheck(new BigDecimalEqualsCheck()) + .withoutSemantic() + .verifyIssues(); + } + + @Test + void test_guava() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/BigDecimalEqualsCheckGuavaSample.java")) + .withCheck(new BigDecimalEqualsCheck()) + .verifyIssues(); + } + + @Test + void test_guava_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/BigDecimalEqualsCheckGuavaSample.java")) + .withCheck(new BigDecimalEqualsCheck()) + .withoutSemantic() + .verifyNoIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9351.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9351.html new file mode 100644 index 00000000000..00efb7cc1a5 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9351.html @@ -0,0 +1,42 @@ +

BigDecimal.equals(Object) compares both the numerical value and the scale of the BigDecimal objects.

+

Why is this an issue?

+

In Java, BigDecimal.equals(Object) returns true only if two BigDecimal objects have the same numerical value +and the same scale (number of digits to the right of the decimal point). Consequently, new BigDecimal("2.0").equals(new +BigDecimal("2.00")) evaluates to false even though both instances represent the same numerical value.

+

In financial, commerce, and scientific applications, comparisons usually intend to verify numerical equivalence regardless of representation +differences. Using equals() or Objects.equals() can introduce subtle bugs when values are formatted, serialized, or computed +with different scale factors. To compare BigDecimal values for numerical equality, use compareTo(other) == 0 instead.

+

Code examples

+

Noncompliant code example

+
+BigDecimal priceA = new BigDecimal("10.0");
+BigDecimal priceB = new BigDecimal("10.00");
+
+if (priceA.equals(priceB)) { // Noncompliant: "BigDecimal.equals()" compares scale as well as value; use "compareTo() == 0" for numerical comparison
+  applyDiscount();
+}
+
+

Compliant solution

+
+BigDecimal priceA = new BigDecimal("10.0");
+BigDecimal priceB = new BigDecimal("10.00");
+
+if (priceA.compareTo(priceB) == 0) {
+  applyDiscount();
+}
+
+

Exceptions

+

This rule ignores BigDecimal.equals() calls inside equals(Object) method declarations. Classes implementing +equals(Object) and hashCode() often require scale-sensitive comparison to satisfy the hashCode contract.

+

If scale-sensitive equality is intentionally desired in other contexts, suppress the issue with an inline // NOSONAR comment and +provide a short rationale explaining why scale sensitivity is necessary (e.g., // NOSONAR: scale-sensitive comparison is +intentional).

+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9351.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9351.json new file mode 100644 index 00000000000..7d40226e1d8 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9351.json @@ -0,0 +1,24 @@ +{ + "title": "\"BigDecimal.compareTo()\" should be used instead of \"equals()\" for numerical comparison", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "unpredictable", + "bad-practice" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9351", + "sqKey": "S9351", + "scope": "Main", + "quickfix": "infeasible", + "code": { + "impacts": { + "RELIABILITY": "HIGH" + }, + "attribute": "LOGICAL" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9351 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9351 new file mode 100644 index 00000000000..e69de29bb2d