GH-1041: reconciler that finds non-registered aot processor beans updated to use spring index instead of symbols

This commit is contained in:
Martin Lippert
2025-01-09 14:30:54 +01:00
parent 02e3ede7ed
commit 44e4974596
4 changed files with 124 additions and 144 deletions

View File

@@ -120,8 +120,8 @@ public class JdtConfig {
return new BeanPostProcessingIgnoreInAotReconciler(server.getQuickfixRegistry());
}
@Bean NotRegisteredBeansReconciler notRegisteredBeansReconciler(SimpleLanguageServer server) {
return new NotRegisteredBeansReconciler(server.getQuickfixRegistry());
@Bean NotRegisteredBeansReconciler notRegisteredBeansReconciler(SimpleLanguageServer server, SpringMetamodelIndex springIndex) {
return new NotRegisteredBeansReconciler(server.getQuickfixRegistry(), springIndex);
}
@Bean EntityIdForRepoReconciler entityIdForRepoReconciler(SimpleLanguageServer server) {

View File

@@ -102,7 +102,18 @@ public class SpringMetamodelIndex {
Bean[] allBeans = this.beansPerProject.get(project);
if (allBeans != null) {
return Arrays.stream(allBeans).filter(bean -> bean.getName().equals(name)).collect(Collectors.toList()).toArray(new Bean[0]);
return Arrays.stream(allBeans).filter(bean -> bean.getName().equals(name)).toArray(Bean[]::new);
}
else {
return null;
}
}
public Bean[] getBeansWithType(String project, String type) {
Bean[] allBeans = this.beansPerProject.get(project);
if (allBeans != null) {
return Arrays.stream(allBeans).filter(bean -> bean.getType().equals(type)).toArray(Bean[]::new);
}
else {
return null;

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023, 2204 VMware, Inc.
* Copyright (c) 2023, 2024 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -27,23 +27,16 @@ import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType;
import org.springframework.ide.vscode.boot.java.beans.BeansSymbolAddOnInformation;
import org.springframework.ide.vscode.boot.java.beans.ConfigBeanSymbolAddOnInformation;
import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation;
import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
import org.springframework.ide.vscode.commons.rewrite.java.DefineMethod;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
@@ -52,19 +45,19 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableSet;
public class NotRegisteredBeansReconciler implements JdtAstReconciler, ApplicationContextAware {
public class NotRegisteredBeansReconciler implements JdtAstReconciler {
private static final List<String> AOT_BEANS = List.of(
"org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor",
"org.springframework.beans.factory.aot.BeanRegistrationAotProcessor"
);
private ApplicationContext applicationContext;
private QuickfixRegistry registry;
private SpringMetamodelIndex springIndex;
public NotRegisteredBeansReconciler(QuickfixRegistry registry) {
this.registry = registry;
public NotRegisteredBeansReconciler(QuickfixRegistry registry, SpringMetamodelIndex springIndex) {
this.registry = registry;
this.springIndex = springIndex;
}
@Override
@@ -77,11 +70,6 @@ public class NotRegisteredBeansReconciler implements JdtAstReconciler, Applicati
return SpringAotJavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) {
@@ -90,79 +78,81 @@ public class NotRegisteredBeansReconciler implements JdtAstReconciler, Applicati
@Override
public boolean visit(TypeDeclaration node) {
if (!node.isInterface() && !Modifier.isAbstract(node.getModifiers())) {
ITypeBinding type = node.resolveBinding();
if (type != null && ReconcileUtils.implementsAnyType(AOT_BEANS, type)) {
String beanClassName =type.getQualifiedName();
SpringSymbolIndex index = applicationContext.getBean(SpringSymbolIndex.class);
List<WorkspaceSymbol> beanSymbols = index.getSymbols(data -> {
SymbolAddOnInformation[] additionalInformation = data.getAdditionalInformation();
if (additionalInformation != null) {
for (SymbolAddOnInformation info : additionalInformation) {
if (info instanceof BeansSymbolAddOnInformation) {
BeansSymbolAddOnInformation info2 = (BeansSymbolAddOnInformation) info;
return beanClassName.equals(info2.getBeanType());
}
}
}
return false;
}).limit(1).collect(Collectors.toList());
String beanClassName = type.getQualifiedName();
if (beanSymbols.isEmpty()) {
Builder<FixDescriptor> fixListBuilder = ImmutableList.builder();
for (EnhancedSymbolInformation s : index.getEnhancedSymbols(project)) {
if (s.getAdditionalInformation() != null) {
ConfigBeanSymbolAddOnInformation configInfo = Arrays.stream(s.getAdditionalInformation()).filter(ConfigBeanSymbolAddOnInformation.class::isInstance).map(ConfigBeanSymbolAddOnInformation.class::cast).findFirst().orElse(null);
if (configInfo != null) {
for (IMethodBinding constructor : type.getDeclaredMethods()) {
if (constructor.isConstructor()) {
String constructorParamsSignature = "(" + Arrays.stream(constructor.getParameterTypes()).map(pt -> typePattern(pt)).collect(Collectors.joining(",")) + ")";
String beanMethodName = "get" + type.getName();
String pattern = beanMethodName + constructorParamsSignature;
String contructorParamsLabel = "(" + Arrays.stream(constructor.getParameterTypes()).map(NotRegisteredBeansReconciler::typeStr).collect(Collectors.joining(", ")) + ")";
Builder<String> paramBuilder = ImmutableList.builder();
for (int i = 0; i < constructor.getParameterNames().length && i < constructor.getParameterTypes().length; i++) {
ITypeBinding paramType = constructor.getParameterTypes()[i];
String paramName = constructor.getParameterNames()[i];
paramBuilder.add(typeStr(paramType) + ' ' + paramName);
}
String paramsStr = String.join(", ", paramBuilder.build().toArray(String[]::new));
final Set<String> allFqTypes = new HashSet<>();
allFqTypes.add(Annotations.BEAN);
allFqTypes.addAll(allFQTypes(constructor));
fixListBuilder.add(new FixDescriptor(DefineMethod.class.getName(), List.of(s.getSymbol().getLocation().getLeft().getUri()), "Define bean in config '" + configInfo.getBeanID() + "' with constructor " + contructorParamsLabel)
.withRecipeScope(RecipeScope.FILE)
.withParameters(Map.of(
"targetFqName", configInfo.getBeanType(),
"signature", pattern,
"template", "@Bean\n"
+ type.getName() + " " + beanMethodName + "(" + paramsStr + ") {\n"
+ "return new " + type.getName() + "(" + Arrays.stream(constructor.getParameterNames()).collect(Collectors.joining(", ")) + ");\n"
+ "}\n",
"imports", allFqTypes.toArray(String[]::new),
"typeStubs", new String[0]/*new String[] { source.printAll() }*/,
"classpath", IClasspathUtil.getAllBinaryRoots(project.getClasspath()).stream().map(f -> f.toPath().toString()).toArray(String[]::new)
))
);
}
}
}
}
}
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), getProblemType().getLabel(), node.getName().getStartPosition(), node.getName().getLength());
ReconcileUtils.setRewriteFixes(registry, problem, fixListBuilder.build());
problemCollector.accept(problem);
Bean[] registeredBeans = springIndex.getBeansWithType(project.getElementName(), beanClassName);
if (registeredBeans == null || registeredBeans.length == 0) {
createProblemAndQuickFixes(project, problemCollector, node, type);
}
}
}
return super.visit(node);
}
};
}
protected void createProblemAndQuickFixes(IJavaProject project, IProblemCollector problemCollector, TypeDeclaration node, ITypeBinding type) {
Builder<FixDescriptor> fixListBuilder = ImmutableList.builder();
Bean[] configBeans = getConfigurationBeans(project);
for (Bean configBean : configBeans) {
for (IMethodBinding constructor : type.getDeclaredMethods()) {
if (constructor.isConstructor()) {
String constructorParamsSignature = "(" + Arrays.stream(constructor.getParameterTypes()).map(pt -> typePattern(pt)).collect(Collectors.joining(",")) + ")";
String beanMethodName = "get" + type.getName();
String pattern = beanMethodName + constructorParamsSignature;
String contructorParamsLabel = "(" + Arrays.stream(constructor.getParameterTypes()).map(NotRegisteredBeansReconciler::typeStr).collect(Collectors.joining(", ")) + ")";
Builder<String> paramBuilder = ImmutableList.builder();
for (int i = 0; i < constructor.getParameterNames().length && i < constructor.getParameterTypes().length; i++) {
ITypeBinding paramType = constructor.getParameterTypes()[i];
String paramName = constructor.getParameterNames()[i];
paramBuilder.add(typeStr(paramType) + ' ' + paramName);
}
String paramsStr = String.join(", ", paramBuilder.build().toArray(String[]::new));
final Set<String> allFqTypes = new HashSet<>();
allFqTypes.add(Annotations.BEAN);
allFqTypes.addAll(allFQTypes(constructor));
fixListBuilder.add(new FixDescriptor(DefineMethod.class.getName(), List.of(configBean.getLocation().getUri()), "Define bean in config '" + configBean.getName() + "' with constructor " + contructorParamsLabel)
.withRecipeScope(RecipeScope.FILE)
.withParameters(Map.of(
"targetFqName", configBean.getType(),
"signature", pattern,
"template", "@Bean\n"
+ type.getName() + " " + beanMethodName + "(" + paramsStr + ") {\n"
+ "return new " + type.getName() + "(" + Arrays.stream(constructor.getParameterNames()).collect(Collectors.joining(", ")) + ");\n"
+ "}\n",
"imports", allFqTypes.toArray(String[]::new),
"typeStubs", new String[0]/*new String[] { source.printAll() }*/,
"classpath", IClasspathUtil.getAllBinaryRoots(project.getClasspath()).stream().map(f -> f.toPath().toString()).toArray(String[]::new)
))
);
}
}
}
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), getProblemType().getLabel(), node.getName().getStartPosition(), node.getName().getLength());
ReconcileUtils.setRewriteFixes(registry, problem, fixListBuilder.build());
problemCollector.accept(problem);
}
private Bean[] getConfigurationBeans(IJavaProject project) {
Bean[] beans = springIndex.getBeansOfProject(project.getElementName());
if (beans != null) {
return Arrays.stream(beans).filter(bean -> bean.isConfiguration()).toArray(Bean[]::new);
}
else {
return new Bean[0];
}
}
private static Set<String> allFQTypes(IBinding binding) {
ImmutableSet.Builder<String> b = ImmutableSet.builder();
if (binding instanceof IMethodBinding) {

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* Copyright (c) 2023, 2024 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -11,36 +11,21 @@
package org.springframework.ide.vscode.boot.java.reconcilers.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType;
import org.springframework.ide.vscode.boot.java.beans.ConfigBeanSymbolAddOnInformation;
import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation;
import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.NotRegisteredBeansReconciler;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
public class NotRegisteredBeansReconcilerTest extends BaseReconcilerTest {
@@ -53,33 +38,14 @@ public class NotRegisteredBeansReconcilerTest extends BaseReconcilerTest {
protected String getProjectName() {
return "test-spring-validations";
}
@SuppressWarnings("unchecked")
@Override
protected JdtAstReconciler getReconciler() {
NotRegisteredBeansReconciler reconciler = new NotRegisteredBeansReconciler(new QuickfixRegistry());
SpringSymbolIndex mockSymbolIndex = mock(SpringSymbolIndex.class);
when(mockSymbolIndex.getSymbols(any(Predicate.class))).thenReturn(Stream.empty());
ApplicationContext context = mock(ApplicationContext.class);
when(context.getBean(SpringSymbolIndex.class)).thenReturn(mockSymbolIndex);
reconciler.setApplicationContext(context);
return reconciler;
return null;
}
@SuppressWarnings("unchecked")
private NotRegisteredBeansReconciler createReconciler(EnhancedSymbolInformation... beanSymbols) {
NotRegisteredBeansReconciler reconciler = new NotRegisteredBeansReconciler(new QuickfixRegistry());
SpringSymbolIndex mockSymbolIndex = mock(SpringSymbolIndex.class);
when(mockSymbolIndex.getSymbols(any(Predicate.class))).thenReturn(Stream.empty());
when(mockSymbolIndex.getEnhancedSymbols(any(IJavaProject.class))).thenReturn(Arrays.asList(beanSymbols));
ApplicationContext context = mock(ApplicationContext.class);
when(context.getBean(SpringSymbolIndex.class)).thenReturn(mockSymbolIndex);
reconciler.setApplicationContext(context);
return reconciler;
private NotRegisteredBeansReconciler createReconciler(SpringMetamodelIndex springIndex) {
return new NotRegisteredBeansReconciler(new QuickfixRegistry(), springIndex);
}
@BeforeEach
@@ -93,7 +59,7 @@ public class NotRegisteredBeansReconcilerTest extends BaseReconcilerTest {
}
@Test
void sanityTest() throws Exception {
void sanityTestAotProcessorIsRegisteredAsBean() throws Exception {
String source = """
package example.demo;
@@ -104,7 +70,30 @@ public class NotRegisteredBeansReconcilerTest extends BaseReconcilerTest {
public A(String k) {}
}
""";
List<ReconcileProblem> problems = reconcile(() -> createReconciler(), "A.java", source, true);
SpringMetamodelIndex springIndex = new SpringMetamodelIndex();
Bean aotBean = new Bean("a", "example.demo.A", new Location("docURI", new Range()), null, null, null, true);
springIndex.updateBeans(getProjectName(), new Bean[] {aotBean});
List<ReconcileProblem> problems = reconcile(() -> createReconciler(springIndex), "A.java", source, true);
assertEquals(0, problems.size());
}
@Test
void sanityTestAotProcessorIsNotRegisteredAsBean() throws Exception {
String source = """
package example.demo;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
class A implements BeanRegistrationAotProcessor {
public A(String k) {}
}
""";
SpringMetamodelIndex emptySpringIndex = new SpringMetamodelIndex();
List<ReconcileProblem> problems = reconcile(() -> createReconciler(emptySpringIndex), "A.java", source, true);
assertEquals(1, problems.size());
@@ -120,17 +109,7 @@ public class NotRegisteredBeansReconcilerTest extends BaseReconcilerTest {
}
@Test
void sanityTestWithQuickFixes() throws Exception {
Path configClassPath = createFile("TestConfig.java", """
package example.demo;
import org.springframework.context.annotation.Configuration;
@Configuration
class TestConfig {
}
""");
void sanityTestAotProcessorIsNotRegisteredAsBeanWithQuickFixes() throws Exception {
String source = """
package example.demo;
@@ -142,10 +121,11 @@ public class NotRegisteredBeansReconcilerTest extends BaseReconcilerTest {
}
""";
WorkspaceSymbol workspaceSymbol = new WorkspaceSymbol("testConfig", SymbolKind.Class, Either.forLeft(new Location(configClassPath.toUri().toASCIIString(), new Range())));
ConfigBeanSymbolAddOnInformation configBeanAddOn = new ConfigBeanSymbolAddOnInformation("testConfig", "example.demo.TestConfig");
List<ReconcileProblem> problems = reconcile(() -> createReconciler(new EnhancedSymbolInformation(workspaceSymbol, new SymbolAddOnInformation[] { configBeanAddOn })), "A.java", source, true);
SpringMetamodelIndex springIndex = new SpringMetamodelIndex();
Bean configBean = new Bean("testConfig", "example.demo.TestConfig", new Location("docURI", new Range()), null, null, null, true);
springIndex.updateBeans(getProjectName(), new Bean[] {configBean});
List<ReconcileProblem> problems = reconcile(() -> createReconciler(springIndex), "A.java", source, true);
assertEquals(1, problems.size());
@@ -159,5 +139,4 @@ public class NotRegisteredBeansReconcilerTest extends BaseReconcilerTest {
assertEquals(1, problem.getQuickfixes().size());
}
}