diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/AotProcessorElement.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/AotProcessorElement.java new file mode 100644 index 000000000..6981c8757 --- /dev/null +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/AotProcessorElement.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.protocol.spring; + +public class AotProcessorElement extends AbstractSpringIndexElement { + + private final String type; + private final String docUri; + + public AotProcessorElement(String type, String docUri) { + this.type = type; + this.docUri = docUri; + } + + public String getType() { + return type; + } + + public String getDocUri() { + return docUri; + } + +} 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 4df105749..383b24648 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 @@ -120,6 +120,18 @@ public class SpringMetamodelIndex { return new Bean[0]; } } + + public Bean getParentBean(Bean bean) { + Bean[] beansOfDocument = getBeansOfDocument(bean.getLocation().getUri()); + + for (Bean candidateBean : beansOfDocument) { + if (candidateBean.getChildren().contains(bean)) { + return candidateBean; + } + } + + return null; + } public Bean[] getMatchingBeans(String projectName, String matchType) { ProjectElement project = this.projectRootElements.get(projectName); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java index 2f0dfac82..a7f59aa50 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java @@ -44,6 +44,8 @@ import org.springframework.ide.vscode.boot.java.events.EventListenerIndexElement import org.springframework.ide.vscode.boot.java.events.EventListenerIndexer; import org.springframework.ide.vscode.boot.java.events.EventPublisherIndexElement; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; +import org.springframework.ide.vscode.boot.java.reconcilers.NotRegisteredBeansReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.ReconcileUtils; import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingIndexer; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; @@ -51,6 +53,7 @@ import org.springframework.ide.vscode.boot.java.utils.CachedSymbol; import org.springframework.ide.vscode.boot.java.utils.DefaultSymbolProvider; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext; import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; +import org.springframework.ide.vscode.commons.protocol.spring.AotProcessorElement; import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.commons.protocol.spring.BeanMethodContainerElement; import org.springframework.ide.vscode.commons.protocol.spring.BeanRegistrarElement; @@ -352,10 +355,24 @@ public class ComponentSymbolProvider implements SymbolProvider { indexEventListenerInterfaceImplementation(null, typeDeclaration, context, doc); indexBeanRegistrarImplementation(null, typeDeclaration, context, doc); indexBeanMethods(null, typeDeclaration, null, null, context, doc); + indexAotProcessors(typeDeclaration, context); } } + private void indexAotProcessors(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context) { + ITypeBinding typeBinding = typeDeclaration.resolveBinding(); + if (typeBinding == null) return; + + if (ReconcileUtils.implementsAnyType(NotRegisteredBeansReconciler.AOT_BEANS, typeBinding)) { + String type = typeBinding.getQualifiedName(); + String docUri = context.getDocURI(); + + AotProcessorElement aotProcessorElement = new AotProcessorElement(type, docUri); + context.getBeans().add(new CachedBean(context.getDocURI(), aotProcessorElement)); + } + } + private void indexEventListenerInterfaceImplementation(Bean bean, TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) { try { ITypeBinding typeBinding = typeDeclaration.resolveBinding(); 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 27e80c125..46fbe7bfe 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 @@ -36,10 +36,13 @@ import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRe 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.AotProcessorElement; import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement; 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; +import org.springframework.ide.vscode.commons.util.UriUtil; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; @@ -47,7 +50,7 @@ import com.google.common.collect.ImmutableSet; public class NotRegisteredBeansReconciler implements JdtAstReconciler { - private static final List AOT_BEANS = List.of( + public static final List AOT_BEANS = List.of( "org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor", "org.springframework.beans.factory.aot.BeanRegistrationAotProcessor" ); @@ -82,6 +85,8 @@ public class NotRegisteredBeansReconciler implements JdtAstReconciler { ITypeBinding type = node.resolveBinding(); if (type != null && ReconcileUtils.implementsAnyType(AOT_BEANS, type)) { + // // reconcile AOT Proceesor itself + if (!context.isIndexComplete()) { throw new RequiredCompleteIndexException(); } @@ -92,6 +97,41 @@ public class NotRegisteredBeansReconciler implements JdtAstReconciler { if (registeredBeans == null || registeredBeans.length == 0) { createProblemAndQuickFixes(project, context.getProblemCollector(), node, type); } + else { + // record dependency, if bean is not coming from the current doc (defined somewhere else) + String uri = docUri.toASCIIString(); + for (Bean bean : registeredBeans) { + String beanDocUri = bean.getLocation().getUri(); + if (!beanDocUri.equals(uri)) { + Bean parentBean = springIndex.getParentBean(bean); + if (parentBean != null) { + context.addDependency(parentBean.getType()); + } + } + } + } + } + else { + + // + // check if new beans have been defined that refer to any AOP processor element + // + + List aotProcessors = springIndex.getNodesOfType(AotProcessorElement.class); + if (aotProcessors != null && aotProcessors.size() > 0) { + + List createdIndexElements = context.getCreatedIndexElements(); + List createdBeanElements = SpringMetamodelIndex.getNodesOfType(Bean.class, createdIndexElements); + Set beanTypes = createdBeanElements.stream() + .filter(bean -> context.getDocURI().equals(bean.getLocation().getUri())) + .map(bean -> bean.getType()) + .collect(Collectors.toSet()); + + aotProcessors.stream() + .filter(aotProcessor -> beanTypes.contains(aotProcessor.getType())) + .map(aotProcessor -> UriUtil.toFileString(aotProcessor.getDocUri())) + .forEach(file -> context.markForAffetcedFilesIndexing(file)); + } } } return super.visit(node); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringFactoriesIndexer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringFactoriesIndexer.java index 2bfd188c4..1371f4e7f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringFactoriesIndexer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringFactoriesIndexer.java @@ -64,7 +64,7 @@ public class SpringFactoriesIndexer implements SpringIndexer { // whenever the implementation of the indexer changes in a way that the stored data in the cache is no longer valid, // we need to change the generation - this will result in a re-indexing due to no up-to-date cache data being found - private static final String GENERATION = "GEN-12"; + private static final String GENERATION = "GEN-13"; private static final String SYMBOL_KEY = "symbols"; private static final String BEANS_KEY = "beans"; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java index ad86df220..a81bfe9b5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java @@ -92,7 +92,7 @@ public class SpringIndexerJava implements SpringIndexer { // whenever the implementation of the indexer changes in a way that the stored data in the cache is no longer valid, // we need to change the generation - this will result in a re-indexing due to no up-to-date cache data being found - private static final String GENERATION = "GEN-18"; + private static final String GENERATION = "GEN-19"; private static final String INDEX_FILES_TASK_ID = "index-java-source-files-task-"; private static final String SYMBOL_KEY = "symbols"; diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansAdvancedReconcilingTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansAdvancedReconcilingTest.java index b976e80d4..e8e721469 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansAdvancedReconcilingTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansAdvancedReconcilingTest.java @@ -13,10 +13,12 @@ package org.springframework.ide.vscode.boot.java.reconcilers.test; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; +import java.nio.charset.Charset; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.FileUtils; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.TextDocumentIdentifier; @@ -31,8 +33,10 @@ import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType; +import org.springframework.ide.vscode.boot.java.utils.test.TestFileScanListener; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.util.Settings; +import org.springframework.ide.vscode.commons.util.UriUtil; import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; import org.springframework.ide.vscode.project.harness.ProjectsHarness; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -127,24 +131,128 @@ public class NotRegisteredBeansAdvancedReconcilingTest { assertEquals(0, diagnostics.size()); } + @Test + @Disabled + void testValidationDisappearsWhenAotProcessorAddedToFactoriesFile() throws Exception { + } + + @Test + @Disabled + void testValidationAppearsWhenAotProcessorRemovedFromFactoriesFile() throws Exception { + } + @Test void testValidationDisappearsWhenComponentAnnotationIsAdded() throws Exception { - // TODO + String docUri = directory.toPath().resolve("src/main/java/org/test/aot/NotRegisteredBeanRegistrationAotProcessor.java").toUri().toString(); + + // now change the config class source code and update doc + TestFileScanListener fileScanListener = new TestFileScanListener(); + indexer.getJavaIndexer().setFileScanListener(fileScanListener); + + String notRegisteredSource = FileUtils.readFileToString(UriUtil.toFile(docUri), Charset.defaultCharset()); + String updatedSource = notRegisteredSource.replace("public class NotRegisteredBeanRegistrationAotProcessor", + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component public class NotRegisteredBeanRegistrationAotProcessor"); + + CompletableFuture updateFuture = indexer.updateDocument(docUri, updatedSource, "test triggered"); + updateFuture.get(5, TimeUnit.SECONDS); + + // check if the bean registrar files have been re-scanned + fileScanListener.assertScannedUri(docUri, 1); + fileScanListener.assertFileScanCount(1); + + // check diagnostics result + PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri); + List diagnostics = diagnosticsResult.getDiagnostics(); + assertEquals(0, diagnostics.size()); } @Test void testValidationAppearsWhenComponentAnnotationIsRemoved() throws Exception { - // TODO + String docUri = directory.toPath().resolve("src/main/java/org/test/aot/RegisteredAsComponentBeanRegistrationAotProcessor.java").toUri().toString(); + + // now change the config class source code and update doc + TestFileScanListener fileScanListener = new TestFileScanListener(); + indexer.getJavaIndexer().setFileScanListener(fileScanListener); + + String registeredSource = FileUtils.readFileToString(UriUtil.toFile(docUri), Charset.defaultCharset()); + String updatedSource = registeredSource.replace("@Component", ""); + + CompletableFuture updateFuture = indexer.updateDocument(docUri, updatedSource, "test triggered"); + updateFuture.get(5, TimeUnit.SECONDS); + + // check if the bean registrar files have been re-scanned + fileScanListener.assertScannedUri(docUri, 1); + fileScanListener.assertFileScanCount(1); + + // check diagnostics result + PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri); + List diagnostics = diagnosticsResult.getDiagnostics(); + assertEquals(1, diagnostics.size()); + assertEquals(SpringAotJavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT.getCode(), diagnostics.get(0).getCode().getLeft()); } @Test void testValidationDisappearsWhenBeanMethodIsAddedToConfig() throws Exception { - // TODO + String docUri = directory.toPath().resolve("src/main/java/org/test/aot/NotRegisteredBeanRegistrationAotProcessor.java").toUri().toString(); + String alreadyRegisteredViaCondigDocUri = directory.toPath().resolve("src/main/java/org/test/aot/RegistetedViaConfigBeanRegistrationAotProcessor.java").toUri().toString(); + String configDocUri = directory.toPath().resolve("src/main/java/org/test/aot/Config.java").toUri().toString(); + + // now change the config class source code and update doc + TestFileScanListener fileScanListener = new TestFileScanListener(); + indexer.getJavaIndexer().setFileScanListener(fileScanListener); + + String configSource = FileUtils.readFileToString(UriUtil.toFile(configDocUri), Charset.defaultCharset()); + String updatedConfigSource = configSource.replace("@Bean", """ + @Bean + NotRegisteredBeanRegistrationAotProcessor registeredViaConfigAotProcessor2() { + return new NotRegisteredBeanRegistrationAotProcessor(); + } + + @Bean + """); + + CompletableFuture updateFuture = indexer.updateDocument(configDocUri, updatedConfigSource, "test triggered"); + updateFuture.get(5, TimeUnit.SECONDS); + + // check if the bean registrar files have been re-scanned + fileScanListener.assertScannedUri(configDocUri, 1); + fileScanListener.assertScannedUri(docUri, 1); + fileScanListener.assertScannedUri(alreadyRegisteredViaCondigDocUri, 1); // because we changed the config class that refers to this one as well + fileScanListener.assertFileScanCount(3); + + // check diagnostics result + PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri); + List diagnostics = diagnosticsResult.getDiagnostics(); + assertEquals(0, diagnostics.size()); } @Test void testValidationAppearsWhenBeanMethodIsRemovedFromConfig() throws Exception { - // TODO + String docUri = directory.toPath().resolve("src/main/java/org/test/aot/RegistetedViaConfigBeanRegistrationAotProcessor.java").toUri().toString(); + String configDocUri = directory.toPath().resolve("src/main/java/org/test/aot/Config.java").toUri().toString(); + + // now change the config class source code and update doc + TestFileScanListener fileScanListener = new TestFileScanListener(); + indexer.getJavaIndexer().setFileScanListener(fileScanListener); + + String configSource = FileUtils.readFileToString(UriUtil.toFile(configDocUri), Charset.defaultCharset()); + String updatedConfigSource = configSource.replace("@Bean", ""); + + CompletableFuture updateFuture = indexer.updateDocument(configDocUri, updatedConfigSource, "test triggered"); + updateFuture.get(5, TimeUnit.SECONDS); + + // check if the bean registrar files have been re-scanned + fileScanListener.assertScannedUri(docUri, 1); + fileScanListener.assertScannedUri(configDocUri, 1); + fileScanListener.assertFileScanCount(2); + + // check diagnostics result + PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri); + List diagnostics = diagnosticsResult.getDiagnostics(); + assertEquals(1, diagnostics.size()); + assertEquals(SpringAotJavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT.getCode(), diagnostics.get(0).getCode().getLeft()); } - + }