Repository navigation
SONARJAVA-7156: Implemented rule S9431 Custom exceptions should extend "Exception" or "RuntimeException" rather than "Throwable" #6331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
romainbrenguier
wants to merge
4
commits into
master
Choose a base branch
from
romain/new-rule-s9431-sonarjava-7156
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
b09cc09
SONARJAVA-7156: Implemented rule S9431 Custom exceptions should exten…
romainbrenguier e2059c1
Update ruling results for PR #6331 (#6335)
github-actions[bot] bbb6127
Update ruling results for PR #6331 (#6341)
github-actions[bot] 690d6fa
SONARJAVA-7156 Remove misplaced ruling result files
romainbrenguier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
5 changes: 5 additions & 0 deletions
5
its/ruling/src/test/resources/expected/eclipse-jetty/java-S9431.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "eclipse-jetty:jetty-util/src/main/java/org/eclipse/jetty/util/ConstantThrowable.java": [ | ||
| 25 | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "guava:src/com/google/common/util/concurrent/ServiceManager.java": [ | ||
| 785 | ||
| ] | ||
| } |
51 changes: 51 additions & 0 deletions
51
...test-sources/default/src/main/java/checks/CustomExceptionExtendsThrowableCheckSample.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package checks; | ||
|
|
||
| class ConnectionFailure extends Throwable { // Noncompliant {{Extend "Exception" or "RuntimeException" instead.}} | ||
| // ^^^^^^^^^ | ||
| ConnectionFailure(String message) { | ||
| super(message); | ||
| } | ||
| } | ||
|
|
||
| class ServiceFailure extends java.lang.Throwable { // Noncompliant | ||
| // ^^^^^^^^^^^^^^^^^^^ | ||
| } | ||
|
|
||
| abstract class AbstractFailure extends Throwable { // Noncompliant | ||
| // ^^^^^^^^^ | ||
| } | ||
|
|
||
| class RequestHandler { | ||
| class NestedFailure extends Throwable { // Noncompliant | ||
| // ^^^^^^^^^ | ||
| } | ||
| } | ||
|
|
||
| class InvalidRequestException extends Exception { // Compliant | ||
| } | ||
|
|
||
| class RetryableFailure extends RuntimeException { // Compliant | ||
| } | ||
|
|
||
| class DomainException extends Exception { // Compliant | ||
| } | ||
|
|
||
| class CustomerNotFoundException extends DomainException { // Compliant | ||
| } | ||
|
|
||
| class SystemFailure extends Error { // Compliant | ||
| } | ||
|
|
||
| class MemoryFailure extends OutOfMemoryError { // Compliant | ||
| } | ||
|
|
||
| class RegularService { // Compliant | ||
| } | ||
|
|
||
| class LocalTypes { | ||
| class Throwable { // Compliant | ||
| } | ||
|
|
||
| class LocalThrowableFailure extends Throwable { // Compliant | ||
| } | ||
| } |
42 changes: 42 additions & 0 deletions
42
java-checks/src/main/java/org/sonar/java/checks/CustomExceptionExtendsThrowableCheck.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /* | ||
| * SonarQube Java | ||
| * Copyright (C) SonarSource Sàrl | ||
| * mailto:info AT sonarsource DOT com | ||
| * | ||
| * You can redistribute and/or modify this program under the terms of | ||
| * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | ||
| * See the Sonar Source-Available License for more details. | ||
| * | ||
| * You should have received a copy of the Sonar Source-Available License | ||
| * along with this program; if not, see https://sonarsource.com/license/ssal/ | ||
| */ | ||
| package org.sonar.java.checks; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import org.sonar.check.Rule; | ||
| import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; | ||
| import org.sonar.plugins.java.api.tree.ClassTree; | ||
| import org.sonar.plugins.java.api.tree.Tree; | ||
| import org.sonar.plugins.java.api.tree.TypeTree; | ||
|
|
||
| @Rule(key = "S9431") | ||
| public class CustomExceptionExtendsThrowableCheck extends IssuableSubscriptionVisitor { | ||
|
|
||
| @Override | ||
| public List<Tree.Kind> nodesToVisit() { | ||
| return Collections.singletonList(Tree.Kind.CLASS); | ||
| } | ||
|
|
||
| @Override | ||
| public void visitNode(Tree tree) { | ||
| TypeTree superClass = ((ClassTree) tree).superClass(); | ||
| if (superClass != null && superClass.symbolType().is("java.lang.Throwable")) { | ||
| reportIssue(superClass, "Extend \"Exception\" or \"RuntimeException\" instead."); | ||
| } | ||
| } | ||
| } |
42 changes: 42 additions & 0 deletions
42
...-checks/src/test/java/org/sonar/java/checks/CustomExceptionExtendsThrowableCheckTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /* | ||
| * SonarQube Java | ||
| * Copyright (C) SonarSource Sàrl | ||
| * mailto:info AT sonarsource DOT com | ||
| * | ||
| * You can redistribute and/or modify this program under the terms of | ||
| * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | ||
| * See the Sonar Source-Available License for more details. | ||
| * | ||
| * You should have received a copy of the Sonar Source-Available License | ||
| * along with this program; if not, see https://sonarsource.com/license/ssal/ | ||
| */ | ||
| package org.sonar.java.checks; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import org.sonar.java.checks.verifier.CheckVerifier; | ||
|
|
||
| import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; | ||
|
|
||
| class CustomExceptionExtendsThrowableCheckTest { | ||
|
|
||
| @Test | ||
| void detects_direct_throwable_inheritance() { | ||
| CheckVerifier.newVerifier() | ||
| .onFile(mainCodeSourcesPath("checks/CustomExceptionExtendsThrowableCheckSample.java")) | ||
| .withCheck(new CustomExceptionExtendsThrowableCheck()) | ||
| .verifyIssues(); | ||
| } | ||
|
|
||
| @Test | ||
| void does_not_report_without_semantic() { | ||
| CheckVerifier.newVerifier() | ||
| .onFile(mainCodeSourcesPath("checks/CustomExceptionExtendsThrowableCheckSample.java")) | ||
| .withCheck(new CustomExceptionExtendsThrowableCheck()) | ||
| .withoutSemantic() | ||
| .verifyIssues(); | ||
| } | ||
| } | ||
55 changes: 55 additions & 0 deletions
55
sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9431.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| <p>This rule raises an issue when a class directly extends the root exception base class in the language’s exception hierarchy.</p> | ||
| <p>In Java, this specifically refers to the <code>java.lang.Throwable</code> class.</p> | ||
| <h2>Why is this an issue?</h2> | ||
| <p>In this language, all exceptions and errors inherit from a root exception class. However, directly extending this root class is considered poor | ||
| practice because it creates ambiguity in your exception hierarchy.</p> | ||
| <p>The exception system is built around two distinct categories:</p> | ||
| <ul> | ||
| <li><strong>Compiler-enforced exceptions</strong> (extending the recoverable exception class): These represent recoverable conditions that calling | ||
| code should handle. The compiler enforces that these exceptions are either caught or declared in the function signature.</li> | ||
| <li><strong>Runtime exceptions</strong> (extending the unchecked exception class): These represent programming errors or unrecoverable conditions. | ||
| The compiler does not require them to be caught or declared.</li> | ||
| </ul> | ||
| <p>When you extend the root exception class directly, it is unclear whether your exception is compiler-enforced or unchecked. This creates confusion | ||
| for developers using your exception class:</p> | ||
| <ul> | ||
| <li>They don’t know if they need to handle it (compiler-enforced) or if it indicates a programming error (unchecked)</li> | ||
| <li>Exception handling code becomes less predictable</li> | ||
| <li>The intent of your exception class is obscured</li> | ||
| </ul> | ||
| <p>The language’s API design guidelines and best practices consistently recommend extending either the recoverable exception class or the unchecked | ||
| exception class to make your intent clear. This convention is followed throughout the standard library and third-party libraries.</p> | ||
| <p>Additionally, the system-level error class also extends the root exception class, but it is reserved for serious system-level problems (like | ||
| out-of-memory conditions). Custom application exceptions should never extend the system-level error class.</p> | ||
| <p>In Java specifically, the root exception class is <code>Throwable</code>, the recoverable exception class is <code>Exception</code>, the unchecked | ||
| exception class is <code>RuntimeException</code>, and the system-level error class is <code>Error</code>. For example, serious system-level problems | ||
| include <code>OutOfMemoryError</code>.</p> | ||
| <h3>What is the potential impact?</h3> | ||
| <p>This issue primarily affects code maintainability and clarity:</p> | ||
| <ul> | ||
| <li>Developers may be confused about whether to handle your error type using standard error handling mechanisms or declare it in function | ||
| interfaces</li> | ||
| <li>Code reviews become more difficult because the error type’s purpose and relationship to the standard error hierarchy is ambiguous</li> | ||
| <li>Error handlers that catch standard error types may not catch your custom error class, leading to unexpected behavior</li> | ||
| <li>The codebase becomes harder to understand and maintain over time</li> | ||
| </ul> | ||
| <h2>How to fix it</h2> | ||
| <p>If you want a checked exception (one that must be declared or caught), extend <code>Exception</code> instead of <code>Throwable</code>. Checked | ||
| exceptions are appropriate for recoverable conditions that calling code should handle.</p> | ||
| <h3>Code examples</h3> | ||
| <h4>Noncompliant code example</h4> | ||
| <pre data-diff-id="1" data-diff-type="noncompliant"> | ||
| public class CustomException extends Throwable { // Noncompliant | ||
| public CustomException(String message) { | ||
| super(message); | ||
| } | ||
| } | ||
| </pre> | ||
| <h4>Compliant solution</h4> | ||
| <pre data-diff-id="1" data-diff-type="compliant"> | ||
| public class CustomException extends Exception { | ||
| public CustomException(String message) { | ||
| super(message); | ||
| } | ||
| } | ||
| </pre> |
25 changes: 25 additions & 0 deletions
25
sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9431.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "title": "Custom exceptions should extend \"Exception\" or \"RuntimeException\" rather than \"Throwable\"", | ||
| "type": "CODE_SMELL", | ||
| "status": "ready", | ||
| "remediation": { | ||
| "func": "Constant\/Issue", | ||
| "constantCost": "5 min" | ||
| }, | ||
| "tags": [ | ||
| "exception", | ||
| "api-design", | ||
| "convention" | ||
| ], | ||
| "defaultSeverity": "Major", | ||
| "ruleSpecification": "RSPEC-9431", | ||
| "sqKey": "S9431", | ||
| "scope": "All", | ||
| "quickfix": "unknown", | ||
| "code": { | ||
| "impacts": { | ||
| "MAINTAINABILITY": "MEDIUM" | ||
| }, | ||
| "attribute": "CONVENTIONAL" | ||
| } | ||
| } |
Empty file.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Quality: Test name says 'does_not_report' but it asserts the issues are reported
does_not_report_without_semanticcalls.withoutSemantic().verifyIssues()on the same sample file, which has// Noncompliantmarkers.verifyIssues()fails unless every marker gets an issue, so this test actually checks that the rule still reports without semantic. I checked why that works:JavaCheckVerifier.withoutSemantic()only nulls theSemain the scanner context (VisitorsBridgeForTests.createScannerContext), while JDT still resolvesjava.lang.Throwablein the tree. So the assertion passes, but the name says the opposite of what the test checks, and nothing tests the no-semantic case where the rule should stay silent. Fix: rename the test to match what it checks, likereports_without_semantic. If the goal really was 'no issues without semantic', useverifyNoIssues()instead, asJpaEagerFetchTypeCheckTestdoes.Rename the test so the name matches the assertion:
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎