Skip to content

SONARJAVA-6892: S8989 Fix FP for class-level @Transactional when no transaction occurs - #6076

Open
romainbrenguier wants to merge 2 commits into
masterfrom
romain/sonarjava-6892
Open

SONARJAVA-6892: S8989 Fix FP for class-level @Transactional when no transaction occurs#6076
romainbrenguier wants to merge 2 commits into
masterfrom
romain/sonarjava-6892

Conversation

@romainbrenguier

Copy link
Copy Markdown
Contributor

Summary

  • When @Transactional is at the class level, only raise S8989 when the method likely performs a transactional operation
  • A method is considered transactional if its name or the name of any method it calls starts with: save, delete, update, persist, merge, or flush
  • Methods with method-level @Transactional are unaffected and always raise as before

Test plan

  • Existing tests for method-level @Transactional still pass
  • Class-level @Transactional with transactional method names (e.g., saveOrder, deleteRecord) raises issues
  • Class-level @Transactional with non-transactional method names (e.g., noConfig, readData) no longer raises
  • Class-level @Transactional with transactional method calls (e.g., calling saveOrder() inside body) raises issues
  • test_without_semantic and test_noDependencies tests pass

🤖 Generated with Claude Code

…ansaction occurs

When @transactional is at the class level, only raise when the method
likely performs a transactional operation: either the method name or a
called method name starts with save, delete, update, persist, merge,
or flush.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

SONARJAVA-6892

public void noConfig() throws IOException { // Compliant - method name does not suggest a transactional operation
}

public void saveOrder() throws IOException { // Noncompliant [[secondary=108]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: New/moved Noncompliant markers carry stale secondary= line numbers

secondary=NNN without a +/- sign is an absolute source line and is strictly asserted (Expectations.relativeValueToInt -> InternalCheckVerifier.validateSecondaryLocations, which fails on any expected line not reported). The markers added or shifted by this PR are mutually inconsistent: lines 113-133 use secondary=108, which is exactly the class-level @Transactional line (108), while the other new/relocated ones keep pre-shift values that point at unrelated lines — line 174 says 155 but its annotation is at 169, line 190 says 169 but the interface annotation is at 186, line 238 says 207 (annotation at 224), line 312 says 290 (annotation at 304), and lines 319/332/340 say 297 (annotation at 317). At least one of these two groups cannot match, so TransactionalMethodCheckedExceptionCheckTest.test will fail on the secondary-location assertion; recompute every touched secondary= against the line of the reporting @Transactional annotation (or switch them to relative -N form so they survive future line shifts).

Point each new marker at the absolute line of the class-level @transactional annotation that is reported as the secondary location.:

  public void saveNestedEntity() throws IOException { // Noncompliant [[secondary=169]]

void saveEntity() throws IOException; // Noncompliant [[secondary=186]]

public void savePublicInClassLevel() throws IOException { // Noncompliant [[secondary=224]]

public void deleteWithAnnotations() throws IOException { // Noncompliant [[secondary=304]]

public void processOrder(Object repository) throws IOException { // Noncompliant [[secondary=317]]
public void delegateToFlush() throws IOException { // Noncompliant [[secondary=317]]
public void delegateToDelete(Object entity) throws IOException { // Noncompliant [[secondary=317]]
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@Rule(key = "S8989")
public class TransactionalMethodCheckedExceptionCheck extends IssuableSubscriptionVisitor implements DependencyVersionAware {

private static final List<String> TRANSACTIONAL_PREFIXES = List.of("save", "delete", "update", "persist", "merge", "flush");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Prefix match has no word boundary, so unrelated names match

hasTransactionalPrefix lowercases the whole identifier and tests raw startsWith, so purely non-persistence names such as updateUiLabel, deleteTempFile, mergeSortResults, flushOutputStream, or a call to savedFilterCount() are classified as transactional and still raise, while the equally common persistence verbs (insert, create, remove, store, executeUpdate) never match. Requiring the prefix to be followed by an upper-case character or end-of-name (e.g. save, saveOrder but not savedSearch) and adding the missing verbs would make the heuristic match the intent described in the PR.

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 2 findings

Fixes false positives for class-level @Transactional by only raising S8989 when methods have transactional names or call transactional methods. However, test markers in TransactionalMethodCheckedExceptionCheckSample.java have stale secondary= line numbers that will cause test failures—recompute them against the actual annotation locations. Additionally, the prefix matching lacks word boundaries, so names like updateUiLabel and deleteTempFile are incorrectly classified as transactional; tighten the check to require an upper-case character or name boundary after the verb and include missing persistence verbs like insert and create.

⚠️ Bug: New/moved Noncompliant markers carry stale secondary= line numbers

📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:113 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:117 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:121 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:125 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:129 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:133 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:174 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:190 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:238 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:312 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:319 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:332 📄 java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:340

secondary=NNN without a +/- sign is an absolute source line and is strictly asserted (Expectations.relativeValueToInt -> InternalCheckVerifier.validateSecondaryLocations, which fails on any expected line not reported). The markers added or shifted by this PR are mutually inconsistent: lines 113-133 use secondary=108, which is exactly the class-level @Transactional line (108), while the other new/relocated ones keep pre-shift values that point at unrelated lines — line 174 says 155 but its annotation is at 169, line 190 says 169 but the interface annotation is at 186, line 238 says 207 (annotation at 224), line 312 says 290 (annotation at 304), and lines 319/332/340 say 297 (annotation at 317). At least one of these two groups cannot match, so TransactionalMethodCheckedExceptionCheckTest.test will fail on the secondary-location assertion; recompute every touched secondary= against the line of the reporting @Transactional annotation (or switch them to relative -N form so they survive future line shifts).

Point each new marker at the absolute line of the class-level @transactional annotation that is reported as the secondary location.
  public void saveNestedEntity() throws IOException { // Noncompliant [[secondary=169]]

void saveEntity() throws IOException; // Noncompliant [[secondary=186]]

public void savePublicInClassLevel() throws IOException { // Noncompliant [[secondary=224]]

public void deleteWithAnnotations() throws IOException { // Noncompliant [[secondary=304]]

public void processOrder(Object repository) throws IOException { // Noncompliant [[secondary=317]]
public void delegateToFlush() throws IOException { // Noncompliant [[secondary=317]]
public void delegateToDelete(Object entity) throws IOException { // Noncompliant [[secondary=317]]
💡 Edge Case: Prefix match has no word boundary, so unrelated names match

📄 java-checks/src/main/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheck.java:57 📄 java-checks/src/main/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheck.java:287-299

hasTransactionalPrefix lowercases the whole identifier and tests raw startsWith, so purely non-persistence names such as updateUiLabel, deleteTempFile, mergeSortResults, flushOutputStream, or a call to savedFilterCount() are classified as transactional and still raise, while the equally common persistence verbs (insert, create, remove, store, executeUpdate) never match. Requiring the prefix to be followed by an upper-case character or end-of-name (e.g. save, saveOrder but not savedSearch) and adding the missing verbs would make the heuristic match the intent described in the PR.

🤖 Prompt for agents
Code Review: Fixes false positives for class-level `@Transactional` by only raising S8989 when methods have transactional names or call transactional methods. However, test markers in `TransactionalMethodCheckedExceptionCheckSample.java` have stale `secondary=` line numbers that will cause test failures—recompute them against the actual annotation locations. Additionally, the prefix matching lacks word boundaries, so names like `updateUiLabel` and `deleteTempFile` are incorrectly classified as transactional; tighten the check to require an upper-case character or name boundary after the verb and include missing persistence verbs like `insert` and `create`.

1. ⚠️ Bug: New/moved Noncompliant markers carry stale secondary= line numbers
   Files: java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:113, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:117, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:121, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:125, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:129, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:133, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:174, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:190, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:238, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:312, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:319, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:332, java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java:340

   `secondary=NNN` without a `+`/`-` sign is an absolute source line and is strictly asserted (`Expectations.relativeValueToInt` -> `InternalCheckVerifier.validateSecondaryLocations`, which fails on any expected line not reported). The markers added or shifted by this PR are mutually inconsistent: lines 113-133 use `secondary=108`, which is exactly the class-level `@Transactional` line (108), while the other new/relocated ones keep pre-shift values that point at unrelated lines — line 174 says 155 but its annotation is at 169, line 190 says 169 but the interface annotation is at 186, line 238 says 207 (annotation at 224), line 312 says 290 (annotation at 304), and lines 319/332/340 say 297 (annotation at 317). At least one of these two groups cannot match, so `TransactionalMethodCheckedExceptionCheckTest.test` will fail on the secondary-location assertion; recompute every touched `secondary=` against the line of the reporting `@Transactional` annotation (or switch them to relative `-N` form so they survive future line shifts).

   Fix (Point each new marker at the absolute line of the class-level @Transactional annotation that is reported as the secondary location.):
     public void saveNestedEntity() throws IOException { // Noncompliant [[secondary=169]]
   
   void saveEntity() throws IOException; // Noncompliant [[secondary=186]]
   
   public void savePublicInClassLevel() throws IOException { // Noncompliant [[secondary=224]]
   
   public void deleteWithAnnotations() throws IOException { // Noncompliant [[secondary=304]]
   
   public void processOrder(Object repository) throws IOException { // Noncompliant [[secondary=317]]
   public void delegateToFlush() throws IOException { // Noncompliant [[secondary=317]]
   public void delegateToDelete(Object entity) throws IOException { // Noncompliant [[secondary=317]]

2. 💡 Edge Case: Prefix match has no word boundary, so unrelated names match
   Files: java-checks/src/main/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheck.java:57, java-checks/src/main/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheck.java:287-299

   `hasTransactionalPrefix` lowercases the whole identifier and tests raw `startsWith`, so purely non-persistence names such as `updateUiLabel`, `deleteTempFile`, `mergeSortResults`, `flushOutputStream`, or a call to `savedFilterCount()` are classified as transactional and still raise, while the equally common persistence verbs (`insert`, `create`, `remove`, `store`, `executeUpdate`) never match. Requiring the prefix to be followed by an upper-case character or end-of-name (e.g. `save`, `saveOrder` but not `savedSearch`) and adding the missing verbs would make the heuristic match the intent described in the PR.

Implementation Status ✅ 2 of 2 objectives covered
SONARJAVA-6892 - 2 of 2 objectives covered

This PR covers the objectives of raising S8989 for methods with direct @Transactional annotations, as well as for methods in @Transactional classes that have transactional prefixes or call transactional methods.

✅ 2 covered here
  • ✅ Raise S8989 issue when the method itself has the @Transactional annotation
  • ✅ Raise S8989 issue when the class has the @Transactional annotation and save, delete, update, persist, merge, or flush is a prefix of the method name or one of the called methods
Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.
Unblock → Override a blocking verdict and allow merging.

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

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

Was this helpful? React with 👍 / 👎 | Gitar

…nsactional prefix matching

Fix all secondary= line numbers in test markers to point at the actual
@transactional annotation lines. Add word-boundary check to prefix
matching so names like "savedSearch", "persisted", and "deletion" are
not incorrectly classified as transactional. Add missing persistence
verbs: insert, create, remove, store, execute.

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

sonarqube-next Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

@romainbrenguier
romainbrenguier marked this pull request as ready for review September 3, 2026 11:51
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.

1 participant