From 5a4b3d4d68e432d70fe0b9373bee524c1a26cbb8 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 24 Aug 2026 12:18:01 +0200 Subject: [PATCH 1/9] SONARJAVA-6825: import metadata --- .../org/sonar/l10n/java/rules/java/S9360.html | 79 +++++++++++++++++++ .../org/sonar/l10n/java/rules/java/S9360.json | 25 ++++++ .../main/resources/profiles/Sonar_way/S9360 | 0 3 files changed, 104 insertions(+) create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9360 diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.html new file mode 100644 index 00000000000..9744d06691a --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.html @@ -0,0 +1,79 @@ +

This rule raises an issue when code contains operations on constants that can be precomputed, or when comparisons place constants on the left side +(Yoda conditions).

+

Why is this an issue?

+

Code that works with constants can often be simplified to improve readability and, in some cases, performance.

+

Constant Function Calls

+

When all arguments to a deterministic function are compile-time constants, the result is also a constant. For example, a function that returns the +maximum of two numbers when called with literal values 5 and 10 always returns 10. Computing this value at +runtime is unnecessary and makes the code less clear.

+

Mathematical functions that compute maximum values, minimum values, square roots, and absolute values produce predictable results when given +constant inputs. Replacing these calls with their precomputed values eliminates function call overhead and makes the intended value immediately +visible to readers.

+

Yoda Conditions

+

Yoda conditions place the constant on the left side of a comparison: 0 == count instead of count == 0. This pattern +originated in C programming to prevent accidental assignment (single equals operator) instead of comparison (double equals operator).

+

In modern type-safe languages, this defensive technique is unnecessary. Type systems that distinguish between assignment and comparison contexts +will reject code that attempts assignment where a boolean expression is expected. For example, attempting to assign a numeric value in a conditional +expression produces a compiler error in languages with strong type checking.

+

Placing constants on the left reduces readability. Natural language flows from subject to comparison: "Is the count zero?" translates more +naturally to count == 0 than to 0 == count.

+

By following conventional comparison order, code becomes more intuitive for developers to read and maintain.

+

What is the potential impact?

+

The impact on code quality is primarily related to maintainability:

+ +

How to fix it

+

For constant method calls, replace the method invocation with the precomputed result. For Yoda conditions, reverse the comparison to place the +variable on the left and the constant on the right.

+

Code examples

+

Noncompliant code example

+
+public class Example {
+    public void calculate() {
+        int max = Math.max(5, 10); // Noncompliant
+        double result = Math.sqrt(16.0) + Math.abs(-5); // Noncompliant
+
+        int count = 0;
+        if (0 == count) { // Noncompliant
+            return;
+        }
+
+        Object obj = null;
+        boolean flag = true;
+        if (null == obj && true == flag) { // Noncompliant
+            System.out.println("Both conditions met");
+        }
+    }
+}
+
+

Compliant solution

+
+public class Example {
+    public void calculate() {
+        int max = 10; // Precomputed constant
+        double result = 4.0 + 5; // Precomputed constants
+
+        int count = 0;
+        if (count == 0) { // Natural comparison order
+            return;
+        }
+
+        Object obj = null;
+        boolean flag = true;
+        if (obj == null && flag == true) { // Natural comparison order
+            System.out.println("Both conditions met");
+        }
+    }
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.json new file mode 100644 index 00000000000..a5b09922431 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.json @@ -0,0 +1,25 @@ +{ + "title": "Constant expressions and comparisons should be simplified", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5 min" + }, + "tags": [ + "confusing", + "convention", + "clarity" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9360", + "sqKey": "S9360", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "CLEAR" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9360 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9360 new file mode 100644 index 00000000000..e69de29bb2d From 36d0e824c68cd53d71839e9c8972419c9a8cb6e3 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 25 Aug 2026 09:48:07 +0200 Subject: [PATCH 2/9] SONARJAVA-6825: Implemented rule S9360 Constant expressions and comparisons should be simplified This rule detects Yoda conditions where a constant literal appears on the left side of a comparison operator (==, !=, <, >). It reports an issue with the message 'Put the variable on the left side of this comparison.' The rule handles all literal types: integers, longs, floats, doubles, booleans, characters, strings, and null. It correctly skips parentheses to detect Yoda conditions in nested expressions like ((0) == count). Test coverage includes: - All literal types with == and != operators - Less than and greater than operators - Nested parentheses handling - Non-comparison contexts (assignments, arithmetic, method calls) - Edge cases: both literals, both variables, ternary operators --- .../checks/YodaConditionCheckSample.java | 10 ++ .../java/checks/YodaConditionCheckSample.java | 119 ++++++++++++++++++ .../sonar/java/checks/YodaConditionCheck.java | 63 ++++++++++ .../java/checks/YodaConditionCheckTest.java | 51 ++++++++ 4 files changed, 243 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/files/non-compiling/checks/YodaConditionCheckSample.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/YodaConditionCheckTest.java diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/YodaConditionCheckSample.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/YodaConditionCheckSample.java new file mode 100644 index 00000000000..6fc2172102d --- /dev/null +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/YodaConditionCheckSample.java @@ -0,0 +1,10 @@ +package checks; + +class YodaConditionCheckSample { + + void unknownLiteralType() { + Object x = new Object(); + if (UNKNOWN_LITERAL == x) { } // Compliant - UNKNOWN_LITERAL is not a valid literal + } + +} diff --git a/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java new file mode 100644 index 00000000000..3a866c780af --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java @@ -0,0 +1,119 @@ +package checks; + +class YodaConditionCheckSample { + + void testIntLiteral() { + int count = 0; + int x = 5; + if (0 == count) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^ + if (5 != x) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^ + if (count == 0) { } // Compliant + if (x != 5) { } // Compliant + } + + void testNullLiteral() { + Object obj = null; + Object myObject = null; + if (null == obj) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^^^ + if (null != myObject) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^^^ + if (obj == null) { } // Compliant + if (myObject != null) { } // Compliant + if (null == null) { } // Compliant + } + + void testBooleanLiteral() { + boolean flag = true; + boolean result = false; + if (true == flag) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^^^ + if (false != result) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^^^^ + if (flag == true) { } // Compliant + if (result != false) { } // Compliant + } + + void testStringLiteral() { + String str = "hello"; + String value = ""; + if ("hello" == str) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^^^^^^ + if ("" != value) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^ + if (str == "hello") { } // Compliant + if (value != "") { } // Compliant + } + + void testCharLiteral() { + char ch = 'a'; + if ('a' == ch) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^^ + if (ch == 'a') { } // Compliant + } + + void testFloatingPointLiteral() { + double doubleValue = 0.0; + if (0.0 == doubleValue) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^^ + if (doubleValue == 0.0) { } // Compliant + } + + void testNestedParentheses() { + int count = 0; + if ((0) == count) { } // Noncompliant {{Put the variable on the left side of this comparison.}} + if (((null)) == count) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^^^ + } + + void testLessThanGreaterThan() { + int count = 0; + int x = 5; + if (0 < count) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^ + if (5 > x) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^ + if (count > 0) { } // Compliant + if (x < 5) { } // Compliant + } + + void testNonComparisonContexts() { + int count = 0; + int a = 1; + int b = 2; + count = 0; // Compliant - assignment + int sum = a + 5; // Compliant - arithmetic + int product = 5 * b; // Compliant - arithmetic + Object obj = Math.max(5, 10); // Compliant - method call + } + + void testTernaryOperator() { + boolean condition = true; + int result = condition ? 5 : 10; // Compliant + if (condition) { } // Compliant + } + + void testArrayAccess() { + int[] array = {1, 2, 3}; + if (0 == array[0]) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^ + if (array[0] == 0) { } // Compliant + } + + void testBothLiterals() { + if (0 == 0) { } // Compliant - both sides are literals + if (5 != 10) { } // Compliant - both sides are literals + if (true == false) { } // Compliant - both sides are literals + } + + void testBothVariables() { + int count = 0; + int otherCount = 0; + Object obj1 = null; + Object obj2 = null; + if (count == otherCount) { } // Compliant - both are variables + if (obj1 == obj2) { } // Compliant - both are variables + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java b/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java new file mode 100644 index 00000000000..8b4f25843da --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java @@ -0,0 +1,63 @@ +/* + * 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.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.tree.BinaryExpressionTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S9360") +public class YodaConditionCheck extends IssuableSubscriptionVisitor { + + @Override + public List nodesToVisit() { + return List.of( + Tree.Kind.EQUAL_TO, + Tree.Kind.NOT_EQUAL_TO, + Tree.Kind.LESS_THAN, + Tree.Kind.GREATER_THAN + ); + } + + @Override + public void visitNode(Tree tree) { + BinaryExpressionTree binaryExpression = (BinaryExpressionTree) tree; + ExpressionTree left = ExpressionUtils.skipParentheses(binaryExpression.leftOperand()); + ExpressionTree right = ExpressionUtils.skipParentheses(binaryExpression.rightOperand()); + + if (isLiteral(left) && !isLiteral(right)) { + reportIssue(left, "Put the variable on the left side of this comparison."); + } + } + + private static boolean isLiteral(ExpressionTree tree) { + return tree.is( + Tree.Kind.INT_LITERAL, + Tree.Kind.LONG_LITERAL, + Tree.Kind.FLOAT_LITERAL, + Tree.Kind.DOUBLE_LITERAL, + Tree.Kind.BOOLEAN_LITERAL, + Tree.Kind.CHAR_LITERAL, + Tree.Kind.STRING_LITERAL, + Tree.Kind.NULL_LITERAL + ); + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/YodaConditionCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/YodaConditionCheckTest.java new file mode 100644 index 00000000000..1ad35320272 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/YodaConditionCheckTest.java @@ -0,0 +1,51 @@ +/* + * 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; +import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; + +class YodaConditionCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/YodaConditionCheckSample.java")) + .withCheck(new YodaConditionCheck()) + .verifyIssues(); + } + + @Test + void no_issue_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/YodaConditionCheckSample.java")) + .withCheck(new YodaConditionCheck()) + .withoutSemantic() + .verifyIssues(); + } + + @Test + void test_non_compiling() { + CheckVerifier.newVerifier() + .onFile(nonCompilingTestSourcesPath("checks/YodaConditionCheckSample.java")) + .withCheck(new YodaConditionCheck()) + .verifyNoIssues(); + } +} From afab0cbaee681fd904d28ca483c8dd34f01f4fc9 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 25 Aug 2026 15:48:36 +0200 Subject: [PATCH 3/9] SONARJAVA-6825: Add constant Math call detection to S9360 Extend YodaConditionCheck to also detect calls to deterministic Math methods where all arguments are compile-time constant literals (e.g., Math.max(5, 10), Math.sqrt(16.0)). Add support for <= and >= comparison operators in Yoda condition detection. Co-Authored-By: Claude Opus 4.6 --- .../java/checks/YodaConditionCheckSample.java | 55 ++++++++++- .../sonar/java/checks/YodaConditionCheck.java | 91 ++++++++++++++++++- .../java/checks/YodaConditionCheckTest.java | 9 -- 3 files changed, 143 insertions(+), 12 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java index 3a866c780af..fd465db3168 100644 --- a/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java @@ -79,6 +79,17 @@ void testLessThanGreaterThan() { if (x < 5) { } // Compliant } + void testLessThanOrEqualGreaterThanOrEqual() { + int count = 0; + int x = 5; + if (0 <= count) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^ + if (5 >= x) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^ + if (count >= 0) { } // Compliant + if (x <= 5) { } // Compliant + } + void testNonComparisonContexts() { int count = 0; int a = 1; @@ -86,7 +97,6 @@ void testNonComparisonContexts() { count = 0; // Compliant - assignment int sum = a + 5; // Compliant - arithmetic int product = 5 * b; // Compliant - arithmetic - Object obj = Math.max(5, 10); // Compliant - method call } void testTernaryOperator() { @@ -116,4 +126,47 @@ void testBothVariables() { if (count == otherCount) { } // Compliant - both are variables if (obj1 == obj2) { } // Compliant - both are variables } + + void testConstantMathCalls() { + int max = Math.max(5, 10); // Noncompliant {{Replace this call to "max" with the precomputed constant value.}} +// ^^^^^^^^ + int min = Math.min(3, 7); // Noncompliant {{Replace this call to "min" with the precomputed constant value.}} +// ^^^^^^^^ + double sqrt = Math.sqrt(16.0); // Noncompliant {{Replace this call to "sqrt" with the precomputed constant value.}} +// ^^^^^^^^^ + int abs = Math.abs(-5); // Noncompliant {{Replace this call to "abs" with the precomputed constant value.}} +// ^^^^^^^^ + double pow = Math.pow(2.0, 3.0); // Noncompliant {{Replace this call to "pow" with the precomputed constant value.}} +// ^^^^^^^^ + long rounded = Math.round(3.14); // Noncompliant {{Replace this call to "round" with the precomputed constant value.}} +// ^^^^^^^^^^ + double floor = Math.floor(3.7); // Noncompliant {{Replace this call to "floor" with the precomputed constant value.}} +// ^^^^^^^^^^ + double ceil = Math.ceil(3.2); // Noncompliant {{Replace this call to "ceil" with the precomputed constant value.}} +// ^^^^^^^^^ + } + + void testConstantMathCallsCompliant() { + int x = 5; + int y = 10; + int max = Math.max(x, 10); // Compliant - x is not a literal + int min = Math.min(3, y); // Compliant - y is not a literal + double sqrt = Math.sqrt(x); // Compliant - x is not a literal + int abs = Math.abs(x); // Compliant - x is not a literal + int maxVar = Math.max(x, y); // Compliant - neither is a literal + } + + void testConstantMathCallsWithUnaryMinus() { + int abs = Math.abs(-10); // Noncompliant {{Replace this call to "abs" with the precomputed constant value.}} +// ^^^^^^^^ + int max = Math.max(-5, -3); // Noncompliant {{Replace this call to "max" with the precomputed constant value.}} +// ^^^^^^^^ + double sqrt = Math.sqrt(+4.0); // Noncompliant {{Replace this call to "sqrt" with the precomputed constant value.}} +// ^^^^^^^^^ + } + + void testNonMathMethodCalls() { + String result = String.valueOf(5); // Compliant - not a Math method + int hash = Integer.hashCode(42); // Compliant - not a Math method + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java b/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java index 8b4f25843da..985e2628838 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java @@ -20,26 +20,89 @@ 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.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.UnaryExpressionTree; @Rule(key = "S9360") public class YodaConditionCheck extends IssuableSubscriptionVisitor { + private static final String JAVA_LANG_MATH = "java.lang.Math"; + + private static final MethodMatchers CONSTANT_MATH_METHODS = MethodMatchers.or( + MethodMatchers.create().ofTypes(JAVA_LANG_MATH) + .names("max", "min") + .addParametersMatcher("int", "int") + .addParametersMatcher("long", "long") + .addParametersMatcher("float", "float") + .addParametersMatcher("double", "double") + .build(), + MethodMatchers.create().ofTypes(JAVA_LANG_MATH) + .names("abs", "absExact", "negateExact", "incrementExact", "decrementExact") + .addParametersMatcher("int") + .addParametersMatcher("long") + .build(), + MethodMatchers.create().ofTypes(JAVA_LANG_MATH) + .names("abs") + .addParametersMatcher("float") + .addParametersMatcher("double") + .build(), + MethodMatchers.create().ofTypes(JAVA_LANG_MATH) + .names("sqrt", "cbrt", "ceil", "floor", "rint", "log", "log10", "exp", + "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", + "toDegrees", "toRadians", "signum", "expm1", "log1p") + .addParametersMatcher("double") + .build(), + MethodMatchers.create().ofTypes(JAVA_LANG_MATH) + .names("round") + .addParametersMatcher("float") + .addParametersMatcher("double") + .build(), + MethodMatchers.create().ofTypes(JAVA_LANG_MATH) + .names("pow", "atan2", "IEEEremainder", "copySign") + .addParametersMatcher("double", "double") + .build(), + MethodMatchers.create().ofTypes(JAVA_LANG_MATH) + .names("addExact", "subtractExact", "multiplyExact", "floorDiv", "floorMod") + .addParametersMatcher("int", "int") + .addParametersMatcher("long", "long") + .build(), + MethodMatchers.create().ofTypes(JAVA_LANG_MATH) + .names("toIntExact") + .addParametersMatcher("long") + .build(), + MethodMatchers.create().ofTypes(JAVA_LANG_MATH) + .names("signum") + .addParametersMatcher("float") + .build() + ); + @Override public List nodesToVisit() { return List.of( Tree.Kind.EQUAL_TO, Tree.Kind.NOT_EQUAL_TO, Tree.Kind.LESS_THAN, - Tree.Kind.GREATER_THAN + Tree.Kind.GREATER_THAN, + Tree.Kind.LESS_THAN_OR_EQUAL_TO, + Tree.Kind.GREATER_THAN_OR_EQUAL_TO, + Tree.Kind.METHOD_INVOCATION ); } @Override public void visitNode(Tree tree) { - BinaryExpressionTree binaryExpression = (BinaryExpressionTree) tree; + if (tree.is(Tree.Kind.METHOD_INVOCATION)) { + checkConstantMathCall((MethodInvocationTree) tree); + } else { + checkYodaCondition((BinaryExpressionTree) tree); + } + } + + private void checkYodaCondition(BinaryExpressionTree binaryExpression) { ExpressionTree left = ExpressionUtils.skipParentheses(binaryExpression.leftOperand()); ExpressionTree right = ExpressionUtils.skipParentheses(binaryExpression.rightOperand()); @@ -48,6 +111,30 @@ public void visitNode(Tree tree) { } } + private void checkConstantMathCall(MethodInvocationTree methodInvocation) { + if (CONSTANT_MATH_METHODS.matches(methodInvocation) && allArgumentsAreLiterals(methodInvocation)) { + reportIssue(methodInvocation.methodSelect(), + String.format("Replace this call to \"%s\" with the precomputed constant value.", methodInvocation.methodSymbol().name())); + } + } + + private static boolean allArgumentsAreLiterals(MethodInvocationTree methodInvocation) { + return methodInvocation.arguments().stream().allMatch(YodaConditionCheck::isNumericLiteral); + } + + private static boolean isNumericLiteral(ExpressionTree tree) { + ExpressionTree expr = ExpressionUtils.skipParentheses(tree); + if (expr.is(Tree.Kind.UNARY_MINUS, Tree.Kind.UNARY_PLUS)) { + expr = ExpressionUtils.skipParentheses(((UnaryExpressionTree) expr).expression()); + } + return expr.is( + Tree.Kind.INT_LITERAL, + Tree.Kind.LONG_LITERAL, + Tree.Kind.FLOAT_LITERAL, + Tree.Kind.DOUBLE_LITERAL + ); + } + private static boolean isLiteral(ExpressionTree tree) { return tree.is( Tree.Kind.INT_LITERAL, diff --git a/java-checks/src/test/java/org/sonar/java/checks/YodaConditionCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/YodaConditionCheckTest.java index 1ad35320272..f010f56d4c9 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/YodaConditionCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/YodaConditionCheckTest.java @@ -32,15 +32,6 @@ void test() { .verifyIssues(); } - @Test - void no_issue_without_semantic() { - CheckVerifier.newVerifier() - .onFile(mainCodeSourcesPath("checks/YodaConditionCheckSample.java")) - .withCheck(new YodaConditionCheck()) - .withoutSemantic() - .verifyIssues(); - } - @Test void test_non_compiling() { CheckVerifier.newVerifier() From 1546ee8f67b61ff5b167f4ba140398d9cfe62c1d Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 25 Aug 2026 15:56:13 +0200 Subject: [PATCH 4/9] SONARJAVA-6825: Fix compilation error in YodaConditionCheckSample Compare null to a reference type (Object) instead of a primitive (int) to fix illegal operand types for binary operator '=='. Co-Authored-By: Claude Opus 4.6 --- .../default/src/main/java/checks/YodaConditionCheckSample.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java index fd465db3168..8fe8540554a 100644 --- a/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java @@ -63,8 +63,9 @@ void testFloatingPointLiteral() { void testNestedParentheses() { int count = 0; + Object obj = new Object(); if ((0) == count) { } // Noncompliant {{Put the variable on the left side of this comparison.}} - if (((null)) == count) { } // Noncompliant {{Put the variable on the left side of this comparison.}} + if (((null)) == obj) { } // Noncompliant {{Put the variable on the left side of this comparison.}} // ^^^^ } From d305216c547ade260a0e0dea694b85d5b580b04b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:12:01 +0100 Subject: [PATCH 5/9] Update ruling results for PR #6036 (#6037) Co-authored-by: github-actions[bot] --- .../commons-beanutils/java-S9360.json | 8 ++ .../resources/eclipse-jetty/java-S9360.json | 75 +++++++++++++++++++ .../src/test/resources/guava/java-S9360.json | 25 +++++++ 3 files changed, 108 insertions(+) create mode 100644 its/ruling/src/test/resources/commons-beanutils/java-S9360.json create mode 100644 its/ruling/src/test/resources/eclipse-jetty/java-S9360.json create mode 100644 its/ruling/src/test/resources/guava/java-S9360.json diff --git a/its/ruling/src/test/resources/commons-beanutils/java-S9360.json b/its/ruling/src/test/resources/commons-beanutils/java-S9360.json new file mode 100644 index 00000000000..55309c93efd --- /dev/null +++ b/its/ruling/src/test/resources/commons-beanutils/java-S9360.json @@ -0,0 +1,8 @@ +{ +"commons-beanutils:commons-beanutils:src/main/java/org/apache/commons/beanutils2/ConstructorUtils.java": [ +106, +154, +218, +267 +] +} diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S9360.json b/its/ruling/src/test/resources/eclipse-jetty/java-S9360.json new file mode 100644 index 00000000000..e4627a8f3bd --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S9360.json @@ -0,0 +1,75 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java": [ +343, +358, +373, +388, +403 +], +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java": [ +1028 +], +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java": [ +431, +436, +445, +453, +455, +459, +465, +471, +477, +483, +489, +495, +497, +501, +503, +514, +514, +515, +575, +615, +617, +622, +624, +628, +634, +640, +646, +652, +658, +664, +666, +670, +677, +684 +], +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java": [ +93, +414 +], +"org.eclipse.jetty:jetty-project:jetty-io/src/test/java/org/eclipse/jetty/io/ArrayByteBufferPoolTest.java": [ +94 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java": [ +744, +835 +], +"org.eclipse.jetty:jetty-project:jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/AsyncJSON.java": [ +1275 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java": [ +901, +907, +925, +942, +957, +962, +978 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/security/Password.java": [ +117, +132 +] +} diff --git a/its/ruling/src/test/resources/guava/java-S9360.json b/its/ruling/src/test/resources/guava/java-S9360.json new file mode 100644 index 00000000000..57860c32edb --- /dev/null +++ b/its/ruling/src/test/resources/guava/java-S9360.json @@ -0,0 +1,25 @@ +{ +"com.google.guava:guava:src/com/google/common/base/SmallCharMatcher.java": [ +61 +], +"com.google.guava:guava:src/com/google/common/hash/BloomFilter.java": [ +436, +453, +453 +], +"com.google.guava:guava:src/com/google/common/io/LittleEndianDataInputStream.java": [ +82 +], +"com.google.guava:guava:src/com/google/common/math/BigIntegerMath.java": [ +195, +196 +], +"com.google.guava:guava:src/com/google/common/math/DoubleMath.java": [ +220 +], +"com.google.guava:guava:src/com/google/common/net/MediaType.java": [ +628, +631, +632 +] +} From 1eecee6f513732766a265d52ae5156a8d66e9c4d Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 25 Aug 2026 16:21:45 +0200 Subject: [PATCH 6/9] SONARJAVA-6825: Fix S9360 review findings and improve rule quality - Extract duplicated "float"/"double" string literals into constants (S1192) - Fix isLiteral to handle unary minus/plus (e.g. -1 == index now detected) - Use distinct message for relational Yoda conditions ("...and invert the operator") - Remove BOOLEAN_LITERAL from Yoda detection (delegate to S1125) - Remove transcendental Math functions from constant detection (results may vary across JVMs) - Update ruling expectations and test samples accordingly Co-Authored-By: Claude Opus 4.6 --- .../src/test/resources/guava/java-S9360.json | 7 ---- .../java/checks/YodaConditionCheckSample.java | 37 ++++++++++++----- .../sonar/java/checks/YodaConditionCheck.java | 40 +++++++++++-------- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/its/ruling/src/test/resources/guava/java-S9360.json b/its/ruling/src/test/resources/guava/java-S9360.json index 57860c32edb..6c76bcccf41 100644 --- a/its/ruling/src/test/resources/guava/java-S9360.json +++ b/its/ruling/src/test/resources/guava/java-S9360.json @@ -10,13 +10,6 @@ "com.google.guava:guava:src/com/google/common/io/LittleEndianDataInputStream.java": [ 82 ], -"com.google.guava:guava:src/com/google/common/math/BigIntegerMath.java": [ -195, -196 -], -"com.google.guava:guava:src/com/google/common/math/DoubleMath.java": [ -220 -], "com.google.guava:guava:src/com/google/common/net/MediaType.java": [ 628, 631, diff --git a/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java index 8fe8540554a..54583ff3ab7 100644 --- a/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java @@ -28,10 +28,8 @@ void testNullLiteral() { void testBooleanLiteral() { boolean flag = true; boolean result = false; - if (true == flag) { } // Noncompliant {{Put the variable on the left side of this comparison.}} -// ^^^^ - if (false != result) { } // Noncompliant {{Put the variable on the left side of this comparison.}} -// ^^^^^ + if (true == flag) { } // Compliant - boolean literal comparisons handled by S1125 + if (false != result) { } // Compliant - boolean literal comparisons handled by S1125 if (flag == true) { } // Compliant if (result != false) { } // Compliant } @@ -72,9 +70,9 @@ void testNestedParentheses() { void testLessThanGreaterThan() { int count = 0; int x = 5; - if (0 < count) { } // Noncompliant {{Put the variable on the left side of this comparison.}} + if (0 < count) { } // Noncompliant {{Put the variable on the left side of this comparison and invert the operator.}} // ^ - if (5 > x) { } // Noncompliant {{Put the variable on the left side of this comparison.}} + if (5 > x) { } // Noncompliant {{Put the variable on the left side of this comparison and invert the operator.}} // ^ if (count > 0) { } // Compliant if (x < 5) { } // Compliant @@ -83,9 +81,9 @@ void testLessThanGreaterThan() { void testLessThanOrEqualGreaterThanOrEqual() { int count = 0; int x = 5; - if (0 <= count) { } // Noncompliant {{Put the variable on the left side of this comparison.}} + if (0 <= count) { } // Noncompliant {{Put the variable on the left side of this comparison and invert the operator.}} // ^ - if (5 >= x) { } // Noncompliant {{Put the variable on the left side of this comparison.}} + if (5 >= x) { } // Noncompliant {{Put the variable on the left side of this comparison and invert the operator.}} // ^ if (count >= 0) { } // Compliant if (x <= 5) { } // Compliant @@ -113,10 +111,23 @@ void testArrayAccess() { if (array[0] == 0) { } // Compliant } + void testUnaryMinusPlusYoda() { + int index = 0; + if (-1 == index) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^ + if (+1 == index) { } // Noncompliant {{Put the variable on the left side of this comparison.}} +// ^^ + if (index == -1) { } // Compliant + if (index == +1) { } // Compliant + } + void testBothLiterals() { if (0 == 0) { } // Compliant - both sides are literals if (5 != 10) { } // Compliant - both sides are literals if (true == false) { } // Compliant - both sides are literals + if (0 == -1) { } // Compliant - both sides are literals + if (-1 == 0) { } // Compliant - both sides are literals + if (-1 == -2) { } // Compliant - both sides are literals } void testBothVariables() { @@ -137,8 +148,6 @@ void testConstantMathCalls() { // ^^^^^^^^^ int abs = Math.abs(-5); // Noncompliant {{Replace this call to "abs" with the precomputed constant value.}} // ^^^^^^^^ - double pow = Math.pow(2.0, 3.0); // Noncompliant {{Replace this call to "pow" with the precomputed constant value.}} -// ^^^^^^^^ long rounded = Math.round(3.14); // Noncompliant {{Replace this call to "round" with the precomputed constant value.}} // ^^^^^^^^^^ double floor = Math.floor(3.7); // Noncompliant {{Replace this call to "floor" with the precomputed constant value.}} @@ -166,6 +175,14 @@ void testConstantMathCallsWithUnaryMinus() { // ^^^^^^^^^ } + void testTranscendentalMathCallsCompliant() { + double pow = Math.pow(2.0, 3.0); // Compliant - transcendental functions excluded (result may vary across JVMs) + double sin = Math.sin(0.5); // Compliant + double cos = Math.cos(0.5); // Compliant + double log = Math.log(2.0); // Compliant + double exp = Math.exp(1.0); // Compliant + } + void testNonMathMethodCalls() { String result = String.valueOf(5); // Compliant - not a Math method int hash = Integer.hashCode(42); // Compliant - not a Math method diff --git a/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java b/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java index 985e2628838..54f7cea4e7d 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java @@ -31,14 +31,16 @@ public class YodaConditionCheck extends IssuableSubscriptionVisitor { private static final String JAVA_LANG_MATH = "java.lang.Math"; + private static final String FLOAT = "float"; + private static final String DOUBLE = "double"; private static final MethodMatchers CONSTANT_MATH_METHODS = MethodMatchers.or( MethodMatchers.create().ofTypes(JAVA_LANG_MATH) .names("max", "min") .addParametersMatcher("int", "int") .addParametersMatcher("long", "long") - .addParametersMatcher("float", "float") - .addParametersMatcher("double", "double") + .addParametersMatcher(FLOAT, FLOAT) + .addParametersMatcher(DOUBLE, DOUBLE) .build(), MethodMatchers.create().ofTypes(JAVA_LANG_MATH) .names("abs", "absExact", "negateExact", "incrementExact", "decrementExact") @@ -47,23 +49,21 @@ public class YodaConditionCheck extends IssuableSubscriptionVisitor { .build(), MethodMatchers.create().ofTypes(JAVA_LANG_MATH) .names("abs") - .addParametersMatcher("float") - .addParametersMatcher("double") + .addParametersMatcher(FLOAT) + .addParametersMatcher(DOUBLE) .build(), MethodMatchers.create().ofTypes(JAVA_LANG_MATH) - .names("sqrt", "cbrt", "ceil", "floor", "rint", "log", "log10", "exp", - "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", - "toDegrees", "toRadians", "signum", "expm1", "log1p") - .addParametersMatcher("double") + .names("sqrt", "cbrt", "ceil", "floor", "rint", "toDegrees", "toRadians", "signum") + .addParametersMatcher(DOUBLE) .build(), MethodMatchers.create().ofTypes(JAVA_LANG_MATH) .names("round") - .addParametersMatcher("float") - .addParametersMatcher("double") + .addParametersMatcher(FLOAT) + .addParametersMatcher(DOUBLE) .build(), MethodMatchers.create().ofTypes(JAVA_LANG_MATH) - .names("pow", "atan2", "IEEEremainder", "copySign") - .addParametersMatcher("double", "double") + .names("copySign") + .addParametersMatcher(DOUBLE, DOUBLE) .build(), MethodMatchers.create().ofTypes(JAVA_LANG_MATH) .names("addExact", "subtractExact", "multiplyExact", "floorDiv", "floorMod") @@ -76,7 +76,7 @@ public class YodaConditionCheck extends IssuableSubscriptionVisitor { .build(), MethodMatchers.create().ofTypes(JAVA_LANG_MATH) .names("signum") - .addParametersMatcher("float") + .addParametersMatcher(FLOAT) .build() ); @@ -107,7 +107,12 @@ private void checkYodaCondition(BinaryExpressionTree binaryExpression) { ExpressionTree right = ExpressionUtils.skipParentheses(binaryExpression.rightOperand()); if (isLiteral(left) && !isLiteral(right)) { - reportIssue(left, "Put the variable on the left side of this comparison."); + if (binaryExpression.is(Tree.Kind.LESS_THAN, Tree.Kind.GREATER_THAN, + Tree.Kind.LESS_THAN_OR_EQUAL_TO, Tree.Kind.GREATER_THAN_OR_EQUAL_TO)) { + reportIssue(left, "Put the variable on the left side of this comparison and invert the operator."); + } else { + reportIssue(left, "Put the variable on the left side of this comparison."); + } } } @@ -136,12 +141,15 @@ private static boolean isNumericLiteral(ExpressionTree tree) { } private static boolean isLiteral(ExpressionTree tree) { - return tree.is( + ExpressionTree expr = tree; + if (expr.is(Tree.Kind.UNARY_MINUS, Tree.Kind.UNARY_PLUS)) { + expr = ExpressionUtils.skipParentheses(((UnaryExpressionTree) expr).expression()); + } + return expr.is( Tree.Kind.INT_LITERAL, Tree.Kind.LONG_LITERAL, Tree.Kind.FLOAT_LITERAL, Tree.Kind.DOUBLE_LITERAL, - Tree.Kind.BOOLEAN_LITERAL, Tree.Kind.CHAR_LITERAL, Tree.Kind.STRING_LITERAL, Tree.Kind.NULL_LITERAL From 8abb0d2e07653f68127c0469efebbc3e96ebaf98 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 25 Aug 2026 14:24:08 +0000 Subject: [PATCH 7/9] Update ruling results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with GitHub Actions --- .../java-S9360.json | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S9360.json diff --git a/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S9360.json b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S9360.json new file mode 100644 index 00000000000..c2cf3131395 --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S9360.json @@ -0,0 +1,59 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java": [ +343, +358, +373, +388, +403 +], +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java": [ +1028 +], +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java": [ +431, +436, +445, +453, +455, +459, +465, +471, +477, +483, +489, +495, +497, +501, +503, +514, +514, +515, +575, +615, +617, +622, +624, +628, +634, +640, +646, +652, +658, +664, +666, +670, +677, +684 +], +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java": [ +93, +414 +], +"org.eclipse.jetty:jetty-project:jetty-io/src/test/java/org/eclipse/jetty/io/ArrayByteBufferPoolTest.java": [ +94 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java": [ +744, +835 +] +} From b918b7dd26c253eb88603c1f15e2e12355bc0057 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 25 Aug 2026 16:39:32 +0200 Subject: [PATCH 8/9] SONARJAVA-6825: Address remaining review findings for S9360 - Remove inexact Math functions (cbrt, toDegrees, toRadians) from constant call detection as their results are not exactly specified across JVMs - Fix HTML doc: remove boolean literal examples that conflict with S1125, correct rationale about Java assignment in conditional contexts - Add test cases for newly excluded Math functions Co-Authored-By: Claude Opus 4.6 --- .../java/checks/YodaConditionCheckSample.java | 3 +++ .../sonar/java/checks/YodaConditionCheck.java | 2 +- .../org/sonar/l10n/java/rules/java/S9360.html | 16 +++++++--------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java index 54583ff3ab7..f996d35538b 100644 --- a/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/YodaConditionCheckSample.java @@ -181,6 +181,9 @@ void testTranscendentalMathCallsCompliant() { double cos = Math.cos(0.5); // Compliant double log = Math.log(2.0); // Compliant double exp = Math.exp(1.0); // Compliant + double cbrt = Math.cbrt(8.0); // Compliant - not exactly specified + double deg = Math.toDegrees(1.0); // Compliant - not exactly specified + double rad = Math.toRadians(90.0); // Compliant - not exactly specified } void testNonMathMethodCalls() { diff --git a/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java b/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java index 54f7cea4e7d..21aafb7e2b1 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/YodaConditionCheck.java @@ -53,7 +53,7 @@ public class YodaConditionCheck extends IssuableSubscriptionVisitor { .addParametersMatcher(DOUBLE) .build(), MethodMatchers.create().ofTypes(JAVA_LANG_MATH) - .names("sqrt", "cbrt", "ceil", "floor", "rint", "toDegrees", "toRadians", "signum") + .names("sqrt", "ceil", "floor", "rint", "signum") .addParametersMatcher(DOUBLE) .build(), MethodMatchers.create().ofTypes(JAVA_LANG_MATH) diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.html index 9744d06691a..f4245e049a2 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9360.html @@ -12,9 +12,9 @@

Why is this an issue?

Yoda Conditions

Yoda conditions place the constant on the left side of a comparison: 0 == count instead of count == 0. This pattern originated in C programming to prevent accidental assignment (single equals operator) instead of comparison (double equals operator).

-

In modern type-safe languages, this defensive technique is unnecessary. Type systems that distinguish between assignment and comparison contexts -will reject code that attempts assignment where a boolean expression is expected. For example, attempting to assign a numeric value in a conditional -expression produces a compiler error in languages with strong type checking.

+

In Java, this defensive technique is largely unnecessary. The compiler rejects assignment in conditional contexts for non-boolean types (for example, +if (count = 0) does not compile when count is an int). While if (flag = true) does compile for +boolean variables, that case is better addressed by removing the redundant boolean comparison entirely.

Placing constants on the left reduces readability. Natural language flows from subject to comparison: "Is the count zero?" translates more naturally to count == 0 than to 0 == count.

By following conventional comparison order, code becomes more intuitive for developers to read and maintain.

@@ -43,9 +43,8 @@

Noncompliant code example

} Object obj = null; - boolean flag = true; - if (null == obj && true == flag) { // Noncompliant - System.out.println("Both conditions met"); + if (null == obj) { // Noncompliant + System.out.println("Condition met"); } } } @@ -63,9 +62,8 @@

Compliant solution

} Object obj = null; - boolean flag = true; - if (obj == null && flag == true) { // Natural comparison order - System.out.println("Both conditions met"); + if (obj == null) { // Natural comparison order + System.out.println("Condition met"); } } } From 676de5f73b4970944a59ec266f4719fd706d3b8e Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 25 Aug 2026 17:07:21 +0200 Subject: [PATCH 9/9] Update ruling results for guava after S9360 behavior changes Remove Math.log(2) detections from BloomFilter (method no longer matched), add new negated literal Yoda conditions in LittleEndianDataInputStream and LongMath. Co-Authored-By: Claude Opus 4.6 --- its/ruling/src/test/resources/guava/java-S9360.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/its/ruling/src/test/resources/guava/java-S9360.json b/its/ruling/src/test/resources/guava/java-S9360.json index 6c76bcccf41..5472011ddcc 100644 --- a/its/ruling/src/test/resources/guava/java-S9360.json +++ b/its/ruling/src/test/resources/guava/java-S9360.json @@ -2,13 +2,12 @@ "com.google.guava:guava:src/com/google/common/base/SmallCharMatcher.java": [ 61 ], -"com.google.guava:guava:src/com/google/common/hash/BloomFilter.java": [ -436, -453, -453 -], "com.google.guava:guava:src/com/google/common/io/LittleEndianDataInputStream.java": [ -82 +82, +225 +], +"com.google.guava:guava:src/com/google/common/math/LongMath.java": [ +234 ], "com.google.guava:guava:src/com/google/common/net/MediaType.java": [ 628,