diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8346.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8346.json new file mode 100644 index 0000000000..ceab99e5fc --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8346.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S8346", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} diff --git a/its/ruling/src/test/resources/commons-beanutils/java-S8346.json b/its/ruling/src/test/resources/commons-beanutils/java-S8346.json new file mode 100644 index 0000000000..5aed9739c9 --- /dev/null +++ b/its/ruling/src/test/resources/commons-beanutils/java-S8346.json @@ -0,0 +1,5 @@ +{ +"commons-beanutils:commons-beanutils:src/main/java/org/apache/commons/beanutils2/MethodUtils.java": [ +1136 +] +} diff --git a/its/ruling/src/test/resources/sonar-server/java-S8346.json b/its/ruling/src/test/resources/sonar-server/java-S8346.json new file mode 100644 index 0000000000..a226609f00 --- /dev/null +++ b/its/ruling/src/test/resources/sonar-server/java-S8346.json @@ -0,0 +1,5 @@ +{ +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java": [ +85 +] +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/IncDecOnFloatingPointCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IncDecOnFloatingPointCheck.java new file mode 100644 index 0000000000..34963b7c3f --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/IncDecOnFloatingPointCheck.java @@ -0,0 +1,71 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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.List; +import java.util.Optional; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.semantic.Type.Primitives; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.UnaryExpressionTree; + +@Rule(key = "S8346") +public class IncDecOnFloatingPointCheck extends IssuableSubscriptionVisitor { + + @Override + public List nodesToVisit() { + return List.of( + Tree.Kind.POSTFIX_INCREMENT, + Tree.Kind.PREFIX_INCREMENT, + Tree.Kind.POSTFIX_DECREMENT, + Tree.Kind.PREFIX_DECREMENT + ); + } + + @Override + public void visitNode(Tree tree) { + Optional.of(tree) + .filter(UnaryExpressionTree.class::isInstance) + .map(UnaryExpressionTree.class::cast) + .filter(unaryExpr -> + unaryExpr.expression() instanceof IdentifierTree identifierTree + && isFloatingPoint(identifierTree.symbolType()) + ) + .ifPresent(unaryExpr -> reportIssue( + unaryExpr, + "%s operator (%s) should not be used with floating point variables".formatted( + isIncrement(unaryExpr) ? "Increment" : "Decrement", + unaryExpr.operatorToken().text() + ) + )); + } + + + private static boolean isFloatingPoint(Type type) { + return type.isPrimitive(Primitives.FLOAT) || type.isPrimitive(Primitives.DOUBLE); + } + + private static boolean isIncrement(UnaryExpressionTree unaryExp) { + return unaryExp.is( + Tree.Kind.PREFIX_INCREMENT, + Tree.Kind.POSTFIX_INCREMENT + ); + } +} diff --git a/java-checks/src/test/files/checks/IncDecOnFloatingPointCheck.java b/java-checks/src/test/files/checks/IncDecOnFloatingPointCheck.java new file mode 100644 index 0000000000..e3d0c18a21 --- /dev/null +++ b/java-checks/src/test/files/checks/IncDecOnFloatingPointCheck.java @@ -0,0 +1,100 @@ +class A { + void specNonCompliantExamples() { + for (float i = 16_000_000; i < 17_000_000; i++) { // Noncompliant {{Increment operator (++) should not be used with floating point variables}} +// ^^^ + // ... + } + + + float x = 0f; + double y = 1.0; + + x++; // Noncompliant {{Increment operator (++) should not be used with floating point variables}} +// ^^^ + y--; // Noncompliant {{Decrement operator (--) should not be used with floating point variables}} +// ^^^ + } + + void specCompliantExamples() { + for (int i = 16_000_000; i < 17_000_000; i++) { // Compliant + // ... + } + + float x = 0f; + double y = 1.0; + + x += 1.0; // Compliant + y -= 1.0; // Compliant + } + + void floatIsNotCompliant() { + float y = 0.1f; + y++; // Noncompliant {{Increment operator (++) should not be used with floating point variables}} +// ^^^ + ++y; // Noncompliant {{Increment operator (++) should not be used with floating point variables}} +// ^^^ + y--; // Noncompliant {{Decrement operator (--) should not be used with floating point variables}} +// ^^^ + --y; // Noncompliant {{Decrement operator (--) should not be used with floating point variables}} +// ^^^ + } + + void doubleIsNotCompliant() { + double y = 0.1; + y++; // Noncompliant {{Increment operator (++) should not be used with floating point variables}} +// ^^^ + ++y; // Noncompliant {{Increment operator (++) should not be used with floating point variables}} +// ^^^ + y--; // Noncompliant {{Decrement operator (--) should not be used with floating point variables}} +// ^^^ + --y; // Noncompliant {{Decrement operator (--) should not be used with floating point variables}} +// ^^^ + } + + void intIsCompliant() { + int z = 0; + z++; // Compliant + ++z; // Compliant + z--; // Compliant + --z; // Compliant + } + + void longIsCompliant() { + long w = 0L; + w++; // Compliant + ++w; // Compliant + w--; // Compliant + --w; // Compliant + } + + void charIsCompliant() { + char c = 'a'; + c++; // Compliant + ++c; // Compliant + c--; // Compliant + --c; // Compliant + } + + void otherOperatorsNotAffected() { + float f = 1f; + // test unary operator - + float g = -f; // compliant + // test binary operators just in case + float h = f + 1f; // compliant + } + + // variable shadowing test + private double d = 3.4; + + void variableShadowingTestCompliant() { + for (int d = 0; d < 10; d++) { // Compliant + } + } + // variable shadowing test + private int i = 3; + + void variableShadowingTestCompliant() { + for (float i = 0; i < 10; i++) { // Noncompliant {{Increment operator (++) should not be used with floating point variables}} + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/IncDecOnFloatingPointCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/IncDecOnFloatingPointCheckTest.java new file mode 100644 index 0000000000..3211f1d6b0 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/IncDecOnFloatingPointCheckTest.java @@ -0,0 +1,40 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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; + +class IncDecOnFloatingPointCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile("src/test/files/checks/IncDecOnFloatingPointCheck.java") + .withCheck(new IncDecOnFloatingPointCheck()) + .verifyIssues(); + } + + @Test + void testNoSemantic() { + CheckVerifier.newVerifier() + .withoutSemantic() + .onFile("src/test/files/checks/IncDecOnFloatingPointCheck.java") + .withCheck(new IncDecOnFloatingPointCheck()) + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8346.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8346.html new file mode 100644 index 0000000000..f2e4f3b53f --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8346.html @@ -0,0 +1,43 @@ +

This rule raises an issue when increment (++) or decrement (--) operators are used with floating-point variables.

+

Why is this an issue?

+

Increment and decrement operators (++ and --) shouldn’t be used with floating-point variables (like float or +double). While the language allows it, the usage is not idiomatic, and most developers intuitively expect x++ to apply to +integer types. Using it on a float violates this common expectation and can lead to misleading code.

+

What is the potential impact?

+

Floating-point arithmetic has some non-intuitive properties, which can lead to unexpected bugs. For example, the following loop will not terminate. +This happens because float has only 24 bits of precision (mantissa) and once a number gets large enough, adding 1.0 becomes +insignificant and the increment operation does nothing:

+
+for (float x = 16_000_000; x < 17_000_000; x++) {
+  // ...
+}
+// The loop does not terminate
+
+

The problem would not occur if int is used instead of float, even though both types occupy 32 bits:

+
+for (int x = 16_000_000; x < 17_000_000; x++) {
+  // ...
+}
+// This loop terminates.
+
+

How to fix it

+

Using the compound assignment operators (+= and -=) makes the intent clearer and avoids the surprising use of +++ and -- on floating-point types.

+

Code examples

+

Noncompliant code example

+
+float x = 0f;
+double y = 1.0;
+
+x++;
+y--;
+
+

Compliant solution

+
+float x = 0f;
+double y = 1.0;
+
+x += 1.0;
+y -= 1.0;
+
+ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8346.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8346.json new file mode 100644 index 0000000000..7798c66ac2 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8346.json @@ -0,0 +1,22 @@ +{ + "title": "Increment and decrement operators (++\/--) should not be used with floating point variables", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-8346", + "sqKey": "S8346", + "scope": "All", + "quickfix": "targeted", + "code": { + "impacts": { + "MAINTAINABILITY": "HIGH", + "RELIABILITY": "MEDIUM" + }, + "attribute": "CONVENTIONAL" + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index 43ff183d61..b6bce63ab8 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -516,6 +516,7 @@ "S7479", "S7481", "S7482", - "S7629" + "S7629", + "S8346" ] }