Skip to content

SONARJAVA-7215 Do not report S2695 when part of the query cannot be resolved - #6379

Merged
asya-vorobeva merged 5 commits into
masterfrom
asya/s2695-unresolved-query-fragment
Oct 26, 2026
Merged

asya-vorobeva merged 5 commits into
masterfrom
asya/s2695-unresolved-query-fragment

Conversation

@asya-vorobeva

@asya-vorobeva asya-vorobeva commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

The placeholder count was a lower bound compared as if it were an upper bound: an unresolvable query fragment was counted as zero placeholders, so the rule asserted "has no parameters" on queries it had not resolved.

  • propagate unknown through concatenation and "+=" instead of substituting zero for an unresolved fragment
  • resolve a conditional expression as the maximum of its branches
  • count over the folded constant via asConstant, which resolves cross-class constants, whole constant chains and text blocks

Summary by Gitar

  • Primitive fragments:
    • Numbers and booleans concatenated into a query are treated as adding no placeholders, keeping the count exact
    • Characters are excluded because '?' is a valid placeholder
  • Test coverage:
    • Added samples for unknown fragments, ternaries, constants from other classes, and numeric concatenation
  • Known limitations:
    • A char constant '?' concatenated into a query is a false positive, tracked by SONARJAVA-7228

This will update automatically on new commits.

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

SONARJAVA-7215

@datadog-sonarsource

datadog-sonarsource Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Pipelines

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 1f8701a | Docs | Give us feedback!

…esolved

The placeholder count was a lower bound compared as if it were an upper
bound: an unresolvable query fragment was counted as zero placeholders,
so the rule asserted "has no parameters" on queries it had not resolved.

- propagate unknown through concatenation and "+=" instead of
  substituting zero for an unresolved fragment
- resolve a conditional expression as the maximum of its branches
- count over the folded constant via asConstant, which resolves
  cross-class constants, whole constant chains and text blocks

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@asya-vorobeva
asya-vorobeva force-pushed the asya/s2695-unresolved-query-fragment branch from 150fbea to e3724dd Compare October 21, 2026 12:07
…ment

Propagating "unknown" through every unresolved operand went too far: a
non-constant number or boolean concatenated into the query made the whole
count unknown, dropping correct reports on queries such as
"... WHERE id = ? LIMIT " + limit.

Such an operand cannot contribute a placeholder whatever its runtime
value, so it now counts as zero and the surrounding count stays exact.
Characters are excluded, since '?' is a valid one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@asya-vorobeva

Copy link
Copy Markdown
Contributor Author

/pvf

@hashicorp-vault-sonar-prod

Copy link
Copy Markdown
Contributor

📊 Performance Validation complete
🆕 0 new · 🗑️ 0 lost issues
View dashboard

@lijun-chen-sonarsource lijun-chen-sonarsource 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.

Overall good improvements!
I left some comments and questions.

asya-vorobeva and others added 2 commits October 23, 2026 11:10
A char concatenated into the query was treated as wholly unknown, which
made the surrounding count unknown too. But a char is one character, so
it adds at most one placeholder: the count is bounded, not unresolvable,
and an index past that bound is still reportable.

Replace the boolean guard with a bound - zero for a number or boolean,
one for a character, unknown for anything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ONARJAVA-7228

Constant folding renders a char as its numeric value, so a query built as
"WHERE id = " + QUESTION_MARK resolves to "WHERE id = 63" and the rule
reports a valid index 1 as out of range.

The defect is in ExpressionUtils, not in the check: it corrupts every
asConstant(String.class) caller that folds a concatenation holding a char
constant. Record the false positive here and fix it in SONARJAVA-7228.

Also drop conditionalClauseGuardedBySameCondition: its ternary path is
already covered by ternaryWithDifferentBranches and its constant path by
constantsFromAnotherClass, and the rule does not inspect the enclosing
condition its name refers to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@asya-vorobeva
asya-vorobeva force-pushed the asya/s2695-unresolved-query-fragment branch from 3d74a5a to 1f8701a Compare October 26, 2026 07:39
@gitar-bot

gitar-bot Bot commented Oct 26, 2026

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

🟡 Medium risk · Updating S2695 placeholder counting for unresolved fragments and conditional branches could suppress valid excess-parameter findings if the new bounds are wrong.

Fixes S2695 false positives by propagating unknown placeholder counts through concatenation, +=, and conditional expressions instead of treating unresolved query fragments as zero, and by counting over folded constants via asConstant. Numeric and boolean fragments add no placeholders, and known char literals are counted as possible '?' placeholders. Two resolved findings addressed the numeric-operand and char-literal handling. Remaining known limitations, the escaped '?' char literal case and the '?' char constant false positive tracked as SONARJAVA-7228, were closed without a fix. No open issues found.

✅ 4 closed
✅ Edge Case: Numeric operands now make the whole query unknown, hiding real issues

📄 java-checks/src/main/java/org/sonar/java/checks/PreparedStatementAndResultSetCheck.java:118-132 📄 java-checks/src/main/java/org/sonar/java/checks/PreparedStatementAndResultSetCheck.java:174-179 📄 java-checks-test-sources/default/src/main/java/checks/PreparedStatementAndResultSetCheckSample.java:318-329
getNumberQuery now returns null for anything that is not a resolvable String, and sumOrNull passes that null up through the whole concatenation. A non-constant numeric operand therefore makes the entire query count unknown. Take "SELECT * FROM t WHERE id = ? LIMIT " + limit where limit is an int parameter, or ... + getIntValue(). The limit identifier has no String assignment, so handleVariableUsedAsQuery returns null, the sum becomes null, and ps.setInt(2, ...) is no longer reported. Before this PR it was reported, correctly.

The PR justifies returning null by saying an unresolved fragment "may itself hold parameter placeholders". That doesn't hold for primitive numeric or boolean values (or their boxed types): their string form can never contain ?. ternaryWithNonConstantFragment (the + maxTurns + case) treats this as "part of the query is unknown", but that test would still pass if an int counted as 0, since 2 + max(2, 0) = 4. So nothing tests the lost case.

Suggested fix: in getNumberQuery, when the expression has a primitive numeric/boolean type or its boxed type, return 0 before falling back to null. Leave char/Character out, since a char can be ?.

✅ Edge Case: Known char literals are counted as a possible '?' placeholder

📄 java-checks/src/main/java/org/sonar/java/checks/PreparedStatementAndResultSetCheck.java:122-125 📄 java-checks/src/main/java/org/sonar/java/checks/PreparedStatementAndResultSetCheck.java:184-192
unresolvedPlaceholderBound gives any char-typed operand an upper bound of 1, and it runs before countQuery. That covers char literals whose value is already known, such as ')' or ','. resolveAsConstant has no CHAR_LITERAL branch, so a literal can never be resolved to an exact count anyway. Example: in "SELECT * FROM t WHERE id = ? LIMIT " + limit + ')' (with int limit), the count becomes 2, so ps.setInt(2, ..) is no longer reported. On master this case counted exactly 1 and raised the issue. The fix is to resolve a char literal exactly: it adds 1 only when its value is '?'. Keep the bound of 1 only for char values that really are unknown.

Closed: Quality: charLocalVariableFragment comment misstates what the test exercises

📄 java-checks-test-sources/default/src/main/java/checks/PreparedStatementAndResultSetCheckSample.java:391-398 📄 java-checks/src/main/java/org/sonar/java/checks/PreparedStatementAndResultSetCheck.java:190-193
The comment says the literal "is read directly" and that this is why the case resolves correctly. In fact, questionMark is an IDENTIFIER of type char, so unresolvedPlaceholderBound returns the type-based bound of 1 without looking at the initializer. If you change the local to 'y', the count is still 1, so the test does not check that the value is resolved. The claim "a local is never a compile-time constant" is also wrong per the JLS: a final local initialized with a constant expression is a constant variable. The real reason this case avoids the charConstantFragment false positive is that JVariableSymbol.constantValue() only folds static final fields. Fix: reword the comment to say the char-typed fragment is bounded at one placeholder. Or, if you want to check value resolution, add a 'y' variant that expects the FN.

Closed: Edge Case: Escaped '?' char literals are counted as zero placeholders

📄 java-checks/src/main/java/org/sonar/java/checks/PreparedStatementAndResultSetCheck.java:187-189
unresolvedPlaceholderBound compares the raw token text of a char literal with "'?'". LiteralTree.value() gives back the source text, so a literal that evaluates to '?' but is written another way, such as '\77' (octal) or '\u003F' (unicode escape), returns 0. Before this commit every char returned 1, so this is a new under-count. For example, "... id = " + '\77' with ps.setInt(1, ...) would now be reported as "has no parameters". Fix: return 0 only when the literal is a single non-escaped character other than '?', and keep the bound of 1 for any escape sequence. Decoding the escape would also work.

Verification Rules 2/2 met

Functional validation No results

Rules 2 passed 1 not applicable

✅ Internal artifact registries
✅ Repository license compliance

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

Options

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

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

@sonarqube-next

Copy link
Copy Markdown
Contributor

@asya-vorobeva
asya-vorobeva merged commit 78d6706 into master Oct 26, 2026
21 checks passed
@asya-vorobeva
asya-vorobeva deleted the asya/s2695-unresolved-query-fragment branch October 26, 2026 09:25
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.

3 participants