Skip to content

Commit 0962558

Browse files
committed
Validation, quick fix for component annotations on bean registrar
1 parent 56caf89 commit 0962558

File tree

5 files changed

+213
-45
lines changed

5 files changed

+213
-45
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot4JavaProblemType.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222

2323
public enum Boot4JavaProblemType implements ProblemType {
2424

25-
REGISTRAR_BEAN_DECLARATION(WARNING, "Bean derived from BeanRegistrar should be registered via `@Import` over configuration bean", "Not registered via `@Import` in a configuration bean");
25+
REGISTRAR_BEAN_INVALID_ANNOTATION(WARNING, "Bean Registrar cannot be registered as a bean via `@Component` annotations", "Invalid annotation over bean registrar"),
26+
REGISTRAR_BEAN_DECLARATION(WARNING, "Bean Registrar should be added to a configurarion bean via `@Import`", "Not added to configurartion via `@Import`");
2627

2728
private final ProblemSeverity defaultSeverity;
2829
private final String description;

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java

+69-41
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
import java.util.stream.Stream;
2323

2424
import org.eclipse.jdt.core.dom.ASTVisitor;
25+
import org.eclipse.jdt.core.dom.Annotation;
2526
import org.eclipse.jdt.core.dom.CompilationUnit;
2627
import org.eclipse.jdt.core.dom.ITypeBinding;
2728
import org.eclipse.jdt.core.dom.TypeDeclaration;
29+
import org.openrewrite.java.RemoveAnnotation;
2830
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
2931
import org.springframework.ide.vscode.boot.java.Annotations;
3032
import org.springframework.ide.vscode.boot.java.Boot4JavaProblemType;
33+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
3134
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
3235
import org.springframework.ide.vscode.commons.Version;
3336
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
@@ -89,54 +92,79 @@ public boolean visit(TypeDeclaration node) {
8992
if (!context.isIndexComplete()) {
9093
throw new RequiredCompleteIndexException();
9194
}
95+
96+
checkComponentAnnotations(context, project, docURI, cu, node, type);
97+
checkRegistrationViaImport(context, project, docURI, node, type);
9298

93-
List<Bean> configBeans = new ArrayList<>();
94-
Path p = Path.of(docURI);
95-
List<Path> sourceFolders = IClasspathUtil.getSourceFolders(project.getClasspath()).map(f -> f.toPath()).filter(f -> p.startsWith(f)).collect(Collectors.toList());
99+
return true;
100+
}
101+
102+
};
103+
}
104+
105+
private void checkRegistrationViaImport(ReconcilingContext context, IJavaProject project, URI docURI, TypeDeclaration node, ITypeBinding type) {
106+
List<Bean> configBeans = new ArrayList<>();
107+
Path p = Path.of(docURI);
108+
List<Path> sourceFolders = IClasspathUtil.getSourceFolders(project.getClasspath()).map(f -> f.toPath()).filter(f -> p.startsWith(f)).collect(Collectors.toList());
96109

97-
for (Bean b : springIndex.getBeansOfProject(project.getElementName())) {
98-
// if (b.getType().equals(type.getQualifiedName())) {
99-
// return true;
100-
// }
101-
if (b.isConfiguration() && b.getLocation() != null) {
102-
Path configBeanPath = Path.of(URI.create(b.getLocation().getUri()));
103-
if (sourceFolders.stream().anyMatch(configBeanPath::startsWith)) {
104-
configBeans.add(b);
105-
}
106-
}
110+
for (Bean b : springIndex.getBeansOfProject(project.getElementName())) {
111+
if (b.isConfiguration() && b.getLocation() != null) {
112+
Path configBeanPath = Path.of(URI.create(b.getLocation().getUri()));
113+
if (sourceFolders.stream().anyMatch(configBeanPath::startsWith)) {
114+
configBeans.add(b);
107115
}
116+
}
117+
}
108118

109-
List<String> importingBeanRegistrarConfigs = getImportedBeanRegistrarConfigs(configBeans, type);
110-
if (configBeans.isEmpty() || importingBeanRegistrarConfigs.size() == 0) {
119+
List<String> importingBeanRegistrarConfigs = getImportedBeanRegistrarConfigs(configBeans, type);
120+
if (configBeans.isEmpty() || importingBeanRegistrarConfigs.size() == 0) {
111121

112-
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), "No @Import found for bean registrar", node.getName().getStartPosition(), node.getName().getLength());
113-
List<FixDescriptor> fixes = configBeans.stream()
114-
.filter(b -> b.getLocation() != null && b.getLocation().getUri() != null)
115-
.map(b -> new FixDescriptor(ImportBeanRegistrarInConfigRecipe.class.getName(), List.of(b.getLocation().getUri()), "Add %s to `@Import` in %s".formatted(type.getName(), b.getName()))
116-
.withParameters(Map.of(
117-
"configBeanFqn", b.getType(),
118-
"beanRegFqn", type.getQualifiedName()
119-
))
120-
.withRecipeScope(RecipeScope.FILE)
121-
).toList();
122-
ReconcileUtils.setRewriteFixes(registry, problem, fixes);
123-
context.getProblemCollector().accept(problem);
124-
125-
// record dependencies on types where we found import annotations for this bean registrar
126-
// mark this file
127-
128-
}
129-
else {
130-
// record dependencies on types where we found import annotations for this bean registrar
131-
for (String typeOfConfigClassWithImport : importingBeanRegistrarConfigs) {
132-
context.addDependency(typeOfConfigClassWithImport);
133-
}
122+
ReconcileProblemImpl problem = new ReconcileProblemImpl(Boot4JavaProblemType.REGISTRAR_BEAN_DECLARATION, "No @Import found for bean registrar", node.getName().getStartPosition(), node.getName().getLength());
123+
List<FixDescriptor> fixes = configBeans.stream()
124+
.filter(b -> b.getLocation() != null && b.getLocation().getUri() != null)
125+
.map(b -> new FixDescriptor(ImportBeanRegistrarInConfigRecipe.class.getName(), List.of(b.getLocation().getUri()), "Add %s to `@Import` in %s".formatted(type.getName(), b.getName()))
126+
.withParameters(Map.of(
127+
"configBeanFqn", b.getType(),
128+
"beanRegFqn", type.getQualifiedName()
129+
))
130+
.withRecipeScope(RecipeScope.FILE)
131+
).toList();
132+
ReconcileUtils.setRewriteFixes(registry, problem, fixes);
133+
context.getProblemCollector().accept(problem);
134+
135+
// record dependencies on types where we found import annotations for this bean registrar
136+
// mark this file
137+
138+
}
139+
else {
140+
// record dependencies on types where we found import annotations for this bean registrar
141+
for (String typeOfConfigClassWithImport : importingBeanRegistrarConfigs) {
142+
context.addDependency(typeOfConfigClassWithImport);
143+
}
134144

135-
}
136-
return true;
145+
}
146+
}
147+
148+
private void checkComponentAnnotations(ReconcilingContext context, IJavaProject project, URI docURI, CompilationUnit cu, TypeDeclaration node, ITypeBinding type) {
149+
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(node);
150+
for (Object o : node.modifiers()) {
151+
if (o instanceof Annotation a) {
152+
ITypeBinding ab = a.resolveTypeBinding();
153+
if (ab != null && annotationHierarchies.isAnnotatedWith(ab, Annotations.COMPONENT)) {
154+
ReconcileProblemImpl problem = new ReconcileProblemImpl(
155+
Boot4JavaProblemType.REGISTRAR_BEAN_INVALID_ANNOTATION,
156+
Boot4JavaProblemType.REGISTRAR_BEAN_INVALID_ANNOTATION.getLabel(),
157+
a.getTypeName().getStartPosition(), a.getTypeName().getLength());
158+
ReconcileUtils.setRewriteFixes(registry, problem, List.of(
159+
new FixDescriptor(RemoveAnnotation.class.getName(), List.of(docURI.toASCIIString()), "Remove `@%s`".formatted(ab.getName()))
160+
.withParameters(Map.of("annotationPattern", "@" + ab.getQualifiedName()))
161+
.withRecipeScope(RecipeScope.NODE)
162+
.withRangeScope(ReconcileUtils.createOpenRewriteRange(cu, node, null))
163+
));
164+
context.getProblemCollector().accept(problem);
165+
}
137166
}
138-
139-
};
167+
}
140168
}
141169

142170
private List<String> getImportedBeanRegistrarConfigs(List<Bean> configBeans, ITypeBinding beanRegType) {

headless-services/spring-boot-language-server/src/main/resources/problem-types.json

+8-2
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,16 @@
138138
},
139139
"order": 3,
140140
"problemTypes": [
141+
{
142+
"code": "REGISTRAR_BEAN_INVALID_ANNOTATION",
143+
"label": "Invalid annotation over bean registrar",
144+
"description": "Bean Registrar cannot be registered as a bean via `@Component` annotations",
145+
"defaultSeverity": "WARNING"
146+
},
141147
{
142148
"code": "REGISTRAR_BEAN_DECLARATION",
143-
"label": "Not registered via `@Import` in a configuration bean",
144-
"description": "Bean derived from BeanRegistrar should be registered via `@Import` over configuration bean",
149+
"label": "Not added to configurartion via `@Import`",
150+
"description": "Bean Registrar should be added to a configurarion bean via `@Import`",
145151
"defaultSeverity": "WARNING"
146152
}
147153
]

headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BeanRegistrarDeclarationReconcilerTest.java

+121
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,20 @@
2020
import org.junit.jupiter.api.AfterEach;
2121
import org.junit.jupiter.api.BeforeEach;
2222
import org.junit.jupiter.api.Test;
23+
import org.openrewrite.java.RemoveAnnotation;
2324
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
2425
import org.springframework.ide.vscode.boot.java.Annotations;
26+
import org.springframework.ide.vscode.boot.java.Boot4JavaProblemType;
2527
import org.springframework.ide.vscode.boot.java.reconcilers.BeanRegistrarDeclarationReconciler;
2628
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
2729
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
30+
import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData;
2831
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
2932
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
3033
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationAttributeValue;
3134
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
3235
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
36+
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
3337

3438
public class BeanRegistrarDeclarationReconcilerTest extends BaseReconcilerTest {
3539

@@ -268,4 +272,121 @@ public void register(BeanRegistry registry, Environment env) {
268272

269273
assertEquals(1, problems.size());
270274
}
275+
276+
@Test
277+
void componentAnnotationOver() throws Throwable {
278+
String source = """
279+
package com.example.demo;
280+
281+
import org.springframework.beans.factory.BeanRegistrar;
282+
import org.springframework.beans.factory.BeanRegistry;
283+
import org.springframework.core.env.Environment;
284+
import org.springframework.stereotype.Component;
285+
286+
@Component
287+
public class MyBeanRegistrar implements BeanRegistrar {
288+
289+
public void register(BeanRegistry registry, Environment env) {
290+
}
291+
292+
}
293+
""";
294+
List<ReconcileProblem> problems = reconcile(() -> {
295+
SpringMetamodelIndex springIndex = new SpringMetamodelIndex();
296+
297+
Location l = new Location();
298+
Path sourceFolder = IClasspathUtil.getSourceFolders(project.getClasspath()).map(f -> f.toPath()).filter(p -> p.endsWith(Path.of("src", "main", "java"))).findFirst().orElseThrow();
299+
l.setUri(sourceFolder.resolve("com/example/demo/B.java").toUri().toASCIIString());
300+
301+
AnnotationMetadata annotationMetadata = new AnnotationMetadata(Annotations.CONFIGURATION, false, null, Map.of());
302+
AnnotationMetadata importMetadata = new AnnotationMetadata(Annotations.IMPORT, false, null,
303+
Map.of("value", new AnnotationAttributeValue[] {
304+
new AnnotationAttributeValue("com.example.demo.MyBeanRegistrar", null) }));
305+
AnnotationMetadata[] annotations = new AnnotationMetadata[] {annotationMetadata, importMetadata};
306+
Bean configBean = new Bean("conf", "com.example.demo.Conf", l, null, null, annotations, true, "symbolLabel");
307+
Bean[] beans = new Bean[] {configBean};
308+
springIndex.updateBeans(getProjectName(), beans);
309+
310+
BeanRegistrarDeclarationReconciler r = new BeanRegistrarDeclarationReconciler(new QuickfixRegistry(), springIndex);
311+
312+
return r;
313+
}, "A.java", source, false);
314+
315+
assertEquals(1, problems.size());
316+
317+
ReconcileProblem p = problems.get(0);
318+
319+
assertEquals(Boot4JavaProblemType.REGISTRAR_BEAN_INVALID_ANNOTATION.getLabel(), p.getType().getLabel());
320+
321+
assertEquals(1, p.getQuickfixes().size());
322+
323+
QuickfixData<?> qf = p.getQuickfixes().get(0);
324+
325+
assertEquals("Remove `@Component`", qf.title);
326+
327+
FixDescriptor fd = (FixDescriptor) qf.params;
328+
329+
assertEquals(RemoveAnnotation.class.getName(), fd.getRecipeId());
330+
assertEquals("@org.springframework.stereotype.Component", fd.getParameters().get("annotationPattern"));
331+
332+
}
333+
334+
@Test
335+
void serviceAnnotationOver() throws Throwable {
336+
String source = """
337+
package com.example.demo;
338+
339+
import org.springframework.beans.factory.BeanRegistrar;
340+
import org.springframework.beans.factory.BeanRegistry;
341+
import org.springframework.core.env.Environment;
342+
import org.springframework.stereotype.Service;
343+
344+
@Service("myService")
345+
public class MyBeanRegistrar implements BeanRegistrar {
346+
347+
public void register(BeanRegistry registry, Environment env) {
348+
}
349+
350+
}
351+
""";
352+
List<ReconcileProblem> problems = reconcile(() -> {
353+
SpringMetamodelIndex springIndex = new SpringMetamodelIndex();
354+
355+
Location l = new Location();
356+
Path sourceFolder = IClasspathUtil.getSourceFolders(project.getClasspath()).map(f -> f.toPath()).filter(p -> p.endsWith(Path.of("src", "main", "java"))).findFirst().orElseThrow();
357+
l.setUri(sourceFolder.resolve("com/example/demo/B.java").toUri().toASCIIString());
358+
359+
AnnotationMetadata annotationMetadata = new AnnotationMetadata(Annotations.CONFIGURATION, false, null, Map.of());
360+
AnnotationMetadata importMetadata = new AnnotationMetadata(Annotations.IMPORT, false, null,
361+
Map.of("value", new AnnotationAttributeValue[] {
362+
new AnnotationAttributeValue("com.example.demo.MyBeanRegistrar", null) }));
363+
AnnotationMetadata[] annotations = new AnnotationMetadata[] {annotationMetadata, importMetadata};
364+
Bean configBean = new Bean("conf", "com.example.demo.Conf", l, null, null, annotations, true, "symbolLabel");
365+
Bean[] beans = new Bean[] {configBean};
366+
springIndex.updateBeans(getProjectName(), beans);
367+
368+
BeanRegistrarDeclarationReconciler r = new BeanRegistrarDeclarationReconciler(new QuickfixRegistry(), springIndex);
369+
370+
return r;
371+
}, "A.java", source, false);
372+
373+
assertEquals(1, problems.size());
374+
375+
ReconcileProblem p = problems.get(0);
376+
377+
assertEquals(Boot4JavaProblemType.REGISTRAR_BEAN_INVALID_ANNOTATION.getLabel(), p.getType().getLabel());
378+
379+
assertEquals(1, p.getQuickfixes().size());
380+
381+
QuickfixData<?> qf = p.getQuickfixes().get(0);
382+
383+
assertEquals("Remove `@Service`", qf.title);
384+
385+
FixDescriptor fd = (FixDescriptor) qf.params;
386+
387+
assertEquals(RemoveAnnotation.class.getName(), fd.getRecipeId());
388+
assertEquals("@org.springframework.stereotype.Service", fd.getParameters().get("annotationPattern"));
389+
390+
}
391+
271392
}

vscode-extensions/vscode-spring-boot/package.json

+13-1
Original file line numberDiff line numberDiff line change
@@ -739,10 +739,22 @@
739739
"ON"
740740
]
741741
},
742+
"spring-boot.ls.problem.boot4.REGISTRAR_BEAN_INVALID_ANNOTATION": {
743+
"type": "string",
744+
"default": "WARNING",
745+
"description": "Bean Registrar cannot be registered as a bean via `@Component` annotations",
746+
"enum": [
747+
"IGNORE",
748+
"INFO",
749+
"WARNING",
750+
"HINT",
751+
"ERROR"
752+
]
753+
},
742754
"spring-boot.ls.problem.boot4.REGISTRAR_BEAN_DECLARATION": {
743755
"type": "string",
744756
"default": "WARNING",
745-
"description": "Bean derived from BeanRegistrar should be registered via `@Import` over configuration bean",
757+
"description": "Bean Registrar should be added to a configurarion bean via `@Import`",
746758
"enum": [
747759
"IGNORE",
748760
"INFO",

0 commit comments

Comments
 (0)