diff --git a/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9431.json b/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9431.json new file mode 100644 index 00000000000..b44133eb223 --- /dev/null +++ b/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9431.json @@ -0,0 +1,5 @@ +{ +"eclipse-jetty:jetty-util/src/main/java/org/eclipse/jetty/util/ConstantThrowable.java": [ +25 +] +} diff --git a/its/ruling/src/test/resources/expected/guava/java-S9431.json b/its/ruling/src/test/resources/expected/guava/java-S9431.json new file mode 100644 index 00000000000..71374fe5e6c --- /dev/null +++ b/its/ruling/src/test/resources/expected/guava/java-S9431.json @@ -0,0 +1,5 @@ +{ +"guava:src/com/google/common/util/concurrent/ServiceManager.java": [ +785 +] +} diff --git a/java-checks-test-sources/default/src/main/java/checks/CustomExceptionExtendsThrowableCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/CustomExceptionExtendsThrowableCheckSample.java new file mode 100644 index 00000000000..e1911ba10d8 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/CustomExceptionExtendsThrowableCheckSample.java @@ -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 + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/CustomExceptionExtendsThrowableCheck.java b/java-checks/src/main/java/org/sonar/java/checks/CustomExceptionExtendsThrowableCheck.java new file mode 100644 index 00000000000..365b0cc0cbe --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/CustomExceptionExtendsThrowableCheck.java @@ -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 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."); + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/CustomExceptionExtendsThrowableCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/CustomExceptionExtendsThrowableCheckTest.java new file mode 100644 index 00000000000..a1d5fae71cc --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/CustomExceptionExtendsThrowableCheckTest.java @@ -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 reports_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/CustomExceptionExtendsThrowableCheckSample.java")) + .withCheck(new CustomExceptionExtendsThrowableCheck()) + .withoutSemantic() + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9431.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9431.html new file mode 100644 index 00000000000..80e2e38ad2f --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9431.html @@ -0,0 +1,55 @@ +

This rule raises an issue when a class directly extends the root exception base class in the language’s exception hierarchy.

+

In Java, this specifically refers to the java.lang.Throwable class.

+

Why is this an issue?

+

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.

+

The exception system is built around two distinct categories:

+ +

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:

+ +

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.

+

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.

+

In Java specifically, the root exception class is Throwable, the recoverable exception class is Exception, the unchecked +exception class is RuntimeException, and the system-level error class is Error. For example, serious system-level problems +include OutOfMemoryError.

+

What is the potential impact?

+

This issue primarily affects code maintainability and clarity:

+ +

How to fix it

+

If you want a checked exception (one that must be declared or caught), extend Exception instead of Throwable. Checked +exceptions are appropriate for recoverable conditions that calling code should handle.

+

Code examples

+

Noncompliant code example

+
+public class CustomException extends Throwable { // Noncompliant
+    public CustomException(String message) {
+        super(message);
+    }
+}
+
+

Compliant solution

+
+public class CustomException extends Exception {
+    public CustomException(String message) {
+        super(message);
+    }
+}
+
diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9431.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9431.json new file mode 100644 index 00000000000..f4de4b058bf --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9431.json @@ -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" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9431 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9431 new file mode 100644 index 00000000000..e69de29bb2d