GH-1530: keeps track of dependencies to beans defined somewhere else in the project to trigger reconciling
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String> AOT_BEANS = List.of(
|
||||
public static final List<String> 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<AotProcessorElement> aotProcessors = springIndex.getNodesOfType(AotProcessorElement.class);
|
||||
if (aotProcessors != null && aotProcessors.size() > 0) {
|
||||
|
||||
List<SpringIndexElement> createdIndexElements = context.getCreatedIndexElements();
|
||||
List<Bean> createdBeanElements = SpringMetamodelIndex.getNodesOfType(Bean.class, createdIndexElements);
|
||||
Set<String> 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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<Void> 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<Diagnostic> 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<Void> 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<Diagnostic> 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<Void> 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<Diagnostic> 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<Void> 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<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
|
||||
assertEquals(1, diagnostics.size());
|
||||
assertEquals(SpringAotJavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT.getCode(), diagnostics.get(0).getCode().getLeft());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user