Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
af9fc21
Implement S8910 inherited DaoFactory detection
romainbrenguier Jun 23, 2026
934ba15
Improve test coverage for S8910 MapperWithoutDaoFactoryCheck
romainbrenguier Jun 24, 2026
701c66f
Remove undefined UnknownType reference from test sample
romainbrenguier Jun 24, 2026
130971a
Handle unresolved supertypes gracefully in S8910
romainbrenguier Jun 25, 2026
13bbacd
Handle unresolved supertypes gracefully in S8910
romainbrenguier Jun 25, 2026
3285726
Improve test coverage for S8910 MapperWithoutDaoFactoryCheck
romainbrenguier Jun 25, 2026
16e8cf1
Add diamond inheritance test case for S8910
romainbrenguier Jun 25, 2026
acfa855
Add non-compliant diamond inheritance test for S8910
romainbrenguier Jun 25, 2026
15c3dcb
Merge remote-tracking branch 'origin/master' into new-rule/SONARJAVA-…
romainbrenguier Jun 25, 2026
d7f27e5
Merge remote-tracking branch 'origin/master' into new-rule/SONARJAVA-…
romainbrenguier Jun 29, 2026
fbb7b73
Address review comment from gitar-bot on java-checks/src/main/java/or…
romainbrenguier Jun 29, 2026
3b97b7d
Address review comment from NoemieBenard on java-checks-test-sources/…
romainbrenguier Jun 29, 2026
6a13976
Address review comment from NoemieBenard on java-checks-test-sources/…
romainbrenguier Jun 29, 2026
19a20a4
Fix CI failures
romainbrenguier Jun 29, 2026
454eb4e
Fix CI: Fixed invalid Noncompliant comment format in MapperWithoutDao…
romainbrenguier Jun 29, 2026
e8f7818
Merge remote-tracking branch 'origin/master' into new-rule/SONARJAVA-…
romainbrenguier Jun 30, 2026
ed31db3
Fix CI: Fixed incorrect expected rule count in JavaAgenticWayProfileT…
romainbrenguier Jun 30, 2026
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,6 @@
{
"ruleKey": "S8910",
"hasTruePositives": true,
"falseNegatives": 0,
"falsePositives": 0
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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<Tree.Kind> nodesToVisit() {
return Collections.singletonList(Tree.Kind.INTERFACE);
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.

@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)) {
Comment thread
gitar-bot[bot] marked this conversation as resolved.
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<AnnotationTree> annotations, String fullyQualifiedName) {
return annotations.stream().anyMatch(annotation -> annotation.annotationType().symbolType().is(fullyQualifiedName));
}

}
Loading
Loading