Skip to content

Commit 164c6cd

Browse files
committed
goto definition for beans and methods in spel expressions
1 parent 86af018 commit 164c6cd

File tree

5 files changed

+652
-1
lines changed

5 files changed

+652
-1
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
@@ -70,6 +70,7 @@
7070
import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler;
7171
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
7272
import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler;
73+
import org.springframework.ide.vscode.boot.java.spel.SpelDefinitionProvider;
7374
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
7475
import org.springframework.ide.vscode.boot.java.value.ValueDefinitionProvider;
7576
import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceDefinitionProvider;
@@ -406,7 +407,8 @@ JavaDefinitionHandler javaDefinitionHandler(SimpleLanguageServer server, Compila
406407
new ResourceDefinitionProvider(springIndex),
407408
new QualifierDefinitionProvider(springIndex),
408409
new NamedDefinitionProvider(springIndex),
409-
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens)));
410+
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens),
411+
new SpelDefinitionProvider(springIndex, cuCache)));
410412
}
411413

412414
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
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+
100+
if (parent != null && parent instanceof Annotation) {
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 parseSpelAndFetchLocation(cancelToken, project, cu, offset);
106+
}
107+
}
108+
}
109+
return Collections.emptyList();
110+
}
111+
112+
private List<LocationLink> parseSpelAndFetchLocation(CancelChecker cancelToken, IJavaProject project,
113+
CompilationUnit cu, int offset) {
114+
List<LocationLink> locationLink = new ArrayList<>();
115+
cu.accept(new ASTVisitor() {
116+
@Override
117+
public boolean visit(SingleMemberAnnotation node) {
118+
Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
119+
.map(o -> o.get()).forEach(snippet -> {
120+
List<TokenData> beanReferenceTokens = computeTokens(snippet, offset);
121+
if (beanReferenceTokens != null && beanReferenceTokens.size() > 0) {
122+
locationLink.addAll(findLocationLinksForBeanRef(project, offset, beanReferenceTokens));
123+
}
124+
125+
Optional<Tuple2<String, String>> result = parseAndExtractMethodClassPairFromSpel(snippet,
126+
offset);
127+
result.ifPresent(tuple -> {
128+
locationLink.addAll(findLocationLinksForMethodRef(tuple.getT1(), tuple.getT2(), project));
129+
});
130+
});
131+
return super.visit(node);
132+
}
133+
134+
@Override
135+
public boolean visit(NormalAnnotation node) {
136+
Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
137+
.map(o -> o.get()).forEach(snippet -> {
138+
List<TokenData> beanReferenceTokens = computeTokens(snippet, offset);
139+
if (beanReferenceTokens != null && beanReferenceTokens.size() > 0) {
140+
locationLink.addAll(findLocationLinksForBeanRef(project, offset, beanReferenceTokens));
141+
}
142+
parseAndExtractMethodClassPairFromSpel(snippet, offset);
143+
Optional<Tuple2<String, String>> result = parseAndExtractMethodClassPairFromSpel(snippet,
144+
offset);
145+
result.ifPresent(tuple -> {
146+
locationLink.addAll(findLocationLinksForMethodRef(tuple.getT1(), tuple.getT2(), project));
147+
});
148+
});
149+
150+
return super.visit(node);
151+
}
152+
153+
});
154+
return locationLink;
155+
}
156+
157+
private List<LocationLink> findLocationLinksForBeanRef(IJavaProject project, int offset,
158+
List<TokenData> beanReferenceTokens) {
159+
return beanReferenceTokens.stream().flatMap(t -> findBeansWithName(project, t.text()).stream())
160+
.collect(Collectors.toList());
161+
}
162+
163+
private List<LocationLink> findLocationLinksForMethodRef(String methodName, String className,
164+
IJavaProject project) {
165+
URI docUri = null;
166+
try {
167+
if (className.startsWith("T")) {
168+
String classFqName = className.substring(2, className.length() - 1);
169+
Optional<URL> sourceUrl = SourceLinks.source(project, classFqName);
170+
if (sourceUrl.isPresent()) {
171+
172+
docUri = sourceUrl.get().toURI();
173+
}
174+
} else if (className.startsWith("@")) {
175+
String bean = className.substring(1);
176+
List<LocationLink> beanLoc = findBeansWithName(project, bean);
177+
if (beanLoc != null && beanLoc.size() > 0) {
178+
docUri = new URI(beanLoc.get(0).getTargetUri());
179+
}
180+
}
181+
182+
if (docUri != null) {
183+
return findMethodPositionInDoc(docUri, methodName, project);
184+
}
185+
} catch (Exception e) {
186+
logger.error("", e);
187+
}
188+
return Collections.emptyList();
189+
}
190+
191+
private List<LocationLink> findMethodPositionInDoc(URI docUrl, String methodName, IJavaProject project) {
192+
193+
return cuCache.withCompilationUnit(project, docUrl, cu -> {
194+
List<LocationLink> locationLinks = new ArrayList<>();
195+
try {
196+
if (cu != null) {
197+
TextDocument document = new TextDocument(docUrl.toString(), LanguageId.JAVA);
198+
document.setText(cuCache.fetchContent(docUrl));
199+
cu.accept(new ASTVisitor() {
200+
201+
@Override
202+
public boolean visit(MethodDeclaration node) {
203+
SimpleName nameNode = node.getName();
204+
if (nameNode.getIdentifier().equals(methodName)) {
205+
int start = nameNode.getStartPosition();
206+
int end = start + nameNode.getLength();
207+
DocumentRegion region = new DocumentRegion(document, start, end);
208+
try {
209+
Range docRange = document.toRange(region);
210+
locationLinks.add(new LocationLink(document.getUri(), docRange, docRange));
211+
} catch (BadLocationException e) {
212+
logger.error("", e);
213+
}
214+
}
215+
return super.visit(node);
216+
}
217+
});
218+
}
219+
} catch (URISyntaxException e) {
220+
logger.error("Error parsing the document url: " + docUrl);
221+
} catch (Exception e) {
222+
logger.error("error finding method location in doc '", e);
223+
}
224+
return locationLinks;
225+
});
226+
}
227+
228+
private List<LocationLink> findBeansWithName(IJavaProject project, String beanName) {
229+
Bean[] beans = this.springIndex.getBeansWithName(project.getElementName(), beanName);
230+
return Arrays.stream(beans).map(bean -> {
231+
return new LocationLink(bean.getLocation().getUri(), bean.getLocation().getRange(),
232+
bean.getLocation().getRange());
233+
}).collect(Collectors.toList());
234+
}
235+
236+
private List<TokenData> computeTokens(Snippet snippet, int offset) {
237+
SpelLexer lexer = new SpelLexer(CharStreams.fromString(snippet.text()));
238+
CommonTokenStream antlrTokens = new CommonTokenStream(lexer);
239+
SpelParser parser = new SpelParser(antlrTokens);
240+
241+
List<TokenData> beanReferenceTokens = new ArrayList<>();
242+
243+
lexer.removeErrorListener(ConsoleErrorListener.INSTANCE);
244+
parser.removeErrorListener(ConsoleErrorListener.INSTANCE);
245+
246+
parser.addParseListener(new SpelParserBaseListener() {
247+
248+
@Override
249+
public void exitBeanReference(BeanReferenceContext ctx) {
250+
if (ctx.IDENTIFIER() != null) {
251+
addTokenData(ctx.IDENTIFIER().getSymbol(), offset);
252+
}
253+
if (ctx.STRING_LITERAL() != null) {
254+
addTokenData(ctx.STRING_LITERAL().getSymbol(), offset);
255+
}
256+
}
257+
258+
private void addTokenData(Token sym, int offset) {
259+
int start = sym.getStartIndex() + snippet.offset();
260+
int end = sym.getStartIndex() + sym.getText().length() + snippet.offset();
261+
if (isOffsetWithinToken(start, end, offset)) {
262+
beanReferenceTokens.add(new TokenData(sym.getText(), start, end));
263+
}
264+
}
265+
266+
private boolean isOffsetWithinToken(int tokenStartIndex, int tokenEndIndex, int offset) {
267+
return tokenStartIndex <= (offset) && (offset) <= tokenEndIndex;
268+
}
269+
270+
});
271+
272+
parser.spelExpr();
273+
274+
return beanReferenceTokens;
275+
}
276+
277+
private Optional<Tuple2<String, String>> parseAndExtractMethodClassPairFromSpel(Snippet snippet, int offset) {
278+
SpelExpressionParser parser = new SpelExpressionParser();
279+
try {
280+
org.springframework.expression.Expression expression = parser.parseExpression(snippet.text());
281+
282+
SpelExpression spelExpressionAST = (SpelExpression) expression;
283+
SpelNode rootNode = spelExpressionAST.getAST();
284+
return extractMethodClassPairFromSpelNodes(rootNode, null, snippet, offset);
285+
} catch (ParseException e) {
286+
logger.error("", e);
287+
}
288+
return Optional.empty();
289+
}
290+
291+
private Optional<Tuple2<String, String>> extractMethodClassPairFromSpelNodes(SpelNode node, SpelNode parent,
292+
Snippet snippet, int offset) {
293+
if (node instanceof MethodReference && checkOffsetInMethodName(node, snippet.offset(), offset)) {
294+
MethodReference methodRef = (MethodReference) node;
295+
String methodName = methodRef.getName();
296+
String className = extractClassNameFromParent(parent);
297+
if (className != null) {
298+
return Optional.of(Tuples.of(methodName, className));
299+
}
300+
}
301+
302+
for (int i = 0; i < node.getChildCount(); i++) {
303+
Optional<Tuple2<String, String>> result = extractMethodClassPairFromSpelNodes(node.getChild(i), node,
304+
snippet, offset);
305+
if (result.isPresent()) {
306+
return result;
307+
}
308+
}
309+
return Optional.empty();
310+
}
311+
312+
private String extractClassNameFromParent(SpelNode parent) {
313+
if (parent != null) {
314+
if (parent instanceof PropertyOrFieldReference) {
315+
return ((PropertyOrFieldReference) parent).getName();
316+
} else if (parent instanceof TypeReference) {
317+
return ((TypeReference) parent).toStringAST();
318+
} else if (parent instanceof CompoundExpression) {
319+
for (int i = 0; i < parent.getChildCount(); i++) {
320+
SpelNode child = parent.getChild(i);
321+
if (child instanceof PropertyOrFieldReference || child instanceof BeanReference
322+
|| child instanceof TypeReference) {
323+
return child.toStringAST();
324+
}
325+
}
326+
}
327+
}
328+
return null;
329+
}
330+
331+
private boolean checkOffsetInMethodName(SpelNode node, int nodeOffset, int offset) {
332+
int start = node.getStartPosition() + nodeOffset;
333+
int end = node.getEndPosition() + nodeOffset;
334+
return start <= (offset) && (offset) <= end;
335+
}
336+
337+
}

0 commit comments

Comments
 (0)