From e7eb27c4332006bd859e45e33081e7a108ff7f77 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Tue, 4 Mar 2025 08:21:09 +0100 Subject: [PATCH] GH-1491: index query methods and show them as child nodes for repository document symbols, incl. query strings when defined --- .../ide/vscode/boot/java/Annotations.java | 5 +- .../data/DataRepositorySymbolProvider.java | 68 +++++++++- .../java/data/QueryMethodIndexElement.java | 63 +++++++++ .../jpa/queries/JdtQueryVisitorUtils.java | 4 +- .../test/DataRepositoryIndexElementsTest.java | 121 ++++++++++++++++++ .../DataRepositorySymbolProviderTest.java | 61 +++++++-- .../java/org/test/CustomerRepository.java | 3 + .../org/test/CustomerRepositoryWithQuery.java | 12 ++ 8 files changed, 322 insertions(+), 15 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodIndexElement.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryIndexElementsTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/CustomerRepositoryWithQuery.java diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java index 07d14d7e3..7648ced38 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java @@ -19,6 +19,7 @@ import java.util.Set; * @author Kris De Volder */ public class Annotations { + public static final String BEAN = "org.springframework.context.annotation.Bean"; public static final String PROFILE = "org.springframework.context.annotation.Profile"; public static final String CONDITIONAL = "org.springframework.context.annotation.Conditional"; @@ -40,7 +41,9 @@ public class Annotations { public static final String JPA_JAVAX_ID_CLASS = "javax.persistence.IdClass"; public static final String JPA_JAKARTA_NAMED_QUERY = "jakarta.persistence.NamedQuery"; public static final String JPA_JAVAX_NAMED_QUERY = "javax.persistence.NamedQuery"; - public static final String DATA_QUERY = "org.springframework.data.jpa.repository.Query"; + + public static final String DATA_QUERY_META_ANNOTATION = "org.springframework.data.annotation.QueryAnnotation"; + public static final String DATA_JPA_QUERY = "org.springframework.data.jpa.repository.Query"; public static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired"; public static final String QUALIFIER = "org.springframework.beans.factory.annotation.Qualifier"; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java index da81c2d30..5a6c739ad 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java @@ -16,15 +16,25 @@ import java.util.Set; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.Modifier; +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.TypeDeclaration; import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolKind; import org.eclipse.lsp4j.WorkspaceSymbol; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.beans.BeanUtils; import org.springframework.ide.vscode.boot.java.beans.CachedBean; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.CachedSymbol; @@ -62,6 +72,9 @@ public class DataRepositorySymbolProvider implements SymbolProvider { SymbolKind.Interface, Either.forLeft(location)); + context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol)); + + // index elements InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(typeDeclaration, doc); ITypeBinding concreteBeanTypeBindung = typeDeclaration.resolveBinding(); @@ -75,8 +88,8 @@ public class DataRepositorySymbolProvider implements SymbolProvider { AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc); Bean beanDefinition = new Bean(beanName, concreteRepoType, location, injectionPoints, supertypes, annotations, false, symbol.getName()); + indexQueryMethods(beanDefinition, typeDeclaration, context, doc); - context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol)); context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition)); } catch (BadLocationException e) { @@ -85,6 +98,59 @@ public class DataRepositorySymbolProvider implements SymbolProvider { } } + private void indexQueryMethods(Bean beanDefinition, TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) { + MethodDeclaration[] methods = typeDeclaration.getMethods(); + if (methods == null) return; + + for (MethodDeclaration method : methods) { + int modifiers = method.getModifiers(); + SimpleName nameNode = method.getName(); + + if (nameNode != null && (modifiers & Modifier.DEFAULT) == 0) { + String methodName = nameNode.getFullyQualifiedName(); + DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, method); + + try { + Range range = doc.toRange(nodeRegion); + + if (methodName != null) { + String queryString = identifyQueryString(method); + beanDefinition.addChild(new QueryMethodIndexElement(methodName, queryString, range)); + } + + } catch (BadLocationException e) { + log.error("query method range computation failed", e); + } + } + } + } + + private String identifyQueryString(MethodDeclaration method) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(method); + + EmbeddedQueryExpression queryExpression = null; + + Collection annotations = ASTUtils.getAnnotations(method); + for (Annotation annotation : annotations) { + ITypeBinding typeBinding = annotation.resolveTypeBinding(); + + if (typeBinding != null && annotationHierarchies.isAnnotatedWith(typeBinding, Annotations.DATA_QUERY_META_ANNOTATION)) { + if (annotation instanceof SingleMemberAnnotation) { + queryExpression = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, (SingleMemberAnnotation)annotation); + } + else if (annotation instanceof NormalAnnotation) { + queryExpression = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, (NormalAnnotation)annotation); + } + } + } + + if (queryExpression != null) { + return queryExpression.query().getText(); + } + + return null; + } + protected String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) { StringBuilder symbolLabel = new StringBuilder(); symbolLabel.append("@+"); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodIndexElement.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodIndexElement.java new file mode 100644 index 000000000..17366ea8f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodIndexElement.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +import java.util.List; + +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolKind; +import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement; +import org.springframework.ide.vscode.commons.protocol.spring.SymbolElement; + +public class QueryMethodIndexElement extends AbstractSpringIndexElement implements SymbolElement { + + private final String methodName; + private final String queryString; + private final Range range; + + public QueryMethodIndexElement(String methodName, String queryString, Range range) { + this.methodName = methodName; + this.queryString = queryString; + this.range = range; + } + + public String getMethodName() { + return methodName; + } + + public String getQueryString() { + return queryString; + } + + @Override + public DocumentSymbol getDocumentSymbol() { + DocumentSymbol symbol = new DocumentSymbol(); + + symbol.setName(methodName); + symbol.setKind(SymbolKind.Method); + symbol.setRange(range); + symbol.setSelectionRange(range); + + if (queryString != null) { + DocumentSymbol querySymbol = new DocumentSymbol(); + querySymbol.setName(queryString); + querySymbol.setKind(SymbolKind.Constant); + querySymbol.setRange(range); + querySymbol.setSelectionRange(range); + + symbol.setChildren(List.of(querySymbol)); + } + + return symbol; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java index dcb938481..1c4525e4e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java @@ -102,8 +102,8 @@ public class JdtQueryVisitorUtils { } static boolean isQueryAnnotation(AnnotationHierarchies annotationHierarchies, Annotation a) { - if (Annotations.DATA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) { - return annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.DATA_QUERY); + if (Annotations.DATA_JPA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) { + return annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.DATA_JPA_QUERY); } return false; } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryIndexElementsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryIndexElementsTest.java new file mode 100644 index 000000000..965a782ad --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryIndexElementsTest.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.ArrayUtils; +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.boot.java.data.QueryMethodIndexElement; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.protocol.spring.DocumentElement; +import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class DataRepositoryIndexElementsTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + @Autowired private SpringMetamodelIndex springIndex; + + private File directory; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-data-symbols/").toURI()); + String projectDir = directory.toURI().toString(); + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + void testSimpleRepositoryElements() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString(); + + DocumentElement document = springIndex.getDocument(docUri); + List children = document.getChildren(); + Bean repositoryElement = (Bean) children.get(0); + assertEquals("customerRepository", repositoryElement.getName()); + assertEquals(1, children.size()); + + Bean[] repoBean = this.springIndex.getBeansWithName("test-spring-data-symbols", "customerRepository"); + assertEquals(1, repoBean.length); + assertEquals("customerRepository", repoBean[0].getName()); + assertEquals("org.test.CustomerRepository", repoBean[0].getType()); + + Bean[] matchingBeans = springIndex.getMatchingBeans("test-spring-data-symbols", "org.springframework.data.repository.CrudRepository"); + assertEquals(3, matchingBeans.length); + ArrayUtils.contains(matchingBeans, repoBean[0]); + } + + @Test + void testSimpleQueryMethodElements() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString(); + + DocumentElement document = springIndex.getDocument(docUri); + List children = document.getChildren(); + Bean repositoryElement = (Bean) children.get(0); + + List queryMethods = repositoryElement.getChildren(); + assertEquals(1, queryMethods.size()); + + QueryMethodIndexElement queryMethod = (QueryMethodIndexElement) queryMethods.get(0); + assertEquals("findByLastName", queryMethod.getMethodName()); + } + + @Test + void testQueryMethodElementWithQueryString() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepositoryWithQuery.java").toUri().toString(); + + DocumentElement document = springIndex.getDocument(docUri); + List children = document.getChildren(); + Bean repositoryElement = (Bean) children.get(0); + + List queryMethods = repositoryElement.getChildren(); + assertEquals(1, queryMethods.size()); + + QueryMethodIndexElement queryMethod = (QueryMethodIndexElement) queryMethods.get(0); + assertEquals("findPetTypes", queryMethod.getMethodName()); + assertEquals("SELECT ptype FROM PetType ptype ORDER BY ptype.name", queryMethod.getQueryString()); + + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositorySymbolProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositorySymbolProviderTest.java index a0c419e93..fd4ce97ca 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositorySymbolProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositorySymbolProviderTest.java @@ -19,7 +19,8 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang3.ArrayUtils; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.SymbolKind; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceSymbol; import org.junit.jupiter.api.BeforeEach; @@ -30,9 +31,7 @@ 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.languageserver.java.JavaProjectFinder; -import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; import org.springframework.ide.vscode.project.harness.ProjectsHarness; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -48,7 +47,6 @@ public class DataRepositorySymbolProviderTest { @Autowired private BootLanguageServerHarness harness; @Autowired private JavaProjectFinder projectFinder; @Autowired private SpringSymbolIndex indexer; - @Autowired private SpringMetamodelIndex springIndex; private File directory; @@ -72,15 +70,40 @@ public class DataRepositorySymbolProviderTest { List symbols = indexer.getSymbols(docUri); assertEquals(1, symbols.size()); assertTrue(containsSymbol(symbols, "@+ 'customerRepository' (Customer) Repository", docUri, 6, 17, 6, 35)); + } - Bean[] repoBean = this.springIndex.getBeansWithName("test-spring-data-symbols", "customerRepository"); - assertEquals(1, repoBean.length); - assertEquals("customerRepository", repoBean[0].getName()); - assertEquals("org.test.CustomerRepository", repoBean[0].getType()); + @Test + void testDocumentSymbolsForRepository() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString(); + List symbols = indexer.getDocumentSymbolsFromMetamodelIndex(docUri); + assertEquals(1, symbols.size()); + assertTrue(containsDocumentSymbol(symbols, "@+ 'customerRepository' (Customer) Repository", docUri, 6, 17, 6, 35)); - Bean[] matchingBeans = springIndex.getMatchingBeans("test-spring-data-symbols", "org.springframework.data.repository.CrudRepository"); - assertEquals(2, matchingBeans.length); - ArrayUtils.contains(matchingBeans, repoBean[0]); + DocumentSymbol documentSymbol = symbols.get(0); + List children = documentSymbol.getChildren(); + DocumentSymbol childSymbol = children.get(0); + assertEquals("findByLastName", childSymbol.getName()); + + assertEquals(1, children.size()); + } + + @Test + void testNestedDocumentSymbolsForRepositoryWithQuery() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepositoryWithQuery.java").toUri().toString(); + List symbols = indexer.getDocumentSymbolsFromMetamodelIndex(docUri); + assertEquals(1, symbols.size()); + assertTrue(containsDocumentSymbol(symbols, "@+ 'customerRepositoryWithQuery' (Customer) Repository", docUri, 7, 17, 7, 44)); + + DocumentSymbol documentSymbol = symbols.get(0); + List children = documentSymbol.getChildren(); + DocumentSymbol queryMethodSymbol = children.get(0); + assertEquals("findPetTypes", queryMethodSymbol.getName()); + assertEquals(1, children.size()); + + List queryChildren = queryMethodSymbol.getChildren(); + DocumentSymbol queryStringSymbol = queryChildren.get(0); + assertEquals("SELECT ptype FROM PetType ptype ORDER BY ptype.name", queryStringSymbol.getName()); + assertEquals(SymbolKind.Constant, queryStringSymbol.getKind()); } private boolean containsSymbol(List symbols, String name, String uri, int startLine, int startCHaracter, int endLine, int endCharacter) { @@ -100,4 +123,20 @@ public class DataRepositorySymbolProviderTest { return false; } + private boolean containsDocumentSymbol(List symbols, String name, String uri, int startLine, int startCHaracter, int endLine, int endCharacter) { + for (Iterator iterator = symbols.iterator(); iterator.hasNext();) { + DocumentSymbol symbol = iterator.next(); + + if (symbol.getName().equals(name) + && symbol.getRange().getStart().getLine() == startLine + && symbol.getRange().getStart().getCharacter() == startCHaracter + && symbol.getRange().getEnd().getLine() == endLine + && symbol.getRange().getEnd().getCharacter() == endCharacter) { + return true; + } + } + + return false; + } + } diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/CustomerRepository.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/CustomerRepository.java index 029104f19..bb0871a82 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/CustomerRepository.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/CustomerRepository.java @@ -7,4 +7,7 @@ import org.springframework.data.repository.CrudRepository; public interface CustomerRepository extends CrudRepository { List findByLastName(String lastName); + + default List findByWhatever(String lastName) {return null;} + } diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/CustomerRepositoryWithQuery.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/CustomerRepositoryWithQuery.java new file mode 100644 index 000000000..b0a2dfa14 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/CustomerRepositoryWithQuery.java @@ -0,0 +1,12 @@ +package org.test; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +public interface CustomerRepositoryWithQuery extends CrudRepository { + + @Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name") + List findPetTypes(); +}