SONARJAVA-6039 Fix S2139 false positive when exception is wrapped in constructor call#5545
Conversation
Tests cover the scenario where a caught exception is logged and then wrapped in a new exception object (e.g., throw new RuntimeException(msg, e)). The fix changes all existing noncompliant cases that use constructor wrapping to compliant, and adds noncompliant cases for direct rethrow (throw e) to preserve true positive coverage. Relates to SONARJAVA-6039
Eliminate false positives where a caught exception is logged and then wrapped in a new exception object (e.g., throw new FileSystemException(msg, e)). The fix adds a check for Tree.Kind.NEW_CLASS before calling isExceptionUsed(): when the thrown expression is a constructor call, the exception is being wrapped rather than rethrown directly, so no issue is raised. Direct rethrows (throw e;) after logging are still flagged as before. Updated ruling test baselines to remove the false positive entries and retain true positive entries (direct rethrows). Updated autoscan diff from 5 to 1 false negatives to reflect the reduced Noncompliant test cases. Updated rule.adoc to show throw e; as noncompliant and logging+wrapping as a compliant solution. Relates to SONARJAVA-6039
| "falseNegatives": 5, | ||
| "falseNegatives": 1, | ||
| "falsePositives": 0 | ||
| } No newline at end of file |
There was a problem hiding this comment.
Should we have trailing newline?
| ExpressionTree thrown = ((ThrowStatementTree) statementTree).expression(); | ||
| if (!thrown.is(Tree.Kind.NEW_CLASS) && isExceptionUsed(exceptionIdentifier, thrown)) { | ||
| secondaryLocations.add(new Location("Thrown exception.", thrown)); | ||
| reportIssue(catchTree.parameter(), "Either log this exception and handle it, or rethrow it with some contextual information.", secondaryLocations, 0); |
There was a problem hiding this comment.
Extract message to a class field
…sources/autoscan/diffs/diff_S2139.json Comment: Should we have trailing newline?
…a/org/sonar/java/checks/LoggedRethrownExceptionsCheck.java Comment: Extract message to a class field
|
SummaryFixes a false positive in S2139 (logged-then-rethrown exceptions) by allowing exception wrapping patterns. The rule previously flagged any catch block where a logged exception appeared anywhere in a subsequent throw statement—including as a constructor argument. The fix adds a guard to skip constructor calls ( What reviewers should knowCore change: In
|




Rule Profile
S2139— Exceptions should be either logged or rethrown but not bothGenuine violation (direct rethrow after logging):
False Positive Pattern
The rule was incorrectly flagging cases where a caught exception is logged and then wrapped inside a new exception object. This is a legitimate exception transformation pattern: the original exception is preserved as the cause while additional context is added at the point of re-throw. Because the wrapped exception carries its own type and message, it will not produce duplicate log output at higher layers.
The false positive occurs because
ExceptionUsageVisitorwalks the entire expression tree of thethrowstatement, finding the exception variableewherever it appears — including as a constructor argument — and flagging it regardless of whethereitself is rethrown.FP pattern — wrapping a caught exception in a new type:
False Negative Risk
The fix skips any
throwwhose expression is aNewClassTree(constructor call). A genuine violation where the same exception type is re-constructed without added context would be suppressed:This is a known and accepted trade-off discussed during review. The risk is bounded: the missed case involves constructing an identical exception type with a hardcoded message, which is uncommon in practice and distinct from the widespread legitimate wrapping pattern. The fix eliminates ~7,836 estimated false positives on SonarQube Cloud while the false negative scenario is a narrow edge case.
Implementation
How the rule works today:
visitNodeiterates through the body of eachcatchblock. When it encounters a logging call that includes the caught exception variable, it sets a flag. If a subsequentthrowstatement contains the exception variable anywhere in its expression (viaExceptionUsageVisitor), an issue is reported.What changed: Before invoking
isExceptionUsed, the fix checks whether the thrown expression is a constructor call (NewClassTree). If it is, the statement is treated as exception wrapping and skipped.The
!thrown.is(Tree.Kind.NEW_CLASS)guard allowsthrow new FileSystemException(msg, e)(wrapping) to pass silently, whilethrow e(direct rethrow) still triggers the check and is flagged. The message string was also extracted to a named constantMESSAGEto reduce duplication.Technical Summary
java-checks/src/main/java/org/sonar/java/checks/LoggedRethrownExceptionsCheck.java— added!thrown.is(Tree.Kind.NEW_CLASS)guard beforeisExceptionUsed; extracted inline message literal to aMESSAGEconstantjava-checks-test-sources/default/src/main/java/checks/LoggedRethrownExceptionsCheckSample.java— updated test fixtures: wrapping patterns changed fromNoncompliantto compliant; direct rethrow cases added/retained asNoncompliantits/autoscan/src/test/resources/autoscan/diffs/diff_S2139.json— updated autoscan diff baseline (reduced false negative count from 5 to 1)its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json— updated aggregate autoscan diff baselineits/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S2139.json— removed false positive entries from ruling baselineits/ruling/src/test/resources/eclipse-jetty/java-S2139.json— removed false positive entries from ruling baselineNet change: +55 / −46 lines across 6 files.