SONARJAVA-6413 Implement BeanDefinitionGatherer to collect Spring bean definitions#5665
Conversation
592816e to
facd96e
Compare
| 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); | ||
| } |
There was a problem hiding this comment.
💡 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 👍 / 👎
facd96e to
cd82edc
Compare
| @@ -43,16 +49,23 @@ public static String packageName(@Nullable PackageDeclarationTree packageDeclara | |||
| pieces.push(separator); | |||
| expr = mse.expression(); | |||
| } | |||
| if (expr.is(Tree.Kind.IDENTIFIER)) { | |||
| IdentifierTree idt = (IdentifierTree) expr; | |||
| pieces.push(idt.name()); | |||
| } | |||
|
|
|||
| pieces.push(((IdentifierTree) expr).name()); | |||
| StringBuilder sb = new StringBuilder(); | |||
| for (String piece: pieces) { | |||
| for (String piece : pieces) { | |||
There was a problem hiding this comment.
💡 Edge Case: packageName removes guard, casts expr to IdentifierTree unconditionally
The refactored PackageUtils.packageName (PackageUtils.java:52) replaces the previous defensive block
if (expr.is(Tree.Kind.IDENTIFIER)) { ... }
with an unconditional cast pieces.push(((IdentifierTree) expr).name());. For normal qualified package declarations the leftmost expression is always an identifier, so this works for the cases covered by the new tests. However, the previous code tolerated a non-identifier leftmost expression by simply skipping it, whereas the new code will throw a ClassCastException. The new PackageUtilsTest does not exercise any non-identifier path (the old branch is now untested). This is low risk in practice but reduces robustness compared to the original; if you intend the cast to always be safe, a comment or an assert documenting the invariant would help, otherwise restore the guard.
Was this helpful? React with 👍 / 👎
cd82edc to
548b900
Compare
548b900 to
24c4376
Compare
…n definitions - Add BeanDefinitionGatherer that discovers beans from stereotype annotations (@component, @service, @repository, @controller, @RestController, @configuration) and @bean methods inside configuration/component classes - Capture @primary flag and dependencies (@Autowired fields, constructors, setters; @bean method parameters) - Register BeanDefinitionGatherer in SpringContextModelGatherers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
24c4376 to
700b5be
Compare
aurelien-coet-sonarsource
left a comment
There was a problem hiding this comment.
Overall LGTM, just a few small comments. I think that the first of the two remaining suggestions by the Gitar bot is also correct ?
| for (MethodTree method : methodsOf(classTree)) { | ||
| if (method.symbol().metadata().isAnnotatedWith(SpringUtils.BEAN_ANNOTATION)) { | ||
| collectBeanMethod(method, pkg); | ||
| } | ||
| } |
There was a problem hiding this comment.
Suggestion: the call to methodsOf needs to iterate over the class members to filter them and only keep methods, then instantiates a list, then this list is iterated over again. It would probably be more efficient to directly operate on the stream:
| for (MethodTree method : methodsOf(classTree)) { | |
| if (method.symbol().metadata().isAnnotatedWith(SpringUtils.BEAN_ANNOTATION)) { | |
| collectBeanMethod(method, pkg); | |
| } | |
| } | |
| classTree.members().stream() | |
| .filter(m -> m.is(Tree.Kind.METHOD)) | |
| .map(MethodTree.class::cast) | |
| .filter(m -> m.symbol().metadata().isAnnotatedWith(SpringUtils.BEAN_ANNOTATION)) | |
| .forEach(method -> collectBeanMethod(method, pkg)); |
There was a problem hiding this comment.
I extract the method already used in another visitor to SpringUtils.
| return false; | ||
| } | ||
|
|
||
| private static boolean isSpringBeanDefinitionsClass(SymbolMetadata meta) { |
There was a problem hiding this comment.
Would it make sense to move this method in SpringUtils ?
There was a problem hiding this comment.
Better solution is to remove this method :) I do it in following commit
| // ---- ComponentScanPackageGatherer ----------------------------------------- | ||
|
|
||
| @Test | ||
| void componentScanPackageGatherer_collects_package_from_springBootApplication() { |
There was a problem hiding this comment.
Why was this test deleted ?
There was a problem hiding this comment.
Because we provide the tests for this gatherer in ComponentScanPackageGathererTest.
There was a problem hiding this comment.
Ah ok, got it!
This one as added as TODO in Jira ticket. Feel free to do it in follow-up
This is really super-low risk, I don't think that such situations will happen in real-world apps. |
|
Code Review 👍 Approved with suggestions 4 resolved / 6 findingsImplements BeanDefinitionGatherer to discover Spring bean definitions and their dependencies. Consider explicitly handling implicit constructor injection and adding null-safety guards to the package identifier casting logic. 💡 Edge Case: Implicit single-constructor injection not captured as dependencies📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:206-220
💡 Edge Case: packageName removes guard, casts expr to IdentifierTree unconditionally📄 java-frontend/src/main/java/org/sonar/java/utils/PackageUtils.java:40-54 The refactored if (expr.is(Tree.Kind.IDENTIFIER)) { ... } with an unconditional cast ✅ 4 resolved✅ Bug: BeanDefinitionGatherer loses beans on incremental (cached) analysis
✅ Quality: isConfigurationOrComponentClass duplicates isSpringBeanClass set
✅ Quality: Redundant duplicate isSpringBeanClass(meta) check in visitNode
✅ Edge Case: defaultBeanName diverges from Spring's decapitalization rule
🤖 Prompt for agentsOptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |



What's not done here: