Repository navigation
SONARJAVA-7243: Improve S8989 to ignore checked exceptions thrown before any write - #6401
romainbrenguier wants to merge 1 commit into
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>
Code Review
|
| Auto-apply | Compact | Unblock |
|
|
|
Was this helpful? React with 👍 / 👎 | Gitar
| private static boolean allCheckedExceptionsThrownBeforeWrites(MethodTree method) { | ||
| ControlFlowGraph cfg = method.cfg(); | ||
| if (cfg == null) { | ||
| return false; | ||
| } | ||
| boolean hasWrite = false; | ||
| boolean hasThrowPoint = false; | ||
| for (ControlFlowGraph.Block block : cfg.blocks()) { | ||
| List<Tree> items = blockItems(block); | ||
| for (int i = 0; i < items.size(); i++) { | ||
| hasThrowPoint |= isCheckedThrowPoint(items.get(i)); | ||
| if (isWriteOperation(items.get(i))) { | ||
| hasWrite = true; | ||
| if (items.subList(i, items.size()).stream().anyMatch(TransactionalMethodCheckedExceptionCheck::isCheckedThrowPoint) | ||
| || hasReachableThrowPoint(block)) { |
There was a problem hiding this comment.
⚠️ Bug: Implicit close() in try-with-resources is not counted as a throw point
The CFG does not model the implicit close() calls of try-with-resources. CFG.java builds only the resource list and says so in a comment: "FIXME only path with no failure constructed for now, (not taking try with resources into consideration)". So hasReachableThrowPoint never sees the close() that runs after the body. Example: try (BufferedReader r = Files.newBufferedReader(p)) { repository.save(o); } in a method declaring throws IOException. newBufferedReader is a checked throw point before the write, and nothing checked is found after save, so the issue is now skipped. But r.close() can throw IOException after the save, and Spring will then commit the write instead of rolling it back. Fix: if a write sits inside a try-with-resources whose resource types have a close() that declares checked exceptions, treat it as having a reachable throw point, or skip the optimisation when the method contains try-with-resources. Add a Noncompliant sample for this case.
Was this helpful? React with 👍 / 👎
| for (ControlFlowGraph.Block block : cfg.blocks()) { | ||
| List<Tree> items = blockItems(block); | ||
| for (int i = 0; i < items.size(); i++) { | ||
| hasThrowPoint |= isCheckedThrowPoint(items.get(i)); | ||
| if (isWriteOperation(items.get(i))) { | ||
| hasWrite = true; | ||
| if (items.subList(i, items.size()).stream().anyMatch(TransactionalMethodCheckedExceptionCheck::isCheckedThrowPoint) | ||
| || hasReachableThrowPoint(block)) { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return hasWrite && hasThrowPoint; |
There was a problem hiding this comment.
💡 Edge Case: Writes not matching the name prefixes before a throw now cause false negatives
The issue is skipped as soon as every detected write comes after every checked throw point. Writes are detected only by method-name prefix (save, delete, update, ...). Any write that doesn't match a prefix and runs before a checked throw is invisible, and the issue is wrongly suppressed. Example: em.createQuery("UPDATE ...").executeUpdate(); Validator.validate(o); /* throws IOException */ repository.save(o);. Here executeUpdate, jdbcTemplate.batchUpdate and execute don't match a prefix, save does, and validate comes before save, so the check returns true. The executeUpdate write is committed when IOException is thrown. Before this PR the method was always reported. Fix: only suppress when no method invocation of unknown write status (e.g. any non-getter call on a repository, EntityManager or JdbcTemplate receiver) runs before the last throw point, or widen the write detection used for this suppression.
Was this helpful? React with 👍 / 👎
|




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.txt