From 5c50917a966a3bb304309e46f6a4d299ae708ec4 Mon Sep 17 00:00:00 2001 From: aboyko Date: Mon, 5 May 2025 10:50:46 -0400 Subject: [PATCH] Navigate to query method implementation --- .../ls/commons/classpath/ClasspathUtil.java | 13 +- .../boot/app/BootLanguageServerBootApp.java | 4 +- .../GenAotQueryMethodDefinitionProvider.java | 188 ++++++++++++++++++ ...nAotQueryMethodDefinitionProviderTest.java | 112 +++++++++++ 4 files changed, 310 insertions(+), 7 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/GenAotQueryMethodDefinitionProvider.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/GenAotQueryMethodDefinitionProviderTest.java diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathUtil.java b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathUtil.java index 9132e9afd..3fec13e2d 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathUtil.java +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathUtil.java @@ -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()) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index b56b6b0f1..6bc6f9ec9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -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 diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/GenAotQueryMethodDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/GenAotQueryMethodDefinitionProvider.java new file mode 100644 index 000000000..a4bb25904 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/GenAotQueryMethodDefinitionProvider.java @@ -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 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 defs = findInSourceFolder(project, relativeGenSourcePath, docId, md, methodBinding, genRepoFqn); + return defs.isEmpty() ? findInBuildFolder(project, relativeGenSourcePath, docId, md, methodBinding, genRepoFqn) : defs; + } + } + } + return List.of(); + } + + private List 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 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 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 findInBuildFolder(IJavaProject project, Path relativeGenSourcePath, TextDocumentIdentifier docId, MethodDeclaration md, IMethodBinding methodBinding, String genRepoFqn) { + Path buildDirRelativePath = null; + Path projectPath = Paths.get(project.getLocationUri()); + Set 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 genSourceFilePathRef = new AtomicReference<>(); + try { + Files.walkFileTree(projectPath.resolve(buildDirRelativePath), new SimpleFileVisitor() { + + @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); + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/GenAotQueryMethodDefinitionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/GenAotQueryMethodDefinitionProviderTest.java new file mode 100644 index 000000000..03891d407 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/GenAotQueryMethodDefinitionProviderTest.java @@ -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 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()); + } +}