Repository navigation
SONARJAVA-7243: Improve S8989 to ignore checked exceptions thrown before any write - #6401
Conversation
…ns are thrown before any write Use the control flow graph of the method to skip the issue when every point that may throw a checked exception is executed before any write operation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…writes in S8989 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
/pvf S8989 |
|
📊 Performance Validation complete |
nathsou
left a comment
There was a problem hiding this comment.
Approving: the CFG-based approach removes the reported false positive. A few gaps where the new suppression now hides true positives; each case below was reproduced with a CheckVerifier probe and is reported on master. The underlying asymmetry is that throw points are over-approximated (unknown methods count as throwing) while writes are under-approximated (names and a list of persistence types).
Also, the S8989 RSPEC has an explicit == Exceptions list (NOT_SUPPORTED, readOnly = true). The new suppression should probably be documented there in a companion RSPEC PR, as was done for SONARJAVA-7238 and SONARJAVA-7239.
| toVisit.addAll(block.exceptions()); | ||
| } | ||
|
|
||
| private static boolean isWriteOperation(Tree tree) { |
There was a problem hiding this comment.
Method references are not seen as writes: the visitor only checks method invocations. This is suppressed, although the first forEach may have saved items when validate throws:
list.forEach(repository::save);
Checker.validate(list); // throws IOException
repository.save(list);There was a problem hiding this comment.
Fixed in a5d6027: method references whose method is a potential write (e.g. repository::save) are now considered as writes. Added a Noncompliant sample for this case.
| * or when it is invoked on a persistence API (EntityManager, JdbcTemplate, repository, ...) and its name | ||
| * does not look like a read operation. | ||
| */ | ||
| private static boolean isPotentialWrite(MethodInvocationTree mit) { |
There was a problem hiding this comment.
Design question: writes made through calls that don't match a prefix or a persistence type are invisible here, so this is suppressed too:
service.placeOrder(o); // writes, joins the same transaction
Checker.validate(o); // throws IOException
repository.save(o);Similarly, line 413 treats unresolved methods as "not a write", while mayThrowCheckedException treats them as "may throw". Would it make sense to be conservative on the write side as well (e.g. count unresolved calls and calls to project code as potential writes), or is this trade-off intended?
There was a problem hiding this comment.
The current version risks FNs but being conservative on the write side would mean potential FPs. So this is intended that way.
| * The control flow graph does not model the implicit close() calls of try-with-resources, which are executed | ||
| * after the body and may throw checked exceptions. | ||
| */ | ||
| private static boolean isInTryWithResourcesClosingWithCheckedException(Tree tree) { |
There was a problem hiding this comment.
This only covers writes nested inside the try-with-resources. A resource closed after the write is missed when its creation does not throw:
if (o == null) {
throw new IOException();
}
repository.save(o);
try (Res r = new Res()) { // Res.close() throws IOException
r.hashCode();
}Treating a resource whose close() may throw a checked exception as a throw point itself (in isCheckedThrowPoint) would cover both cases and replace this parent walk.
There was a problem hiding this comment.
Fixed in a5d6027: a resource whose close() may throw a checked exception is now a throw point itself, which covers writes made before the try. The parent walk is kept for writes nested in the body, since the CFG places the resource declaration before the body and not after it. Added Noncompliant and Compliant samples.
…ites in S8989 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@nathsou Regarding the RSPEC |
Co-authored-by: Romain Brenguier <11854194+romainbrenguier@users.noreply.github.com>
Code Review ✅ Approved 3 closed / 3 findings🟡 Medium risk · The CFG-based S8989 exception/write analysis could misclassify transaction paths, suppressing needed rollback warnings or producing false positives. Improves S8989 to skip reporting when all checked exceptions are thrown before any write, using the method's control flow graph to detect writes and throw points. Resolved issues include implicit close() in try-with-resources not being counted as a throw point, false negatives for writes not matching the name prefixes, and writes in catch/finally of try-with-resources being wrongly treated as pre-close. No open issues remain. ✅ 3 closed✅ Bug: Implicit close() in try-with-resources is not counted as a throw point
✅ Edge Case: Writes not matching the name prefixes before a throw now cause false negatives
✅ Edge Case: Writes in catch/finally of try-with-resources wrongly treated as pre-close
Verification Rules 2/2 met Repos 5 inspected · PRs 1 inspectedFunctional validation No results Rules 2 passed 1 not applicable
Broader impact Repos 5 inspected PRs 1 inspectedCross-repo 5 repositories inspected 0 open impacts
Cross-PR 1 PR inspected 0 open impacts
Auto-approval Not enabledFor more verification details, comment OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
|




Summary
Fixes a false positive of S8989 (methods with
@Transactionalshould specify rollback behavior for checked exceptions): when all checked exceptions are thrown before any save is made, no write can be rolled back, so the rollback configuration is irrelevant.The check now uses the method's control flow graph:
save,delete,update,persist,merge,flush,insert).throwstatements of a checked (or unknown) type, and invocations/constructors that declare checked (or unknown) exceptions, or whose symbol is unknown.catchblocks).Methods without a body, without detected writes, or without any detected throw point keep the previous behavior.
Tests
TransactionalMethodCheckedExceptionCheckSample.java(throws before writes, writes before throws, loops, lambdas, try/catch rethrow, unchecked throws).🤖 Generated with Claude Code
Agent workflow
Tool link: https://github.com/SonarSource/languages-experimental-tooling/tree/romain/my-tickets/personal/romain-brenguier
PR created using
uv run create_with_claude.py SONARJAVA-7243.txtAddressed review comments in pr_report_6401.md using
uv run address_reviews.py pr_report_6401.mdSummary by Gitar
EntityManager,Query,JdbcOperationsand repositories as writes unless the name is a readclose()of resources declaring checked exceptions as throw pointsrepository::saveas write operationsThis will update automatically on new commits.
Addressed review comments in pr_report_6401.md using
uv run address_reviews.py pr_report_6401.md