Skip to content

Commit 7ee4e07

Browse files
committed
GH-1254: support for <go to definition> for bean names in @dependsOn annotation
1 parent c5764ed commit 7ee4e07

File tree

6 files changed

+247
-4
lines changed

6 files changed

+247
-4
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ BootJavaCompletionEngine javaCompletionEngine(
101101
@Qualifier("adHocProperties") ProjectBasedPropertyIndexProvider adHocProperties,
102102
JavaSnippetManager snippetManager,
103103
CompilationUnitCache cuCache) {
104+
104105
SpringPropertyIndexProvider indexProvider = params.indexProvider;
105106
JavaProjectFinder javaProjectFinder = params.projectFinder;
107+
106108
Map<String, CompletionProvider> providers = new HashMap<>();
107109

108110
providers.put(Annotations.SCOPE, new ScopeCompletionProcessor());

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDisc;
4646
import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid;
4747
import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler;
48+
import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvider;
4849
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider;
4950
import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine;
5051
import org.springframework.ide.vscode.boot.java.handlers.JavaCodeActionHandler;
@@ -391,8 +392,8 @@ BootJavaCodeActionProvider getBootJavaCodeActionProvider(JavaProjectFinder proje
391392
}
392393

393394
@Bean
394-
JavaDefinitionHandler javaDefinitionHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) {
395-
return new JavaDefinitionHandler(cuCache, projectFinder, List.of(new PropertyValueAnnotationDefProvider()));
395+
JavaDefinitionHandler javaDefinitionHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) {
396+
return new JavaDefinitionHandler(cuCache, projectFinder, List.of(new PropertyValueAnnotationDefProvider(), new DependsOnDefinitionProvider(springIndex)));
396397
}
397398

398399
@Bean

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,6 @@ public class Annotations {
7272

7373
public static final String VALUE = "org.springframework.beans.factory.annotation.Value";
7474
public static final String SCOPE = "org.springframework.context.annotation.Scope";
75+
public static final String DEPENDS_ON = "org.springframework.context.annotation.DependsOn";
7576

7677
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 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.boot.java.beans;
12+
13+
import java.util.Arrays;
14+
import java.util.Collections;
15+
import java.util.List;
16+
import java.util.stream.Collectors;
17+
18+
import org.eclipse.jdt.core.dom.ASTNode;
19+
import org.eclipse.jdt.core.dom.Annotation;
20+
import org.eclipse.jdt.core.dom.CompilationUnit;
21+
import org.eclipse.jdt.core.dom.IAnnotationBinding;
22+
import org.eclipse.jdt.core.dom.StringLiteral;
23+
import org.eclipse.lsp4j.LocationLink;
24+
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
25+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
26+
import org.springframework.ide.vscode.boot.java.Annotations;
27+
import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider;
28+
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
29+
import org.springframework.ide.vscode.commons.java.IJavaProject;
30+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
31+
32+
/**
33+
* @author Martin Lippert
34+
*/
35+
public class DependsOnDefinitionProvider implements IJavaDefinitionProvider {
36+
37+
private final SpringMetamodelIndex springIndex;
38+
39+
public DependsOnDefinitionProvider(SpringMetamodelIndex springIndex) {
40+
this.springIndex = springIndex;
41+
}
42+
43+
@Override
44+
public List<LocationLink> getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, ASTNode n) {
45+
if (n instanceof StringLiteral) {
46+
StringLiteral valueNode = (StringLiteral) n;
47+
48+
ASTNode parent = ASTUtils.getNearestAnnotationParent(valueNode);
49+
50+
if (parent != null && parent instanceof Annotation) {
51+
Annotation a = (Annotation) parent;
52+
IAnnotationBinding binding = a.resolveAnnotationBinding();
53+
if (binding != null && binding.getAnnotationType() != null && Annotations.DEPENDS_ON.equals(binding.getAnnotationType().getQualifiedName())) {
54+
String beanName = valueNode.getLiteralValue();
55+
56+
if (beanName != null && beanName.length() > 0) {
57+
return findBeansWithName(project, beanName);
58+
}
59+
}
60+
}
61+
}
62+
return Collections.emptyList();
63+
}
64+
65+
private List<LocationLink> findBeansWithName(IJavaProject project, String beanName) {
66+
Bean[] beans = this.springIndex.getBeansWithName(project.getElementName(), beanName);
67+
68+
return Arrays.stream(beans)
69+
.map(bean -> {
70+
return new LocationLink(bean.getLocation().getUri(), bean.getLocation().getRange(), bean.getLocation().getRange());
71+
})
72+
.collect(Collectors.toList());
73+
}
74+
75+
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,12 @@ public static InjectionPoint[] findInjectionPoints(TypeDeclaration type, TextDoc
453453

454454
return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS;
455455
}
456-
457-
456+
457+
public static ASTNode getNearestAnnotationParent(ASTNode node) {
458+
while (node != null && !(node instanceof Annotation)) {
459+
node = node.getParent();
460+
}
461+
return node;
462+
}
458463

459464
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 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.boot.java.beans.test;
12+
13+
import static org.junit.Assert.assertEquals;
14+
15+
import java.io.File;
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
import java.util.concurrent.CompletableFuture;
19+
import java.util.concurrent.TimeUnit;
20+
21+
import org.eclipse.lsp4j.Location;
22+
import org.eclipse.lsp4j.LocationLink;
23+
import org.eclipse.lsp4j.Position;
24+
import org.eclipse.lsp4j.Range;
25+
import org.eclipse.lsp4j.TextDocumentIdentifier;
26+
import org.junit.jupiter.api.BeforeEach;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.extension.ExtendWith;
29+
import org.springframework.beans.factory.annotation.Autowired;
30+
import org.springframework.context.annotation.Import;
31+
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
32+
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
33+
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
34+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
35+
import org.springframework.ide.vscode.commons.java.IJavaProject;
36+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
37+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
38+
import org.springframework.ide.vscode.commons.util.text.LanguageId;
39+
import org.springframework.ide.vscode.languageserver.testharness.Editor;
40+
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
41+
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
42+
import org.springframework.test.context.junit.jupiter.SpringExtension;
43+
44+
/**
45+
* @author Martin Lippert
46+
*/
47+
@ExtendWith(SpringExtension.class)
48+
@BootLanguageServerTest
49+
@Import(SymbolProviderTestConf.class)
50+
public class DependsOnDefinitionProviderTest {
51+
52+
@Autowired private BootLanguageServerHarness harness;
53+
@Autowired private JavaProjectFinder projectFinder;
54+
@Autowired private SpringMetamodelIndex springIndex;
55+
@Autowired private SpringSymbolIndex indexer;
56+
57+
private File directory;
58+
private IJavaProject project;
59+
60+
@BeforeEach
61+
public void setup() throws Exception {
62+
harness.intialize(null);
63+
64+
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-indexing/").toURI());
65+
66+
String projectDir = directory.toURI().toString();
67+
project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
68+
69+
CompletableFuture<Void> initProject = indexer.waitOperation();
70+
initProject.get(5, TimeUnit.SECONDS);
71+
}
72+
73+
@Test
74+
public void testSingleDependsOnBeanDefinitionLink() throws Exception {
75+
String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
76+
77+
Editor editor = harness.newEditor(LanguageId.JAVA, """
78+
package org.test;
79+
80+
import org.springframework.context.annotation.DependsOn;
81+
82+
@Component
83+
@DependsOn("bean1")
84+
public class TestDependsOnClass {
85+
}""", tempJavaDocUri);
86+
87+
String expectedDefinitionUri = directory.toPath().resolve("src/main/java/org/test/MainClass.java").toUri().toString();
88+
89+
Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1");
90+
assertEquals(1, beans.length);
91+
92+
LocationLink expectedLocation = new LocationLink(expectedDefinitionUri,
93+
beans[0].getLocation().getRange(), beans[0].getLocation().getRange(),
94+
null);
95+
96+
editor.assertLinkTargets("bean1", List.of(expectedLocation));
97+
}
98+
99+
@Test
100+
public void testMultipleDependsOnBeanDefinitionLink() throws Exception {
101+
String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
102+
103+
Editor editor = harness.newEditor(LanguageId.JAVA, """
104+
package org.test;
105+
106+
import org.springframework.context.annotation.DependsOn;
107+
108+
@Component
109+
@DependsOn({"bean1", "bean2"})
110+
public class TestDependsOnClass {
111+
}""", tempJavaDocUri);
112+
113+
String expectedDefinitionUri = directory.toPath().resolve("src/main/java/org/test/MainClass.java").toUri().toString();
114+
115+
Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1");
116+
assertEquals(1, beans.length);
117+
118+
LocationLink expectedLocation = new LocationLink(expectedDefinitionUri,
119+
beans[0].getLocation().getRange(), beans[0].getLocation().getRange(),
120+
null);
121+
122+
editor.assertLinkTargets("bean1", List.of(expectedLocation));
123+
}
124+
125+
@Test
126+
public void testDependsOnWithMultipleBeanDefinitionLinks() throws Exception {
127+
String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
128+
129+
Editor editor = harness.newEditor(LanguageId.JAVA, """
130+
package org.test;
131+
132+
import org.springframework.context.annotation.DependsOn;
133+
134+
@Component
135+
@DependsOn("bean1")
136+
public class TestDependsOnClass {
137+
}""", tempJavaDocUri);
138+
139+
String expectedDefinitionUri = directory.toPath().resolve("src/main/java/org/test/MainClass.java").toUri().toString();
140+
141+
List<Bean> beansOfDoc = new ArrayList<>(List.of(springIndex.getBeansOfDocument(expectedDefinitionUri)));
142+
beansOfDoc.add(new Bean("bean1", "type", new Location(expectedDefinitionUri, new Range(new Position(20, 1), new Position(20, 10))), null, null, null));
143+
springIndex.updateBeans(project.getElementName(), expectedDefinitionUri, beansOfDoc.toArray(new Bean[0]));
144+
145+
Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1");
146+
assertEquals(2, beans.length);
147+
148+
LocationLink expectedLocation1 = new LocationLink(expectedDefinitionUri,
149+
beans[0].getLocation().getRange(), beans[0].getLocation().getRange(),
150+
null);
151+
152+
LocationLink expectedLocation2 = new LocationLink(expectedDefinitionUri,
153+
beans[1].getLocation().getRange(), beans[1].getLocation().getRange(),
154+
null);
155+
156+
editor.assertLinkTargets("bean1", List.of(expectedLocation1, expectedLocation2));
157+
}
158+
159+
}

0 commit comments

Comments
 (0)