Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
]
}
5 changes: 5 additions & 0 deletions its/ruling/src/test/resources/expected/guava/java-S9431.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"guava:src/com/google/common/util/concurrent/ServiceManager.java": [
785
]
}
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
}
}
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.");
}
}
}
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();
}
Comment on lines +35 to +41

Copy link
Copy Markdown

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_semantic calls .withoutSemantic().verifyIssues() on the same sample file, which has // Noncompliant markers. 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 the Sema in the scanner context (VisitorsBridgeForTests.createScannerContext), while JDT still resolves java.lang.Throwable in 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, like reports_without_semantic. If the goal really was 'no issues without semantic', use verifyNoIssues() instead, as JpaEagerFetchTypeCheckTest does.

Rename the test so the name matches the assertion:

@Test
void reports_without_semantic() {
  CheckVerifier.newVerifier()
    .onFile(mainCodeSourcesPath("checks/CustomExceptionExtendsThrowableCheckSample.java"))
    .withCheck(new CustomExceptionExtendsThrowableCheck())
    .withoutSemantic()
    .verifyIssues();
}
  • Apply fix

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

}
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>
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.
Loading