From 3f183e6d6afd2fcfff4ed569b354484d9220a695 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Tue, 3 Sep 2024 17:35:14 +0200 Subject: [PATCH] GH-1313: added support for content-assist, find references, and go to definition for Named annotation usage in Spring apps --- .../BootJavaCompletionEngineConfigurer.java | 4 + .../boot/app/BootLanguageServerBootApp.java | 2 + .../ide/vscode/boot/java/Annotations.java | 3 + .../BootJavaLanguageServerComponents.java | 5 + .../java/beans/ComponentSymbolProvider.java | 3 +- .../java/beans/NamedCompletionProvider.java | 68 ++++++ .../java/beans/NamedDefinitionProvider.java | 76 +++++++ .../java/beans/NamedReferencesProvider.java | 95 +++++++++ .../test/NamedCompletionProviderTest.java | 196 ++++++++++++++++++ .../test/NamedDefinitionProviderTest.java | 154 ++++++++++++++ .../test/NamedReferencesProviderTest.java | 140 +++++++++++++ 11 files changed, 744 insertions(+), 2 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedCompletionProvider.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedDefinitionProvider.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedReferencesProvider.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedCompletionProviderTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedDefinitionProviderTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedReferencesProviderTest.java diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java index 4fcb784e6..aa19143c2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java @@ -26,6 +26,7 @@ import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProcessor; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor; +import org.springframework.ide.vscode.boot.java.beans.NamedCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.ResourceCompletionProvider; @@ -127,6 +128,9 @@ public class BootJavaCompletionEngineConfigurer { providers.put(Annotations.RESOURCE_JAVAX, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("name", new ResourceCompletionProvider(springIndex)))); providers.put(Annotations.RESOURCE_JAKARTA, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("name", new ResourceCompletionProvider(springIndex)))); + providers.put(Annotations.NAMED_JAKARTA, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new NamedCompletionProvider(springIndex)))); + providers.put(Annotations.NAMED_JAVAX, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new NamedCompletionProvider(springIndex)))); + return new BootJavaCompletionEngine(cuCache, providers, snippetManager); } 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 8bd40d6e2..5e7e11c2e 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 @@ -46,6 +46,7 @@ import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDisc; import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid; import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler; import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvider; +import org.springframework.ide.vscode.boot.java.beans.NamedDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.ResourceDefinitionProvider; import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider; @@ -402,6 +403,7 @@ public class BootLanguageServerBootApp { new DependsOnDefinitionProvider(springIndex), new ResourceDefinitionProvider(springIndex), new QualifierDefinitionProvider(springIndex), + new NamedDefinitionProvider(springIndex), new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens))); } 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 a28d52091..b2aa8884b 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 @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.boot.java; import java.util.Map; +import java.util.Set; /** * Constants containing various fully-qualified annotation names. @@ -82,8 +83,10 @@ public class Annotations { public static final String INJECT_JAVAX = "javax.inject.Inject"; public static final String INJECT_JAKARTA = "jakarta.inject.Inject"; + public static final String NAMED_JAVAX = "javax.inject.Named"; public static final String NAMED_JAKARTA = "jakarta.inject.Named"; + public static final Set NAMED_ANNOTATIONS = Set.of(Annotations.NAMED_JAKARTA, Annotations.NAMED_JAVAX); public static final String SCHEDULED = "org.springframework.scheduling.annotation.Scheduled"; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index cee9d246d..b5f1199e4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -27,6 +27,7 @@ import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareLookup; import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; +import org.springframework.ide.vscode.boot.java.beans.NamedReferencesProvider; import org.springframework.ide.vscode.boot.java.beans.ProfileReferencesProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierReferencesProvider; import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider; @@ -308,9 +309,13 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent protected ReferencesHandler createReferenceHandler(SimpleLanguageServer server, JavaProjectFinder projectFinder, SpringMetamodelIndex index, SpringSymbolIndex symbolIndex, CompilationUnitCache cuCache) { + Map providers = new HashMap<>(); + providers.put(Annotations.VALUE, new ValuePropertyReferencesProvider(server)); providers.put(Annotations.QUALIFIER, new QualifierReferencesProvider(index, symbolIndex)); + providers.put(Annotations.NAMED_JAKARTA, new NamedReferencesProvider(index, symbolIndex)); + providers.put(Annotations.NAMED_JAVAX, new NamedReferencesProvider(index, symbolIndex)); providers.put(Annotations.PROFILE, new ProfileReferencesProvider(index, symbolIndex)); return new BootJavaReferencesHandler(this, cuCache, projectFinder, providers); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java index f1cbdc8e7..759356cd8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java @@ -48,7 +48,6 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; public class ComponentSymbolProvider extends AbstractSymbolProvider { private static final Logger log = LoggerFactory.getLogger(ComponentSymbolProvider.class); - private static final Set NAMED_ANNOTATIONS = Set.of(Annotations.NAMED_JAKARTA, Annotations.NAMED_JAVAX); @Override protected void addSymbolsPass1(Annotation node, ITypeBinding annotationType, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) { @@ -61,7 +60,7 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider { context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol)); context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition)); } - else if (NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName())) { + else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName())) { WorkspaceSymbol symbol = DefaultSymbolProvider.provideDefaultSymbol(node, doc); EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, null); context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol)); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedCompletionProvider.java new file mode 100644 index 000000000..895e27099 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedCompletionProvider.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) 2024 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.beans; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; + +/** + * @author Martin Lippert + */ +public class NamedCompletionProvider implements AnnotationAttributeCompletionProvider { + + private final SpringMetamodelIndex springIndex; + + public NamedCompletionProvider(SpringMetamodelIndex springIndex) { + this.springIndex = springIndex; + } + + @Override + public List getCompletionCandidates(IJavaProject project) { + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + + return Stream.concat( + findAllNamedValues(beans), + Arrays.stream(beans).map(bean -> bean.getName())) + .distinct() + .toList(); + } + + private Stream findAllNamedValues(Bean[] beans) { + + Stream qualifiersFromBeans = Arrays.stream(beans) + // annotations from beans themselves + .flatMap(bean -> Arrays.stream(bean.getAnnotations())) + .filter(annotation -> Annotations.NAMED_ANNOTATIONS.contains(annotation.getAnnotationType())) + .filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value") && annotation.getAttributes().get("value").length == 1) + .map(annotation -> annotation.getAttributes().get("value")[0]); + + Stream qualifiersFromInjectionPoints = Arrays.stream(beans) + // annotations from beans themselves + .filter(bean -> bean.getInjectionPoints() != null) + .flatMap(bean -> Arrays.stream(bean.getInjectionPoints())) + .filter(injectionPoint -> injectionPoint.getAnnotations() != null) + .flatMap(injectionPoint -> Arrays.stream(injectionPoint.getAnnotations())) + .filter(annotation -> Annotations.NAMED_ANNOTATIONS.contains(annotation.getAnnotationType())) + .filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value") && annotation.getAttributes().get("value").length == 1) + .map(annotation -> annotation.getAttributes().get("value")[0]); + + return Stream.concat(qualifiersFromBeans, qualifiersFromInjectionPoints); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedDefinitionProvider.java new file mode 100644 index 000000000..2f5fce370 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedDefinitionProvider.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2024 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.beans; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.core.dom.ASTNode; +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.StringLiteral; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +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.utils.ASTUtils; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; + +/** + * @author Martin Lippert + */ +public class NamedDefinitionProvider implements IJavaDefinitionProvider { + + private final SpringMetamodelIndex springIndex; + + public NamedDefinitionProvider(SpringMetamodelIndex springIndex) { + this.springIndex = springIndex; + } + + @Override + public List 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.NAMED_ANNOTATIONS.contains(binding.getAnnotationType().getQualifiedName())) { + String beanName = valueNode.getLiteralValue(); + + if (beanName != null && beanName.length() > 0) { + return findBeansWithName(project, beanName); + } + } + } + } + return Collections.emptyList(); + } + + private List 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()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedReferencesProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedReferencesProvider.java new file mode 100644 index 000000000..e83f279aa --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedReferencesProvider.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2024 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.beans; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; + +/** + * @author Martin Lippert + */ +public class NamedReferencesProvider implements ReferenceProvider { + + private final SpringMetamodelIndex springIndex; + private final SpringSymbolIndex symbolIndex; + + public NamedReferencesProvider(SpringMetamodelIndex springIndex, SpringSymbolIndex symbolIndex) { + this.springIndex = springIndex; + this.symbolIndex = symbolIndex; + } + + @Override + public List provideReferences(CancelChecker cancelToken, IJavaProject project, ASTNode node, Annotation annotation, ITypeBinding type, int offset) { + + cancelToken.checkCanceled(); + + try { + // case: @Value("prefix<*>") + if (node instanceof StringLiteral && node.getParent() instanceof Annotation) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + return provideReferences(project, ((StringLiteral) node).getLiteralValue()); + } + } + // case: @Value(value="prefix<*>") + else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair + && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + return provideReferences(project, ((StringLiteral) node).getLiteralValue()); + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + private List provideReferences(IJavaProject project, String value) { + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + + // beans with name + Stream beanLocations = Arrays.stream(beans) + .filter(bean -> bean.getName().equals(value)) + .map(bean -> bean.getLocation()); + + String exactPhrase1 = "@Named(\"" + value + "\")"; + String exactPhrase2 = "@Named(value=\"" + value + "\")"; + + // qualifier annotations + List qualifierSymbols1 = symbolIndex.getAllSymbols(exactPhrase1); + List qualifierSymbols2 = symbolIndex.getAllSymbols(exactPhrase2); + + Stream qualifierLocations = Stream.concat(qualifierSymbols1.stream(), qualifierSymbols2.stream()) + .filter(symbol -> symbol.getName().contains(exactPhrase1) || symbol.getName().contains(exactPhrase2)) + .map(symbol -> symbol.getLocation()) + .filter(location -> location.isLeft()) + .map(location -> location.getLeft()); + + return Stream.concat(qualifierLocations, beanLocations).toList(); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedCompletionProviderTest.java new file mode 100644 index 000000000..77c9ee251 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedCompletionProviderTest.java @@ -0,0 +1,196 @@ +/******************************************************************************* + * Copyright (c) 2024 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.beans.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.jupiter.api.AfterEach; +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.AnnotationMetadata; +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; + +/** + * @author Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class NamedCompletionProviderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringMetamodelIndex springIndex; + @Autowired private SpringSymbolIndex indexer; + + private File directory; + private IJavaProject project; + private Bean[] indexedBeans; + private String tempJavaDocUri; + private Bean bean1; + private Bean bean2; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-symbols-for-jakarta-javax/").toURI()); + + String projectDir = directory.toURI().toString(); + project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + + indexedBeans = springIndex.getBeansOfProject(project.getElementName()); + + tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString(); + AnnotationMetadata annotationBean1 = new AnnotationMetadata("jakarta.inject.Named", false, Map.of("value", new String[] {"named1"})); + AnnotationMetadata annotationBean2 = new AnnotationMetadata("jakarta.inject.Named", false, Map.of("value", new String[] {"named2"})); + + bean1 = new Bean("bean1", "type1", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {annotationBean1}); + bean2 = new Bean("bean2", "type2", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {annotationBean2}); + + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2}); + } + + @AfterEach + public void restoreIndexState() { + this.springIndex.updateBeans(project.getElementName(), indexedBeans); + } + + @Test + public void testQualifierCompletionWithoutQuotesWithoutPrefix() throws Exception { + assertCompletions("@Named(<*>)", new String[] {"named1", "named2", "bean1", "bean2"}, 0, "@Named(\"named1\"<*>)"); + } + + @Test + public void testQualifierCompletionWithoutQuotesWithPrefix() throws Exception { + assertCompletions("@Named(be<*>)", 2, "@Named(\"bean1\"<*>)"); + } + + @Test + public void testQualifierCompletionWithoutQuotesWithPrefixFromExistingQualifier() throws Exception { + assertCompletions("@Named(na<*>)", new String[] {"named1", "named2"}, 0, "@Named(\"named1\"<*>)"); + } + + @Test + public void testQualifierCompletionWithoutQuotesWithAttributeName() throws Exception { + assertCompletions("@Named(value=<*>)", 4, "@Named(value=\"named1\"<*>)"); + } + + @Test + public void testQualifierCompletionInsideOfQuotesWithoutPrefix() throws Exception { + assertCompletions("@Named(\"<*>\")", 4, "@Named(\"named1<*>\")"); + } + + @Test + public void testQualifierCompletionInsideOfQuotesWithPrefix() throws Exception { + assertCompletions("@Named(\"be<*>\")", 2, "@Named(\"bean1<*>\")"); + } + + @Test + public void testQualifierCompletionInsideOfQuotesWithPrefixButWithoutMatches() throws Exception { + assertCompletions("@Named(\"XXX<*>\")", 0, null); + } + + @Test + public void testQualifierCompletionOutsideOfAnnotation1() throws Exception { + assertCompletions("@Named(\"XXX\")<*>", 0, null); + } + + @Test + public void testQualifierCompletionOutsideOfAnnotation2() throws Exception { + assertCompletions("@Named<*>(\"XXX\")", 0, null); + } + + @Test + public void testQualifierCompletionInsideOfQuotesWithPrefixAndReplacedPostfix() throws Exception { + assertCompletions("@Named(\"be<*>xxx\")", 2, "@Named(\"bean1<*>\")"); + } + + private void assertCompletions(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { + assertCompletions(completionLine, noOfExpectedCompletions, null, 0, expectedCompletedLine); + } + + private void assertCompletions(String completionLine, String[] expectedCompletions, int chosenCompletion, String expectedCompletedLine) throws Exception { + assertCompletions(completionLine, expectedCompletions.length, expectedCompletions, chosenCompletion, expectedCompletedLine); + } + + private void assertCompletions(String completionLine, int noOfExcpectedCompletions, String[] expectedCompletions, int chosenCompletion, String expectedCompletedLine) throws Exception { + String editorContent = """ + package org.test; + + import jakarta.inject.Named; + + """ + + completionLine + "\n" + + """ + public class TestDependsOnClass { + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri); + + List completions = editor.getCompletions(); + assertEquals(noOfExcpectedCompletions, completions.size()); + + if (expectedCompletions != null) { + String[] completionItems = completions.stream() + .map(item -> item.getLabel()) + .toArray(size -> new String[size]); + + assertArrayEquals(expectedCompletions, completionItems); + } + + if (noOfExcpectedCompletions > 0) { + editor.apply(completions.get(chosenCompletion)); + assertEquals(""" + package org.test; + + import jakarta.inject.Named; + + """ + expectedCompletedLine + "\n" + + """ + public class TestDependsOnClass { + } + """, editor.getText()); + } + } + + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedDefinitionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedDefinitionProviderTest.java new file mode 100644 index 000000000..61c900ede --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedDefinitionProviderTest.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (c) 2024 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.beans.test; + +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.AnnotationMetadata; +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; + +/** + * @author Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class NamedDefinitionProviderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringMetamodelIndex springIndex; + @Autowired private SpringSymbolIndex indexer; + + private File directory; + private IJavaProject project; + private Bean bean1; + private Bean bean2; + + private String tempJavaDocUri1; + private String tempJavaDocUri2; + private String tempJavaDocUri; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-symbols-for-jakarta-javax/").toURI()); + + String projectDir = directory.toURI().toString(); + project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + + tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TestDependsOnClass.java").toUri().toString(); + tempJavaDocUri1 = directory.toPath().resolve("src/main/java/org/test/TempClass1.java").toUri().toString(); + tempJavaDocUri2 = directory.toPath().resolve("src/main/java/org/test/TempClass2.java").toUri().toString(); + + bean1 = new Bean("bean1", "type1", new Location(tempJavaDocUri1, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {}); + bean2 = new Bean("bean2", "type2", new Location(tempJavaDocUri2, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {}); + + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2}); + } + + @Test + public void testNamedClassRefersToBeanDefinitionLink() throws Exception { + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import jakarta.inject.Named; + + @Named("bean1") + public class TestDependsOnClass { + }""", tempJavaDocUri); + + Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1"); + assertEquals(1, beans.length); + + LocationLink expectedLocation = new LocationLink(tempJavaDocUri1, + beans[0].getLocation().getRange(), beans[0].getLocation().getRange(), + null); + + editor.assertLinkTargets("bean1", List.of(expectedLocation)); + } + + @Test + public void testNamedDependencyRefersToBeanDefinitionLink() throws Exception { + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.stereotype.Component; + import jakarta.inject.Named; + + @Component + public class TestDependsOnClass { + + public void setDependency(@Named("bean1") Object bean) { + } + + }""", tempJavaDocUri); + + Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1"); + assertEquals(1, beans.length); + + LocationLink expectedLocation = new LocationLink(tempJavaDocUri1, + beans[0].getLocation().getRange(), beans[0].getLocation().getRange(), + null); + + editor.assertLinkTargets("bean1", List.of(expectedLocation)); + } + + @Test + public void testNamedDependencyRefersToNonExistingBeanDefinitionLink() throws Exception { + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.stereotype.Component; + import jakarta.inject.Named; + + @Component + public class TestDependsOnClass { + + public void setDependency(@Named("XXX") Object bean1) { + } + + }""", tempJavaDocUri1); + + editor.assertNoLinkTargets("XXX"); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedReferencesProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedReferencesProviderTest.java new file mode 100644 index 000000000..8e38fbde2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/NamedReferencesProviderTest.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2024 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.beans.test; + +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.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.AnnotationMetadata; +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; + +/** + * @author Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class NamedReferencesProviderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringMetamodelIndex springIndex; + @Autowired private SpringSymbolIndex indexer; + + private File directory; + private IJavaProject project; + private Bean bean1; + private Bean bean2; + + private String tempJavaDocUri1; + private String tempJavaDocUri2; + private String tempJavaDocUri; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-symbols-for-jakarta-javax/").toURI()); + + String projectDir = directory.toURI().toString(); + project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + + tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TestDependsOnClass.java").toUri().toString(); + tempJavaDocUri1 = directory.toPath().resolve("src/main/java/org/test/TempClass1.java").toUri().toString(); + tempJavaDocUri2 = directory.toPath().resolve("src/main/java/org/test/TempClass2.java").toUri().toString(); + + bean1 = new Bean("bean1", "type1", new Location(tempJavaDocUri1, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {}); + bean2 = new Bean("bean2", "type2", new Location(tempJavaDocUri2, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {}); + + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2}); + } + + @Test + public void testNamedRefersToBean() throws Exception { + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import jakarta.inject.Named; + + @Named("be<*>an1") + public class TestDependsOnClass { + }""", tempJavaDocUri); + + Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1"); + assertEquals(1, beans.length); + + Location expectedLocation = new Location(tempJavaDocUri1, + beans[0].getLocation().getRange()); + + List references = editor.getReferences(); + assertEquals(1, references.size()); + + Location foundLocation = references.get(0); + assertEquals(expectedLocation, foundLocation); + } + + @Test + public void testNamedRefersToOtherNamedValues() throws Exception { + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import jakarta.inject.Named; + + @Named("specificFin<*>der") + public class TestDependsOnClass { + }""", tempJavaDocUri); + + String expectedDefinitionUri1 = directory.toPath().resolve("src/main/java/org/test/jakarta/SimpleMovieLister.java").toUri().toString(); + Location expectedLocation1 = new Location(expectedDefinitionUri1, + new Range(new Position(24, 38), new Position(24, 62))); + + String expectedDefinitionUri2 = directory.toPath().resolve("src/main/java/org/test/javax/SimpleMovieLister.java").toUri().toString(); + Location expectedLocation2 = new Location(expectedDefinitionUri2, + new Range(new Position(24, 38), new Position(24, 62))); + + List references = editor.getReferences(); + assertEquals(2, references.size()); + + Location foundLocation1 = references.get(1); + assertEquals(expectedLocation1, foundLocation1); + + Location foundLocation2 = references.get(0); + assertEquals(expectedLocation2, foundLocation2); + } + +}