From 9f9b9dd3ceeb973c01593602746239ae999f7df7 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 17 Aug 2026 15:10:33 +0200 Subject: [PATCH 1/8] Implement new rule S7438 Detect incompatible bit masks in equality comparisons where bitwise AND or OR operations are compared against values that are impossible given the mask, making the comparison always true or always false. --- .../IncompatibleBitMaskCheckSample.java | 135 ++++++++++++++++++ .../java/checks/IncompatibleBitMaskCheck.java | 82 +++++++++++ .../checks/IncompatibleBitMaskCheckTest.java | 42 ++++++ .../org/sonar/l10n/java/rules/java/S7438.html | 32 +++++ .../org/sonar/l10n/java/rules/java/S7438.json | 23 +++ .../main/resources/profiles/Sonar_way/S7438 | 0 6 files changed, 314 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/IncompatibleBitMaskCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S7438.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S7438.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S7438 diff --git a/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java new file mode 100644 index 00000000000..44244e0025b --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java @@ -0,0 +1,135 @@ +package checks; + +class IncompatibleBitMaskCheckSample { + + void bitwiseAndNoncompliant(int x, long status) { + // AND mask 1 (0b01) cannot produce 2 (0b10) + if ((x & 1) == 2) {} // Noncompliant {{This comparison is always false.}} + // ^^ + + // AND mask 0x0F cannot produce 0x10 + if ((x & 0x0F) == 0x10) {} // Noncompliant {{This comparison is always false.}} + + // AND mask 3 (0b11) cannot produce 4 (0b100) + if ((x & 3) == 4) {} // Noncompliant {{This comparison is always false.}} + + // != with incompatible values is always true + if ((x & 1) != 2) {} // Noncompliant {{This comparison is always true.}} + + // Long variant: AND mask 0xFF cannot produce 0x100 + if ((status & 0xFFL) == 0x100L) {} // Noncompliant {{This comparison is always false.}} + } + + void bitwiseOrNoncompliant(int x, long data) { + // OR with 1 always sets bit 0, result can never be 0 + if ((x | 1) == 0) {} // Noncompliant {{This comparison is always false.}} + + // OR with 3 always sets bits 0 and 1, result can never be 2 (missing bit 0) + if ((x | 3) == 2) {} // Noncompliant {{This comparison is always false.}} + + // != with incompatible OR is always true + if ((x | 2) != 1) {} // Noncompliant {{This comparison is always true.}} + + // Long variant: OR with 0xFF always sets low 8 bits + if ((data | 0xFFL) == 0L) {} // Noncompliant {{This comparison is always false.}} + } + + void bitwiseAndCompliant(int x, long status) { + // Mask 2 can produce 2 + if ((x & 2) == 2) {} // Compliant + + // 0x04 is within mask 0x0F + if ((x & 0x0F) == 0x04) {} // Compliant + + // Comparing AND result to 0 is always valid + if ((x & 1) == 0) {} // Compliant + + // 3 is within mask 7 + if ((x & 7) == 3) {} // Compliant + + // 2 is reachable by AND with 3 + if ((x & 3) != 2) {} // Compliant + + // Long: 0x80 is within mask 0xFF + if ((status & 0xFFL) == 0x80L) {} // Compliant + } + + void bitwiseOrCompliant(int x, long data) { + // 1 includes all mask bits + if ((x | 1) == 1) {} // Compliant + + // 3 includes all mask bits + if ((x | 3) == 3) {} // Compliant + + // 0xFF includes all mask bits 0x0F + if ((x | 0x0F) == 0xFF) {} // Compliant + + // 3 includes mask bit 2, comparison is meaningful + if ((x | 2) != 3) {} // Compliant + + // Value includes all mask bits + if ((data | 0xFFL) == 0xFFL) {} // Compliant + } + + void maskOnLeftSide(int x) { + // Mask on left side of bitwise operation + if ((1 & x) == 2) {} // Noncompliant {{This comparison is always false.}} + + if ((1 & x) == 0) {} // Compliant + } + + void constantOnLeftSideOfComparison(int x) { + // Constant on left side of comparison + if (2 == (x & 1)) {} // Noncompliant {{This comparison is always false.}} + + if (0 == (x & 1)) {} // Compliant + } + + void hexAndBinaryLiterals(int x) { + // Hex literals + if ((x & 0xFF) == 0x100) {} // Noncompliant {{This comparison is always false.}} + + // Binary literals + if ((x & 0b1111) == 0b10000) {} // Noncompliant {{This comparison is always false.}} + + if ((x & 0b1111) == 0b1010) {} // Compliant + } + + void edgeCases(int x) { + // Mask of 0: AND with 0 always produces 0 + if ((x & 0) == 1) {} // Noncompliant {{This comparison is always false.}} + if ((x & 0) == 0) {} // Compliant + + // OR with 0: result is x, any comparison is meaningful + if ((x | 0) == 5) {} // Compliant + } + + void noConstantOperands(int x, int y, int z) { + // No constant mask - no issue + if ((x & y) == 2) {} // Compliant + + // No constant comparison value - no issue + if ((x & 1) == y) {} // Compliant + + // No constant at all + if ((x & y) == z) {} // Compliant + } + + void notBitwiseOperations(int x) { + // XOR is not covered + if ((x ^ 1) == 2) {} // Compliant + + // Regular comparison without bitwise + if (x == 2) {} // Compliant + + // Addition, not bitwise + if ((x + 1) == 2) {} // Compliant + } + + void parenthesizedExpressions(int x) { + // Extra parentheses around bitwise operation + if (((x & 1)) == 2) {} // Noncompliant {{This comparison is always false.}} + + if (((x & 2)) == 2) {} // Compliant + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java new file mode 100644 index 00000000000..71f0b5a36bb --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java @@ -0,0 +1,82 @@ +/* + * 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 javax.annotation.Nullable; +import org.sonar.check.Rule; +import org.sonar.java.model.ExpressionUtils; +import org.sonar.java.model.LiteralUtils; +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; +import org.sonar.plugins.java.api.tree.Tree.Kind; + +@Rule(key = "S7438") +public class IncompatibleBitMaskCheck extends IssuableSubscriptionVisitor { + + @Override + public List nodesToVisit() { + return List.of(Kind.EQUAL_TO, Kind.NOT_EQUAL_TO); + } + + @Override + public void visitNode(Tree tree) { + BinaryExpressionTree comparison = (BinaryExpressionTree) tree; + ExpressionTree left = comparison.leftOperand(); + ExpressionTree right = comparison.rightOperand(); + check(left, right, comparison); + check(right, left, comparison); + } + + private void check(ExpressionTree possibleBitwiseOp, ExpressionTree possibleConstant, BinaryExpressionTree comparison) { + ExpressionTree unwrapped = ExpressionUtils.skipParentheses(possibleBitwiseOp); + if (!unwrapped.is(Kind.AND, Kind.OR)) { + return; + } + BinaryExpressionTree bitwiseOp = (BinaryExpressionTree) unwrapped; + Long mask = extractMask(bitwiseOp); + Long value = LiteralUtils.longLiteralValue(possibleConstant); + if (mask == null || value == null) { + return; + } + if (isIncompatible(unwrapped.kind(), mask, value)) { + String message = comparison.is(Kind.EQUAL_TO) + ? "This comparison is always false." + : "This comparison is always true."; + reportIssue(comparison.operatorToken(), message); + } + } + + @Nullable + private static Long extractMask(BinaryExpressionTree bitwiseOp) { + Long leftValue = LiteralUtils.longLiteralValue(bitwiseOp.leftOperand()); + if (leftValue != null) { + return leftValue; + } + return LiteralUtils.longLiteralValue(bitwiseOp.rightOperand()); + } + + private static boolean isIncompatible(Kind bitwiseKind, long mask, long value) { + if (bitwiseKind == Kind.AND) { + return (value & mask) != value; + } + // OR + return (value | mask) != value; + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/IncompatibleBitMaskCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/IncompatibleBitMaskCheckTest.java new file mode 100644 index 00000000000..e49be0ffa0c --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/IncompatibleBitMaskCheckTest.java @@ -0,0 +1,42 @@ +/* + * 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 IncompatibleBitMaskCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/IncompatibleBitMaskCheckSample.java")) + .withCheck(new IncompatibleBitMaskCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/IncompatibleBitMaskCheckSample.java")) + .withCheck(new IncompatibleBitMaskCheck()) + .withoutSemantic() + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S7438.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S7438.html new file mode 100644 index 00000000000..8b183173d42 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S7438.html @@ -0,0 +1,32 @@ +

Why is this an issue?

+

When performing bitwise operations in comparisons, the relationship between the bit mask and the compared value determines what results are +possible. If this relationship makes certain outcomes impossible, the comparison becomes a constant expression.

+

For bitwise AND operations (&), the result can only have bits set where the mask has bits set. For example, x & 1 +can only produce values 0 or 1, never 2. Comparing this result to an impossible value like 2 creates dead code.

+

For bitwise OR operations (|), the result always includes all bits set in the mask. If the compared value doesn't include all mask +bits, the comparison can never be equal.

+

These constant comparisons indicate logical errors in the code.

+

How to fix it

+

Review the bit mask and comparison value to ensure they are logically compatible. For AND operations, verify that the compared value only has bits +that exist in the mask. For OR operations, verify that the compared value includes all bits from the mask.

+

Code examples

+

Noncompliant code example

+
+int x = getUserPermissions();
+if ((x & 1) == 2) { // Noncompliant
+    grantAccess();
+}
+
+

Compliant solution

+
+int x = getUserPermissions();
+if ((x & 2) == 2) {
+    grantAccess();
+}
+
+

Resources

+

Documentation

+ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S7438.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S7438.json new file mode 100644 index 00000000000..82f20de063b --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S7438.json @@ -0,0 +1,23 @@ +{ + "title": "Incompatible bit masks should not be used in comparisons", + "type": "BUG", + "code": { + "impacts": { + "RELIABILITY": "HIGH" + }, + "attribute": "LOGICAL" + }, + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "suspicious" + ], + "defaultSeverity": "Blocker", + "ruleSpecification": "RSPEC-7438", + "sqKey": "S7438", + "scope": "All", + "quickfix": "unknown" +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S7438 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S7438 new file mode 100644 index 00000000000..e69de29bb2d From abd1a0f3bc058b68d020566bdd2cac4d44327036 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 18 Aug 2026 11:37:21 +0200 Subject: [PATCH 2/8] Fix S7438: handle int-width sign extension in bit mask comparisons Fix test marker alignment and add int-type-aware normalization to avoid false positives when high-bit int hex masks (e.g., 0xFFFFFFFF) are compared with negative int values. In no-semantic mode, falls back to checking literal types (int vs long) to determine operand width. Co-Authored-By: Claude Opus 4.6 --- .../IncompatibleBitMaskCheckSample.java | 19 ++++++++++++++++- .../java/checks/IncompatibleBitMaskCheck.java | 21 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java index 44244e0025b..ae06cca08fe 100644 --- a/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java @@ -5,7 +5,7 @@ class IncompatibleBitMaskCheckSample { void bitwiseAndNoncompliant(int x, long status) { // AND mask 1 (0b01) cannot produce 2 (0b10) if ((x & 1) == 2) {} // Noncompliant {{This comparison is always false.}} - // ^^ + // ^^ // AND mask 0x0F cannot produce 0x10 if ((x & 0x0F) == 0x10) {} // Noncompliant {{This comparison is always false.}} @@ -132,4 +132,21 @@ void parenthesizedExpressions(int x) { if (((x & 2)) == 2) {} // Compliant } + + void intWidthHighBitMasks(int x) { + // 0xFFFFFFFF as int is -1, so (x & 0xFFFFFFFF) == -1 is valid when x == -1 + if ((x & 0xFFFFFFFF) == -1) {} // Compliant + + // 0xFFFFFFFF as int is -1, AND with -1 is identity, so comparing to 0 is valid + if ((x & 0xFFFFFFFF) == 0) {} // Compliant + + // 0x80000000 as int is Integer.MIN_VALUE (-2147483648), valid comparison + if ((x & 0x80000000) == -2147483648) {} // Compliant + + // High-bit hex mask with matching hex value + if ((x & 0xF0000000) == 0xF0000000) {} // Compliant + + // AND with 3 cannot produce -1 (only bits 0 and 1 can be set) + if ((x & 3) == -1) {} // Noncompliant {{This comparison is always false.}} + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java index 71f0b5a36bb..03cdeceea35 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java @@ -22,6 +22,7 @@ import org.sonar.java.model.ExpressionUtils; import org.sonar.java.model.LiteralUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.Tree; @@ -55,6 +56,10 @@ private void check(ExpressionTree possibleBitwiseOp, ExpressionTree possibleCons if (mask == null || value == null) { return; } + if (isIntOperation(bitwiseOp, possibleConstant)) { + mask = (long) mask.intValue(); + value = (long) value.intValue(); + } if (isIncompatible(unwrapped.kind(), mask, value)) { String message = comparison.is(Kind.EQUAL_TO) ? "This comparison is always false." @@ -72,6 +77,22 @@ private static Long extractMask(BinaryExpressionTree bitwiseOp) { return LiteralUtils.longLiteralValue(bitwiseOp.rightOperand()); } + private static boolean isIntOperation(BinaryExpressionTree bitwiseOp, ExpressionTree comparisonValue) { + Type type = bitwiseOp.symbolType(); + if (type.is("int")) { + return true; + } + if (type.is("long")) { + return false; + } + // Type is unknown (no-semantic mode): assume int if no long literals are involved + return !hasLongLiteral(bitwiseOp) && !comparisonValue.is(Kind.LONG_LITERAL); + } + + private static boolean hasLongLiteral(BinaryExpressionTree bitwiseOp) { + return bitwiseOp.leftOperand().is(Kind.LONG_LITERAL) || bitwiseOp.rightOperand().is(Kind.LONG_LITERAL); + } + private static boolean isIncompatible(Kind bitwiseKind, long mask, long value) { if (bitwiseKind == Kind.AND) { return (value & mask) != value; From 1b77619421de1336f2d743d5d6335061a8a3b132 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 18 Aug 2026 12:14:22 +0200 Subject: [PATCH 3/8] Fix S7438: simplify isIntOperation and add test coverage Simplify isIntOperation to rely solely on symbolType() since ECJ always resolves primitive types even in no-semantic mode, making the long-literal heuristic fallback unreachable dead code. Add additional test cases for OR+!=, long literal mask on left, and comparison value on left with long. Co-Authored-By: Claude Opus 4.6 --- .../IncompatibleBitMaskCheckSample.java | 46 +++++++++++++++++++ .../java/checks/IncompatibleBitMaskCheck.java | 19 ++------ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java index ae06cca08fe..fb19c6a2ada 100644 --- a/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java @@ -133,6 +133,42 @@ void parenthesizedExpressions(int x) { if (((x & 2)) == 2) {} // Compliant } + void bitwiseOrNotEqual(int x, long data) { + // != with OR where mask bits are not all present in value: always true + if ((x | 3) != 0) {} // Noncompliant {{This comparison is always true.}} + + // Long variant: OR with != always true when mask bits missing + if ((data | 0xFFL) != 0L) {} // Noncompliant {{This comparison is always true.}} + + // Constant on left side with != and OR + if (0 != (x | 1)) {} // Noncompliant {{This comparison is always true.}} + + // Compliant: value includes mask bits + if ((x | 1) != 1) {} // Compliant + } + + void longLiteralMaskOnLeft(long x) { + // Long literal as left operand of bitwise AND + if ((0xFFL & x) == 0x100L) {} // Noncompliant {{This comparison is always false.}} + + // Long literal as left operand of bitwise OR + if ((0xFFL | x) == 0L) {} // Noncompliant {{This comparison is always false.}} + + // != with long literal mask on left + if ((0xFFL & x) != 0x100L) {} // Noncompliant {{This comparison is always true.}} + + // Compliant long literal mask on left + if ((0xFFL & x) == 0x80L) {} // Compliant + } + + void longLiteralOnRightOfBitwiseOp(long x) { + // Long literal on right side of bitwise AND + if ((x & 0xFFL) == 0x100L) {} // Noncompliant {{This comparison is always false.}} + + // Long literal on left side of bitwise AND (exercises left-operand path in hasLongLiteral) + if ((0xFFL & x) == 0x80L) {} // Compliant + } + void intWidthHighBitMasks(int x) { // 0xFFFFFFFF as int is -1, so (x & 0xFFFFFFFF) == -1 is valid when x == -1 if ((x & 0xFFFFFFFF) == -1) {} // Compliant @@ -148,5 +184,15 @@ void intWidthHighBitMasks(int x) { // AND with 3 cannot produce -1 (only bits 0 and 1 can be set) if ((x & 3) == -1) {} // Noncompliant {{This comparison is always false.}} + + // != with int-width high-bit mask + if ((x & 3) != -1) {} // Noncompliant {{This comparison is always true.}} + } + + void comparisonValueOnLeftWithLong(long x) { + // Long constant on left side of comparison, bitwise on right + if (0x100L == (x & 0xFFL)) {} // Noncompliant {{This comparison is always false.}} + + if (0x80L == (x & 0xFFL)) {} // Compliant } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java index 03cdeceea35..d8dc9d4ce11 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java @@ -22,7 +22,6 @@ import org.sonar.java.model.ExpressionUtils; import org.sonar.java.model.LiteralUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; -import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.Tree; @@ -56,7 +55,7 @@ private void check(ExpressionTree possibleBitwiseOp, ExpressionTree possibleCons if (mask == null || value == null) { return; } - if (isIntOperation(bitwiseOp, possibleConstant)) { + if (isIntOperation(bitwiseOp)) { mask = (long) mask.intValue(); value = (long) value.intValue(); } @@ -77,20 +76,8 @@ private static Long extractMask(BinaryExpressionTree bitwiseOp) { return LiteralUtils.longLiteralValue(bitwiseOp.rightOperand()); } - private static boolean isIntOperation(BinaryExpressionTree bitwiseOp, ExpressionTree comparisonValue) { - Type type = bitwiseOp.symbolType(); - if (type.is("int")) { - return true; - } - if (type.is("long")) { - return false; - } - // Type is unknown (no-semantic mode): assume int if no long literals are involved - return !hasLongLiteral(bitwiseOp) && !comparisonValue.is(Kind.LONG_LITERAL); - } - - private static boolean hasLongLiteral(BinaryExpressionTree bitwiseOp) { - return bitwiseOp.leftOperand().is(Kind.LONG_LITERAL) || bitwiseOp.rightOperand().is(Kind.LONG_LITERAL); + private static boolean isIntOperation(BinaryExpressionTree bitwiseOp) { + return !bitwiseOp.symbolType().is("long"); } private static boolean isIncompatible(Kind bitwiseKind, long mask, long value) { From 8dc0a6dc5c308ad51eddfefea3b3c86570814cb7 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 18 Aug 2026 15:38:08 +0200 Subject: [PATCH 4/8] Fix S7438: sign-extend int literals and improve no-semantic mode - Sign-extend int hex literals (e.g. 0xFFFFFFFF) through int cast to match Java's actual sign-extension behavior when used in long context. Fixes false positives on patterns like (longX & 0xFFFFFFFF) == -1L. - Improve isIntOperation to use fullyQualifiedName() and fall back to a long-literal heuristic when semantic info is unavailable, preventing silent truncation of long operations to int width. - Add test cases for int mask sign extension and long literal edge cases. Co-Authored-By: Claude Opus 4.6 --- .../IncompatibleBitMaskCheckSample.java | 26 +++++++++ .../java/checks/IncompatibleBitMaskCheck.java | 57 +++++++++++++++++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java index fb19c6a2ada..d00e3c8100a 100644 --- a/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java @@ -195,4 +195,30 @@ void comparisonValueOnLeftWithLong(long x) { if (0x80L == (x & 0xFFL)) {} // Compliant } + + void intHexMaskSignExtensionInLongContext(long longX) { + // Int literal 0xFFFFFFFF sign-extends to -1L in long context, so (longX & -1L) == longX + // This comparison is valid when longX == -1 + if ((longX & 0xFFFFFFFF) == -1L) {} // Compliant + + // 0xFFFFFFFF sign-extends to -1L, AND with -1L is identity, so any value is reachable + if ((longX & 0xFFFFFFFF) == 4294967295L) {} // Compliant + + // 0x80000000 as int is MIN_VALUE, sign-extends to 0xFFFFFFFF_80000000L + // AND with that mask can produce 0x80000000L (the low 32 bits match) + if ((longX & 0x80000000) == 0x80000000L) {} // Compliant + + // Int mask 0x0F does not sign-extend (no high bit set), stays 0x0F + // AND with 0x0F cannot produce 0x100L + if ((longX & 0x0F) == 0x100L) {} // Noncompliant {{This comparison is always false.}} + } + + void longBitwiseOpsWithLongLiterals(long x) { + // OR with 0xFFL always sets low 8 bits; result must include those bits + // 0x10000000000L does not include any of the low 8 bits + if ((x | 0xFFL) == 0x10000000000L) {} // Noncompliant {{This comparison is always false.}} + + // Long AND mask: 0xFFL cannot produce value beyond 0xFF + if ((x & 0xFFL) == 0x10000000000L) {} // Noncompliant {{This comparison is always false.}} + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java index d8dc9d4ce11..119b2e0268b 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java @@ -25,6 +25,7 @@ import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.UnaryExpressionTree; import org.sonar.plugins.java.api.tree.Tree.Kind; @Rule(key = "S7438") @@ -51,11 +52,11 @@ private void check(ExpressionTree possibleBitwiseOp, ExpressionTree possibleCons } BinaryExpressionTree bitwiseOp = (BinaryExpressionTree) unwrapped; Long mask = extractMask(bitwiseOp); - Long value = LiteralUtils.longLiteralValue(possibleConstant); + Long value = signExtendedLongValue(possibleConstant); if (mask == null || value == null) { return; } - if (isIntOperation(bitwiseOp)) { + if (isIntOperation(bitwiseOp, possibleConstant)) { mask = (long) mask.intValue(); value = (long) value.intValue(); } @@ -67,17 +68,61 @@ private void check(ExpressionTree possibleBitwiseOp, ExpressionTree possibleCons } } + @Nullable + private static Long signExtendedLongValue(ExpressionTree operand) { + Long value = LiteralUtils.longLiteralValue(operand); + if (value != null && isIntLiteral(operand)) { + value = (long) value.intValue(); + } + return value; + } + @Nullable private static Long extractMask(BinaryExpressionTree bitwiseOp) { - Long leftValue = LiteralUtils.longLiteralValue(bitwiseOp.leftOperand()); + Long leftValue = maskOperandValue(bitwiseOp.leftOperand()); if (leftValue != null) { return leftValue; } - return LiteralUtils.longLiteralValue(bitwiseOp.rightOperand()); + return maskOperandValue(bitwiseOp.rightOperand()); + } + + @Nullable + private static Long maskOperandValue(ExpressionTree operand) { + Long value = LiteralUtils.longLiteralValue(operand); + if (value != null && isIntLiteral(operand)) { + value = (long) value.intValue(); + } + return value; } - private static boolean isIntOperation(BinaryExpressionTree bitwiseOp) { - return !bitwiseOp.symbolType().is("long"); + private static boolean isIntLiteral(ExpressionTree tree) { + ExpressionTree expr = ExpressionUtils.skipParentheses(tree); + if (expr.is(Kind.UNARY_MINUS, Kind.UNARY_PLUS)) { + expr = ((UnaryExpressionTree) expr).expression(); + } + return expr.is(Kind.INT_LITERAL); + } + + private static boolean isIntOperation(BinaryExpressionTree bitwiseOp, ExpressionTree comparisonValue) { + String typeName = bitwiseOp.symbolType().fullyQualifiedName(); + if ("long".equals(typeName)) { + return false; + } + if ("int".equals(typeName)) { + return true; + } + // No semantic information: use heuristic based on literal kinds + return !hasLongLiteral(bitwiseOp.leftOperand()) + && !hasLongLiteral(bitwiseOp.rightOperand()) + && !hasLongLiteral(comparisonValue); + } + + private static boolean hasLongLiteral(ExpressionTree tree) { + ExpressionTree expr = ExpressionUtils.skipParentheses(tree); + if (expr.is(Kind.UNARY_MINUS, Kind.UNARY_PLUS)) { + expr = ((UnaryExpressionTree) expr).expression(); + } + return expr.is(Kind.LONG_LITERAL); } private static boolean isIncompatible(Kind bitwiseKind, long mask, long value) { From 321eb00c72f799c99e2008176976404c414cc79f Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Wed, 19 Aug 2026 09:04:59 +0200 Subject: [PATCH 5/8] Fix S7438: remove duplicate maskOperandValue method Replace maskOperandValue calls in extractMask with signExtendedLongValue, which has an identical implementation. This removes the duplicate code flagged in review and fixes uncovered branches that prevented the Quality Gate from passing (84.5% < 90% threshold). Co-Authored-By: Claude Opus 4.6 --- .../java/checks/IncompatibleBitMaskCheck.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java index 119b2e0268b..9e4d0e4e463 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java @@ -79,20 +79,8 @@ private static Long signExtendedLongValue(ExpressionTree operand) { @Nullable private static Long extractMask(BinaryExpressionTree bitwiseOp) { - Long leftValue = maskOperandValue(bitwiseOp.leftOperand()); - if (leftValue != null) { - return leftValue; - } - return maskOperandValue(bitwiseOp.rightOperand()); - } - - @Nullable - private static Long maskOperandValue(ExpressionTree operand) { - Long value = LiteralUtils.longLiteralValue(operand); - if (value != null && isIntLiteral(operand)) { - value = (long) value.intValue(); - } - return value; + Long leftValue = signExtendedLongValue(bitwiseOp.leftOperand()); + return leftValue != null ? leftValue : signExtendedLongValue(bitwiseOp.rightOperand()); } private static boolean isIntLiteral(ExpressionTree tree) { From 713910b9edb49a47f962e8227c19b38d01633fc0 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Wed, 19 Aug 2026 09:30:47 +0200 Subject: [PATCH 6/8] Fix S7438: improve coverage for no-semantic heuristic path Add non-compiling test file with unresolvable types to exercise the literal-based heuristic in isIntOperation and all branches of hasLongLiteral. Co-Authored-By: Claude Opus 4.6 --- .../IncompatibleBitMaskCheckSample.java | 43 +++++++++++++++++++ .../checks/IncompatibleBitMaskCheckTest.java | 11 +++++ 2 files changed, 54 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/files/non-compiling/checks/IncompatibleBitMaskCheckSample.java diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/IncompatibleBitMaskCheckSample.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/IncompatibleBitMaskCheckSample.java new file mode 100644 index 00000000000..ac3c73e1da1 --- /dev/null +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/IncompatibleBitMaskCheckSample.java @@ -0,0 +1,43 @@ +package checks; + +class IncompatibleBitMaskCheckSample { + + // When semantic info is unavailable, the rule uses a heuristic based on literal kinds + // to decide if the operation is int or long. + + void noSemanticWithIntLiterals(UnknownType obj) { + // No long literals present: heuristic treats as int operation + if ((obj.getValue() & 0x0F) == 0x10) {} // Noncompliant {{This comparison is always false.}} + if ((obj.getValue() | 3) == 2) {} // Noncompliant {{This comparison is always false.}} + if ((obj.getValue() & 0x0F) == 0x04) {} // Compliant + if ((obj.getValue() & 1) != 2) {} // Noncompliant {{This comparison is always true.}} + } + + void noSemanticWithLongLiterals(UnknownType obj) { + // Long literal in mask: heuristic treats as long operation + if ((obj.getValue() & 0xFFL) == 0x100L) {} // Noncompliant {{This comparison is always false.}} + if ((obj.getValue() & 0xFFL) == 0x80L) {} // Compliant + } + + void noSemanticWithNegativeLongLiteral(UnknownType obj) { + // Negative long literal (unary minus on LONG_LITERAL) exercises hasLongLiteral unary path + if ((obj.getValue() & -1L) == 0x100L) {} // Compliant - mask is -1L (all bits), any value is reachable + if ((obj.getValue() | -1L) == 0L) {} // Noncompliant {{This comparison is always false.}} + } + + void noSemanticWithPositiveLongLiteral(UnknownType obj) { + // Positive unary plus on long literal exercises hasLongLiteral unary path + if ((obj.getValue() & +1L) == 2L) {} // Noncompliant {{This comparison is always false.}} + } + + void noSemanticLongLiteralInComparisonValue(UnknownType obj) { + // Long literal only in the comparison value, not in the mask + if ((obj.getValue() & 0x0F) == 0x100L) {} // Noncompliant {{This comparison is always false.}} + } + + void noSemanticLongLiteralAsLeftOperand(UnknownType obj) { + // Long literal as left operand of bitwise operation: hasLongLiteral(leftOperand) returns true + if ((0xFFL & obj.getValue()) == 0x100L) {} // Noncompliant {{This comparison is always false.}} + if ((0xFFL & obj.getValue()) == 0x80L) {} // Compliant + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/IncompatibleBitMaskCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/IncompatibleBitMaskCheckTest.java index e49be0ffa0c..d65980e6180 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/IncompatibleBitMaskCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/IncompatibleBitMaskCheckTest.java @@ -16,10 +16,12 @@ */ package org.sonar.java.checks; +import java.util.Collections; 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 IncompatibleBitMaskCheckTest { @@ -39,4 +41,13 @@ void test_without_semantic() { .withoutSemantic() .verifyIssues(); } + + @Test + void test_non_compiling_without_semantic() { + CheckVerifier.newVerifier() + .onFile(nonCompilingTestSourcesPath("checks/IncompatibleBitMaskCheckSample.java")) + .withCheck(new IncompatibleBitMaskCheck()) + .withClassPath(Collections.emptyList()) + .verifyIssues(); + } } From a1f119b74c7ef41c7181dca4b8d9d34f0fc886f7 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Wed, 19 Aug 2026 13:57:32 +0200 Subject: [PATCH 7/8] Fix S7438: handle parenthesized literals, int-vs-long comparison, and unsigned long masks - Unwrap parentheses in signExtendedLongValue via parseLongLiteral so expressions like (x & (1)) == 2 are correctly detected - Detect incompatibility when int bitwise result is compared to a long literal outside int range (e.g. (intVar & 0xFFFFFFFF) == 0xFFFFFFFFL) - Add parseLongLiteral helper using Long.parseUnsignedLong to handle hex long literals above Long.MAX_VALUE (e.g. 0xFFFF_FFFF_FFFF_FFFEL) Co-Authored-By: Claude Opus 4.6 --- .../IncompatibleBitMaskCheckSample.java | 32 ++++++++++++ .../java/checks/IncompatibleBitMaskCheck.java | 50 +++++++++++++++++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java index d00e3c8100a..235b2ecbc3c 100644 --- a/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/IncompatibleBitMaskCheckSample.java @@ -221,4 +221,36 @@ void longBitwiseOpsWithLongLiterals(long x) { // Long AND mask: 0xFFL cannot produce value beyond 0xFF if ((x & 0xFFL) == 0x10000000000L) {} // Noncompliant {{This comparison is always false.}} } + + void parenthesizedLiterals(int x) { + // Parenthesized mask literal + if ((x & (1)) == 2) {} // Noncompliant {{This comparison is always false.}} + + // Parenthesized comparison value + if ((x & 1) == (2)) {} // Noncompliant {{This comparison is always false.}} + + // Parenthesized hex mask and value + if ((x & (0xFF)) == (0x1FF)) {} // Noncompliant {{This comparison is always false.}} + + // Compliant with parenthesized literals + if ((x & (3)) == (2)) {} // Compliant + } + + void intOperationWithLongComparisonValue(int intVar) { + // int bitwise result is promoted to long for comparison with long literal + // (intVar & 0xFFFFFFFF) is int -1, promoted to long -1L, which != 0xFFFFFFFFL (4294967295) + if ((intVar & 0xFFFFFFFF) == 0xFFFFFFFFL) {} // Noncompliant {{This comparison is always false.}} + + // != variant + if ((intVar & 0xFFFFFFFF) != 0xFFFFFFFFL) {} // Noncompliant {{This comparison is always true.}} + } + + void unsignedHighBitLongMasks(long flags) { + // 0xFFFF_FFFF_FFFF_FFFEL has all bits set except LSB + // AND with that mask clears bit 0 only, so result can never be -1L (all bits set) + if ((flags & 0xFFFF_FFFF_FFFF_FFFEL) == -1L) {} // Noncompliant {{This comparison is always false.}} + + // 0x8000_0000_0000_0000L is Long.MIN_VALUE, AND with it can produce 0L + if ((flags & 0x8000_0000_0000_0000L) == 0L) {} // Compliant + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java index 9e4d0e4e463..52ae7e8c989 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java @@ -24,6 +24,7 @@ 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.LiteralTree; import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.UnaryExpressionTree; import org.sonar.plugins.java.api.tree.Tree.Kind; @@ -58,25 +59,64 @@ private void check(ExpressionTree possibleBitwiseOp, ExpressionTree possibleCons } if (isIntOperation(bitwiseOp, possibleConstant)) { mask = (long) mask.intValue(); + if (hasLongLiteral(possibleConstant)) { + // int bitwise result is sign-extended to long for comparison; + // if the long value is outside int range, the comparison is always incompatible + if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) { + reportIncompatible(comparison); + return; + } + } value = (long) value.intValue(); } if (isIncompatible(unwrapped.kind(), mask, value)) { - String message = comparison.is(Kind.EQUAL_TO) - ? "This comparison is always false." - : "This comparison is always true."; - reportIssue(comparison.operatorToken(), message); + reportIncompatible(comparison); } } + private void reportIncompatible(BinaryExpressionTree comparison) { + String message = comparison.is(Kind.EQUAL_TO) + ? "This comparison is always false." + : "This comparison is always true."; + reportIssue(comparison.operatorToken(), message); + } + @Nullable private static Long signExtendedLongValue(ExpressionTree operand) { - Long value = LiteralUtils.longLiteralValue(operand); + Long value = parseLongLiteral(operand); if (value != null && isIntLiteral(operand)) { value = (long) value.intValue(); } return value; } + @Nullable + private static Long parseLongLiteral(ExpressionTree expr) { + ExpressionTree unwrapped = ExpressionUtils.skipParentheses(expr); + Long value = LiteralUtils.longLiteralValue(unwrapped); + if (value != null) { + return value; + } + // Handle unsigned hex long literals above Long.MAX_VALUE (e.g. 0xFFFF_FFFF_FFFF_FFFEL) + ExpressionTree literal = unwrapped; + if (literal.is(Kind.UNARY_MINUS, Kind.UNARY_PLUS)) { + literal = ((UnaryExpressionTree) literal).expression(); + } + if (literal.is(Kind.LONG_LITERAL)) { + String text = ((LiteralTree) literal).value(); + text = text.substring(0, text.length() - 1).replace("_", ""); + if (text.startsWith("0x") || text.startsWith("0X")) { + try { + long parsed = Long.parseUnsignedLong(text.substring(2), 16); + return unwrapped.is(Kind.UNARY_MINUS) ? -parsed : parsed; + } catch (NumberFormatException e) { + return null; + } + } + } + return null; + } + @Nullable private static Long extractMask(BinaryExpressionTree bitwiseOp) { Long leftValue = signExtendedLongValue(bitwiseOp.leftOperand()); From 43975cf851fa8ab8ac879e5f270ad271750dc683 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Wed, 19 Aug 2026 14:19:49 +0200 Subject: [PATCH 8/8] Fix S7438: resolve SonarQube issues S125 and S1066 Merge nested if statements (S1066) and reword comment to not look like commented-out code (S125). Co-Authored-By: Claude Opus 4.6 --- .../sonar/java/checks/IncompatibleBitMaskCheck.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java index 52ae7e8c989..f87e050c6e0 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java @@ -59,13 +59,11 @@ private void check(ExpressionTree possibleBitwiseOp, ExpressionTree possibleCons } if (isIntOperation(bitwiseOp, possibleConstant)) { mask = (long) mask.intValue(); - if (hasLongLiteral(possibleConstant)) { - // int bitwise result is sign-extended to long for comparison; - // if the long value is outside int range, the comparison is always incompatible - if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) { - reportIncompatible(comparison); - return; - } + // When comparing an int bitwise result against a long literal outside int range, the comparison is always incompatible + // because the int result is sign-extended to long and can never match a value outside the int range. + if (hasLongLiteral(possibleConstant) && (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE)) { + reportIncompatible(comparison); + return; } value = (long) value.intValue(); }