Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -16,7 +16,6 @@
*/
package org.sonar.java.model.springcontext;

import java.beans.Introspector;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
Expand Down Expand Up @@ -105,8 +104,7 @@ public void visitNode(Tree tree) {
String pkg = PackageUtils.packageNameOf(classTree.symbol());

if (SpringUtils.STEREOTYPE_ANNOTATIONS.stream().anyMatch(meta::isAnnotatedWith)) {
String beanName = extractBeanName(meta)
.orElseGet(() -> defaultBeanName(classTree.simpleName().name()));
String beanName = SpringUtils.resolveStereotypeBeanName(meta, classTree.simpleName().name());
List<String> deps = collectAutowiredDependencies(classTree);
// Class-level bean (stereotype annotations)
collectedBeans.add(new BeanData(
Expand Down Expand Up @@ -229,43 +227,9 @@ private static BeanData deserializeBean(String line, InputFile inputFile) {
return new BeanData(beanName, type, beanPackage, inputFile, textSpan, isPrimary, deps);
}

private static Optional<String> extractBeanName(SymbolMetadata meta) {
for (String annotation : SpringUtils.STEREOTYPE_ANNOTATIONS) {
List<SymbolMetadata.AnnotationValue> attrs = meta.valuesForAnnotation(annotation);
if (attrs != null) {
Optional<String> name = attrs.stream()
.filter(v -> "value".equals(v.name()) || "name".equals(v.name()))
.map(v -> (String) v.value())
.filter(s -> !s.isBlank())
.findFirst();
if (name.isPresent()) {
return name;
}
}
}
return Optional.empty();
}

private static String defaultBeanName(String simpleName) {
return Introspector.decapitalize(simpleName);
}

private void collectBeanMethod(MethodTree method, String pkg) {
SymbolMetadata beanMeta = method.symbol().metadata();
List<SymbolMetadata.AnnotationValue> 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()))
.map(v -> {
Object val = v.value();
if (val instanceof Object[] arr && arr.length > 0) {
return (String) arr[0];
}
return val instanceof String s ? s : null;
})
.filter(s -> s != null && !s.isBlank())
.findFirst())
.orElseGet(() -> method.simpleName().name());
String beanName = SpringUtils.resolveBeanMethodName(method);

String returnTypeFqn = method.returnType() != null
? method.returnType().symbolType().fullyQualifiedName()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ private SpringContextModelGatherers() {
public static List<JavaCheck> getAllGatherers() {
return List.of(
new ComponentScanPackageGatherer(),
new BeanDefinitionGatherer()
new BeanDefinitionGatherer(),
new TypeToBeanNamesIndexGatherer()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ public void addBeanForType(String beanType, String beanName) {
public Set<String> getNamesForType(String beanType) {
return Collections.unmodifiableSet(beanNamesByType.getOrDefault(beanType, Set.of()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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 java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.sonar.java.utils.SpringUtils;
import org.sonar.plugins.java.api.ModuleScannerContext;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;

/**
* Populates {@link TypeToBeanNamesIndex} by mapping every type in a bean's hierarchy
* (concrete class, superclasses, interfaces) to the bean's name.
*/
public class TypeToBeanNamesIndexGatherer extends SpringContextModelGatherer {

private record BeanTypeEntry(String beanName, Set<String> typeHierarchy) {}

private final List<BeanTypeEntry> collectedEntries = new ArrayList<>();

@Override
public List<Tree.Kind> nodesToVisit() {
return List.of(Tree.Kind.CLASS);
}

@Override
public void visitNode(Tree tree) {
ClassTree classTree = (ClassTree) tree;
if (classTree.simpleName() == null) {
return;
}

var meta = classTree.symbol().metadata();
if (SpringUtils.STEREOTYPE_ANNOTATIONS.stream().anyMatch(meta::isAnnotatedWith)) {
String beanName = SpringUtils.resolveStereotypeBeanName(meta, classTree.simpleName().name());
collectedEntries.add(new BeanTypeEntry(beanName, collectTypeHierarchy(classTree.symbol())));

for (MethodTree method : SpringUtils.getBeanMethods(classTree)) {
Set<String> typeHierarchy = collectTypeHierarchy(method.returnType().symbolType().symbol());
for (String methodBeanName : SpringUtils.resolveBeanMethodNames(method)) {
collectedEntries.add(new BeanTypeEntry(methodBeanName, typeHierarchy));
}
}
}
}

@Override
public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) {
TypeToBeanNamesIndex index = springContextModel.getTypeToBeanNamesIndex();
for (BeanTypeEntry entry : collectedEntries) {
for (String typeFqn : entry.typeHierarchy()) {
index.addBeanForType(typeFqn, entry.beanName());
}
}
}

private static Set<String> collectTypeHierarchy(Symbol.TypeSymbol symbol) {
Set<String> visited = new LinkedHashSet<>();
walkTypeHierarchy(symbol, visited);
return visited;
}

private static void walkTypeHierarchy(Symbol.TypeSymbol symbol, Set<String> visited) {
String fqn = symbol.type().fullyQualifiedName();
if ("java.lang.Object".equals(fqn) || symbol.type().isUnknown() || !visited.add(fqn)) {
return;
}
Type superClass = symbol.superClass();
if (superClass != null && !superClass.isUnknown()) {
walkTypeHierarchy(superClass.symbol(), visited);
}
for (Type iface : symbol.interfaces()) {
if (!iface.isUnknown()) {
walkTypeHierarchy(iface.symbol(), visited);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
*/
package org.sonar.java.utils;

import java.beans.Introspector;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import org.sonar.java.model.ExpressionUtils;
import org.sonar.plugins.java.api.semantic.Symbol;
Expand Down Expand Up @@ -52,6 +56,8 @@ public final class SpringUtils {
CONFIGURATION_ANNOTATION
);

public static final String VALUE_ATTRIBUTE = "value";

private SpringUtils() {
// Utils class
}
Expand All @@ -63,7 +69,7 @@ public static boolean isScopeSingleton(SymbolMetadata clazzMeta) {
return true;
}
for (SymbolMetadata.AnnotationValue annotationValue : values) {
if ("value".equals(annotationValue.name()) || "scopeName".equals(annotationValue.name())) {
if (VALUE_ATTRIBUTE.equals(annotationValue.name()) || "scopeName".equals(annotationValue.name())) {
Object value = annotationValue.value();
if (value instanceof String stringValue && !"singleton".equals(stringValue)) {
return false;
Expand All @@ -90,6 +96,58 @@ public static boolean isSpringBootUnitTest(MethodTree methodTree) {
return UnitTestUtils.isUnitTest(methodTree) && SpringUtils.isSpringBootTestClass(parentClass.symbol());
}

/**
* Resolves the Spring bean name for a stereotype-annotated class.
* Returns the explicit name from the annotation if present, otherwise the decapitalized simple class name.
*/
public static String resolveStereotypeBeanName(SymbolMetadata meta, String simpleName) {
for (String annotation : STEREOTYPE_ANNOTATIONS) {
List<SymbolMetadata.AnnotationValue> attrs = meta.valuesForAnnotation(annotation);
if (attrs != null) {
Optional<String> name = attrs.stream()
.filter(v -> VALUE_ATTRIBUTE.equals(v.name()))
.map(v -> (String) v.value())
.filter(s -> s != null && !s.isBlank())
.findFirst();
Comment thread
gitar-bot[bot] marked this conversation as resolved.
if (name.isPresent()) {
return name.get();
}
}
}
return Introspector.decapitalize(simpleName);
}

/**
* Resolves all Spring bean names for a {@code @Bean} factory method, including aliases.
* Returns the explicit names from the annotation if present, otherwise a singleton list of the method name.
*/
public static List<String> resolveBeanMethodNames(MethodTree method) {
List<SymbolMetadata.AnnotationValue> attrs = method.symbol().metadata().valuesForAnnotation(BEAN_ANNOTATION);
if (attrs == null) {
return List.of(method.simpleName().name());
}
List<String> names = attrs.stream()
.filter(v -> VALUE_ATTRIBUTE.equals(v.name()) || "name".equals(v.name()))
.flatMap(v -> {
Object val = v.value();
if (val instanceof Object[] arr) {
return Arrays.stream(arr).filter(String.class::isInstance).map(String.class::cast);
}
return Stream.empty();
})
.filter(s -> !s.isBlank())
.toList();
return names.isEmpty() ? List.of(method.simpleName().name()) : names;
}

/**
* Resolves the primary Spring bean name for a {@code @Bean} factory method.
* Returns the first explicit name from the annotation if present, otherwise the method name.
*/
public static String resolveBeanMethodName(MethodTree method) {
return resolveBeanMethodNames(method).get(0);
}

public static List<MethodTree> getBeanMethods(ClassTree classTree) {
return classTree.members().stream()
.filter(member -> member.is(Tree.Kind.METHOD))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package checks.spring.context;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
class ComponentImplementingInterface implements ApplicationContextAware {

@Override
public void setApplicationContext(ApplicationContext ctx) {
// not needed for test
}
}
Loading
Loading