Skip to content

Commit f4c4121

Browse files
vudayaniBoykoAlex
andauthored
goto definition for beans and methods references in spel expressions (#1360)
* goto definition for beans and methods in spel expressions * remove redundant parse all spel expressions logic * Polish. Fix tests. --------- Co-authored-by: aboyko <[email protected]>
1 parent 19e3b38 commit f4c4121

File tree

9 files changed

+658
-30
lines changed

9 files changed

+658
-30
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler;
7373
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
7474
import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler;
75+
import org.springframework.ide.vscode.boot.java.spel.SpelDefinitionProvider;
7576
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
7677
import org.springframework.ide.vscode.boot.java.value.ValueDefinitionProvider;
7778
import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService;
@@ -407,7 +408,8 @@ JavaDefinitionHandler javaDefinitionHandler(SimpleLanguageServer server, Compila
407408
new ResourceDefinitionProvider(springIndex),
408409
new QualifierDefinitionProvider(springIndex),
409410
new NamedDefinitionProvider(springIndex),
410-
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens)));
411+
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens),
412+
new SpelDefinitionProvider(springIndex, cuCache)));
411413
}
412414

413415
@Bean

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public List<LocationLink> getDefinitions(CancelChecker cancelToken, IJavaProject
5353

5454
TextDocument doc = documents.getLatestSnapshot(docId.getUri());
5555

56-
if (a.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) {
56+
if (a != null && a.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) {
5757
Collector<SemanticTokenData> collector = new Collector<>();
5858
a.accept(semanticTokensProvider.getTokensComputer(project, doc, cu, collector));
5959
for (SemanticTokenData t : collector.get()) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2017, 2024 Broadcom, Inc.
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, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.spel;
12+
13+
import java.net.URI;
14+
import java.net.URISyntaxException;
15+
import java.net.URL;
16+
import java.util.ArrayList;
17+
import java.util.Arrays;
18+
import java.util.Collections;
19+
import java.util.List;
20+
import java.util.Optional;
21+
import java.util.stream.Collectors;
22+
23+
import org.antlr.v4.runtime.CharStreams;
24+
import org.antlr.v4.runtime.CommonTokenStream;
25+
import org.antlr.v4.runtime.ConsoleErrorListener;
26+
import org.antlr.v4.runtime.Token;
27+
import org.eclipse.jdt.core.dom.ASTNode;
28+
import org.eclipse.jdt.core.dom.ASTVisitor;
29+
import org.eclipse.jdt.core.dom.Annotation;
30+
import org.eclipse.jdt.core.dom.CompilationUnit;
31+
import org.eclipse.jdt.core.dom.IAnnotationBinding;
32+
import org.eclipse.jdt.core.dom.MethodDeclaration;
33+
import org.eclipse.jdt.core.dom.NormalAnnotation;
34+
import org.eclipse.jdt.core.dom.SimpleName;
35+
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
36+
import org.eclipse.jdt.core.dom.StringLiteral;
37+
import org.eclipse.lsp4j.LocationLink;
38+
import org.eclipse.lsp4j.Range;
39+
import org.eclipse.lsp4j.TextDocumentIdentifier;
40+
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
43+
import org.springframework.expression.ParseException;
44+
import org.springframework.expression.spel.SpelNode;
45+
import org.springframework.expression.spel.ast.BeanReference;
46+
import org.springframework.expression.spel.ast.CompoundExpression;
47+
import org.springframework.expression.spel.ast.MethodReference;
48+
import org.springframework.expression.spel.ast.PropertyOrFieldReference;
49+
import org.springframework.expression.spel.ast.TypeReference;
50+
import org.springframework.expression.spel.standard.SpelExpression;
51+
import org.springframework.expression.spel.standard.SpelExpressionParser;
52+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
53+
import org.springframework.ide.vscode.boot.java.Annotations;
54+
import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider;
55+
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
56+
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor.Snippet;
57+
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
58+
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
59+
import org.springframework.ide.vscode.commons.java.IJavaProject;
60+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
61+
import org.springframework.ide.vscode.commons.util.BadLocationException;
62+
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
63+
import org.springframework.ide.vscode.commons.util.text.LanguageId;
64+
import org.springframework.ide.vscode.commons.util.text.TextDocument;
65+
import org.springframework.ide.vscode.parser.spel.SpelLexer;
66+
import org.springframework.ide.vscode.parser.spel.SpelParser;
67+
import org.springframework.ide.vscode.parser.spel.SpelParser.BeanReferenceContext;
68+
import org.springframework.ide.vscode.parser.spel.SpelParserBaseListener;
69+
70+
import reactor.util.function.Tuple2;
71+
import reactor.util.function.Tuples;
72+
73+
/**
74+
* @author Udayani V
75+
*/
76+
public class SpelDefinitionProvider implements IJavaDefinitionProvider {
77+
78+
protected static Logger logger = LoggerFactory.getLogger(SpelDefinitionProvider.class);
79+
80+
private final SpringMetamodelIndex springIndex;
81+
82+
private final CompilationUnitCache cuCache;
83+
84+
private final AnnotationParamSpelExtractor[] spelExtractors = AnnotationParamSpelExtractor.SPEL_EXTRACTORS;
85+
86+
public record TokenData(String text, int start, int end) {};
87+
88+
public SpelDefinitionProvider(SpringMetamodelIndex springIndex, CompilationUnitCache cuCache) {
89+
this.springIndex = springIndex;
90+
this.cuCache = cuCache;
91+
}
92+
93+
@Override
94+
public List<LocationLink> getDefinitions(CancelChecker cancelToken, IJavaProject project,
95+
TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) {
96+
if (n instanceof StringLiteral) {
97+
StringLiteral valueNode = (StringLiteral) n;
98+
ASTNode parent = ASTUtils.getNearestAnnotationParent(valueNode);
99+
if (parent != null && parent instanceof Annotation) {
100+
101+
Annotation a = (Annotation) parent;
102+
IAnnotationBinding binding = a.resolveAnnotationBinding();
103+
if (binding != null && binding.getAnnotationType() != null
104+
&& Annotations.VALUE.equals(binding.getAnnotationType().getQualifiedName())) {
105+
return getLocationLinks(project, offset, a);
106+
}
107+
}
108+
}
109+
return Collections.emptyList();
110+
}
111+
112+
private List<LocationLink> getLocationLinks(IJavaProject project, int offset, Annotation a) {
113+
List<LocationLink> locationLink = new ArrayList<>();
114+
Arrays.stream(spelExtractors).map(e -> {
115+
if (a instanceof SingleMemberAnnotation)
116+
return e.getSpelRegion((SingleMemberAnnotation) a);
117+
else if (a instanceof NormalAnnotation)
118+
return e.getSpelRegion((NormalAnnotation) a);
119+
return Optional.<Snippet>empty();
120+
}).filter(o -> o.isPresent()).map(o -> o.get())
121+
.filter(snippet -> {
122+
int tokenEndIndex = snippet.offset() + snippet.text().length();
123+
return snippet.offset() <= (offset) && (offset) <= tokenEndIndex;
124+
}).forEach(snippet -> {
125+
List<TokenData> beanReferenceTokens = computeTokens(snippet, offset);
126+
if (beanReferenceTokens != null && beanReferenceTokens.size() > 0) {
127+
locationLink.addAll(findLocationLinksForBeanRef(project, offset, beanReferenceTokens));
128+
}
129+
130+
Optional<Tuple2<String, String>> result = parseAndExtractMethodClassPairFromSpel(snippet, offset);
131+
result.ifPresent(tuple -> {
132+
locationLink.addAll(findLocationLinksForMethodRef(tuple.getT1(), tuple.getT2(), project));
133+
});
134+
});
135+
return locationLink;
136+
}
137+
138+
private List<LocationLink> findLocationLinksForBeanRef(IJavaProject project, int offset,
139+
List<TokenData> beanReferenceTokens) {
140+
return beanReferenceTokens.stream().flatMap(t -> findBeansWithName(project, t.text()).stream())
141+
.collect(Collectors.toList());
142+
}
143+
144+
private List<LocationLink> findLocationLinksForMethodRef(String methodName, String className,
145+
IJavaProject project) {
146+
URI docUri = null;
147+
try {
148+
if (className.startsWith("T")) {
149+
String classFqName = className.substring(2, className.length() - 1);
150+
Optional<URL> sourceUrl = SourceLinks.source(project, classFqName);
151+
if (sourceUrl.isPresent()) {
152+
docUri = sourceUrl.get().toURI();
153+
}
154+
} else if (className.startsWith("@")) {
155+
String bean = className.substring(1);
156+
List<LocationLink> beanLoc = findBeansWithName(project, bean);
157+
if (beanLoc != null && beanLoc.size() > 0) {
158+
docUri = new URI(beanLoc.get(0).getTargetUri());
159+
}
160+
}
161+
162+
if (docUri != null) {
163+
return findMethodPositionInDoc(docUri, methodName, project);
164+
}
165+
} catch (Exception e) {
166+
logger.error("", e);
167+
}
168+
return Collections.emptyList();
169+
}
170+
171+
private List<LocationLink> findMethodPositionInDoc(URI docUrl, String methodName, IJavaProject project) {
172+
173+
return cuCache.withCompilationUnit(project, docUrl, cu -> {
174+
List<LocationLink> locationLinks = new ArrayList<>();
175+
try {
176+
if (cu != null) {
177+
TextDocument document = new TextDocument(docUrl.toString(), LanguageId.JAVA);
178+
document.setText(cuCache.fetchContent(docUrl));
179+
cu.accept(new ASTVisitor() {
180+
181+
@Override
182+
public boolean visit(MethodDeclaration node) {
183+
SimpleName nameNode = node.getName();
184+
if (nameNode.getIdentifier().equals(methodName)) {
185+
int start = nameNode.getStartPosition();
186+
int end = start + nameNode.getLength();
187+
DocumentRegion region = new DocumentRegion(document, start, end);
188+
try {
189+
Range docRange = document.toRange(region);
190+
locationLinks.add(new LocationLink(document.getUri(), docRange, docRange));
191+
} catch (BadLocationException e) {
192+
logger.error("", e);
193+
}
194+
}
195+
return super.visit(node);
196+
}
197+
});
198+
}
199+
} catch (URISyntaxException e) {
200+
logger.error("Error parsing the document url: " + docUrl);
201+
} catch (Exception e) {
202+
logger.error("error finding method location in doc '", e);
203+
}
204+
return locationLinks;
205+
});
206+
}
207+
208+
private List<LocationLink> findBeansWithName(IJavaProject project, String beanName) {
209+
Bean[] beans = this.springIndex.getBeansWithName(project.getElementName(), beanName);
210+
return Arrays.stream(beans).map(bean -> {
211+
return new LocationLink(bean.getLocation().getUri(), bean.getLocation().getRange(),
212+
bean.getLocation().getRange());
213+
}).collect(Collectors.toList());
214+
}
215+
216+
private List<TokenData> computeTokens(Snippet snippet, int offset) {
217+
SpelLexer lexer = new SpelLexer(CharStreams.fromString(snippet.text()));
218+
CommonTokenStream antlrTokens = new CommonTokenStream(lexer);
219+
SpelParser parser = new SpelParser(antlrTokens);
220+
221+
List<TokenData> beanReferenceTokens = new ArrayList<>();
222+
223+
lexer.removeErrorListener(ConsoleErrorListener.INSTANCE);
224+
parser.removeErrorListener(ConsoleErrorListener.INSTANCE);
225+
226+
parser.addParseListener(new SpelParserBaseListener() {
227+
228+
@Override
229+
public void exitBeanReference(BeanReferenceContext ctx) {
230+
if (ctx.IDENTIFIER() != null) {
231+
addTokenData(ctx.IDENTIFIER().getSymbol(), offset);
232+
}
233+
if (ctx.STRING_LITERAL() != null) {
234+
addTokenData(ctx.STRING_LITERAL().getSymbol(), offset);
235+
}
236+
}
237+
238+
private void addTokenData(Token sym, int offset) {
239+
int start = sym.getStartIndex() + snippet.offset();
240+
int end = sym.getStartIndex() + sym.getText().length() + snippet.offset();
241+
if (isOffsetWithinToken(start, end, offset)) {
242+
beanReferenceTokens.add(new TokenData(sym.getText(), start, end));
243+
}
244+
}
245+
246+
private boolean isOffsetWithinToken(int tokenStartIndex, int tokenEndIndex, int offset) {
247+
return tokenStartIndex <= (offset) && (offset) <= tokenEndIndex;
248+
}
249+
250+
});
251+
252+
parser.spelExpr();
253+
254+
return beanReferenceTokens;
255+
}
256+
257+
private Optional<Tuple2<String, String>> parseAndExtractMethodClassPairFromSpel(Snippet snippet, int offset) {
258+
SpelExpressionParser parser = new SpelExpressionParser();
259+
try {
260+
org.springframework.expression.Expression expression = parser.parseExpression(snippet.text());
261+
262+
SpelExpression spelExpressionAST = (SpelExpression) expression;
263+
SpelNode rootNode = spelExpressionAST.getAST();
264+
return extractMethodClassPairFromSpelNodes(rootNode, null, snippet, offset);
265+
} catch (ParseException e) {
266+
logger.error("", e);
267+
}
268+
return Optional.empty();
269+
}
270+
271+
private Optional<Tuple2<String, String>> extractMethodClassPairFromSpelNodes(SpelNode node, SpelNode parent,
272+
Snippet snippet, int offset) {
273+
if (node instanceof MethodReference && checkOffsetInMethodName(node, snippet.offset(), offset)) {
274+
MethodReference methodRef = (MethodReference) node;
275+
String methodName = methodRef.getName();
276+
String className = extractClassNameFromParent(parent);
277+
if (className != null) {
278+
return Optional.of(Tuples.of(methodName, className));
279+
}
280+
}
281+
282+
for (int i = 0; i < node.getChildCount(); i++) {
283+
Optional<Tuple2<String, String>> result = extractMethodClassPairFromSpelNodes(node.getChild(i), node,
284+
snippet, offset);
285+
if (result.isPresent()) {
286+
return result;
287+
}
288+
}
289+
return Optional.empty();
290+
}
291+
292+
private String extractClassNameFromParent(SpelNode parent) {
293+
if (parent != null) {
294+
if (parent instanceof PropertyOrFieldReference) {
295+
return ((PropertyOrFieldReference) parent).getName();
296+
} else if (parent instanceof TypeReference) {
297+
return ((TypeReference) parent).toStringAST();
298+
} else if (parent instanceof CompoundExpression) {
299+
for (int i = 0; i < parent.getChildCount(); i++) {
300+
SpelNode child = parent.getChild(i);
301+
if (child instanceof PropertyOrFieldReference || child instanceof BeanReference
302+
|| child instanceof TypeReference) {
303+
return child.toStringAST();
304+
}
305+
}
306+
}
307+
}
308+
return null;
309+
}
310+
311+
private boolean checkOffsetInMethodName(SpelNode node, int nodeOffset, int offset) {
312+
int start = node.getStartPosition() + nodeOffset;
313+
int end = node.getEndPosition() + nodeOffset;
314+
return start <= (offset) && (offset) <= end;
315+
}
316+
317+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ void testBeansNameAndTypeFromBeanAnnotatedMethod() throws Exception {
8484
List<Bean> beans = result.get(5, TimeUnit.SECONDS);
8585

8686
assertNotNull(beans);
87-
assertEquals(17, beans.size());
87+
assertEquals(19, beans.size());
8888
}
8989

9090
@Test
@@ -98,7 +98,7 @@ void testMatchingBeansForObject() throws Exception {
9898
List<Bean> beans = result.get(5, TimeUnit.SECONDS);
9999

100100
assertNotNull(beans);
101-
assertEquals(16, beans.size());
101+
assertEquals(18, beans.size());
102102
}
103103

104104
@Test

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ void testUpdateNotificationAfterProjectCreation() {
7575
@Test
7676
void testDeleteProject() throws Exception {
7777
Bean[] beans = springIndex.getBeansOfProject("test-spring-indexing");
78-
assertEquals(17, beans.length);
78+
assertEquals(19, beans.length);
7979

8080
CompletableFuture<Void> deleteProject = indexer.deleteProject(project);
8181
deleteProject.get(5, TimeUnit.SECONDS);
@@ -92,7 +92,7 @@ void testRemoveSymbolsFromDeletedDocument() throws Exception {
9292
String deletedDocURI = directory.toPath().resolve("src/main/java/org/test/injections/ConstructorInjectionService.java").toUri().toString();
9393

9494
Bean[] allBeansOfProject = springIndex.getBeansOfProject("test-spring-indexing");
95-
assertEquals(17, allBeansOfProject.length);
95+
assertEquals(19, allBeansOfProject.length);
9696

9797
Bean[] beans = springIndex.getBeansOfDocument(deletedDocURI);
9898
assertEquals(1, beans.length);

0 commit comments

Comments
 (0)