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 <alex.boyko@broadcom.com>
This commit is contained in:
Udayani Vaka
2024-10-05 04:58:40 +05:30
committed by GitHub
parent 19e3b38804
commit f4c4121033
9 changed files with 658 additions and 30 deletions

View File

@@ -72,6 +72,7 @@ import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDa
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;
@@ -407,7 +408,8 @@ public class BootLanguageServerBootApp {
new ResourceDefinitionProvider(springIndex),
new QualifierDefinitionProvider(springIndex),
new NamedDefinitionProvider(springIndex),
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens)));
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens),
new SpelDefinitionProvider(springIndex, cuCache)));
}
@Bean

View File

@@ -53,7 +53,7 @@ public class DataQueryParameterDefinitionProvider implements IJavaDefinitionProv
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()) {

View File

@@ -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;
}
}

View File

@@ -84,7 +84,7 @@ public class SpringIndexViaLSPMethodTest {
List<Bean> beans = result.get(5, TimeUnit.SECONDS);
assertNotNull(beans);
assertEquals(17, beans.size());
assertEquals(19, beans.size());
}
@Test
@@ -98,7 +98,7 @@ public class SpringIndexViaLSPMethodTest {
List<Bean> beans = result.get(5, TimeUnit.SECONDS);
assertNotNull(beans);
assertEquals(16, beans.size());
assertEquals(18, beans.size());
}
@Test

View File

@@ -75,7 +75,7 @@ public class SpringMetamodelIndexingTest {
@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);
@@ -92,7 +92,7 @@ public class SpringMetamodelIndexingTest {
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);

View File

@@ -0,0 +1,248 @@
/*******************************************************************************
* 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 static org.junit.Assert.assertEquals;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
public class SpelDefinitionProviderTest {
@Autowired
private BootLanguageServerHarness harness;
@Autowired
private JavaProjectFinder projectFinder;
@Autowired
private SpringMetamodelIndex springIndex;
@Autowired
private SpringSymbolIndex indexer;
private File directory;
private IJavaProject project;
private Bean visitService;
private Bean spelExpressionsClass;
private String expectedDefinitionUriVisitService;
private String expectedDefinitionUriSpelClass;
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-indexing/").toURI());
String projectDir = directory.toURI().toString();
project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
expectedDefinitionUriVisitService = directory.toPath().resolve("src/main/java/org/test/VisitService.java").toUri().toString();
expectedDefinitionUriSpelClass = directory.toPath().resolve("src/main/java/org/test/SpelExpressionsClass.java").toUri().toString();
visitService = new Bean("visitService", "org.test.VisitService",
new Location(expectedDefinitionUriVisitService, new Range(new Position(4, 0), new Position(4, 8))),
null, null, null);
spelExpressionsClass = new Bean("spelExpressionsClass", "org.test.SpelExpressionsClass",
new Location(expectedDefinitionUriSpelClass, new Range(new Position(7, 0), new Position(7, 11))), null,
null, null);
springIndex.updateBeans(project.getElementName(), new Bean[] { visitService, spelExpressionsClass });
CompletableFuture<Void> initProject = indexer.waitOperation();
initProject.get(5, TimeUnit.SECONDS);
}
@Test
public void testBeanDefinitionLinkInSpel() throws Exception {
String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
Editor editor = harness.newEditor(LanguageId.JAVA, """
package org.test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class SpelExpressionsClass {
@Value("${app.version}")
private String appVersion;
@Value(value = "#{@visitService.isValidVersion('${app.version}') ? 'Valid Version' :'Invalid Version'}")
private String versionValidity;
}""", tempJavaDocUri);
Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "visitService");
assertEquals(1, beans.length);
LocationLink expectedLocation = new LocationLink(expectedDefinitionUriVisitService,
beans[0].getLocation().getRange(), beans[0].getLocation().getRange(), null);
editor.assertLinkTargets("visitService", List.of(expectedLocation));
}
@Test
public void testMultipleBeanDefinitionLinksInSpel() throws Exception {
String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
Editor editor = harness.newEditor(LanguageId.JAVA,
"""
package org.test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class SpelExpressionsClass {
@Value("${app.version}")
private String appVersion;
@Value("#{@visitService.isValidVersion('${app.version}') ? @spelExpressionsClass.toUpperCase('valid') :@spelExpressionsClass.text2('invalid version')}")
private String fetchVersion;
}""",
tempJavaDocUri);
Bean[] visitServiceBean = springIndex.getBeansWithName(project.getElementName(), "visitService");
assertEquals(1, visitServiceBean.length);
Bean[] spelExpBean = springIndex.getBeansWithName(project.getElementName(), "spelExpressionsClass");
assertEquals(1, spelExpBean.length);
LocationLink expectedLocation1 = new LocationLink(expectedDefinitionUriVisitService,
visitServiceBean[0].getLocation().getRange(), visitServiceBean[0].getLocation().getRange(), null);
LocationLink expectedLocation2 = new LocationLink(expectedDefinitionUriSpelClass,
spelExpBean[0].getLocation().getRange(), spelExpBean[0].getLocation().getRange(), null);
editor.assertLinkTargets("visitService", List.of(expectedLocation1));
editor.assertLinkTargets("spelExpressionsClass", List.of(expectedLocation2));
}
@Test
public void testMultipleBeanDefinitionLinksWithTypeRefInSpel() throws Exception {
String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
Editor editor = harness.newEditor(LanguageId.JAVA,
"""
package org.test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class SpelExpressionsClass {
@Value("#{T(org.test.SpelExpressionClass).toUpperCase('hello') + ' ' + @spelExpressionsClass.concat('world', '!')}")
private String greeting;
}""",
tempJavaDocUri);
Bean[] spelExpBean = springIndex.getBeansWithName(project.getElementName(), "spelExpressionsClass");
assertEquals(1, spelExpBean.length);
LocationLink expectedLocation2 = new LocationLink(expectedDefinitionUriSpelClass,
spelExpBean[0].getLocation().getRange(), spelExpBean[0].getLocation().getRange(), null);
editor.assertLinkTargets("spelExpressionsClass", List.of(expectedLocation2));
}
@Test
public void testMethodDefinitionLinkInSpel() throws Exception {
String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
Editor editor = harness.newEditor(LanguageId.JAVA, """
package org.test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class SpelExpressionsClass {
@Value("${app.version}")
private String appVersion;
@Value(value = "#{@visitService.isValidVersion('${app.version}') ? 'Valid Version' :'Invalid Version'}")
private String versionValidity;
@Value("#{T(org.test.SpelExpressionClass).toUpperCase('hello') + ' ' + @spelExpressionsClass.concat('world', '!')}")
private String greeting;
public static boolean isValidVersion(String version) {
if (version.matches("\\d+\\.\\d+\\.\\d+")) {
String[] parts = version.split("\\.");
int major = Integer.parseInt(parts[0]);
int minor = Integer.parseInt(parts[1]);
int patch = Integer.parseInt(parts[2]);
return (major > 3) || (major == 3 && (minor > 0 || (minor == 0 && patch >= 0)));
}
return false;
}
public static String toUpperCase(String input) {
return input.toUpperCase();
}
public static String concat(String str1, String str2) {
return str1 + str2;
}
}""", tempJavaDocUri);
LocationLink expectedLocation1 = new LocationLink(expectedDefinitionUriVisitService,
new Range(new Position(7, 23), new Position(7, 37)), new Range(new Position(7, 23), new Position(7, 37)), null);
LocationLink expectedLocation2 = new LocationLink(expectedDefinitionUriSpelClass,
new Range(new Position(37, 22), new Position(37, 28)), new Range(new Position(37, 22), new Position(37, 28)), null);
editor.assertLinkTargets("isValidVersion", List.of(expectedLocation1));
editor.assertLinkTargets("concat", List.of(expectedLocation2));
}
}

View File

@@ -0,0 +1,42 @@
package org.test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class SpelExpressionsClass {
@Value("${app.version}")
private String appVersion;
@Value("#{@visitService.isValidVersion('${app.version}') ? 'Valid Version' :'Invalid Version'}")
private String versionValidity;
@Value("value = #{@visitService.isValidVersion('${app.version}') ? @spelExpressionClass.toUpperCase('valid') :@spelExpressionClass.text2('invalid version')}")
private String fetchVersion;
@Value("#{T(org.test.SpelExpressionClass).toUpperCase('hello') + ' ' + @spelExpressionsClass.concat('world', '!')}")
private String greeting;
public static boolean isValidVersion(String version) {
if (version.matches("\\d+\\.\\d+\\.\\d+")) {
String[] parts = version.split("\\.");
int major = Integer.parseInt(parts[0]);
int minor = Integer.parseInt(parts[1]);
int patch = Integer.parseInt(parts[2]);
return (major > 3) || (major == 3 && (minor > 0 || (minor == 0 && patch >= 0)));
}
return false;
}
public static String toUpperCase(String input) {
return input.toUpperCase();
}
public static String concat(String str1, String str2) {
return str1 + str2;
}
}

View File

@@ -0,0 +1,19 @@
package org.test;
import org.springframework.stereotype.Service;
@Service
public class VisitService {
public static boolean isValidVersion(String version) {
if (version.matches("\\d+\\.\\d+\\.\\d+")) {
String[] parts = version.split("\\.");
int major = Integer.parseInt(parts[0]);
int minor = Integer.parseInt(parts[1]);
int patch = Integer.parseInt(parts[2]);
return (major > 3) || (major == 3 && (minor > 0 || (minor == 0 && patch >= 0)));
}
return false;
}
}

View File

@@ -53,7 +53,7 @@
"./jars/sts-gradle-tooling.jar"
],
"languages": [
{
{
"id": "spring-boot-properties-yaml",
"aliases": [
"Spring Boot Properties Yaml"
@@ -1318,28 +1318,28 @@
"path": "./properties-support/spring-factories.tmLanguage.json"
}
],
"configurationDefaults": {
"[spring-boot-properties-yaml]": {
"editor.quickSuggestions": {
"strings": true
}
},
"[spring-boot-properties]": {
"editor.quickSuggestions": {
"strings": true
}
},
"[jpa-query-properties]": {
"editor.quickSuggestions": {
"strings": true
}
},
"[spring-factories]": {
"editor.quickSuggestions": {
"strings": true
}
}
}
"configurationDefaults": {
"[spring-boot-properties-yaml]": {
"editor.quickSuggestions": {
"strings": true
}
},
"[spring-boot-properties]": {
"editor.quickSuggestions": {
"strings": true
}
},
"[jpa-query-properties]": {
"editor.quickSuggestions": {
"strings": true
}
},
"[spring-factories]": {
"editor.quickSuggestions": {
"strings": true
}
}
}
},
"main": "./out/lib/Main",
"scripts": {
@@ -1367,4 +1367,4 @@
"extensionDependencies": [
"redhat.java"
]
}
}