Skip to content
Merged
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 @@ -40,7 +40,7 @@ public void visitNode(Tree tree) {
return;
}

var beanMethods = getBeanMethods(classTree);
var beanMethods = SpringUtils.getBeanMethods(classTree);
var foundNames = new HashSet<String>();
for (MethodTree beanMethod : beanMethods) {
if (!foundNames.add(beanMethod.simpleName().name())) {
Expand All @@ -53,12 +53,4 @@ private static boolean isConfigurationClass(ClassTree classTree) {
return classTree.symbol().metadata().isAnnotatedWith(SpringUtils.CONFIGURATION_ANNOTATION);
}

private static List<MethodTree> getBeanMethods(ClassTree classTree) {
return classTree.members().stream()
.filter(member -> member.is(Tree.Kind.METHOD))
.map(MethodTree.class::cast)
.filter(method -> method.symbol().metadata().isAnnotatedWith(SpringUtils.BEAN_ANNOTATION))
.toList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import java.util.ArrayList;
import java.util.List;
import org.sonar.check.Rule;
import org.sonar.java.model.PackageUtils;
import org.sonar.java.utils.PackageUtils;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import java.util.List;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.QuickFixHelper;
import org.sonar.java.model.PackageUtils;
import org.sonar.java.utils.PackageUtils;
import org.sonar.java.reporting.JavaQuickFix;
import org.sonar.java.reporting.JavaTextEdit;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import org.sonar.check.RuleProperty;
import org.sonar.java.ast.visitors.PublicApiChecker;
import org.sonar.java.checks.helpers.Javadoc;
import org.sonar.java.model.PackageUtils;
import org.sonar.java.utils.PackageUtils;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import java.util.regex.Pattern;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.java.model.PackageUtils;
import org.sonar.java.utils.PackageUtils;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.check.Rule;
import org.sonar.java.utils.PackageUtils;
import org.sonar.java.utils.SpringUtils;
import org.sonar.java.model.DefaultJavaFileScannerContext;
import org.sonar.java.model.DefaultModuleScannerContext;
Expand Down Expand Up @@ -115,7 +116,7 @@ public void visitNode(Tree tree) {
return;
}

String classPackageName = packageNameOf(classTree.symbol());
String classPackageName = PackageUtils.packageNameOf(classTree.symbol());
SymbolMetadata classSymbolMetadata = classTree.symbol().metadata();

// try to apply "direct" annotation first
Expand Down Expand Up @@ -199,7 +200,7 @@ private static List<String> targetedPackages(String classPackageName, SymbolMeta
if (!isClassBased && element instanceof String s) {
packages.add(s);
} else if (isClassBased && element instanceof Symbol s) {
packages.add(packageNameOf(s));
packages.add(PackageUtils.packageNameOf(s));
}
}
}
Expand All @@ -221,20 +222,12 @@ private void addToScannedPackages(SymbolMetadata.AnnotationValue annotationValue
packagesScannedBySpringAtProjectLevel.add(oString);
}
if (o instanceof Symbol oSymbol) {
packagesScannedBySpringAtProjectLevel.add(packageNameOf(oSymbol));
packagesScannedBySpringAtProjectLevel.add(PackageUtils.packageNameOf(oSymbol));
}
}
}
}

private static String packageNameOf(Symbol symbol) {
Symbol owner = symbol.owner();
while (!owner.isPackageSymbol()) {
owner = owner.owner();
}
return owner.name();
}

private static boolean hasAnnotation(SymbolMetadata classSymbolMetadata, String... annotationName) {
return Arrays.stream(annotationName).anyMatch(classSymbolMetadata::isAnnotatedWith);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* 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.beans.Introspector;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.java.reporting.AnalyzerMessage;
import org.sonar.java.utils.PackageUtils;
import org.sonar.java.utils.SpringUtils;
import org.sonar.plugins.java.api.InputFileScannerContext;
import org.sonar.plugins.java.api.ModuleScannerContext;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

/**
* Collects Spring bean definitions discovered during AST traversal, and registers them in the
* {@link BeanDefinitionRegistry} of the shared {@link SpringContextModel} at the end of the module analysis.
*
* <p>Discovers beans from:
* <ul>
* <li>Classes annotated with stereotype annotations: {@code @Component}, {@code @Service},
* {@code @Repository}, {@code @Controller}, {@code @RestController}, {@code @Configuration}</li>
* <li>{@code @Bean} methods inside {@code @Configuration} or {@code @Component} classes</li>
* </ul>
*
* <p>Also captures:
* <ul>
* <li>{@code @Primary} designation</li>
* <li>Dependencies via {@code @Autowired} fields, constructors, and setters for class-level beans</li>
* <li>Dependencies via method parameters for {@code @Bean} method beans</li>
* </ul>
*/
public class BeanDefinitionGatherer extends SpringContextModelGatherer {

private static final String PRIMARY_ANNOTATION = "org.springframework.context.annotation.Primary";

private final List<BeanData> collectedBeans = new ArrayList<>();

private record BeanData(
String beanName,
String type,
String beanPackage,
InputFile inputFile,
AnalyzerMessage.TextSpan textSpan,
boolean isPrimary,
List<String> dependingBeans) {
}

@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;
}

SymbolMetadata meta = classTree.symbol().metadata();
String fqn = classTree.symbol().type().fullyQualifiedName();
String pkg = PackageUtils.packageNameOf(classTree.symbol());
Comment thread
gitar-bot[bot] marked this conversation as resolved.

if (SpringUtils.STEREOTYPE_ANNOTATIONS.stream().anyMatch(meta::isAnnotatedWith)) {
String beanName = extractBeanName(meta)
.orElseGet(() -> defaultBeanName(classTree.simpleName().name()));
List<String> deps = collectAutowiredDependencies(classTree);
// Class-level bean (stereotype annotations)
collectedBeans.add(new BeanData(
beanName, fqn, pkg,
context.getInputFile(),
AnalyzerMessage.textSpanFor(classTree.simpleName()),
meta.isAnnotatedWith(PRIMARY_ANNOTATION),
deps));

// @Bean methods — only if class is a configuration/component class
for (MethodTree method : SpringUtils.getBeanMethods(classTree)) {
collectBeanMethod(method, pkg);
}
}
}

@Override
public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) {
for (BeanData data : collectedBeans) {
var location = new BeanLocation(data.inputFile(), data.textSpan());
var holderBuilder = new BeanDefinitionHolder.Builder(
data.type(), context.getModuleKey(), data.beanPackage(), location)
.dependingBeans(data.dependingBeans());
if (data.isPrimary()) {
holderBuilder.primary();
}
springContextModel.getBeanDefinitionRegistry()
.addBeanDefinition(data.beanName(), holderBuilder.build());
}
}

@Override
public boolean scanWithoutParsing(InputFileScannerContext ctx) {
// Bean data is not cached yet; force parsing so beans are
// always collected even for unchanged files in incremental runs.
return false;
}

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);
}
Comment thread
asya-vorobeva marked this conversation as resolved.

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 returnTypeFqn = method.returnType() != null
? method.returnType().symbolType().fullyQualifiedName()
: "";

List<String> paramDeps = method.parameters().stream()
.map(p -> p.symbol().type().fullyQualifiedName())
.toList();

collectedBeans.add(new BeanData(
beanName, returnTypeFqn, pkg,
context.getInputFile(),
AnalyzerMessage.textSpanFor(method.simpleName()),
beanMeta.isAnnotatedWith(PRIMARY_ANNOTATION),
paramDeps));
}

private static List<String> collectAutowiredDependencies(ClassTree classTree) {
List<String> 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());
}
} 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())
.forEach(deps::add);
}
Comment on lines +180 to +194

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Implicit single-constructor injection not captured as dependencies

collectAutowiredDependencies only records dependencies for fields/methods/constructors explicitly annotated with @Autowired (BeanDefinitionGatherer.java:206-224). Since Spring 4.3, a bean class with a single constructor is autowired implicitly without requiring @Autowired. Such beans will be registered with an empty dependingBeans list, so downstream checks relying on the dependency graph (e.g. unused/unsatisfied bean detection) will see incomplete data. Consider also collecting parameters of a class's sole constructor when no member is @Autowired-annotated.

Was this helpful? React with 👍 / 👎

}
}
return deps;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.java.utils.PackageUtils;
import org.sonar.java.utils.SpringUtils;
import org.sonar.plugins.java.api.InputFileScannerContext;
import org.sonar.plugins.java.api.JavaFileScannerContext;
Expand Down Expand Up @@ -127,7 +128,7 @@ private void collectFromSpringBootApplication(Symbol classSymbol, SymbolMetadata
if (!metadata.isAnnotatedWith(SpringUtils.SPRING_BOOT_APP_ANNOTATION)) {
return;
}
var packages = targetedPackages(packageNameOf(classSymbol), metadata, packagesCollectedAtFileLevel.isEmpty());
var packages = targetedPackages(PackageUtils.packageNameOf(classSymbol), metadata, packagesCollectedAtFileLevel.isEmpty());
collectedPackages.addAll(packages);
packagesCollectedAtFileLevel.addAll(packages);
}
Expand Down Expand Up @@ -159,7 +160,7 @@ private void addAnnotationValueToCollectedPackages(SymbolMetadata.AnnotationValu
collectedPackages.add(oString);
packagesCollectedAtFileLevel.add(oString);
} else if (o instanceof Symbol oSymbol) {
var pkg = packageNameOf(oSymbol);
var pkg = PackageUtils.packageNameOf(oSymbol);
if (!pkg.isBlank()) {
collectedPackages.add(pkg);
packagesCollectedAtFileLevel.add(pkg);
Expand All @@ -173,20 +174,12 @@ private static Optional<String> resolvePackage(Object element, boolean isClassBa
if (!isClassBased && element instanceof String s && !s.isBlank()) {
return Optional.of(s);
} else if (isClassBased && element instanceof Symbol s) {
var pkg = packageNameOf(s);
var pkg = PackageUtils.packageNameOf(s);
return pkg.isBlank() ? Optional.empty() : Optional.of(pkg);
}
return Optional.empty();
}

private static String packageNameOf(Symbol symbol) {
Symbol owner = symbol.owner();
while (!owner.isPackageSymbol()) {
owner = owner.owner();
}
return owner.name();
}

private static String cacheKey(InputFile inputFile) {
return CACHE_KEY_PREFIX + inputFile.key();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ private SpringContextModelGatherers() {
*/
public static List<JavaCheck> getAllGatherers() {
return List.of(
new ComponentScanPackageGatherer()
new ComponentScanPackageGatherer(),
new BeanDefinitionGatherer()
);
}

Expand Down
Loading
Loading