From 31aacbde665982c85a68dd564d7184673176f3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Mon, 17 Aug 2026 16:53:20 +0200 Subject: [PATCH 1/3] Handle `@Qualifier` annotations in BeanDefinitionGatherer --- .../model/springcontext/BeanDependency.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDependency.java diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDependency.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDependency.java new file mode 100644 index 00000000000..2e0aef75b75 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDependency.java @@ -0,0 +1,28 @@ +/* + * 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.model.springcontext; + +import javax.annotation.Nullable; + +/** + * Represents a single Spring autowiring dependency: the required type and an optional {@code @Qualifier} name. + * + * @param typeFqn fully-qualified name of the required type + * @param qualifier value of the {@code @Qualifier} annotation, or {@code null} if absent + */ +public record BeanDependency(String typeFqn, @Nullable String qualifier) { +} From cd56907ebfd14b3330ff46d5bded81b7e4e334f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Tue, 18 Aug 2026 14:49:35 +0200 Subject: [PATCH 2/3] Collect dependencies from implicit single-constructor injection Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../springcontext/BeanDefinitionGatherer.java | 10 +++++++-- .../MultipleConstructorsNoDependencies.java | 22 +++++++++++++++++++ .../SingleConstructorDependencies.java | 17 ++++++++++++++ .../BeanDefinitionGathererTest.java | 14 +++++++++++- 4 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 java-frontend/src/test/files/springcontext/MultipleConstructorsNoDependencies.java create mode 100644 java-frontend/src/test/files/springcontext/SingleConstructorDependencies.java diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java index 3e8aca5bd99..1c9f79cfb94 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java @@ -306,6 +306,7 @@ private void collectBeanMethod(MethodTree method, String pkg) { private static Map> collectAutowiredDependencies(ClassTree classTree) { Map> deps = new LinkedHashMap<>(); + List unannotatedConstructors = new ArrayList<>(); for (Tree member : classTree.members()) { if (member instanceof VariableTree field) { if (field.symbol().metadata().isAnnotatedWith(SpringUtils.AUTOWIRED_ANNOTATION)) { @@ -313,14 +314,19 @@ private static Map> collectAutowiredDependencies(ClassTree c String name = dependencyKey(field.simpleName().name(), extractQualifier(field.symbol().metadata())); deps.computeIfAbsent(typeFqn, k -> new LinkedHashSet<>()).add(name); } - } else if (member.is(Tree.Kind.CONSTRUCTOR, Tree.Kind.METHOD)) { - MethodTree method = (MethodTree) member; + } else if (member instanceof MethodTree method) { if (method.symbol().metadata().isAnnotatedWith(SpringUtils.AUTOWIRED_ANNOTATION)) { parameterDependencies(method).forEach((type, names) -> deps.computeIfAbsent(type, k -> new LinkedHashSet<>()).addAll(names)); + } else if (method.is(Tree.Kind.CONSTRUCTOR)) { + unannotatedConstructors.add(method); } } } + if (deps.isEmpty() && unannotatedConstructors.size() == 1) { + parameterDependencies(unannotatedConstructors.get(0)).forEach((type, names) -> + deps.computeIfAbsent(type, k -> new LinkedHashSet<>()).addAll(names)); + } return deps; } diff --git a/java-frontend/src/test/files/springcontext/MultipleConstructorsNoDependencies.java b/java-frontend/src/test/files/springcontext/MultipleConstructorsNoDependencies.java new file mode 100644 index 00000000000..9efb0906c9f --- /dev/null +++ b/java-frontend/src/test/files/springcontext/MultipleConstructorsNoDependencies.java @@ -0,0 +1,22 @@ +package checks.spring.context; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +class MultipleConstructorsNoDependencies { + + private final ApplicationContext applicationContext; + private final Environment environment; + + MultipleConstructorsNoDependencies(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + this.environment = null; + } + + MultipleConstructorsNoDependencies(ApplicationContext applicationContext, Environment environment) { + this.applicationContext = applicationContext; + this.environment = environment; + } +} diff --git a/java-frontend/src/test/files/springcontext/SingleConstructorDependencies.java b/java-frontend/src/test/files/springcontext/SingleConstructorDependencies.java new file mode 100644 index 00000000000..a48f1cb31c5 --- /dev/null +++ b/java-frontend/src/test/files/springcontext/SingleConstructorDependencies.java @@ -0,0 +1,17 @@ +package checks.spring.context; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +class SingleConstructorDependencies { + + private final ApplicationContext applicationContext; + private final Environment environment; + + SingleConstructorDependencies(ApplicationContext applicationContext, Environment environment) { + this.applicationContext = applicationContext; + this.environment = environment; + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java b/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java index ff99b05f174..d4ac03cedbc 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java @@ -201,10 +201,22 @@ static Stream dependencyCollectionArguments() { return Stream.of( Arguments.of("src/test/files/springcontext/AutowiredDependencies.java", "autowiredDependencies"), Arguments.of("src/test/files/springcontext/AutowiredConstructorDependencies.java", "autowiredConstructorDependencies"), - Arguments.of("src/test/files/springcontext/BeanMethodWithDependencies.java", "myBean") + Arguments.of("src/test/files/springcontext/BeanMethodWithDependencies.java", "myBean"), + Arguments.of("src/test/files/springcontext/SingleConstructorDependencies.java", "singleConstructorDependencies") ); } + // ---- Implicit single-constructor injection -------------------------------- + + @Test + void multiple_constructors_without_autowired_yields_no_dependencies() { + scan("src/test/files/springcontext/MultipleConstructorsNoDependencies.java"); + + var beans = model.getBeanDefinitionRegistry().getByName("multipleConstructorsNoDependencies"); + assertThat(beans).hasSize(1); + assertThat(beans.get(0).getDependingBeans()).isEmpty(); + } + // ---- @Qualifier handling -------------------------------------------------- @ParameterizedTest(name = "{0}") From 2331cc82499b68ca17b74734c042a6f2e1716342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Fri, 21 Aug 2026 14:22:18 +0200 Subject: [PATCH 3/3] Remove unused BeanDependency --- .../model/springcontext/BeanDependency.java | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDependency.java diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDependency.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDependency.java deleted file mode 100644 index 2e0aef75b75..00000000000 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDependency.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.model.springcontext; - -import javax.annotation.Nullable; - -/** - * Represents a single Spring autowiring dependency: the required type and an optional {@code @Qualifier} name. - * - * @param typeFqn fully-qualified name of the required type - * @param qualifier value of the {@code @Qualifier} annotation, or {@code null} if absent - */ -public record BeanDependency(String typeFqn, @Nullable String qualifier) { -}