Skip to content

Commit 1b30b47

Browse files
committed
GH-1530: keeps track of dependencies to beans defined somewhere else in the project to trigger reconciling
1 parent e548a8f commit 1b30b47

File tree

7 files changed

+216
-8
lines changed

7 files changed

+216
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.commons.protocol.spring;
12+
13+
public class AotProcessorElement extends AbstractSpringIndexElement {
14+
15+
private final String type;
16+
private final String docUri;
17+
18+
public AotProcessorElement(String type, String docUri) {
19+
this.type = type;
20+
this.docUri = docUri;
21+
}
22+
23+
public String getType() {
24+
return type;
25+
}
26+
27+
public String getDocUri() {
28+
return docUri;
29+
}
30+
31+
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java

+12
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ public Bean[] getBeansWithType(String projectName, String type) {
120120
return new Bean[0];
121121
}
122122
}
123+
124+
public Bean getParentBean(Bean bean) {
125+
Bean[] beansOfDocument = getBeansOfDocument(bean.getLocation().getUri());
126+
127+
for (Bean candidateBean : beansOfDocument) {
128+
if (candidateBean.getChildren().contains(bean)) {
129+
return candidateBean;
130+
}
131+
}
132+
133+
return null;
134+
}
123135

124136
public Bean[] getMatchingBeans(String projectName, String matchType) {
125137
ProjectElement project = this.projectRootElements.get(projectName);

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java

+17
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,16 @@
4444
import org.springframework.ide.vscode.boot.java.events.EventListenerIndexer;
4545
import org.springframework.ide.vscode.boot.java.events.EventPublisherIndexElement;
4646
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
47+
import org.springframework.ide.vscode.boot.java.reconcilers.NotRegisteredBeansReconciler;
48+
import org.springframework.ide.vscode.boot.java.reconcilers.ReconcileUtils;
4749
import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException;
4850
import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingIndexer;
4951
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
5052
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
5153
import org.springframework.ide.vscode.boot.java.utils.DefaultSymbolProvider;
5254
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
5355
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
56+
import org.springframework.ide.vscode.commons.protocol.spring.AotProcessorElement;
5457
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
5558
import org.springframework.ide.vscode.commons.protocol.spring.BeanMethodContainerElement;
5659
import org.springframework.ide.vscode.commons.protocol.spring.BeanRegistrarElement;
@@ -352,10 +355,24 @@ public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext
352355
indexEventListenerInterfaceImplementation(null, typeDeclaration, context, doc);
353356
indexBeanRegistrarImplementation(null, typeDeclaration, context, doc);
354357
indexBeanMethods(null, typeDeclaration, null, null, context, doc);
358+
indexAotProcessors(typeDeclaration, context);
355359
}
356360

357361
}
358362

363+
private void indexAotProcessors(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context) {
364+
ITypeBinding typeBinding = typeDeclaration.resolveBinding();
365+
if (typeBinding == null) return;
366+
367+
if (ReconcileUtils.implementsAnyType(NotRegisteredBeansReconciler.AOT_BEANS, typeBinding)) {
368+
String type = typeBinding.getQualifiedName();
369+
String docUri = context.getDocURI();
370+
371+
AotProcessorElement aotProcessorElement = new AotProcessorElement(type, docUri);
372+
context.getBeans().add(new CachedBean(context.getDocURI(), aotProcessorElement));
373+
}
374+
}
375+
359376
private void indexEventListenerInterfaceImplementation(Bean bean, TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
360377
try {
361378
ITypeBinding typeBinding = typeDeclaration.resolveBinding();

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java

+41-1
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,21 @@
3636
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
3737
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
3838
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
39+
import org.springframework.ide.vscode.commons.protocol.spring.AotProcessorElement;
3940
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
41+
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
4042
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
4143
import org.springframework.ide.vscode.commons.rewrite.java.DefineMethod;
4244
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
45+
import org.springframework.ide.vscode.commons.util.UriUtil;
4346

4447
import com.google.common.collect.ImmutableList;
4548
import com.google.common.collect.ImmutableList.Builder;
4649
import com.google.common.collect.ImmutableSet;
4750

4851
public class NotRegisteredBeansReconciler implements JdtAstReconciler {
4952

50-
private static final List<String> AOT_BEANS = List.of(
53+
public static final List<String> AOT_BEANS = List.of(
5154
"org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor",
5255
"org.springframework.beans.factory.aot.BeanRegistrationAotProcessor"
5356
);
@@ -82,6 +85,8 @@ public boolean visit(TypeDeclaration node) {
8285
ITypeBinding type = node.resolveBinding();
8386
if (type != null && ReconcileUtils.implementsAnyType(AOT_BEANS, type)) {
8487

88+
// // reconcile AOT Proceesor itself
89+
8590
if (!context.isIndexComplete()) {
8691
throw new RequiredCompleteIndexException();
8792
}
@@ -92,6 +97,41 @@ public boolean visit(TypeDeclaration node) {
9297
if (registeredBeans == null || registeredBeans.length == 0) {
9398
createProblemAndQuickFixes(project, context.getProblemCollector(), node, type);
9499
}
100+
else {
101+
// record dependency, if bean is not coming from the current doc (defined somewhere else)
102+
String uri = docUri.toASCIIString();
103+
for (Bean bean : registeredBeans) {
104+
String beanDocUri = bean.getLocation().getUri();
105+
if (!beanDocUri.equals(uri)) {
106+
Bean parentBean = springIndex.getParentBean(bean);
107+
if (parentBean != null) {
108+
context.addDependency(parentBean.getType());
109+
}
110+
}
111+
}
112+
}
113+
}
114+
else {
115+
116+
//
117+
// check if new beans have been defined that refer to any AOP processor element
118+
//
119+
120+
List<AotProcessorElement> aotProcessors = springIndex.getNodesOfType(AotProcessorElement.class);
121+
if (aotProcessors != null && aotProcessors.size() > 0) {
122+
123+
List<SpringIndexElement> createdIndexElements = context.getCreatedIndexElements();
124+
List<Bean> createdBeanElements = SpringMetamodelIndex.getNodesOfType(Bean.class, createdIndexElements);
125+
Set<String> beanTypes = createdBeanElements.stream()
126+
.filter(bean -> context.getDocURI().equals(bean.getLocation().getUri()))
127+
.map(bean -> bean.getType())
128+
.collect(Collectors.toSet());
129+
130+
aotProcessors.stream()
131+
.filter(aotProcessor -> beanTypes.contains(aotProcessor.getType()))
132+
.map(aotProcessor -> UriUtil.toFileString(aotProcessor.getDocUri()))
133+
.forEach(file -> context.markForAffetcedFilesIndexing(file));
134+
}
95135
}
96136
}
97137
return super.visit(node);

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringFactoriesIndexer.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public class SpringFactoriesIndexer implements SpringIndexer {
6464

6565
// whenever the implementation of the indexer changes in a way that the stored data in the cache is no longer valid,
6666
// we need to change the generation - this will result in a re-indexing due to no up-to-date cache data being found
67-
private static final String GENERATION = "GEN-12";
67+
private static final String GENERATION = "GEN-13";
6868

6969
private static final String SYMBOL_KEY = "symbols";
7070
private static final String BEANS_KEY = "beans";

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public class SpringIndexerJava implements SpringIndexer {
9292

9393
// whenever the implementation of the indexer changes in a way that the stored data in the cache is no longer valid,
9494
// we need to change the generation - this will result in a re-indexing due to no up-to-date cache data being found
95-
private static final String GENERATION = "GEN-18";
95+
private static final String GENERATION = "GEN-19";
9696
private static final String INDEX_FILES_TASK_ID = "index-java-source-files-task-";
9797

9898
private static final String SYMBOL_KEY = "symbols";

headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansAdvancedReconcilingTest.java

+113-5
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
import static org.junit.jupiter.api.Assertions.assertEquals;
1414

1515
import java.io.File;
16+
import java.nio.charset.Charset;
1617
import java.util.List;
1718
import java.util.concurrent.CompletableFuture;
1819
import java.util.concurrent.TimeUnit;
1920

21+
import org.apache.commons.io.FileUtils;
2022
import org.eclipse.lsp4j.Diagnostic;
2123
import org.eclipse.lsp4j.PublishDiagnosticsParams;
2224
import org.eclipse.lsp4j.TextDocumentIdentifier;
@@ -31,8 +33,10 @@
3133
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
3234
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
3335
import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType;
36+
import org.springframework.ide.vscode.boot.java.utils.test.TestFileScanListener;
3437
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
3538
import org.springframework.ide.vscode.commons.languageserver.util.Settings;
39+
import org.springframework.ide.vscode.commons.util.UriUtil;
3640
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
3741
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
3842
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -127,24 +131,128 @@ void testBasicValidationOfAotProcessorRegisteredViaFactoriesFile() throws Except
127131
assertEquals(0, diagnostics.size());
128132
}
129133

134+
@Test
135+
@Disabled
136+
void testValidationDisappearsWhenAotProcessorAddedToFactoriesFile() throws Exception {
137+
}
138+
139+
@Test
140+
@Disabled
141+
void testValidationAppearsWhenAotProcessorRemovedFromFactoriesFile() throws Exception {
142+
}
143+
130144
@Test
131145
void testValidationDisappearsWhenComponentAnnotationIsAdded() throws Exception {
132-
// TODO
146+
String docUri = directory.toPath().resolve("src/main/java/org/test/aot/NotRegisteredBeanRegistrationAotProcessor.java").toUri().toString();
147+
148+
// now change the config class source code and update doc
149+
TestFileScanListener fileScanListener = new TestFileScanListener();
150+
indexer.getJavaIndexer().setFileScanListener(fileScanListener);
151+
152+
String notRegisteredSource = FileUtils.readFileToString(UriUtil.toFile(docUri), Charset.defaultCharset());
153+
String updatedSource = notRegisteredSource.replace("public class NotRegisteredBeanRegistrationAotProcessor",
154+
"import org.springframework.stereotype.Component;\n" +
155+
"\n" +
156+
"@Component public class NotRegisteredBeanRegistrationAotProcessor");
157+
158+
CompletableFuture<Void> updateFuture = indexer.updateDocument(docUri, updatedSource, "test triggered");
159+
updateFuture.get(5, TimeUnit.SECONDS);
160+
161+
// check if the bean registrar files have been re-scanned
162+
fileScanListener.assertScannedUri(docUri, 1);
163+
fileScanListener.assertFileScanCount(1);
164+
165+
// check diagnostics result
166+
PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri);
167+
List<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
168+
assertEquals(0, diagnostics.size());
133169
}
134170

135171
@Test
136172
void testValidationAppearsWhenComponentAnnotationIsRemoved() throws Exception {
137-
// TODO
173+
String docUri = directory.toPath().resolve("src/main/java/org/test/aot/RegisteredAsComponentBeanRegistrationAotProcessor.java").toUri().toString();
174+
175+
// now change the config class source code and update doc
176+
TestFileScanListener fileScanListener = new TestFileScanListener();
177+
indexer.getJavaIndexer().setFileScanListener(fileScanListener);
178+
179+
String registeredSource = FileUtils.readFileToString(UriUtil.toFile(docUri), Charset.defaultCharset());
180+
String updatedSource = registeredSource.replace("@Component", "");
181+
182+
CompletableFuture<Void> updateFuture = indexer.updateDocument(docUri, updatedSource, "test triggered");
183+
updateFuture.get(5, TimeUnit.SECONDS);
184+
185+
// check if the bean registrar files have been re-scanned
186+
fileScanListener.assertScannedUri(docUri, 1);
187+
fileScanListener.assertFileScanCount(1);
188+
189+
// check diagnostics result
190+
PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri);
191+
List<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
192+
assertEquals(1, diagnostics.size());
193+
assertEquals(SpringAotJavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT.getCode(), diagnostics.get(0).getCode().getLeft());
138194
}
139195

140196
@Test
141197
void testValidationDisappearsWhenBeanMethodIsAddedToConfig() throws Exception {
142-
// TODO
198+
String docUri = directory.toPath().resolve("src/main/java/org/test/aot/NotRegisteredBeanRegistrationAotProcessor.java").toUri().toString();
199+
String alreadyRegisteredViaCondigDocUri = directory.toPath().resolve("src/main/java/org/test/aot/RegistetedViaConfigBeanRegistrationAotProcessor.java").toUri().toString();
200+
String configDocUri = directory.toPath().resolve("src/main/java/org/test/aot/Config.java").toUri().toString();
201+
202+
// now change the config class source code and update doc
203+
TestFileScanListener fileScanListener = new TestFileScanListener();
204+
indexer.getJavaIndexer().setFileScanListener(fileScanListener);
205+
206+
String configSource = FileUtils.readFileToString(UriUtil.toFile(configDocUri), Charset.defaultCharset());
207+
String updatedConfigSource = configSource.replace("@Bean", """
208+
@Bean
209+
NotRegisteredBeanRegistrationAotProcessor registeredViaConfigAotProcessor2() {
210+
return new NotRegisteredBeanRegistrationAotProcessor();
211+
}
212+
213+
@Bean
214+
""");
215+
216+
CompletableFuture<Void> updateFuture = indexer.updateDocument(configDocUri, updatedConfigSource, "test triggered");
217+
updateFuture.get(5, TimeUnit.SECONDS);
218+
219+
// check if the bean registrar files have been re-scanned
220+
fileScanListener.assertScannedUri(configDocUri, 1);
221+
fileScanListener.assertScannedUri(docUri, 1);
222+
fileScanListener.assertScannedUri(alreadyRegisteredViaCondigDocUri, 1); // because we changed the config class that refers to this one as well
223+
fileScanListener.assertFileScanCount(3);
224+
225+
// check diagnostics result
226+
PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri);
227+
List<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
228+
assertEquals(0, diagnostics.size());
143229
}
144230

145231
@Test
146232
void testValidationAppearsWhenBeanMethodIsRemovedFromConfig() throws Exception {
147-
// TODO
233+
String docUri = directory.toPath().resolve("src/main/java/org/test/aot/RegistetedViaConfigBeanRegistrationAotProcessor.java").toUri().toString();
234+
String configDocUri = directory.toPath().resolve("src/main/java/org/test/aot/Config.java").toUri().toString();
235+
236+
// now change the config class source code and update doc
237+
TestFileScanListener fileScanListener = new TestFileScanListener();
238+
indexer.getJavaIndexer().setFileScanListener(fileScanListener);
239+
240+
String configSource = FileUtils.readFileToString(UriUtil.toFile(configDocUri), Charset.defaultCharset());
241+
String updatedConfigSource = configSource.replace("@Bean", "");
242+
243+
CompletableFuture<Void> updateFuture = indexer.updateDocument(configDocUri, updatedConfigSource, "test triggered");
244+
updateFuture.get(5, TimeUnit.SECONDS);
245+
246+
// check if the bean registrar files have been re-scanned
247+
fileScanListener.assertScannedUri(docUri, 1);
248+
fileScanListener.assertScannedUri(configDocUri, 1);
249+
fileScanListener.assertFileScanCount(2);
250+
251+
// check diagnostics result
252+
PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri);
253+
List<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
254+
assertEquals(1, diagnostics.size());
255+
assertEquals(SpringAotJavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT.getCode(), diagnostics.get(0).getCode().getLeft());
148256
}
149-
257+
150258
}

0 commit comments

Comments
 (0)