diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8910.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8910.json new file mode 100644 index 00000000000..8ee95a43f8d --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8910.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S8910", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/MapperWithoutDaoFactoryCheckSample.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/MapperWithoutDaoFactoryCheckSample.java new file mode 100644 index 00000000000..c9f116b2562 --- /dev/null +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/MapperWithoutDaoFactoryCheckSample.java @@ -0,0 +1,20 @@ +package checks; + +import com.datastax.oss.quarkus.runtime.api.mapper.Mapper; + +class MapperWithoutDaoFactoryCheckSample { + + // Case with unresolved supertype - should not raise issue to avoid false positive + @Mapper + interface MapperWithUnresolvedSupertype extends UnresolvedInterface { + } + + // Case with partially resolved inheritance + @Mapper + interface MapperExtendingResolvableAndUnresolvable extends ResolvableInterface, AnotherUnresolvedInterface { + } + + interface ResolvableInterface { + String getName(); + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/MapperWithoutDaoFactoryCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/MapperWithoutDaoFactoryCheckSample.java new file mode 100644 index 00000000000..6e4ad30fd6e --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/MapperWithoutDaoFactoryCheckSample.java @@ -0,0 +1,206 @@ +package checks; + +import com.datastax.oss.quarkus.runtime.api.mapper.Mapper; +import com.datastax.oss.quarkus.runtime.api.mapper.DaoFactory; + +class MapperWithoutDaoFactoryCheckSample { + + @Mapper + interface EmptyMapper { // Noncompliant {{Add at least one "@DaoFactory" method to this "@Mapper" interface.}} [[sc=13;ec=24]] + } + + @Mapper + public interface FruitMapper { // Noncompliant + } + + @Mapper + interface MapperWithOtherMethods { // Noncompliant + String getVersion(); + default int count() { + return 0; + } + } + + @Mapper + interface ExtendingMapper extends BaseInterface { + } + + interface BaseInterface { + @DaoFactory + BaseDao baseDao(); + } + + interface BaseDao { + } + + @Mapper + public interface CompliantFruitMapper { + @DaoFactory + FruitDao fruitDao(); + } + + interface FruitDao { + } + + @Mapper + interface MultipleFactories { + @DaoFactory + UserDao userDao(); + + @DaoFactory + ProductDao productDao(); + } + + interface UserDao { + } + + interface ProductDao { + } + + @Mapper + interface FactoryWithParameter { + @DaoFactory + KeyspaceDao dao(String keyspace); + } + + interface KeyspaceDao { + } + + @Mapper + interface MixedMethods { + @DaoFactory + OrderDao orderDao(); + + String getVersion(); + } + + interface OrderDao { + } + + // Compliant: the rule only checks @Mapper interfaces. + @Mapper + abstract class MapperClass { + } + + // Compliant: the rule only checks @Mapper interfaces. + @Mapper + enum MapperEnum { + } + + // Compliant: the rule only checks @Mapper interfaces. + @Mapper + record MapperRecord() { + } + + @Mapper + interface MultiLevelInheritance extends IntermediateInterface { + } + + interface IntermediateInterface extends BaseFactoryInterface { + } + + interface BaseFactoryInterface { + @DaoFactory + MultiLevelDao dao(); + } + + interface MultiLevelDao { + } + + @Mapper + interface MultipleInheritance extends Interface1, Interface2 { + } + + interface Interface1 { + String method1(); + } + + interface Interface2 { + @DaoFactory + MultiInheritDao dao(); + } + + interface MultiInheritDao { + } + + @Mapper + interface ComplexInheritance extends InterfaceWithFactory, InterfaceWithoutFactory { + } + + interface InterfaceWithFactory { + @DaoFactory + ComplexDao dao(); + } + + interface InterfaceWithoutFactory { + String getName(); + } + + interface ComplexDao { + } + + @Mapper + interface CircularReference extends SelfReferencingBase { + } + + interface SelfReferencingBase { + @DaoFactory + CircularDao dao(); + } + + interface CircularDao { + } + + @Mapper + interface DeepInheritanceChain extends Level1 { + } + + interface Level1 extends Level2 { + } + + interface Level2 extends Level3 { + } + + interface Level3 { + @DaoFactory + DeepDao dao(); + } + + interface DeepDao { + } + + // Diamond inheritance pattern - tests visiting same interface multiple times + @Mapper + interface DiamondMapper extends DiamondLeft, DiamondRight { + } + + interface DiamondLeft extends DiamondBase { + } + + interface DiamondRight extends DiamondBase { + } + + interface DiamondBase { + @DaoFactory + DiamondDao dao(); + } + + interface DiamondDao { + } + + // Diamond without DaoFactory - tests visited set when no factory found + @Mapper + interface DiamondWithoutFactory extends DiamondLeftNoFactory, DiamondRightNoFactory { // Noncompliant + } + + interface DiamondLeftNoFactory extends DiamondBaseNoFactory { + } + + interface DiamondRightNoFactory extends DiamondBaseNoFactory { + } + + interface DiamondBaseNoFactory { + String getData(); + } + +} diff --git a/java-checks-test-sources/default/src/main/java/com/datastax/oss/quarkus/runtime/api/mapper/DaoFactory.java b/java-checks-test-sources/default/src/main/java/com/datastax/oss/quarkus/runtime/api/mapper/DaoFactory.java new file mode 100644 index 00000000000..c52334a6ec4 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/com/datastax/oss/quarkus/runtime/api/mapper/DaoFactory.java @@ -0,0 +1,27 @@ +/* + * 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 com.datastax.oss.quarkus.runtime.api.mapper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DaoFactory { +} diff --git a/java-checks-test-sources/default/src/main/java/com/datastax/oss/quarkus/runtime/api/mapper/Mapper.java b/java-checks-test-sources/default/src/main/java/com/datastax/oss/quarkus/runtime/api/mapper/Mapper.java new file mode 100644 index 00000000000..ba8238f9031 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/com/datastax/oss/quarkus/runtime/api/mapper/Mapper.java @@ -0,0 +1,27 @@ +/* + * 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 com.datastax.oss.quarkus.runtime.api.mapper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Mapper { +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/MapperWithoutDaoFactoryCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MapperWithoutDaoFactoryCheck.java new file mode 100644 index 00000000000..a2c9e7490d5 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/MapperWithoutDaoFactoryCheck.java @@ -0,0 +1,95 @@ +/* + * 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.Symbol; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.AnnotationTree; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S8910") +public class MapperWithoutDaoFactoryCheck extends IssuableSubscriptionVisitor { + + private static final String MAPPER_ANNOTATION = "com.datastax.oss.quarkus.runtime.api.mapper.Mapper"; + private static final String DAO_FACTORY_ANNOTATION = "com.datastax.oss.quarkus.runtime.api.mapper.DaoFactory"; + private static final String MESSAGE = "Add at least one \"@DaoFactory\" method to this \"@Mapper\" interface."; + + @Override + public List nodesToVisit() { + return Collections.singletonList(Tree.Kind.INTERFACE); + } + + @Override + public void visitNode(Tree tree) { + ClassTree classTree = (ClassTree) tree; + + if (!hasAnnotation(classTree.modifiers().annotations(), MAPPER_ANNOTATION)) { + return; + } + + boolean hasExplicitSuperInterfaces = !classTree.superInterfaces().isEmpty(); + if (!hasDaoFactoryMethod(classTree.symbol(), hasExplicitSuperInterfaces)) { + reportIssue(classTree.simpleName(), MESSAGE); + } + } + + private static boolean hasDaoFactoryMethod(Symbol.TypeSymbol typeSymbol, boolean hasExplicitSuperInterfaces) { + // Check the mapper's own members + if (typeSymbol.memberSymbols().stream() + .filter(Symbol::isMethodSymbol) + .map(Symbol.MethodSymbol.class::cast) + .anyMatch(MapperWithoutDaoFactoryCheck::hasDaoFactoryAnnotation)) { + return true; + } + + // Check all supertypes (superTypes() already returns the full transitive closure) + for (Type superType : typeSymbol.superTypes()) { + Symbol.TypeSymbol superTypeSymbol = superType.symbol(); + if (superTypeSymbol.isUnknown() && hasExplicitSuperInterfaces) { + // If the interface explicitly extends other types and we encounter an unresolved supertype, + // assume it might provide the required @DaoFactory method to avoid false positives + // in projects with incomplete classpaths + return true; + } + if (!superTypeSymbol.isUnknown() && superTypeSymbol.memberSymbols().stream() + .filter(Symbol::isMethodSymbol) + .map(Symbol.MethodSymbol.class::cast) + .anyMatch(MapperWithoutDaoFactoryCheck::hasDaoFactoryAnnotation)) { + return true; + } + } + return false; + } + + private static boolean hasDaoFactoryAnnotation(Symbol.MethodSymbol methodSymbol) { + MethodTree declaration = methodSymbol.declaration(); + return declaration != null + ? hasAnnotation(declaration.modifiers().annotations(), DAO_FACTORY_ANNOTATION) + : methodSymbol.metadata().isAnnotatedWith(DAO_FACTORY_ANNOTATION); + } + + private static boolean hasAnnotation(List annotations, String fullyQualifiedName) { + return annotations.stream().anyMatch(annotation -> annotation.annotationType().symbolType().is(fullyQualifiedName)); + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/MapperWithoutDaoFactoryCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/MapperWithoutDaoFactoryCheckTest.java new file mode 100644 index 00000000000..2f03e5192b6 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/MapperWithoutDaoFactoryCheckTest.java @@ -0,0 +1,50 @@ +/* + * 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 org.sonar.java.checks.verifier.TestUtils; + +class MapperWithoutDaoFactoryCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(TestUtils.mainCodeSourcesPath("checks/MapperWithoutDaoFactoryCheckSample.java")) + .withCheck(new MapperWithoutDaoFactoryCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantics() { + CheckVerifier.newVerifier() + .onFile(TestUtils.mainCodeSourcesPath("checks/MapperWithoutDaoFactoryCheckSample.java")) + .withCheck(new MapperWithoutDaoFactoryCheck()) + .withoutSemantic() + .verifyNoIssues(); + } + + @Test + void test_non_compiling() { + CheckVerifier.newVerifier() + .onFile(TestUtils.nonCompilingTestSourcesPath("checks/MapperWithoutDaoFactoryCheckSample.java")) + .withCheck(new MapperWithoutDaoFactoryCheck()) + .verifyNoIssues(); + } + +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8910.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8910.html new file mode 100644 index 00000000000..e446803a695 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8910.html @@ -0,0 +1,42 @@ +

Why is this an issue?

+

In object-relational mapping frameworks that use code generation, a mapper interface serves as a factory for constructing Data Access Object (DAO) instances. This is a core design pattern of such libraries.

+

The mapper's sole purpose is to provide factory method declarations that create and return DAO beans. These factory methods enable dependency injection of DAOs throughout your application. Without any factory method declarations, the mapper interface:

+ +

The framework's code generation mechanism generates implementation code based on the factory method declarations present in the mapper interface. An empty mapper results in generated code that does nothing useful.

+

In the DataStax Object Mapper framework, the mapper interface uses the @Mapper annotation, and factory methods are declared using the @DaoFactory annotation. According to the Quarkus documentation: "If you intend to construct and inject a specific DAO bean in your own code, then you first must add a @DaoFactory method for it in a @Mapper interface."

+

What is the potential impact?

+

An interface marked for code generation without the expected factory methods creates unnecessary code that serves no purpose. This can:

+ +

How to fix it

+

Add at least one @DaoFactory method to the mapper interface. The method should return a DAO interface that is annotated with @Dao.

+

Code examples

+

Noncompliant code example

+
+@Mapper
+public interface FruitMapper {
+    // No @DaoFactory methods // Noncompliant
+}
+
+

Compliant solution

+
+@Mapper
+public interface FruitMapper {
+    @DaoFactory
+    FruitDao fruitDao();
+}
+
+

Resources

+

Documentation

+ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8910.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8910.json new file mode 100644 index 00000000000..7b7e11a4910 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8910.json @@ -0,0 +1,22 @@ +{ + "title": "Interfaces annotated with \"@Mapper\" should contain at least one \"@DaoFactory\" method", + "type": "CODE_SMELL", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "CONVENTIONAL" + }, + "status": "ready", + "remediation": { + "func": "Constant/Issue", + "constantCost": "5min" + }, + "tags": [ + "confusing" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-8910", + "sqKey": "S8910", + "scope": "All" +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json index a669258ac60..e5d26f0d3cc 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json @@ -465,6 +465,7 @@ "S8696", "S8715", "S8745", - "S8786" + "S8786", + "S8910" ] } 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..ee2994bc184 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 @@ -534,6 +534,7 @@ "S8745", "S8786", "S8909", + "S8910", "S8911", "S8924" ] diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java index 5d255874ee4..9be3f9bc28c 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java @@ -53,7 +53,7 @@ void profile_is_registered_as_expected() { BuiltInQualityProfilesDefinition.BuiltInQualityProfile actualProfile = profilesPerLanguages.get("java").get("Sonar agentic AI"); assertThat(actualProfile.isDefault()).isFalse(); assertThat(actualProfile.rules()) - .hasSize(465) + .hasSize(466) .extracting(BuiltInQualityProfilesDefinition.BuiltInActiveRule::ruleKey) .doesNotContainAnyElementsOf(List.of( "S101",