Skip to content

Commit 5c50917

Browse files
committed
Navigate to query method implementation
1 parent ae8cfdf commit 5c50917

File tree

4 files changed

+310
-7
lines changed

4 files changed

+310
-7
lines changed

headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathUtil.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import org.eclipse.jdt.core.JavaModelException;
3939
import org.eclipse.jdt.internal.core.JavaProject;
4040
import org.eclipse.m2e.core.MavenPlugin;
41-
import org.eclipse.m2e.core.project.IMavenProjectFacade;
4241
import org.springframework.ide.vscode.commons.protocol.java.Classpath;
4342
import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE;
4443
import org.springframework.ide.vscode.commons.protocol.java.ProjectBuild;
@@ -249,12 +248,14 @@ public static ProjectBuild createProjectBuild(IJavaProject jp, Logger logger) {
249248
boolean likelyMaven = false;
250249
final Path home = System.getProperty("user.home") == null ? null : new File(System.getProperty("user.home")).toPath();
251250
if (MavenPlugin.isMavenProject(jp.getProject())) {
252-
IMavenProjectFacade facade = MavenPlugin.getMavenProjectRegistry().getProject(jp.getProject());
253-
if (facade != null) {
254-
return ProjectBuild.createMavenBuild(facade.getPom().getLocationURI().toASCIIString());
255-
} else {
251+
// Causes deadlock between Maven Repo Registry Initialization and the `getProject(...)` call below
252+
// Repo registry hangs on to Maven settings and wait for a map that the code below locks while waiting for the MavenSettings lock.
253+
// IMavenProjectFacade facade = MavenPlugin.getMavenProjectRegistry().getProject(jp.getProject());
254+
// if (facade != null) {
255+
// return ProjectBuild.createMavenBuild(facade.getPom().getLocationURI().toASCIIString());
256+
// } else {
256257
return ProjectBuild.createMavenBuild(jp.getProject().getFile("pom.xml").getLocationURI().toASCIIString());
257-
}
258+
// }
258259
} else if (GradleProjectNature.isPresentOn(jp.getProject())) {
259260
IFile g = jp.getProject().getFile("build.gradle");
260261
if (!g.exists()) {

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
@@ -54,6 +54,7 @@
5454
import org.springframework.ide.vscode.boot.java.conditionals.ConditionalOnResourceDefinitionProvider;
5555
import org.springframework.ide.vscode.boot.java.copilot.util.ResponseModifier;
5656
import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadataService;
57+
import org.springframework.ide.vscode.boot.java.data.GenAotQueryMethodDefinitionProvider;
5758
import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider;
5859
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider;
5960
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider;
@@ -418,7 +419,8 @@ JavaDefinitionHandler javaDefinitionHandler(SimpleLanguageServer server, Compila
418419
new QualifierDefinitionProvider(springIndex),
419420
new NamedDefinitionProvider(springIndex),
420421
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens),
421-
new SpelDefinitionProvider(springIndex, cuCache)));
422+
new SpelDefinitionProvider(springIndex, cuCache),
423+
new GenAotQueryMethodDefinitionProvider(cuCache, server.getTextDocumentService())));
422424
}
423425

424426
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 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.data;
12+
13+
import java.io.File;
14+
import java.io.IOException;
15+
import java.net.URI;
16+
import java.nio.file.FileVisitResult;
17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
19+
import java.nio.file.Paths;
20+
import java.nio.file.SimpleFileVisitor;
21+
import java.nio.file.attribute.BasicFileAttributes;
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.List;
25+
import java.util.Set;
26+
import java.util.concurrent.atomic.AtomicReference;
27+
import java.util.stream.Collectors;
28+
29+
import org.eclipse.jdt.core.dom.ASTNode;
30+
import org.eclipse.jdt.core.dom.ASTVisitor;
31+
import org.eclipse.jdt.core.dom.CompilationUnit;
32+
import org.eclipse.jdt.core.dom.IMethodBinding;
33+
import org.eclipse.jdt.core.dom.MethodDeclaration;
34+
import org.eclipse.jdt.core.dom.SimpleName;
35+
import org.eclipse.lsp4j.LocationLink;
36+
import org.eclipse.lsp4j.Position;
37+
import org.eclipse.lsp4j.Range;
38+
import org.eclipse.lsp4j.TextDocumentIdentifier;
39+
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
42+
import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider;
43+
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
44+
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
45+
import org.springframework.ide.vscode.commons.Version;
46+
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
47+
import org.springframework.ide.vscode.commons.java.IJavaProject;
48+
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
49+
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
50+
import org.springframework.ide.vscode.commons.util.BadLocationException;
51+
52+
public class GenAotQueryMethodDefinitionProvider implements IJavaDefinitionProvider {
53+
54+
private static Logger log = LoggerFactory.getLogger(GenAotQueryMethodDefinitionProvider.class);
55+
56+
private final CompilationUnitCache cuCache;
57+
private final SimpleTextDocumentService docService;
58+
59+
public GenAotQueryMethodDefinitionProvider(CompilationUnitCache cuCache, SimpleTextDocumentService docService) {
60+
this.cuCache = cuCache;
61+
this.docService = docService;
62+
}
63+
64+
@Override
65+
public List<LocationLink> getDefinitions(CancelChecker cancelToken, IJavaProject project,
66+
TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) {
67+
if (n instanceof SimpleName && n.getParent() instanceof MethodDeclaration md) {
68+
Version version = SpringProjectUtil.getDependencyVersion(project, "spring-data-jpa");
69+
if (version != null && version.getMajor() >= 4) {
70+
IMethodBinding methodBinding = md.resolveBinding();
71+
if (methodBinding != null && methodBinding.getDeclaringClass() != null
72+
&& methodBinding.getDeclaringClass().isInterface()
73+
&& methodBinding.getDeclaringClass() != null
74+
&& ASTUtils.isAnyTypeInHierarchy(methodBinding.getDeclaringClass(),
75+
List.of(Constants.REPOSITORY_TYPE))) {
76+
String genRepoFqn = methodBinding.getDeclaringClass().getQualifiedName() + "Impl__Aot";
77+
Path relativeGenSourcePath = Paths.get("%s.java".formatted(genRepoFqn.replace('.', '/')));
78+
List<LocationLink> defs = findInSourceFolder(project, relativeGenSourcePath, docId, md, methodBinding, genRepoFqn);
79+
return defs.isEmpty() ? findInBuildFolder(project, relativeGenSourcePath, docId, md, methodBinding, genRepoFqn) : defs;
80+
}
81+
}
82+
}
83+
return List.of();
84+
}
85+
86+
private List<LocationLink> getLocationInGenFile(IJavaProject project, TextDocumentIdentifier docId, MethodDeclaration md, IMethodBinding methodBinding, Path genRepoSourcePath, String genRepoFqn) {
87+
if (Files.exists(genRepoSourcePath)) {
88+
URI genUri = genRepoSourcePath.toUri();
89+
return cuCache.withCompilationUnit(project, genUri, genCu -> {
90+
List<LocationLink> defs = new ArrayList<>(1);
91+
genCu.accept(new ASTVisitor() {
92+
93+
@Override
94+
public boolean visit(MethodDeclaration node) {
95+
IMethodBinding genBinding = node.resolveBinding();
96+
if (genBinding != null
97+
&& genBinding.getName().equals(methodBinding.getName())
98+
&& Arrays.equals(Arrays.stream(genBinding.getParameterTypes()).map(b -> b.getQualifiedName()).toArray(), Arrays.stream(methodBinding.getParameterTypes()).map(b -> b.getQualifiedName()).toArray() )
99+
&& genRepoFqn.equals(genBinding.getDeclaringClass().getQualifiedName())) {
100+
LocationLink ll = new LocationLink();
101+
ll.setTargetUri(genUri.toASCIIString());
102+
try {
103+
ll.setOriginSelectionRange(docService.getLatestSnapshot(docId.getUri()).toRange(md.getName().getStartPosition(), md.getName().getLength()));
104+
} catch (BadLocationException e) {
105+
log.error("", e);
106+
}
107+
SimpleName genName = node.getName();
108+
int startLine = genCu.getLineNumber(genName.getStartPosition());
109+
Position targetStartPosition = new Position(startLine, genName.getStartPosition() - genCu.getPosition(startLine, 0));
110+
int endLine = genCu.getLineNumber(genName.getStartPosition() + genName.getLength());
111+
Position targetEndPosition = new Position(endLine, genName.getStartPosition() + genName.getLength() - genCu.getPosition(endLine, 0));
112+
Range targetRange = new Range(targetStartPosition, targetEndPosition);
113+
ll.setTargetRange(targetRange);
114+
ll.setTargetSelectionRange(targetRange);
115+
defs.add(ll);
116+
}
117+
return false;
118+
}
119+
120+
});
121+
return defs;
122+
});
123+
}
124+
return List.of();
125+
}
126+
127+
private List<LocationLink> findInSourceFolder(IJavaProject project, Path relativeGenSourcePath, TextDocumentIdentifier docId, MethodDeclaration md, IMethodBinding methodBinding, String genRepoFqn) {
128+
for (File f : IClasspathUtil.getSourceFolders(project.getClasspath()).collect(Collectors.toSet())) {
129+
Path genRepoSourcePath = f.toPath().resolve(relativeGenSourcePath);
130+
return getLocationInGenFile(project, docId, md, methodBinding, genRepoSourcePath, genRepoFqn);
131+
}
132+
return List.of();
133+
}
134+
135+
private List<LocationLink> findInBuildFolder(IJavaProject project, Path relativeGenSourcePath, TextDocumentIdentifier docId, MethodDeclaration md, IMethodBinding methodBinding, String genRepoFqn) {
136+
Path buildDirRelativePath = null;
137+
Path projectPath = Paths.get(project.getLocationUri());
138+
Set<Path> outputFolders = IClasspathUtil.getOutputFolders(project.getClasspath()).map(f -> f.toPath()).collect(Collectors.toSet());
139+
for (Path f : outputFolders) {
140+
Path p = projectPath.relativize(f);
141+
if (buildDirRelativePath == null) {
142+
buildDirRelativePath = p;
143+
} else {
144+
int i = 0;
145+
for (; i < buildDirRelativePath.getNameCount() && i < p.getNameCount() && buildDirRelativePath.getName(i).equals(p.getName(i)); i++) {
146+
// nothing;
147+
}
148+
if (i == 0) {
149+
buildDirRelativePath = Paths.get("");
150+
break;
151+
} else {
152+
buildDirRelativePath = buildDirRelativePath.subpath(0, i);
153+
}
154+
}
155+
}
156+
AtomicReference<Path> genSourceFilePathRef = new AtomicReference<>();
157+
try {
158+
Files.walkFileTree(projectPath.resolve(buildDirRelativePath), new SimpleFileVisitor<Path>() {
159+
160+
@Override
161+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
162+
if (genSourceFilePathRef.get() == null && !outputFolders.stream().anyMatch(dir::startsWith)) {
163+
Path genPath = dir.resolve(relativeGenSourcePath);
164+
if (Files.exists(genPath)) {
165+
genSourceFilePathRef.set(genPath);
166+
} else {
167+
return FileVisitResult.CONTINUE;
168+
}
169+
}
170+
return FileVisitResult.SKIP_SUBTREE;
171+
}
172+
173+
@Override
174+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
175+
if (file.getFileName().toString().endsWith(".class")) {
176+
return FileVisitResult.SKIP_SIBLINGS;
177+
}
178+
return super.visitFile(file, attrs);
179+
}
180+
181+
});
182+
} catch (IOException e) {
183+
log.error("", e);
184+
}
185+
return genSourceFilePathRef.get() == null ? List.of() : getLocationInGenFile(project, docId, md, methodBinding, genSourceFilePathRef.get(), genRepoFqn);
186+
}
187+
188+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 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.data.test;
12+
13+
import java.nio.charset.StandardCharsets;
14+
import java.nio.file.Files;
15+
import java.nio.file.Path;
16+
import java.nio.file.Paths;
17+
import java.util.List;
18+
import java.util.concurrent.CompletableFuture;
19+
import java.util.concurrent.TimeUnit;
20+
21+
import org.eclipse.lsp4j.LocationLink;
22+
import org.eclipse.lsp4j.Position;
23+
import org.eclipse.lsp4j.Range;
24+
import org.eclipse.lsp4j.TextDocumentIdentifier;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.extension.ExtendWith;
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.context.annotation.Import;
30+
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
31+
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
32+
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
33+
import org.springframework.ide.vscode.commons.java.IJavaProject;
34+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
35+
import org.springframework.ide.vscode.commons.util.text.LanguageId;
36+
import org.springframework.ide.vscode.languageserver.testharness.Editor;
37+
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
38+
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
39+
import org.springframework.test.context.junit.jupiter.SpringExtension;
40+
41+
@ExtendWith(SpringExtension.class)
42+
@BootLanguageServerTest
43+
@Import(SymbolProviderTestConf.class)
44+
public class GenAotQueryMethodDefinitionProviderTest {
45+
46+
@Autowired private BootLanguageServerHarness harness;
47+
@Autowired private JavaProjectFinder projectFinder;
48+
@Autowired private SpringSymbolIndex indexer;
49+
50+
private IJavaProject testProject;
51+
52+
@BeforeEach
53+
public void setup() throws Exception {
54+
testProject = ProjectsHarness.INSTANCE.mavenProject("aot-generation");
55+
harness.useProject(testProject);
56+
harness.intialize(null);
57+
58+
// trigger project creation
59+
projectFinder.find(new TextDocumentIdentifier(testProject.getLocationUri().toASCIIString())).get();
60+
61+
CompletableFuture<Void> initProject = indexer.waitOperation();
62+
initProject.get(5, TimeUnit.SECONDS);
63+
}
64+
65+
@Test
66+
void nonAnnotatedMethod() throws Exception {
67+
Path filePath = Paths.get(testProject.getLocationUri())
68+
.resolve("src/main/java/example/springdata/aot/UserRepository.java");
69+
Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString());
70+
71+
LocationLink ll = new LocationLink();
72+
ll.setTargetUri(Paths.get(testProject.getLocationUri())
73+
.resolve("target/spring-aot/main/sources/example/springdata/aot/UserRepositoryImpl__Aot.java").toUri()
74+
.toASCIIString());
75+
ll.setOriginSelectionRange(new Range(new Position(43, 15), new Position(43, 61)));
76+
ll.setTargetRange(new Range(new Position(145, 20), new Position(145, 66)));
77+
ll.setTargetSelectionRange(new Range(new Position(145, 20), new Position(145, 66)));
78+
editor.assertLinkTargets("findUserByLastnameStartingWith", List.of(ll));
79+
}
80+
81+
@Test
82+
void annotatedMethod() throws Exception {
83+
Path filePath = Paths.get(testProject.getLocationUri())
84+
.resolve("src/main/java/example/springdata/aot/UserRepository.java");
85+
Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString());
86+
87+
LocationLink ll = new LocationLink();
88+
ll.setTargetUri(Paths.get(testProject.getLocationUri())
89+
.resolve("target/spring-aot/main/sources/example/springdata/aot/UserRepositoryImpl__Aot.java").toUri()
90+
.toASCIIString());
91+
ll.setOriginSelectionRange(new Range(new Position(54, 15), new Position(54, 45)));
92+
ll.setTargetRange(new Range(new Position(191, 20), new Position(191, 50)));
93+
ll.setTargetSelectionRange(new Range(new Position(191, 20), new Position(191, 50)));
94+
editor.assertLinkTargets("usersWithUsernamesStartingWith", List.of(ll));
95+
}
96+
97+
@Test
98+
void notApplicableInsideRepoInterface() throws Exception {
99+
Path filePath = Paths.get(testProject.getLocationUri())
100+
.resolve("src/main/java/example/springdata/aot/UserRepository.java");
101+
Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString());
102+
editor.assertLinkTargets("user", List.of());
103+
}
104+
105+
@Test
106+
void methodOutsideRepoInterface() throws Exception {
107+
Path filePath = Paths.get(testProject.getLocationUri())
108+
.resolve("src/main/java/example/springdata/aot/User.java");
109+
Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString());
110+
editor.assertLinkTargets("getRegistrationDate", List.of());
111+
}
112+
}

0 commit comments

Comments
 (0)