From 44e4974596a82980d9b06046e0dd530a87cd1796 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 9 Jan 2025 14:30:54 +0100 Subject: [PATCH] GH-1041: reconciler that finds non-registered aot processor beans updated to use spring index instead of symbols --- .../ide/vscode/boot/app/JdtConfig.java | 4 +- .../boot/index/SpringMetamodelIndex.java | 13 +- .../NotRegisteredBeansReconciler.java | 154 ++++++++---------- .../NotRegisteredBeansReconcilerTest.java | 97 +++++------ 4 files changed, 124 insertions(+), 144 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java index 94b135d80e..02e2b4609c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -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) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java index f055f5e5ae..0cabc5ca40 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java @@ -102,7 +102,18 @@ public Bean[] getBeansWithName(String project, String name) { 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; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java index b4620e1ecb..8a4ed10dd4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java @@ -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.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.Builder; import com.google.common.collect.ImmutableSet; -public class NotRegisteredBeansReconciler implements JdtAstReconciler, ApplicationContextAware { +public class NotRegisteredBeansReconciler implements JdtAstReconciler { private static final List 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 ProblemType getProblemType() { 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 ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUni @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 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 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 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 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 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 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 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 allFQTypes(IBinding binding) { ImmutableSet.Builder b = ImmutableSet.builder(); if (binding instanceof IMethodBinding) { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansReconcilerTest.java index 01d80b8ed5..d264526d42 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansReconcilerTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansReconcilerTest.java @@ -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 @@ protected String getFolder() { 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 @@ void tearDown() throws Exception { } @Test - void sanityTest() throws Exception { + void sanityTestAotProcessorIsRegisteredAsBean() throws Exception { String source = """ package example.demo; @@ -104,7 +70,30 @@ class A implements BeanRegistrationAotProcessor { public A(String k) {} } """; - List 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 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 problems = reconcile(() -> createReconciler(emptySpringIndex), "A.java", source, true); assertEquals(1, problems.size()); @@ -120,17 +109,7 @@ public A(String k) {} } @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 A(String k) {} } """; - 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 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 problems = reconcile(() -> createReconciler(springIndex), "A.java", source, true); assertEquals(1, problems.size()); @@ -159,5 +139,4 @@ public A(String k) {} assertEquals(1, problem.getQuickfixes().size()); } - }