Skip to content

SONARJAVA-6768 Implement new rule S7438: Incompatible bit masks should not be used in comparisons - #5939

Merged
romainbrenguier merged 8 commits into
masterfrom
new-rule/SONARJAVA-6768-S7438
Aug 19, 2026
Merged

SONARJAVA-6768 Implement new rule S7438: Incompatible bit masks should not be used in comparisons#5939
romainbrenguier merged 8 commits into
masterfrom
new-rule/SONARJAVA-6768-S7438

Conversation

@romainbrenguier

Copy link
Copy Markdown
Contributor

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.

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.
@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

SONARJAVA-6768

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Comment thread java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java Outdated
- 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 <noreply@anthropic.com>
romainbrenguier and others added 2 commits August 19, 2026 09:04
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@romainbrenguier
romainbrenguier marked this pull request as ready for review August 19, 2026 07:53

@nathsou nathsou left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice rule idea! Found 3 issues in IncompatibleBitMaskCheck

}
if (isIntOperation(bitwiseOp, possibleConstant)) {
mask = (long) mask.intValue();
value = (long) value.intValue();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not truncate a long comparison operand

An int bitwise result is promoted to long for comparison; Java does not truncate the RHS. For if ((x & 0xFFFFFFFF) == 0xFFFFFFFFL), the condition is always false, but both values become -1 here and no issue is raised.

isIntOperation() returns true from the known-type branch whenever bitwiseOp.symbolType() is int, without checking whether the comparison operand is itself a long (unlike the heuristic fallback branch, which does call hasLongLiteral(comparisonValue)). Please preserve the comparison operand's width / account for the sign-extended int result in the known-type branches too.


@Nullable
private static Long signExtendedLongValue(ExpressionTree operand) {
Long value = LiteralUtils.longLiteralValue(operand);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Unwrap parentheses before parsing literals

LiteralUtils.longLiteralValue only unwraps UNARY_MINUS/UNARY_PLUS before requiring INT_LITERAL/LONG_LITERAL; it never calls ExpressionUtils.skipParentheses. So if ((x & (1)) == 2) and if ((x & 1) == (2)) are silently skipped despite being always false. isIntLiteral/hasLongLiteral elsewhere in this file already call skipParentheses, so this looks like an oversight rather than intentional.


@Nullable
private static Long signExtendedLongValue(ExpressionTree operand) {
Long value = LiteralUtils.longLiteralValue(operand);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Support unsigned high-bit long masks

Long.decode throws NumberFormatException for hex literals above Long.MAX_VALUE, such as 0xFFFF_FFFF_FFFF_FFFEL (this is even called out in a comment in LiteralUtils.java). Consequently if ((flags & 0xFFFF_FFFF_FFFF_FFFEL) == -1L) is missed, since longLiteralValue returns null and the check bails out via the mask == null || value == null guard.

romainbrenguier and others added 2 commits August 19, 2026 13:57
… 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 <noreply@anthropic.com>
Merge nested if statements (S1066) and reword comment to not
look like commented-out code (S125).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqube-next

Copy link
Copy Markdown
Contributor

@romainbrenguier
romainbrenguier merged commit b1924eb into master Aug 19, 2026
16 checks passed
@romainbrenguier
romainbrenguier deleted the new-rule/SONARJAVA-6768-S7438 branch August 19, 2026 13:09
@gitar-bot

gitar-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 4 resolved / 4 findings

Implements new rule S7438 to detect incompatible bit masks in comparisons, addressing sign mismatches on int masks, missing sign extension in long comparisons, and truncation issues in no-semantic mode.

✅ 4 resolved
Edge Case: Sign mismatch on int masks can cause false positives

📄 java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java:66-80
LiteralUtils.longLiteralValue decodes an int hex literal such as 0xFFFFFFFF to the positive long 4294967295, but at runtime int x & 0xFFFFFFFF treats the literal as the int value -1. Because isIncompatible compares the mask and value as longs without considering the int operand width, a comparison like (x & 0xFFFFFFFF) == -1 (which is true when x == -1) is reported as "always false". This is a false positive on a Blocker BUG rule. Consider restricting extracted mask/value handling to the operand's actual type (mask the value to 32 bits when the bitwise expression is int), or bail out when the constants require sign-extension reasoning; add test cases with negative literals and high-bit hex int masks.

Edge Case: int hex mask not sign-extended in long comparisons → false positive

📄 java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java:59-62 📄 java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java:72-86
isIntOperation returns false for long-typed bitwise operations, so no width normalization is applied. But an int hex literal such as 0xFFFFFFFF used in a long operation is sign-extended by Java: (longX & 0xFFFFFFFF) really masks with -1L, not 4294967295L. extractMask/longLiteralValue decode 0xFFFFFFFF as 4294967295L, so if ((longX & 0xFFFFFFFF) == -1L) {} is incorrectly reported as "always false" even though it is true when longX == -1. Fix by sign-extending mask (and value) operands that are INT_LITERALs to long width regardless of the operation width, e.g. compute the mask as (long)(int)decoded when the mask operand is an int literal.

Edge Case: Long bitwise ops truncated to int in no-semantic mode

📄 java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java:79-81 📄 java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java:58-61
The simplified isIntOperation returns !symbolType().is("long"). When semantics are unavailable, symbolType() is unknown and .is("long") returns false, so the operation is treated as int and both mask and value are truncated via intValue(). The old code guarded this case with hasLongLiteral, so long bitwise expressions using literals above the int range (e.g. (x | 0xFFFFFFFFFFL) == 0x10000000000L) are now silently truncated, producing incorrect results (missed detections) without semantics. Consider restoring the long-literal check for the unknown-type branch, e.g. treat as long when either bitwise operand or the comparison value is a LONG_LITERAL.

Quality: signExtendedLongValue and maskOperandValue are identical

📄 java-checks/src/main/java/org/sonar/java/checks/IncompatibleBitMaskCheck.java:71-85
The two private static helpers signExtendedLongValue (L71-78) and maskOperandValue (L89-96) have byte-for-byte identical bodies (fetch longLiteralValue, sign-extend when isIntLiteral). Collapse them into a single helper to avoid divergence during future maintenance.

Implementation Status ✅ 1 / 1 issues implemented
SONARJAVA-6768 — 1 / 1 objectives

The PR implements the new rule S7438 checking for incompatible bit masks in comparisons.

✅ 1 complete
  • ✅ Implement new rule S7438: Incompatible bit masks should not be used in comparisons
Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants