diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8913.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8913.json new file mode 100644 index 00000000000..0eee55e9f3a --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8913.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S8913", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} \ No newline at end of file diff --git a/java-checks-test-sources/default/src/main/java/checks/RestDataPanacheResourceImplementationCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/RestDataPanacheResourceImplementationCheckSample.java new file mode 100644 index 00000000000..56ec4a21e9e --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/RestDataPanacheResourceImplementationCheckSample.java @@ -0,0 +1,91 @@ +package checks; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import io.quarkus.hibernate.orm.rest.data.panache.PanacheEntityResource; +import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource; +import io.quarkus.mongodb.panache.PanacheMongoEntityBase; +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.mongodb.rest.data.panache.PanacheMongoEntityResource; +import io.quarkus.mongodb.rest.data.panache.PanacheMongoRepositoryResource; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import java.util.Collections; +import java.util.List; + +class Person extends PanacheEntityBase { + public Long id; + public String name; +} + +class PersonRepository implements PanacheRepositoryBase { +} + +class MongoPerson extends PanacheMongoEntityBase { + public Long id; + public String name; +} + +class MongoPersonRepository implements PanacheMongoRepositoryBase { +} + +interface PeopleResource extends PanacheEntityResource { +} + +class PeopleResourceImpl implements PeopleResource { // Noncompliant {{Remove this implementation class; Quarkus generates the resource implementation automatically.}} +} + +interface PersonRepositoryResource extends PanacheRepositoryResource { +} + +class PersonRepositoryResourceImpl implements PersonRepositoryResource { // Noncompliant +} + +interface MongoPersonResource extends PanacheMongoEntityResource { +} + +class MongoPersonResourceImpl implements MongoPersonResource { // Noncompliant +} + +interface MongoPersonRepositoryResource extends PanacheMongoRepositoryResource { +} + +class MongoPersonRepositoryResourceImpl implements MongoPersonRepositoryResource { // Noncompliant +} + +abstract class AbstractPersonResource implements PeopleResource { // Noncompliant +} + +class ConcretePersonResource extends AbstractPersonResource { // Noncompliant +} + +interface CompliantResource extends PanacheEntityResource { +} + +interface ResourceWithDefaults extends PanacheEntityResource { + @GET + @Path("/name/{name}") + @Produces("application/json") + default List findByName(@PathParam("name") String name) { + return Collections.emptyList(); + } +} + +interface RegularRestInterface { + Person get(Long id); +} + +class RegularRestImpl implements RegularRestInterface { + @Override + public Person get(Long id) { + return null; + } +} + +class PersonService { + public Person findById(Long id) { + return null; + } +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/panache/PanacheEntityBase.java b/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/panache/PanacheEntityBase.java new file mode 100644 index 00000000000..38b78403cb7 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/panache/PanacheEntityBase.java @@ -0,0 +1,4 @@ +package io.quarkus.hibernate.orm.panache; + +public abstract class PanacheEntityBase { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/panache/PanacheRepositoryBase.java b/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/panache/PanacheRepositoryBase.java new file mode 100644 index 00000000000..9c52ec5abaf --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/panache/PanacheRepositoryBase.java @@ -0,0 +1,4 @@ +package io.quarkus.hibernate.orm.panache; + +public interface PanacheRepositoryBase { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/PanacheEntityResource.java b/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/PanacheEntityResource.java new file mode 100644 index 00000000000..5725d137eea --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/PanacheEntityResource.java @@ -0,0 +1,4 @@ +package io.quarkus.hibernate.orm.rest.data.panache; + +public interface PanacheEntityResource { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/PanacheRepositoryResource.java b/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/PanacheRepositoryResource.java new file mode 100644 index 00000000000..1f9cfebdb1a --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/PanacheRepositoryResource.java @@ -0,0 +1,4 @@ +package io.quarkus.hibernate.orm.rest.data.panache; + +public interface PanacheRepositoryResource { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java b/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java new file mode 100644 index 00000000000..03824cd350d --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java @@ -0,0 +1,4 @@ +package io.quarkus.mongodb.panache; + +public abstract class PanacheMongoEntityBase { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java b/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java new file mode 100644 index 00000000000..de0a9e2098b --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java @@ -0,0 +1,4 @@ +package io.quarkus.mongodb.panache; + +public interface PanacheMongoRepositoryBase { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoEntityResource.java b/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoEntityResource.java new file mode 100644 index 00000000000..9dc5bb856dc --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoEntityResource.java @@ -0,0 +1,4 @@ +package io.quarkus.mongodb.rest.data.panache; + +public interface PanacheMongoEntityResource { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoRepositoryResource.java b/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoRepositoryResource.java new file mode 100644 index 00000000000..e5688d42f17 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoRepositoryResource.java @@ -0,0 +1,4 @@ +package io.quarkus.mongodb.rest.data.panache; + +public interface PanacheMongoRepositoryResource { +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/RestDataPanacheResourceImplementationCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RestDataPanacheResourceImplementationCheck.java new file mode 100644 index 00000000000..e7ff96b3161 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/RestDataPanacheResourceImplementationCheck.java @@ -0,0 +1,62 @@ +/* + * 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.semantic.Type; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S8913") +public class RestDataPanacheResourceImplementationCheck extends IssuableSubscriptionVisitor { + + private static final String PANACHE_ENTITY_RESOURCE = "io.quarkus.hibernate.orm.rest.data.panache.PanacheEntityResource"; + private static final String PANACHE_REPOSITORY_RESOURCE = "io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource"; + private static final String PANACHE_MONGO_ENTITY_RESOURCE = "io.quarkus.mongodb.rest.data.panache.PanacheMongoEntityResource"; + private static final String PANACHE_MONGO_REPOSITORY_RESOURCE = "io.quarkus.mongodb.rest.data.panache.PanacheMongoRepositoryResource"; + + @Override + public List nodesToVisit() { + return Collections.singletonList(Tree.Kind.CLASS); + } + + @Override + public void visitNode(Tree tree) { + ClassTree classTree = (ClassTree) tree; + if (classTree.symbol().isUnknown()) { + return; + } + if (implementsPanacheResourceInterface(classTree)) { + reportIssue(classTree.simpleName(), "Remove this implementation class; Quarkus generates the resource implementation automatically."); + } + } + + private static boolean implementsPanacheResourceInterface(ClassTree classTree) { + Type classType = classTree.symbol().type(); + return isPanacheResourceType(classType); + } + + private static boolean isPanacheResourceType(Type type) { + return type.isSubtypeOf(PANACHE_ENTITY_RESOURCE) + || type.isSubtypeOf(PANACHE_REPOSITORY_RESOURCE) + || type.isSubtypeOf(PANACHE_MONGO_ENTITY_RESOURCE) + || type.isSubtypeOf(PANACHE_MONGO_REPOSITORY_RESOURCE); + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/RestDataPanacheResourceImplementationCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RestDataPanacheResourceImplementationCheckTest.java new file mode 100644 index 00000000000..f33adf1df87 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/RestDataPanacheResourceImplementationCheckTest.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 RestDataPanacheResourceImplementationCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/RestDataPanacheResourceImplementationCheckSample.java")) + .withCheck(new RestDataPanacheResourceImplementationCheck()) + .verifyIssues(); + } + + @Test + void testWithoutSemantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/RestDataPanacheResourceImplementationCheckSample.java")) + .withCheck(new RestDataPanacheResourceImplementationCheck()) + .withoutSemantic() + .verifyNoIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8913.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8913.html new file mode 100644 index 00000000000..e798bed821a --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8913.html @@ -0,0 +1,72 @@ +

This rule raises an issue when a class implements an interface that extends one of the framework’s provided base resource interfaces for REST +endpoints with built-in database entity operations.

+

In Java with Quarkus, these interfaces are specifically: PanacheEntityResource, PanacheRepositoryResource, +PanacheMongoEntityResource, and PanacheMongoRepositoryResource.

+

Why is this an issue?

+

Certain frameworks use code generation mechanisms to automatically create complete implementations from interface definitions or contract +specifications. When you define an interface that extends framework-specific base interfaces designed for resource management, the framework generates +the complete implementation at build time.

+

The framework processes these interfaces during the build phase and creates resource handlers with standard operations (create, read, update, +delete, list). This generation happens automatically without requiring any manual implementation.

+

If you create a class that implements such an interface, the framework will ignore your implementation entirely. The code generator doesn’t check +for existing implementations — it simply creates its own implementation class. This means:

+
    +
  • Your custom implementation code will never be executed
  • +
  • Any method overrides you write will have no effect
  • +
  • The application will use the auto-generated implementation instead
  • +
  • You’ll waste development time writing code that serves no purpose
  • +
+

This silent failure can be particularly confusing because the application will compile and run without errors, but your custom logic won’t execute. +Developers may spend significant time debugging why their code changes aren’t taking effect.

+

The only supported way to add custom functionality is through extension mechanisms that are explicitly recognized by the code generation process, +such as inline method implementations within the interface definition itself. These inline implementations are respected by the code generation +mechanism and will be included in the final generated resource alongside the standard operations.

+

In Quarkus REST Data with Panache, this applies to interfaces extending PanacheEntityResource or +PanacheRepositoryResource. The framework generates Jakarta REST resources at build time. The supported extension mechanism is +default methods defined directly in the resource interface.

+

What is the potential impact?

+

This issue leads to wasted development effort and potential confusion when debugging. Developers may spend time writing and maintaining +implementation code that has no effect on the application’s behavior. The silent nature of this problem makes it particularly problematic, as there +are no build-time errors or runtime warnings to indicate that the custom implementation is being ignored.

+

While this is not a security or data integrity issue, it can delay development and lead to frustration when custom business logic doesn’t execute +as expected.

+

How to fix it

+

Remove the implementation class entirely. If you need custom methods, add them as default methods directly in the resource interface. Default +methods will be included in the generated resource alongside the standard CRUD operations.

+

Code examples

+

Noncompliant code example

+
+public interface PeopleResource extends PanacheEntityResource<Person, Long> {
+}
+
+// Noncompliant: this implementation will be completely ignored
+public class PeopleResourceImpl implements PeopleResource {
+    @Override
+    public Person get(Long id) {
+        return Person.findById(id);
+    }
+}
+
+

Compliant solution

+
+// Compliant: only the interface is needed, with custom methods as defaults
+public interface PeopleResource extends PanacheEntityResource<Person, Long> {
+
+    @GET
+    @Path("/name/{name}")
+    @Produces("application/json")
+    default List<Person> findByName(@PathParam("name") String name) {
+        return Person.find("name = :name", Collections.singletonMap("name", name)).list();
+    }
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8913.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8913.json new file mode 100644 index 00000000000..b97ed1820b8 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8913.json @@ -0,0 +1,25 @@ +{ + "title": "REST Data with Panache resource interfaces should not have implementation classes", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "10min" + }, + "tags": [ + "quarkus", + "rest", + "confusing" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-8913", + "sqKey": "S8913", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "CLEAR" + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index 823ca6f2864..8e0e6914c84 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -535,6 +535,7 @@ "S8786", "S8909", "S8911", + "S8913", "S8924" ] }