Navigate to query method implementation
This commit is contained in:
@@ -38,7 +38,6 @@ import org.eclipse.jdt.core.JavaCore;
|
||||
import org.eclipse.jdt.core.JavaModelException;
|
||||
import org.eclipse.jdt.internal.core.JavaProject;
|
||||
import org.eclipse.m2e.core.MavenPlugin;
|
||||
import org.eclipse.m2e.core.project.IMavenProjectFacade;
|
||||
import org.springframework.ide.vscode.commons.protocol.java.Classpath;
|
||||
import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE;
|
||||
import org.springframework.ide.vscode.commons.protocol.java.ProjectBuild;
|
||||
@@ -249,12 +248,14 @@ public class ClasspathUtil {
|
||||
boolean likelyMaven = false;
|
||||
final Path home = System.getProperty("user.home") == null ? null : new File(System.getProperty("user.home")).toPath();
|
||||
if (MavenPlugin.isMavenProject(jp.getProject())) {
|
||||
IMavenProjectFacade facade = MavenPlugin.getMavenProjectRegistry().getProject(jp.getProject());
|
||||
if (facade != null) {
|
||||
return ProjectBuild.createMavenBuild(facade.getPom().getLocationURI().toASCIIString());
|
||||
} else {
|
||||
// Causes deadlock between Maven Repo Registry Initialization and the `getProject(...)` call below
|
||||
// Repo registry hangs on to Maven settings and wait for a map that the code below locks while waiting for the MavenSettings lock.
|
||||
// IMavenProjectFacade facade = MavenPlugin.getMavenProjectRegistry().getProject(jp.getProject());
|
||||
// if (facade != null) {
|
||||
// return ProjectBuild.createMavenBuild(facade.getPom().getLocationURI().toASCIIString());
|
||||
// } else {
|
||||
return ProjectBuild.createMavenBuild(jp.getProject().getFile("pom.xml").getLocationURI().toASCIIString());
|
||||
}
|
||||
// }
|
||||
} else if (GradleProjectNature.isPresentOn(jp.getProject())) {
|
||||
IFile g = jp.getProject().getFile("build.gradle");
|
||||
if (!g.exists()) {
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.springframework.ide.vscode.boot.java.conditionals.ConditionalOnBeanDe
|
||||
import org.springframework.ide.vscode.boot.java.conditionals.ConditionalOnResourceDefinitionProvider;
|
||||
import org.springframework.ide.vscode.boot.java.copilot.util.ResponseModifier;
|
||||
import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadataService;
|
||||
import org.springframework.ide.vscode.boot.java.data.GenAotQueryMethodDefinitionProvider;
|
||||
import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider;
|
||||
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider;
|
||||
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider;
|
||||
@@ -418,7 +419,8 @@ public class BootLanguageServerBootApp {
|
||||
new QualifierDefinitionProvider(springIndex),
|
||||
new NamedDefinitionProvider(springIndex),
|
||||
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens),
|
||||
new SpelDefinitionProvider(springIndex, cuCache)));
|
||||
new SpelDefinitionProvider(springIndex, cuCache),
|
||||
new GenAotQueryMethodDefinitionProvider(cuCache, server.getTextDocumentService())));
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2025 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.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.core.dom.ASTNode;
|
||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.eclipse.jdt.core.dom.IMethodBinding;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.SimpleName;
|
||||
import org.eclipse.lsp4j.LocationLink;
|
||||
import org.eclipse.lsp4j.Position;
|
||||
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.ide.vscode.boot.java.IJavaDefinitionProvider;
|
||||
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
|
||||
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
|
||||
import org.springframework.ide.vscode.commons.Version;
|
||||
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
|
||||
import org.springframework.ide.vscode.commons.java.IJavaProject;
|
||||
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
|
||||
import org.springframework.ide.vscode.commons.util.BadLocationException;
|
||||
|
||||
public class GenAotQueryMethodDefinitionProvider implements IJavaDefinitionProvider {
|
||||
|
||||
private static Logger log = LoggerFactory.getLogger(GenAotQueryMethodDefinitionProvider.class);
|
||||
|
||||
private final CompilationUnitCache cuCache;
|
||||
private final SimpleTextDocumentService docService;
|
||||
|
||||
public GenAotQueryMethodDefinitionProvider(CompilationUnitCache cuCache, SimpleTextDocumentService docService) {
|
||||
this.cuCache = cuCache;
|
||||
this.docService = docService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LocationLink> getDefinitions(CancelChecker cancelToken, IJavaProject project,
|
||||
TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) {
|
||||
if (n instanceof SimpleName && n.getParent() instanceof MethodDeclaration md) {
|
||||
Version version = SpringProjectUtil.getDependencyVersion(project, "spring-data-jpa");
|
||||
if (version != null && version.getMajor() >= 4) {
|
||||
IMethodBinding methodBinding = md.resolveBinding();
|
||||
if (methodBinding != null && methodBinding.getDeclaringClass() != null
|
||||
&& methodBinding.getDeclaringClass().isInterface()
|
||||
&& methodBinding.getDeclaringClass() != null
|
||||
&& ASTUtils.isAnyTypeInHierarchy(methodBinding.getDeclaringClass(),
|
||||
List.of(Constants.REPOSITORY_TYPE))) {
|
||||
String genRepoFqn = methodBinding.getDeclaringClass().getQualifiedName() + "Impl__Aot";
|
||||
Path relativeGenSourcePath = Paths.get("%s.java".formatted(genRepoFqn.replace('.', '/')));
|
||||
List<LocationLink> defs = findInSourceFolder(project, relativeGenSourcePath, docId, md, methodBinding, genRepoFqn);
|
||||
return defs.isEmpty() ? findInBuildFolder(project, relativeGenSourcePath, docId, md, methodBinding, genRepoFqn) : defs;
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<LocationLink> getLocationInGenFile(IJavaProject project, TextDocumentIdentifier docId, MethodDeclaration md, IMethodBinding methodBinding, Path genRepoSourcePath, String genRepoFqn) {
|
||||
if (Files.exists(genRepoSourcePath)) {
|
||||
URI genUri = genRepoSourcePath.toUri();
|
||||
return cuCache.withCompilationUnit(project, genUri, genCu -> {
|
||||
List<LocationLink> defs = new ArrayList<>(1);
|
||||
genCu.accept(new ASTVisitor() {
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodDeclaration node) {
|
||||
IMethodBinding genBinding = node.resolveBinding();
|
||||
if (genBinding != null
|
||||
&& genBinding.getName().equals(methodBinding.getName())
|
||||
&& Arrays.equals(Arrays.stream(genBinding.getParameterTypes()).map(b -> b.getQualifiedName()).toArray(), Arrays.stream(methodBinding.getParameterTypes()).map(b -> b.getQualifiedName()).toArray() )
|
||||
&& genRepoFqn.equals(genBinding.getDeclaringClass().getQualifiedName())) {
|
||||
LocationLink ll = new LocationLink();
|
||||
ll.setTargetUri(genUri.toASCIIString());
|
||||
try {
|
||||
ll.setOriginSelectionRange(docService.getLatestSnapshot(docId.getUri()).toRange(md.getName().getStartPosition(), md.getName().getLength()));
|
||||
} catch (BadLocationException e) {
|
||||
log.error("", e);
|
||||
}
|
||||
SimpleName genName = node.getName();
|
||||
int startLine = genCu.getLineNumber(genName.getStartPosition());
|
||||
Position targetStartPosition = new Position(startLine, genName.getStartPosition() - genCu.getPosition(startLine, 0));
|
||||
int endLine = genCu.getLineNumber(genName.getStartPosition() + genName.getLength());
|
||||
Position targetEndPosition = new Position(endLine, genName.getStartPosition() + genName.getLength() - genCu.getPosition(endLine, 0));
|
||||
Range targetRange = new Range(targetStartPosition, targetEndPosition);
|
||||
ll.setTargetRange(targetRange);
|
||||
ll.setTargetSelectionRange(targetRange);
|
||||
defs.add(ll);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
return defs;
|
||||
});
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<LocationLink> findInSourceFolder(IJavaProject project, Path relativeGenSourcePath, TextDocumentIdentifier docId, MethodDeclaration md, IMethodBinding methodBinding, String genRepoFqn) {
|
||||
for (File f : IClasspathUtil.getSourceFolders(project.getClasspath()).collect(Collectors.toSet())) {
|
||||
Path genRepoSourcePath = f.toPath().resolve(relativeGenSourcePath);
|
||||
return getLocationInGenFile(project, docId, md, methodBinding, genRepoSourcePath, genRepoFqn);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<LocationLink> findInBuildFolder(IJavaProject project, Path relativeGenSourcePath, TextDocumentIdentifier docId, MethodDeclaration md, IMethodBinding methodBinding, String genRepoFqn) {
|
||||
Path buildDirRelativePath = null;
|
||||
Path projectPath = Paths.get(project.getLocationUri());
|
||||
Set<Path> outputFolders = IClasspathUtil.getOutputFolders(project.getClasspath()).map(f -> f.toPath()).collect(Collectors.toSet());
|
||||
for (Path f : outputFolders) {
|
||||
Path p = projectPath.relativize(f);
|
||||
if (buildDirRelativePath == null) {
|
||||
buildDirRelativePath = p;
|
||||
} else {
|
||||
int i = 0;
|
||||
for (; i < buildDirRelativePath.getNameCount() && i < p.getNameCount() && buildDirRelativePath.getName(i).equals(p.getName(i)); i++) {
|
||||
// nothing;
|
||||
}
|
||||
if (i == 0) {
|
||||
buildDirRelativePath = Paths.get("");
|
||||
break;
|
||||
} else {
|
||||
buildDirRelativePath = buildDirRelativePath.subpath(0, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
AtomicReference<Path> genSourceFilePathRef = new AtomicReference<>();
|
||||
try {
|
||||
Files.walkFileTree(projectPath.resolve(buildDirRelativePath), new SimpleFileVisitor<Path>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (genSourceFilePathRef.get() == null && !outputFolders.stream().anyMatch(dir::startsWith)) {
|
||||
Path genPath = dir.resolve(relativeGenSourcePath);
|
||||
if (Files.exists(genPath)) {
|
||||
genSourceFilePathRef.set(genPath);
|
||||
} else {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
}
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (file.getFileName().toString().endsWith(".class")) {
|
||||
return FileVisitResult.SKIP_SIBLINGS;
|
||||
}
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.error("", e);
|
||||
}
|
||||
return genSourceFilePathRef.get() == null ? List.of() : getLocationInGenFile(project, docId, md, methodBinding, genSourceFilePathRef.get(), genRepoFqn);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2025 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.data.test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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.commons.java.IJavaProject;
|
||||
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
|
||||
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 GenAotQueryMethodDefinitionProviderTest {
|
||||
|
||||
@Autowired private BootLanguageServerHarness harness;
|
||||
@Autowired private JavaProjectFinder projectFinder;
|
||||
@Autowired private SpringSymbolIndex indexer;
|
||||
|
||||
private IJavaProject testProject;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
testProject = ProjectsHarness.INSTANCE.mavenProject("aot-generation");
|
||||
harness.useProject(testProject);
|
||||
harness.intialize(null);
|
||||
|
||||
// trigger project creation
|
||||
projectFinder.find(new TextDocumentIdentifier(testProject.getLocationUri().toASCIIString())).get();
|
||||
|
||||
CompletableFuture<Void> initProject = indexer.waitOperation();
|
||||
initProject.get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonAnnotatedMethod() throws Exception {
|
||||
Path filePath = Paths.get(testProject.getLocationUri())
|
||||
.resolve("src/main/java/example/springdata/aot/UserRepository.java");
|
||||
Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString());
|
||||
|
||||
LocationLink ll = new LocationLink();
|
||||
ll.setTargetUri(Paths.get(testProject.getLocationUri())
|
||||
.resolve("target/spring-aot/main/sources/example/springdata/aot/UserRepositoryImpl__Aot.java").toUri()
|
||||
.toASCIIString());
|
||||
ll.setOriginSelectionRange(new Range(new Position(43, 15), new Position(43, 61)));
|
||||
ll.setTargetRange(new Range(new Position(145, 20), new Position(145, 66)));
|
||||
ll.setTargetSelectionRange(new Range(new Position(145, 20), new Position(145, 66)));
|
||||
editor.assertLinkTargets("findUserByLastnameStartingWith", List.of(ll));
|
||||
}
|
||||
|
||||
@Test
|
||||
void annotatedMethod() throws Exception {
|
||||
Path filePath = Paths.get(testProject.getLocationUri())
|
||||
.resolve("src/main/java/example/springdata/aot/UserRepository.java");
|
||||
Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString());
|
||||
|
||||
LocationLink ll = new LocationLink();
|
||||
ll.setTargetUri(Paths.get(testProject.getLocationUri())
|
||||
.resolve("target/spring-aot/main/sources/example/springdata/aot/UserRepositoryImpl__Aot.java").toUri()
|
||||
.toASCIIString());
|
||||
ll.setOriginSelectionRange(new Range(new Position(54, 15), new Position(54, 45)));
|
||||
ll.setTargetRange(new Range(new Position(191, 20), new Position(191, 50)));
|
||||
ll.setTargetSelectionRange(new Range(new Position(191, 20), new Position(191, 50)));
|
||||
editor.assertLinkTargets("usersWithUsernamesStartingWith", List.of(ll));
|
||||
}
|
||||
|
||||
@Test
|
||||
void notApplicableInsideRepoInterface() throws Exception {
|
||||
Path filePath = Paths.get(testProject.getLocationUri())
|
||||
.resolve("src/main/java/example/springdata/aot/UserRepository.java");
|
||||
Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString());
|
||||
editor.assertLinkTargets("user", List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void methodOutsideRepoInterface() throws Exception {
|
||||
Path filePath = Paths.get(testProject.getLocationUri())
|
||||
.resolve("src/main/java/example/springdata/aot/User.java");
|
||||
Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString());
|
||||
editor.assertLinkTargets("getRegistrationDate", List.of());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user