GH-1530: keeps track of dependencies to beans defined somewhere else in the project to trigger reconciling

This commit is contained in:
Martin Lippert
2025-03-31 14:48:20 +02:00
parent e548a8f797
commit 1b30b47df6
7 changed files with 216 additions and 8 deletions

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);

View File

@@ -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";

View File

@@ -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";

View File

@@ -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());
}
}