From 08b7a50e7ec584581efaaad4d6a9b57d08ebdf6c 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/7] Handle `@Qualifier` annotations in BeanDefinitionGatherer --- .../springcontext/BeanDefinitionGatherer.java | 36 +++++++++++++------ .../springcontext/BeanDefinitionHolder.java | 12 +++---- .../model/springcontext/BeanDependency.java | 28 +++++++++++++++ .../org/sonar/java/utils/SpringUtils.java | 1 + .../QualifiedBeanMethodDependencies.java | 18 ++++++++++ .../QualifiedConstructorDependencies.java | 22 ++++++++++++ .../QualifiedFieldDependencies.java | 18 ++++++++++ .../BeanDefinitionGathererTest.java | 35 ++++++++++++++++++ 8 files changed, 154 insertions(+), 16 deletions(-) create mode 100644 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDependency.java create mode 100644 java-frontend/src/test/files/springcontext/QualifiedBeanMethodDependencies.java create mode 100644 java-frontend/src/test/files/springcontext/QualifiedConstructorDependencies.java create mode 100644 java-frontend/src/test/files/springcontext/QualifiedFieldDependencies.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 c3564122088..987ebcaf1d6 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 @@ -25,6 +25,7 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; import org.sonar.api.batch.fs.InputFile; import org.sonar.java.reporting.AnalyzerMessage; import org.sonar.java.utils.PackageUtils; @@ -66,6 +67,7 @@ public class BeanDefinitionGatherer extends SpringContextModelGatherer { private static final String DEP_SEPARATOR = ","; private static final String PRIMARY_ANNOTATION = "org.springframework.context.annotation.Primary"; + private static final String VALUE_ATTRIBUTE = "value"; private final List collectedBeans = new ArrayList<>(); @@ -79,7 +81,7 @@ private record BeanData( InputFile inputFile, AnalyzerMessage.TextSpan textSpan, boolean isPrimary, - List dependingBeans) { + List dependingBeans) { } @Override @@ -107,7 +109,7 @@ public void visitNode(Tree tree) { if (SpringUtils.STEREOTYPE_ANNOTATIONS.stream().anyMatch(meta::isAnnotatedWith)) { String beanName = extractBeanName(meta) .orElseGet(() -> defaultBeanName(classTree.simpleName().name())); - List deps = collectAutowiredDependencies(classTree); + List deps = collectAutowiredDependencies(classTree); // Class-level bean (stereotype annotations) collectedBeans.add(new BeanData( beanName, fqn, pkg, @@ -234,7 +236,7 @@ private static Optional extractBeanName(SymbolMetadata meta) { List attrs = meta.valuesForAnnotation(annotation); if (attrs != null) { Optional name = attrs.stream() - .filter(v -> "value".equals(v.name()) || "name".equals(v.name())) + .filter(v -> VALUE_ATTRIBUTE.equals(v.name()) || "name".equals(v.name())) .map(v -> (String) v.value()) .filter(s -> !s.isBlank()) .findFirst(); @@ -255,7 +257,7 @@ private void collectBeanMethod(MethodTree method, String pkg) { List attrs = beanMeta.valuesForAnnotation(SpringUtils.BEAN_ANNOTATION); String beanName = Optional.ofNullable(attrs) .flatMap(list -> list.stream() - .filter(v -> "value".equals(v.name()) || "name".equals(v.name())) + .filter(v -> VALUE_ATTRIBUTE.equals(v.name()) || "name".equals(v.name())) .map(v -> { Object val = v.value(); if (val instanceof Object[] arr && arr.length > 0) { @@ -271,8 +273,8 @@ private void collectBeanMethod(MethodTree method, String pkg) { ? method.returnType().symbolType().fullyQualifiedName() : ""; - List paramDeps = method.parameters().stream() - .map(p -> p.symbol().type().fullyQualifiedName()) + List paramDeps = method.parameters().stream() + .map(p -> new BeanDependency(p.symbol().type().fullyQualifiedName(), extractQualifier(p.symbol().metadata()))) .toList(); collectedBeans.add(new BeanData( @@ -289,19 +291,19 @@ private void collectBeanMethod(MethodTree method, String pkg) { paramDeps)); } - private static List collectAutowiredDependencies(ClassTree classTree) { - List deps = new ArrayList<>(); + private static List collectAutowiredDependencies(ClassTree classTree) { + List deps = new ArrayList<>(); for (Tree member : classTree.members()) { if (member.is(Tree.Kind.VARIABLE)) { VariableTree field = (VariableTree) member; if (field.symbol().metadata().isAnnotatedWith(SpringUtils.AUTOWIRED_ANNOTATION)) { - deps.add(field.symbol().type().fullyQualifiedName()); + deps.add(new BeanDependency(field.symbol().type().fullyQualifiedName(), extractQualifier(field.symbol().metadata()))); } } else if (member.is(Tree.Kind.CONSTRUCTOR, Tree.Kind.METHOD)) { MethodTree method = (MethodTree) member; if (method.symbol().metadata().isAnnotatedWith(SpringUtils.AUTOWIRED_ANNOTATION)) { method.parameters().stream() - .map(p -> p.symbol().type().fullyQualifiedName()) + .map(p -> new BeanDependency(p.symbol().type().fullyQualifiedName(), extractQualifier(p.symbol().metadata()))) .forEach(deps::add); } } @@ -309,4 +311,18 @@ private static List collectAutowiredDependencies(ClassTree classTree) { return deps; } + @Nullable + private static String extractQualifier(SymbolMetadata metadata) { + List attrs = metadata.valuesForAnnotation(SpringUtils.QUALIFIER_ANNOTATION); + if (attrs == null) { + return null; + } + return attrs.stream() + .filter(v -> VALUE_ATTRIBUTE.equals(v.name())) + .map(v -> (String) v.value()) + .filter(s -> !s.isBlank()) + .findFirst() + .orElse(null); + } + } diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java index 3cca0218e72..1da2d5c3828 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java @@ -51,8 +51,8 @@ public class BeanDefinitionHolder { /** Source location where the bean definition appears. */ private final BeanLocation location; - /** Names of other beans this bean depends on. */ - private List dependingBeans; + /** Dependencies this bean requires, each capturing the required type and an optional {@code @Qualifier} name. */ + private List dependingBeans; /** Comma-separated Spring profile expressions under which this bean is active, or {@code null} if unconditional. */ @Nullable @@ -68,7 +68,7 @@ private BeanDefinitionHolder(String type, String module, String beanPackage, Bea this.location = location; } - private void setDependingBeans(List beansList) { + private void setDependingBeans(List beansList) { this.dependingBeans = beansList; } @@ -96,7 +96,7 @@ public BeanLocation getLocation() { return location; } - public List getDependingBeans() { + public List getDependingBeans() { return dependingBeans; } @@ -114,7 +114,7 @@ public static class Builder { private final String module; private final String beanPackage; private final BeanLocation location; - private List dependingBeans = new ArrayList<>(); + private List dependingBeans = new ArrayList<>(); @Nullable private String profiles; private boolean isPrimary = false; @@ -126,7 +126,7 @@ public Builder(String type, String module, String beanPackage, BeanLocation loca this.location = location; } - public Builder dependingBeans(List beansList) { + public Builder dependingBeans(List beansList) { this.dependingBeans = beansList; return this; } 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) { +} diff --git a/java-frontend/src/main/java/org/sonar/java/utils/SpringUtils.java b/java-frontend/src/main/java/org/sonar/java/utils/SpringUtils.java index f1dc9d323be..9673af815dd 100644 --- a/java-frontend/src/main/java/org/sonar/java/utils/SpringUtils.java +++ b/java-frontend/src/main/java/org/sonar/java/utils/SpringUtils.java @@ -33,6 +33,7 @@ public final class SpringUtils { public static final String REPOSITORY_ANNOTATION = "org.springframework.stereotype.Repository"; public static final String SERVICE_ANNOTATION = "org.springframework.stereotype.Service"; public static final String AUTOWIRED_ANNOTATION = "org.springframework.beans.factory.annotation.Autowired"; + public static final String QUALIFIER_ANNOTATION = "org.springframework.beans.factory.annotation.Qualifier"; public static final String VALUE_ANNOTATION = "org.springframework.beans.factory.annotation.Value"; public static final String TRANSACTIONAL_ANNOTATION = "org.springframework.transaction.annotation.Transactional"; public static final String BEAN_ANNOTATION = "org.springframework.context.annotation.Bean"; diff --git a/java-frontend/src/test/files/springcontext/QualifiedBeanMethodDependencies.java b/java-frontend/src/test/files/springcontext/QualifiedBeanMethodDependencies.java new file mode 100644 index 00000000000..6e982ea0368 --- /dev/null +++ b/java-frontend/src/test/files/springcontext/QualifiedBeanMethodDependencies.java @@ -0,0 +1,18 @@ +package checks.spring.context; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +@Configuration +class QualifiedBeanMethodDependencies { + + @Bean + Object myBean( + @Qualifier("primaryContext") ApplicationContext applicationContext, + Environment environment) { + return new Object(); + } +} diff --git a/java-frontend/src/test/files/springcontext/QualifiedConstructorDependencies.java b/java-frontend/src/test/files/springcontext/QualifiedConstructorDependencies.java new file mode 100644 index 00000000000..b15ab0dd188 --- /dev/null +++ b/java-frontend/src/test/files/springcontext/QualifiedConstructorDependencies.java @@ -0,0 +1,22 @@ +package checks.spring.context; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +class QualifiedConstructorDependencies { + + private final ApplicationContext applicationContext; + private final Environment environment; + + @Autowired + QualifiedConstructorDependencies( + @Qualifier("primaryContext") ApplicationContext applicationContext, + Environment environment) { + this.applicationContext = applicationContext; + this.environment = environment; + } +} diff --git a/java-frontend/src/test/files/springcontext/QualifiedFieldDependencies.java b/java-frontend/src/test/files/springcontext/QualifiedFieldDependencies.java new file mode 100644 index 00000000000..4a5973814fe --- /dev/null +++ b/java-frontend/src/test/files/springcontext/QualifiedFieldDependencies.java @@ -0,0 +1,18 @@ +package checks.spring.context; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +class QualifiedFieldDependencies { + + @Autowired + @Qualifier("primaryContext") + private ApplicationContext applicationContext; + + @Autowired + private 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 30a7bbad110..9cb36eeb451 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 @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; +import org.sonar.java.model.springcontext.BeanDependency; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -192,6 +193,7 @@ void dependencies_collected_as_depending_beans(String filePath, String expectedB var beans = model.getBeanDefinitionRegistry().getByName(expectedBeanName); assertThat(beans).hasSize(1); assertThat(beans.get(0).getDependingBeans()) + .extracting(BeanDependency::typeFqn) .containsExactlyInAnyOrder( "org.springframework.context.ApplicationContext", "org.springframework.core.env.Environment" @@ -206,6 +208,39 @@ static Stream dependencyCollectionArguments() { ); } + // ---- @Qualifier handling -------------------------------------------------- + + @ParameterizedTest(name = "{0}") + @MethodSource("qualifiedDependencyArguments") + void qualifier_is_captured_on_qualified_dependency(String filePath, String expectedBeanName) { + scan(filePath); + + var beans = model.getBeanDefinitionRegistry().getByName(expectedBeanName); + assertThat(beans).hasSize(1); + assertThat(beans.get(0).getDependingBeans()) + .extracting(BeanDependency::qualifier) + .containsExactlyInAnyOrder("primaryContext", null); + } + + static Stream qualifiedDependencyArguments() { + return Stream.of( + Arguments.of("src/test/files/springcontext/QualifiedFieldDependencies.java", "qualifiedFieldDependencies"), + Arguments.of("src/test/files/springcontext/QualifiedConstructorDependencies.java", "qualifiedConstructorDependencies"), + Arguments.of("src/test/files/springcontext/QualifiedBeanMethodDependencies.java", "myBean") + ); + } + + @Test + void no_qualifier_results_in_null_qualifier() { + scan("src/test/files/springcontext/AutowiredDependencies.java"); + + var beans = model.getBeanDefinitionRegistry().getByName("autowiredDependencies"); + assertThat(beans).hasSize(1); + assertThat(beans.get(0).getDependingBeans()) + .extracting(BeanDependency::qualifier) + .containsOnly((String) null); + } + // ---- Bean location -------------------------------------------------------- @Test From c7788e6c5cf8947fbd3f5b1d356320aa229c93fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Tue, 18 Aug 2026 09:37:34 +0200 Subject: [PATCH 2/7] Remove redundant import --- .../java/model/springcontext/BeanDefinitionGathererTest.java | 1 - 1 file changed, 1 deletion(-) 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 9cb36eeb451..8cf3dfbff71 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 @@ -20,7 +20,6 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; -import org.sonar.java.model.springcontext.BeanDependency; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; From ef6cdf8053e1b80918c0f8d25be6bc1f189eedf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Tue, 18 Aug 2026 17:42:09 +0200 Subject: [PATCH 3/7] Fix serialization issue after rebase --- .../springcontext/BeanDefinitionGatherer.java | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) 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 987ebcaf1d6..d1cb6987ead 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 @@ -19,6 +19,7 @@ import java.beans.Introspector; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.Optional; @@ -65,6 +66,7 @@ public class BeanDefinitionGatherer extends SpringContextModelGatherer { private static final String BEAN_SEPARATOR = "\n"; private static final String FIELD_SEPARATOR = "|"; private static final String DEP_SEPARATOR = ","; + private static final String DEP_QUALIFIER_SEPARATOR = "="; private static final String PRIMARY_ANNOTATION = "org.springframework.context.annotation.Primary"; private static final String VALUE_ATTRIBUTE = "value"; @@ -157,7 +159,9 @@ private static void writeToCache(JavaFileScannerContext context, List } private static String serializeBean(BeanData bean) { - var deps = String.join(DEP_SEPARATOR, bean.dependingBeans()); + var deps = bean.dependingBeans().stream() + .map(BeanDefinitionGatherer::serializeDependency) + .collect(Collectors.joining(DEP_SEPARATOR)); var span = bean.textSpan(); var encodedName = Base64.getEncoder().encodeToString(bean.beanName().getBytes(StandardCharsets.UTF_8)); return String.join(FIELD_SEPARATOR, @@ -169,6 +173,23 @@ private static String serializeBean(BeanData bean) { deps); } + private static BeanDependency deserializeDependency(String serialized) { + int idx = serialized.indexOf(DEP_QUALIFIER_SEPARATOR); + String typeFqn = serialized.substring(0, idx); + String encodedQualifier = serialized.substring(idx + 1); + String qualifier = encodedQualifier.isEmpty() + ? null + : new String(Base64.getDecoder().decode(encodedQualifier), StandardCharsets.UTF_8); + return new BeanDependency(typeFqn, qualifier); + } + + private static String serializeDependency(BeanDependency dep) { + String encodedQualifier = dep.qualifier() != null + ? Base64.getEncoder().encodeToString(dep.qualifier().getBytes(StandardCharsets.UTF_8)) + : ""; + return dep.typeFqn() + DEP_QUALIFIER_SEPARATOR + encodedQualifier; + } + @Override public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) { for (BeanData data : collectedBeans) { @@ -227,7 +248,10 @@ private static BeanData deserializeBean(String line, InputFile inputFile) { Integer.parseInt(spanParts[2]), Integer.parseInt(spanParts[3])); boolean isPrimary = Boolean.parseBoolean(fields[4]); - List deps = fields[5].isEmpty() ? List.of() : List.of(fields[5].split(DEP_SEPARATOR)); + List deps = fields[5].isEmpty() ? List.of() : + Arrays.stream(fields[5].split(DEP_SEPARATOR)) + .map(BeanDefinitionGatherer::deserializeDependency) + .toList(); return new BeanData(beanName, type, beanPackage, inputFile, textSpan, isPrimary, deps); } From 7367ec13526082a283b77f55cb7f09581507947f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Tue, 18 Aug 2026 17:49:25 +0200 Subject: [PATCH 4/7] Extract BeanData creation --- .../springcontext/BeanDefinitionGatherer.java | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) 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 d1cb6987ead..bf01cffb24f 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 @@ -113,18 +113,14 @@ public void visitNode(Tree tree) { .orElseGet(() -> defaultBeanName(classTree.simpleName().name())); List deps = collectAutowiredDependencies(classTree); // Class-level bean (stereotype annotations) - collectedBeans.add(new BeanData( + var beanData = new BeanData( beanName, fqn, pkg, context.getInputFile(), AnalyzerMessage.textSpanFor(classTree.simpleName()), meta.isAnnotatedWith(PRIMARY_ANNOTATION), - deps)); - beansCollectedAtFileLevel.add(new BeanData( - beanName, fqn, pkg, - context.getInputFile(), - AnalyzerMessage.textSpanFor(classTree.simpleName()), - meta.isAnnotatedWith(PRIMARY_ANNOTATION), - deps)); + deps); + collectedBeans.add(beanData); + beansCollectedAtFileLevel.add(beanData); // @Bean methods — only if class is a configuration/component class for (MethodTree method : SpringUtils.getBeanMethods(classTree)) { @@ -301,18 +297,14 @@ private void collectBeanMethod(MethodTree method, String pkg) { .map(p -> new BeanDependency(p.symbol().type().fullyQualifiedName(), extractQualifier(p.symbol().metadata()))) .toList(); - collectedBeans.add(new BeanData( - beanName, returnTypeFqn, pkg, - context.getInputFile(), - AnalyzerMessage.textSpanFor(method.simpleName()), - beanMeta.isAnnotatedWith(PRIMARY_ANNOTATION), - paramDeps)); - beansCollectedAtFileLevel.add(new BeanData( + var beanData = new BeanData( beanName, returnTypeFqn, pkg, context.getInputFile(), AnalyzerMessage.textSpanFor(method.simpleName()), beanMeta.isAnnotatedWith(PRIMARY_ANNOTATION), - paramDeps)); + paramDeps); + collectedBeans.add(beanData); + beansCollectedAtFileLevel.add(beanData); } private static List collectAutowiredDependencies(ClassTree classTree) { From e0ac948536c247db992dae30a5436b4f18a9c196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Wed, 19 Aug 2026 08:58:39 +0200 Subject: [PATCH 5/7] Add tests for dependency serialization and blank qualifier handling Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../BlankQualifierDependency.java | 14 +++++ .../BeanDefinitionGathererTest.java | 62 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 java-frontend/src/test/files/springcontext/BlankQualifierDependency.java diff --git a/java-frontend/src/test/files/springcontext/BlankQualifierDependency.java b/java-frontend/src/test/files/springcontext/BlankQualifierDependency.java new file mode 100644 index 00000000000..7048b6d2d70 --- /dev/null +++ b/java-frontend/src/test/files/springcontext/BlankQualifierDependency.java @@ -0,0 +1,14 @@ +package checks.spring.context; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Component +class BlankQualifierDependency { + + @Autowired + @Qualifier("") + private ApplicationContext applicationContext; +} 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 8cf3dfbff71..832a8585a97 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 @@ -381,6 +381,68 @@ void scanWithoutParsing_returns_false_on_corrupted_cache_entry() { assertThat(gatherer.scanWithoutParsing(context)).isFalse(); } + @Test + void leaveFile_writes_dependencies_with_qualifiers_to_cache() { + WriteCache writeCache = mock(WriteCache.class); + SensorContextTester ctx = SensorContextTester.create(new File("")); + ctx.setCacheEnabled(true); + ctx.setNextCache(writeCache); + + scan(ctx, "src/test/files/springcontext/QualifiedFieldDependencies.java"); + + var dataCaptor = ArgumentCaptor.forClass(byte[].class); + verify(writeCache).write(anyString(), dataCaptor.capture()); + String serialized = new String(dataCaptor.getValue(), StandardCharsets.UTF_8); + + String encodedQualifier = Base64.getEncoder().encodeToString("primaryContext".getBytes(StandardCharsets.UTF_8)); + assertThat(serialized) + .contains("org.springframework.context.ApplicationContext=" + encodedQualifier) + .contains("org.springframework.core.env.Environment="); + } + + @Test + void scanWithoutParsing_restores_dependencies_with_and_without_qualifier_from_cache() { + InputFile inputFile = TestUtils.inputFile(new File("src/test/files/springcontext/QualifiedFieldDependencies.java")); + String cacheKey = "java:spring:bean-definitions:" + inputFile.key(); + String encodedName = Base64.getEncoder().encodeToString("qualifiedFieldDependencies".getBytes(StandardCharsets.UTF_8)); + String encodedQualifier = Base64.getEncoder().encodeToString("primaryContext".getBytes(StandardCharsets.UTF_8)); + String depWithQualifier = "org.springframework.context.ApplicationContext=" + encodedQualifier; + String depWithoutQualifier = "org.springframework.core.env.Environment="; + String serialized = encodedName + "|checks.spring.context.QualifiedFieldDependencies|checks.spring.context|10:6:10:30|false|" + + depWithQualifier + "," + depWithoutQualifier; + + JavaReadCache readCache = mock(JavaReadCache.class); + when(readCache.readBytes(cacheKey)).thenReturn(serialized.getBytes(StandardCharsets.UTF_8)); + CacheContext cacheContext = mockCacheContext(readCache, mock(JavaWriteCache.class)); + + InputFileScannerContext context = mock(InputFileScannerContext.class); + when(context.getInputFile()).thenReturn(inputFile); + when(context.getCacheContext()).thenReturn(cacheContext); + + assertThat(gatherer.scanWithoutParsing(context)).isTrue(); + + ModuleScannerContext moduleScannerContext = mock(ModuleScannerContext.class); + when(moduleScannerContext.getModuleKey()).thenReturn(""); + gatherer.gatherSpringContextData(moduleScannerContext, model); + + var beans = model.getBeanDefinitionRegistry().getByName("qualifiedFieldDependencies"); + assertThat(beans).hasSize(1); + assertThat(beans.get(0).getDependingBeans()) + .extracting(BeanDependency::qualifier) + .containsExactlyInAnyOrder("primaryContext", null); + } + + @Test + void blank_qualifier_value_is_treated_as_no_qualifier() { + scan("src/test/files/springcontext/BlankQualifierDependency.java"); + + var beans = model.getBeanDefinitionRegistry().getByName("blankQualifierDependency"); + assertThat(beans).hasSize(1); + assertThat(beans.get(0).getDependingBeans()) + .extracting(BeanDependency::qualifier) + .containsOnly((String) null); + } + private static CacheContext mockCacheContext(JavaReadCache readCache, JavaWriteCache writeCache) { CacheContext cacheContext = mock(CacheContext.class); when(cacheContext.isCacheEnabled()).thenReturn(true); From 4e9bfc74ef4b4a3b9e915c81cd1373fe2b84d24a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Wed, 19 Aug 2026 17:05:07 +0200 Subject: [PATCH 6/7] Replace the list of depending beans with a Map --- .../springcontext/BeanDefinitionGatherer.java | 72 +++++++++---------- .../springcontext/BeanDefinitionHolder.java | 26 ++++--- .../model/springcontext/BeanDependency.java | 28 -------- .../BeanDefinitionGathererTest.java | 39 ++++------ 4 files changed, 64 insertions(+), 101 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/BeanDefinitionGatherer.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java index bf01cffb24f..761f1df4539 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 @@ -19,9 +19,10 @@ import java.beans.Introspector; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Base64; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -66,7 +67,7 @@ public class BeanDefinitionGatherer extends SpringContextModelGatherer { private static final String BEAN_SEPARATOR = "\n"; private static final String FIELD_SEPARATOR = "|"; private static final String DEP_SEPARATOR = ","; - private static final String DEP_QUALIFIER_SEPARATOR = "="; + private static final String DEP_KEY_VALUE_SEPARATOR = ":"; private static final String PRIMARY_ANNOTATION = "org.springframework.context.annotation.Primary"; private static final String VALUE_ATTRIBUTE = "value"; @@ -83,7 +84,7 @@ private record BeanData( InputFile inputFile, AnalyzerMessage.TextSpan textSpan, boolean isPrimary, - List dependingBeans) { + Map dependingBeans) { } @Override @@ -111,7 +112,7 @@ public void visitNode(Tree tree) { if (SpringUtils.STEREOTYPE_ANNOTATIONS.stream().anyMatch(meta::isAnnotatedWith)) { String beanName = extractBeanName(meta) .orElseGet(() -> defaultBeanName(classTree.simpleName().name())); - List deps = collectAutowiredDependencies(classTree); + Map deps = collectAutowiredDependencies(classTree); // Class-level bean (stereotype annotations) var beanData = new BeanData( beanName, fqn, pkg, @@ -155,8 +156,8 @@ private static void writeToCache(JavaFileScannerContext context, List } private static String serializeBean(BeanData bean) { - var deps = bean.dependingBeans().stream() - .map(BeanDefinitionGatherer::serializeDependency) + var deps = bean.dependingBeans().entrySet().stream() + .map(e -> e.getKey() + DEP_KEY_VALUE_SEPARATOR + e.getValue()) .collect(Collectors.joining(DEP_SEPARATOR)); var span = bean.textSpan(); var encodedName = Base64.getEncoder().encodeToString(bean.beanName().getBytes(StandardCharsets.UTF_8)); @@ -169,23 +170,6 @@ private static String serializeBean(BeanData bean) { deps); } - private static BeanDependency deserializeDependency(String serialized) { - int idx = serialized.indexOf(DEP_QUALIFIER_SEPARATOR); - String typeFqn = serialized.substring(0, idx); - String encodedQualifier = serialized.substring(idx + 1); - String qualifier = encodedQualifier.isEmpty() - ? null - : new String(Base64.getDecoder().decode(encodedQualifier), StandardCharsets.UTF_8); - return new BeanDependency(typeFqn, qualifier); - } - - private static String serializeDependency(BeanDependency dep) { - String encodedQualifier = dep.qualifier() != null - ? Base64.getEncoder().encodeToString(dep.qualifier().getBytes(StandardCharsets.UTF_8)) - : ""; - return dep.typeFqn() + DEP_QUALIFIER_SEPARATOR + encodedQualifier; - } - @Override public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) { for (BeanData data : collectedBeans) { @@ -244,10 +228,13 @@ private static BeanData deserializeBean(String line, InputFile inputFile) { Integer.parseInt(spanParts[2]), Integer.parseInt(spanParts[3])); boolean isPrimary = Boolean.parseBoolean(fields[4]); - List deps = fields[5].isEmpty() ? List.of() : - Arrays.stream(fields[5].split(DEP_SEPARATOR)) - .map(BeanDefinitionGatherer::deserializeDependency) - .toList(); + Map deps = new LinkedHashMap<>(); + if (!fields[5].isEmpty()) { + for (String entry : fields[5].split(DEP_SEPARATOR)) { + int idx = entry.indexOf(DEP_KEY_VALUE_SEPARATOR); + deps.put(entry.substring(0, idx), entry.substring(idx + 1)); + } + } return new BeanData(beanName, type, beanPackage, inputFile, textSpan, isPrimary, deps); } @@ -293,9 +280,7 @@ private void collectBeanMethod(MethodTree method, String pkg) { ? method.returnType().symbolType().fullyQualifiedName() : ""; - List paramDeps = method.parameters().stream() - .map(p -> new BeanDependency(p.symbol().type().fullyQualifiedName(), extractQualifier(p.symbol().metadata()))) - .toList(); + Map paramDeps = parameterDependencies(method); var beanData = new BeanData( beanName, returnTypeFqn, pkg, @@ -307,26 +292,37 @@ private void collectBeanMethod(MethodTree method, String pkg) { beansCollectedAtFileLevel.add(beanData); } - private static List collectAutowiredDependencies(ClassTree classTree) { - List deps = new ArrayList<>(); + private static Map collectAutowiredDependencies(ClassTree classTree) { + Map deps = new LinkedHashMap<>(); for (Tree member : classTree.members()) { - if (member.is(Tree.Kind.VARIABLE)) { - VariableTree field = (VariableTree) member; + if (member instanceof VariableTree field) { if (field.symbol().metadata().isAnnotatedWith(SpringUtils.AUTOWIRED_ANNOTATION)) { - deps.add(new BeanDependency(field.symbol().type().fullyQualifiedName(), extractQualifier(field.symbol().metadata()))); + String typeFqn = field.symbol().type().fullyQualifiedName(); + deps.put(dependencyKey(field.simpleName().name(), extractQualifier(field.symbol().metadata())), typeFqn); } } else if (member.is(Tree.Kind.CONSTRUCTOR, Tree.Kind.METHOD)) { MethodTree method = (MethodTree) member; if (method.symbol().metadata().isAnnotatedWith(SpringUtils.AUTOWIRED_ANNOTATION)) { - method.parameters().stream() - .map(p -> new BeanDependency(p.symbol().type().fullyQualifiedName(), extractQualifier(p.symbol().metadata()))) - .forEach(deps::add); + deps.putAll(parameterDependencies(method)); } } } return deps; } + private static Map parameterDependencies(MethodTree method) { + Map deps = new LinkedHashMap<>(); + for (var p : method.parameters()) { + String typeFqn = p.symbol().type().fullyQualifiedName(); + deps.put(dependencyKey(p.simpleName().name(), extractQualifier(p.symbol().metadata())), typeFqn); + } + return deps; + } + + private static String dependencyKey(String fieldOrParamName, @Nullable String qualifier) { + return qualifier != null ? qualifier : fieldOrParamName; + } + @Nullable private static String extractQualifier(SymbolMetadata metadata) { List attrs = metadata.valuesForAnnotation(SpringUtils.QUALIFIER_ANNOTATION); diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java index 1da2d5c3828..e551e9f4bc2 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java @@ -16,8 +16,8 @@ */ package org.sonar.java.model.springcontext; -import java.util.ArrayList; -import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; import javax.annotation.Nullable; /** @@ -51,8 +51,12 @@ public class BeanDefinitionHolder { /** Source location where the bean definition appears. */ private final BeanLocation location; - /** Dependencies this bean requires, each capturing the required type and an optional {@code @Qualifier} name. */ - private List dependingBeans; + /** + * Dependencies this bean requires. + * Key: the qualifier name if {@code @Qualifier} is present, otherwise the default Spring bean name + * (simple class name, decapitalized). Value: the fully-qualified type name. + */ + private Map dependingBeans; /** Comma-separated Spring profile expressions under which this bean is active, or {@code null} if unconditional. */ @Nullable @@ -68,8 +72,8 @@ private BeanDefinitionHolder(String type, String module, String beanPackage, Bea this.location = location; } - private void setDependingBeans(List beansList) { - this.dependingBeans = beansList; + private void setDependingBeans(Map beans) { + this.dependingBeans = beans; } private void setProfiles(@Nullable String profiles) { @@ -96,7 +100,7 @@ public BeanLocation getLocation() { return location; } - public List getDependingBeans() { + public Map getDependingBeans() { return dependingBeans; } @@ -114,7 +118,7 @@ public static class Builder { private final String module; private final String beanPackage; private final BeanLocation location; - private List dependingBeans = new ArrayList<>(); + private Map dependingBeans = new LinkedHashMap<>(); @Nullable private String profiles; private boolean isPrimary = false; @@ -126,8 +130,8 @@ public Builder(String type, String module, String beanPackage, BeanLocation loca this.location = location; } - public Builder dependingBeans(List beansList) { - this.dependingBeans = beansList; + public Builder dependingBeans(Map beans) { + this.dependingBeans = beans; return this; } @@ -143,7 +147,7 @@ public Builder primary() { public BeanDefinitionHolder build() { BeanDefinitionHolder holder = new BeanDefinitionHolder(type, module, beanPackage, location); - holder.setDependingBeans(List.copyOf(dependingBeans)); + holder.setDependingBeans(Map.copyOf(dependingBeans)); holder.setProfiles(profiles); if (isPrimary) { holder.setPrimary(); 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) { -} 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 832a8585a97..ecf9ddaa24a 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 @@ -36,7 +36,6 @@ import org.sonar.plugins.java.api.caching.CacheContext; import org.sonar.plugins.java.api.caching.JavaReadCache; import org.sonar.plugins.java.api.caching.JavaWriteCache; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.ArgumentMatchers.any; @@ -191,8 +190,7 @@ void dependencies_collected_as_depending_beans(String filePath, String expectedB var beans = model.getBeanDefinitionRegistry().getByName(expectedBeanName); assertThat(beans).hasSize(1); - assertThat(beans.get(0).getDependingBeans()) - .extracting(BeanDependency::typeFqn) + assertThat(beans.get(0).getDependingBeans().values()) .containsExactlyInAnyOrder( "org.springframework.context.ApplicationContext", "org.springframework.core.env.Environment" @@ -216,28 +214,26 @@ void qualifier_is_captured_on_qualified_dependency(String filePath, String expec var beans = model.getBeanDefinitionRegistry().getByName(expectedBeanName); assertThat(beans).hasSize(1); - assertThat(beans.get(0).getDependingBeans()) - .extracting(BeanDependency::qualifier) - .containsExactlyInAnyOrder("primaryContext", null); + assertThat(beans.get(0).getDependingBeans().keySet()) + .containsExactlyInAnyOrder("primaryContext", "environment"); } static Stream qualifiedDependencyArguments() { return Stream.of( - Arguments.of("src/test/files/springcontext/QualifiedFieldDependencies.java", "qualifiedFieldDependencies"), + Arguments.of("src/test/files/springcontext/QualifiedFieldDependencies.java", "qualifiedFieldDependencies"), Arguments.of("src/test/files/springcontext/QualifiedConstructorDependencies.java", "qualifiedConstructorDependencies"), - Arguments.of("src/test/files/springcontext/QualifiedBeanMethodDependencies.java", "myBean") + Arguments.of("src/test/files/springcontext/QualifiedBeanMethodDependencies.java", "myBean") ); } @Test - void no_qualifier_results_in_null_qualifier() { + void unqualified_dependency_uses_default_spring_bean_name_as_key() { scan("src/test/files/springcontext/AutowiredDependencies.java"); var beans = model.getBeanDefinitionRegistry().getByName("autowiredDependencies"); assertThat(beans).hasSize(1); - assertThat(beans.get(0).getDependingBeans()) - .extracting(BeanDependency::qualifier) - .containsOnly((String) null); + assertThat(beans.get(0).getDependingBeans().keySet()) + .containsExactlyInAnyOrder("applicationContext", "environment"); } // ---- Bean location -------------------------------------------------------- @@ -394,10 +390,9 @@ void leaveFile_writes_dependencies_with_qualifiers_to_cache() { verify(writeCache).write(anyString(), dataCaptor.capture()); String serialized = new String(dataCaptor.getValue(), StandardCharsets.UTF_8); - String encodedQualifier = Base64.getEncoder().encodeToString("primaryContext".getBytes(StandardCharsets.UTF_8)); assertThat(serialized) - .contains("org.springframework.context.ApplicationContext=" + encodedQualifier) - .contains("org.springframework.core.env.Environment="); + .contains("primaryContext:org.springframework.context.ApplicationContext") + .contains("environment:org.springframework.core.env.Environment"); } @Test @@ -405,11 +400,9 @@ void scanWithoutParsing_restores_dependencies_with_and_without_qualifier_from_ca InputFile inputFile = TestUtils.inputFile(new File("src/test/files/springcontext/QualifiedFieldDependencies.java")); String cacheKey = "java:spring:bean-definitions:" + inputFile.key(); String encodedName = Base64.getEncoder().encodeToString("qualifiedFieldDependencies".getBytes(StandardCharsets.UTF_8)); - String encodedQualifier = Base64.getEncoder().encodeToString("primaryContext".getBytes(StandardCharsets.UTF_8)); - String depWithQualifier = "org.springframework.context.ApplicationContext=" + encodedQualifier; - String depWithoutQualifier = "org.springframework.core.env.Environment="; String serialized = encodedName + "|checks.spring.context.QualifiedFieldDependencies|checks.spring.context|10:6:10:30|false|" - + depWithQualifier + "," + depWithoutQualifier; + + "primaryContext:org.springframework.context.ApplicationContext" + + ",environment:org.springframework.core.env.Environment"; JavaReadCache readCache = mock(JavaReadCache.class); when(readCache.readBytes(cacheKey)).thenReturn(serialized.getBytes(StandardCharsets.UTF_8)); @@ -427,9 +420,8 @@ void scanWithoutParsing_restores_dependencies_with_and_without_qualifier_from_ca var beans = model.getBeanDefinitionRegistry().getByName("qualifiedFieldDependencies"); assertThat(beans).hasSize(1); - assertThat(beans.get(0).getDependingBeans()) - .extracting(BeanDependency::qualifier) - .containsExactlyInAnyOrder("primaryContext", null); + assertThat(beans.get(0).getDependingBeans().keySet()) + .containsExactlyInAnyOrder("primaryContext", "environment"); } @Test @@ -439,8 +431,7 @@ void blank_qualifier_value_is_treated_as_no_qualifier() { var beans = model.getBeanDefinitionRegistry().getByName("blankQualifierDependency"); assertThat(beans).hasSize(1); assertThat(beans.get(0).getDependingBeans()) - .extracting(BeanDependency::qualifier) - .containsOnly((String) null); + .containsOnlyKeys("applicationContext"); } private static CacheContext mockCacheContext(JavaReadCache readCache, JavaWriteCache writeCache) { From 1bd1b2152bf49eb951c4e18c9a387e4c0a5a7379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Wed, 19 Aug 2026 17:54:17 +0200 Subject: [PATCH 7/7] Address review comments --- .../model/springcontext/BeanDefinitionGatherer.java | 6 ++++-- .../model/springcontext/BeanDefinitionHolder.java | 4 ++-- .../springcontext/BeanDefinitionGathererTest.java | 12 ++++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) 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 761f1df4539..44245d1a390 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 @@ -157,7 +157,8 @@ private static void writeToCache(JavaFileScannerContext context, List private static String serializeBean(BeanData bean) { var deps = bean.dependingBeans().entrySet().stream() - .map(e -> e.getKey() + DEP_KEY_VALUE_SEPARATOR + e.getValue()) + .map(e -> Base64.getEncoder().encodeToString(e.getKey().getBytes(StandardCharsets.UTF_8)) + + DEP_KEY_VALUE_SEPARATOR + e.getValue()) .collect(Collectors.joining(DEP_SEPARATOR)); var span = bean.textSpan(); var encodedName = Base64.getEncoder().encodeToString(bean.beanName().getBytes(StandardCharsets.UTF_8)); @@ -232,7 +233,8 @@ private static BeanData deserializeBean(String line, InputFile inputFile) { if (!fields[5].isEmpty()) { for (String entry : fields[5].split(DEP_SEPARATOR)) { int idx = entry.indexOf(DEP_KEY_VALUE_SEPARATOR); - deps.put(entry.substring(0, idx), entry.substring(idx + 1)); + String key = new String(Base64.getDecoder().decode(entry.substring(0, idx)), StandardCharsets.UTF_8); + deps.put(key, entry.substring(idx + 1)); } } return new BeanData(beanName, type, beanPackage, inputFile, textSpan, isPrimary, deps); diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java index e551e9f4bc2..a857a50ca9f 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java @@ -53,8 +53,8 @@ public class BeanDefinitionHolder { /** * Dependencies this bean requires. - * Key: the qualifier name if {@code @Qualifier} is present, otherwise the default Spring bean name - * (simple class name, decapitalized). Value: the fully-qualified type name. + * Key: the {@code @Qualifier} value if present, otherwise the field or parameter name at the injection point. + * Value: the fully-qualified type name. */ private Map dependingBeans; 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 ecf9ddaa24a..4224b00bb41 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 @@ -390,9 +390,11 @@ void leaveFile_writes_dependencies_with_qualifiers_to_cache() { verify(writeCache).write(anyString(), dataCaptor.capture()); String serialized = new String(dataCaptor.getValue(), StandardCharsets.UTF_8); + String encodedPrimaryContext = Base64.getEncoder().encodeToString("primaryContext".getBytes(StandardCharsets.UTF_8)); + String encodedEnvironment = Base64.getEncoder().encodeToString("environment".getBytes(StandardCharsets.UTF_8)); assertThat(serialized) - .contains("primaryContext:org.springframework.context.ApplicationContext") - .contains("environment:org.springframework.core.env.Environment"); + .contains(encodedPrimaryContext + ":org.springframework.context.ApplicationContext") + .contains(encodedEnvironment + ":org.springframework.core.env.Environment"); } @Test @@ -400,9 +402,11 @@ void scanWithoutParsing_restores_dependencies_with_and_without_qualifier_from_ca InputFile inputFile = TestUtils.inputFile(new File("src/test/files/springcontext/QualifiedFieldDependencies.java")); String cacheKey = "java:spring:bean-definitions:" + inputFile.key(); String encodedName = Base64.getEncoder().encodeToString("qualifiedFieldDependencies".getBytes(StandardCharsets.UTF_8)); + String encodedPrimaryContext = Base64.getEncoder().encodeToString("primaryContext".getBytes(StandardCharsets.UTF_8)); + String encodedEnvironment = Base64.getEncoder().encodeToString("environment".getBytes(StandardCharsets.UTF_8)); String serialized = encodedName + "|checks.spring.context.QualifiedFieldDependencies|checks.spring.context|10:6:10:30|false|" - + "primaryContext:org.springframework.context.ApplicationContext" - + ",environment:org.springframework.core.env.Environment"; + + encodedPrimaryContext + ":org.springframework.context.ApplicationContext" + + "," + encodedEnvironment + ":org.springframework.core.env.Environment"; JavaReadCache readCache = mock(JavaReadCache.class); when(readCache.readBytes(cacheKey)).thenReturn(serialized.getBytes(StandardCharsets.UTF_8));