Skip to content

Commit

Permalink
goto definition for beans and methods references in spel expressions (#…
Browse files Browse the repository at this point in the history
…1360)

* goto definition for beans and methods in spel expressions

* remove redundant parse all spel expressions logic

* Polish. Fix tests.

---------

Co-authored-by: aboyko <alex.boyko@broadcom.com>
  • Loading branch information
vudayani and BoykoAlex authored Oct 4, 2024
1 parent 19e3b38 commit f4c4121
Show file tree
Hide file tree
Showing 9 changed files with 658 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler;
import org.springframework.ide.vscode.boot.java.spel.SpelDefinitionProvider;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.java.value.ValueDefinitionProvider;
import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService;
Expand Down Expand Up @@ -407,7 +408,8 @@ JavaDefinitionHandler javaDefinitionHandler(SimpleLanguageServer server, Compila
new ResourceDefinitionProvider(springIndex),
new QualifierDefinitionProvider(springIndex),
new NamedDefinitionProvider(springIndex),
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens)));
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens),
new SpelDefinitionProvider(springIndex, cuCache)));
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public List<LocationLink> getDefinitions(CancelChecker cancelToken, IJavaProject

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

if (a.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) {
if (a != null && a.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) {
Collector<SemanticTokenData> collector = new Collector<>();
a.accept(semanticTokensProvider.getTokensComputer(project, doc, cu, collector));
for (SemanticTokenData t : collector.get()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* 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, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.spel;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ConsoleErrorListener;
import org.antlr.v4.runtime.Token;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.SpelNode;
import org.springframework.expression.spel.ast.BeanReference;
import org.springframework.expression.spel.ast.CompoundExpression;
import org.springframework.expression.spel.ast.MethodReference;
import org.springframework.expression.spel.ast.PropertyOrFieldReference;
import org.springframework.expression.spel.ast.TypeReference;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider;
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor.Snippet;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import org.springframework.ide.vscode.parser.spel.SpelLexer;
import org.springframework.ide.vscode.parser.spel.SpelParser;
import org.springframework.ide.vscode.parser.spel.SpelParser.BeanReferenceContext;
import org.springframework.ide.vscode.parser.spel.SpelParserBaseListener;

import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;

/**
* @author Udayani V
*/
public class SpelDefinitionProvider implements IJavaDefinitionProvider {

protected static Logger logger = LoggerFactory.getLogger(SpelDefinitionProvider.class);

private final SpringMetamodelIndex springIndex;

private final CompilationUnitCache cuCache;

private final AnnotationParamSpelExtractor[] spelExtractors = AnnotationParamSpelExtractor.SPEL_EXTRACTORS;

public record TokenData(String text, int start, int end) {};

public SpelDefinitionProvider(SpringMetamodelIndex springIndex, CompilationUnitCache cuCache) {
this.springIndex = springIndex;
this.cuCache = cuCache;
}

@Override
public List<LocationLink> getDefinitions(CancelChecker cancelToken, IJavaProject project,
TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) {
if (n instanceof StringLiteral) {
StringLiteral valueNode = (StringLiteral) n;
ASTNode parent = ASTUtils.getNearestAnnotationParent(valueNode);
if (parent != null && parent instanceof Annotation) {

Annotation a = (Annotation) parent;
IAnnotationBinding binding = a.resolveAnnotationBinding();
if (binding != null && binding.getAnnotationType() != null
&& Annotations.VALUE.equals(binding.getAnnotationType().getQualifiedName())) {
return getLocationLinks(project, offset, a);
}
}
}
return Collections.emptyList();
}

private List<LocationLink> getLocationLinks(IJavaProject project, int offset, Annotation a) {
List<LocationLink> locationLink = new ArrayList<>();
Arrays.stream(spelExtractors).map(e -> {
if (a instanceof SingleMemberAnnotation)
return e.getSpelRegion((SingleMemberAnnotation) a);
else if (a instanceof NormalAnnotation)
return e.getSpelRegion((NormalAnnotation) a);
return Optional.<Snippet>empty();
}).filter(o -> o.isPresent()).map(o -> o.get())
.filter(snippet -> {
int tokenEndIndex = snippet.offset() + snippet.text().length();
return snippet.offset() <= (offset) && (offset) <= tokenEndIndex;
}).forEach(snippet -> {
List<TokenData> beanReferenceTokens = computeTokens(snippet, offset);
if (beanReferenceTokens != null && beanReferenceTokens.size() > 0) {
locationLink.addAll(findLocationLinksForBeanRef(project, offset, beanReferenceTokens));
}

Optional<Tuple2<String, String>> result = parseAndExtractMethodClassPairFromSpel(snippet, offset);
result.ifPresent(tuple -> {
locationLink.addAll(findLocationLinksForMethodRef(tuple.getT1(), tuple.getT2(), project));
});
});
return locationLink;
}

private List<LocationLink> findLocationLinksForBeanRef(IJavaProject project, int offset,
List<TokenData> beanReferenceTokens) {
return beanReferenceTokens.stream().flatMap(t -> findBeansWithName(project, t.text()).stream())
.collect(Collectors.toList());
}

private List<LocationLink> findLocationLinksForMethodRef(String methodName, String className,
IJavaProject project) {
URI docUri = null;
try {
if (className.startsWith("T")) {
String classFqName = className.substring(2, className.length() - 1);
Optional<URL> sourceUrl = SourceLinks.source(project, classFqName);
if (sourceUrl.isPresent()) {
docUri = sourceUrl.get().toURI();
}
} else if (className.startsWith("@")) {
String bean = className.substring(1);
List<LocationLink> beanLoc = findBeansWithName(project, bean);
if (beanLoc != null && beanLoc.size() > 0) {
docUri = new URI(beanLoc.get(0).getTargetUri());
}
}

if (docUri != null) {
return findMethodPositionInDoc(docUri, methodName, project);
}
} catch (Exception e) {
logger.error("", e);
}
return Collections.emptyList();
}

private List<LocationLink> findMethodPositionInDoc(URI docUrl, String methodName, IJavaProject project) {

return cuCache.withCompilationUnit(project, docUrl, cu -> {
List<LocationLink> locationLinks = new ArrayList<>();
try {
if (cu != null) {
TextDocument document = new TextDocument(docUrl.toString(), LanguageId.JAVA);
document.setText(cuCache.fetchContent(docUrl));
cu.accept(new ASTVisitor() {

@Override
public boolean visit(MethodDeclaration node) {
SimpleName nameNode = node.getName();
if (nameNode.getIdentifier().equals(methodName)) {
int start = nameNode.getStartPosition();
int end = start + nameNode.getLength();
DocumentRegion region = new DocumentRegion(document, start, end);
try {
Range docRange = document.toRange(region);
locationLinks.add(new LocationLink(document.getUri(), docRange, docRange));
} catch (BadLocationException e) {
logger.error("", e);
}
}
return super.visit(node);
}
});
}
} catch (URISyntaxException e) {
logger.error("Error parsing the document url: " + docUrl);
} catch (Exception e) {
logger.error("error finding method location in doc '", e);
}
return locationLinks;
});
}

private List<LocationLink> findBeansWithName(IJavaProject project, String beanName) {
Bean[] beans = this.springIndex.getBeansWithName(project.getElementName(), beanName);
return Arrays.stream(beans).map(bean -> {
return new LocationLink(bean.getLocation().getUri(), bean.getLocation().getRange(),
bean.getLocation().getRange());
}).collect(Collectors.toList());
}

private List<TokenData> computeTokens(Snippet snippet, int offset) {
SpelLexer lexer = new SpelLexer(CharStreams.fromString(snippet.text()));
CommonTokenStream antlrTokens = new CommonTokenStream(lexer);
SpelParser parser = new SpelParser(antlrTokens);

List<TokenData> beanReferenceTokens = new ArrayList<>();

lexer.removeErrorListener(ConsoleErrorListener.INSTANCE);
parser.removeErrorListener(ConsoleErrorListener.INSTANCE);

parser.addParseListener(new SpelParserBaseListener() {

@Override
public void exitBeanReference(BeanReferenceContext ctx) {
if (ctx.IDENTIFIER() != null) {
addTokenData(ctx.IDENTIFIER().getSymbol(), offset);
}
if (ctx.STRING_LITERAL() != null) {
addTokenData(ctx.STRING_LITERAL().getSymbol(), offset);
}
}

private void addTokenData(Token sym, int offset) {
int start = sym.getStartIndex() + snippet.offset();
int end = sym.getStartIndex() + sym.getText().length() + snippet.offset();
if (isOffsetWithinToken(start, end, offset)) {
beanReferenceTokens.add(new TokenData(sym.getText(), start, end));
}
}

private boolean isOffsetWithinToken(int tokenStartIndex, int tokenEndIndex, int offset) {
return tokenStartIndex <= (offset) && (offset) <= tokenEndIndex;
}

});

parser.spelExpr();

return beanReferenceTokens;
}

private Optional<Tuple2<String, String>> parseAndExtractMethodClassPairFromSpel(Snippet snippet, int offset) {
SpelExpressionParser parser = new SpelExpressionParser();
try {
org.springframework.expression.Expression expression = parser.parseExpression(snippet.text());

SpelExpression spelExpressionAST = (SpelExpression) expression;
SpelNode rootNode = spelExpressionAST.getAST();
return extractMethodClassPairFromSpelNodes(rootNode, null, snippet, offset);
} catch (ParseException e) {
logger.error("", e);
}
return Optional.empty();
}

private Optional<Tuple2<String, String>> extractMethodClassPairFromSpelNodes(SpelNode node, SpelNode parent,
Snippet snippet, int offset) {
if (node instanceof MethodReference && checkOffsetInMethodName(node, snippet.offset(), offset)) {
MethodReference methodRef = (MethodReference) node;
String methodName = methodRef.getName();
String className = extractClassNameFromParent(parent);
if (className != null) {
return Optional.of(Tuples.of(methodName, className));
}
}

for (int i = 0; i < node.getChildCount(); i++) {
Optional<Tuple2<String, String>> result = extractMethodClassPairFromSpelNodes(node.getChild(i), node,
snippet, offset);
if (result.isPresent()) {
return result;
}
}
return Optional.empty();
}

private String extractClassNameFromParent(SpelNode parent) {
if (parent != null) {
if (parent instanceof PropertyOrFieldReference) {
return ((PropertyOrFieldReference) parent).getName();
} else if (parent instanceof TypeReference) {
return ((TypeReference) parent).toStringAST();
} else if (parent instanceof CompoundExpression) {
for (int i = 0; i < parent.getChildCount(); i++) {
SpelNode child = parent.getChild(i);
if (child instanceof PropertyOrFieldReference || child instanceof BeanReference
|| child instanceof TypeReference) {
return child.toStringAST();
}
}
}
}
return null;
}

private boolean checkOffsetInMethodName(SpelNode node, int nodeOffset, int offset) {
int start = node.getStartPosition() + nodeOffset;
int end = node.getEndPosition() + nodeOffset;
return start <= (offset) && (offset) <= end;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ void testBeansNameAndTypeFromBeanAnnotatedMethod() throws Exception {
List<Bean> beans = result.get(5, TimeUnit.SECONDS);

assertNotNull(beans);
assertEquals(17, beans.size());
assertEquals(19, beans.size());
}

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

assertNotNull(beans);
assertEquals(16, beans.size());
assertEquals(18, beans.size());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ void testUpdateNotificationAfterProjectCreation() {
@Test
void testDeleteProject() throws Exception {
Bean[] beans = springIndex.getBeansOfProject("test-spring-indexing");
assertEquals(17, beans.length);
assertEquals(19, beans.length);

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

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

Bean[] beans = springIndex.getBeansOfDocument(deletedDocURI);
assertEquals(1, beans.length);
Expand Down
Loading

0 comments on commit f4c4121

Please sign in to comment.