diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanUtils.java index 6a85ca76f..bc6b75498 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Pivotal, Inc. + * Copyright (c) 2018, 2025 Pivotal, 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 @@ -10,9 +10,61 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.beans; +import java.util.Collection; +import java.util.Optional; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.util.StringUtil; +import com.google.common.collect.ImmutableList; + public class BeanUtils { + + private static final String[] NAME_ATTRIBUTES = {"value", "name"}; + + public static String getBeanNameFromComponentAnnotation(Annotation annotation, TypeDeclaration type) { + Optional attribute = ASTUtils.getAttribute(annotation, "value"); + if (attribute.isPresent()) { + return ASTUtils.getExpressionValueAsString(attribute.get(), (a) -> {}); + } + else { + String beanName = type.getName().toString(); + return BeanUtils.getBeanNameFromType(beanName); + } + } + + public static Collection getBeanNamesFromBeanAnnotation(Annotation node) { + Collection beanNameNodes = getBeanNameLiterals(node); + + if (beanNameNodes != null && !beanNameNodes.isEmpty()) { + return beanNameNodes.stream().map(nameNode -> ASTUtils.getLiteralValue(nameNode)) + .toList(); + } + else { + ASTNode parent = node.getParent(); + if (parent instanceof MethodDeclaration) { + MethodDeclaration method = (MethodDeclaration) parent; + return ImmutableList.of(method.getName().toString()); + } + return ImmutableList.of(); + } + } + + public static Collection getBeanNameLiterals(Annotation node) { + ImmutableList.Builder literals = ImmutableList.builder(); + for (String attrib : NAME_ATTRIBUTES) { + ASTUtils.getAttribute(node, attrib).ifPresent((valueExp) -> { + literals.addAll(ASTUtils.getExpressionValueAsListOfLiterals(valueExp)); + }); + } + return literals.build(); + } public static String getBeanNameFromType(String typeName) { if (StringUtil.hasText(typeName) && typeName.length() > 0 && Character.isUpperCase(typeName.charAt(0))) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java index dd8efdf31..e43b59644 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java @@ -63,7 +63,6 @@ import reactor.util.function.Tuples; public class BeansSymbolProvider extends AbstractSymbolProvider { private static final Logger log = LoggerFactory.getLogger(BeansSymbolProvider.class); - private static final String[] NAME_ATTRIBUTES = {"value", "name"}; @Override public void addSymbols(Annotation node, ITypeBinding typeBinding, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) { @@ -165,7 +164,7 @@ public class BeansSymbolProvider extends AbstractSymbolProvider { } protected Collection> getBeanNames(Annotation node, TextDocument doc) { - Collection beanNameNodes = getBeanNameLiterals(node); + Collection beanNameNodes = BeanUtils.getBeanNameLiterals(node); if (beanNameNodes != null && !beanNameNodes.isEmpty()) { ImmutableList.Builder> namesAndRegions = ImmutableList.builder(); @@ -204,16 +203,6 @@ public class BeansSymbolProvider extends AbstractSymbolProvider { return symbolLabel.toString(); } - protected Collection getBeanNameLiterals(Annotation node) { - ImmutableList.Builder literals = ImmutableList.builder(); - for (String attrib : NAME_ATTRIBUTES) { - ASTUtils.getAttribute(node, attrib).ifPresent((valueExp) -> { - literals.addAll(ASTUtils.getExpressionValueAsListOfLiterals(valueExp)); - }); - } - return literals.build(); - } - protected ITypeBinding getBeanType(MethodDeclaration method) { return method.getReturnType2().resolveBinding(); } 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 f34dbee69..2ef353afd 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 @@ -13,13 +13,11 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jdt.core.dom.Annotation; -import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Location; @@ -82,7 +80,7 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider { TypeDeclaration type = (TypeDeclaration) node.getParent(); - String beanName = getBeanName(node, type); + String beanName = BeanUtils.getBeanNameFromComponentAnnotation(node, type); ITypeBinding beanType = getBeanType(type); Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength())); @@ -140,18 +138,6 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider { return symbolLabel.toString(); } - public static String getBeanName(Annotation annotation, TypeDeclaration type) { - Optional attribute = ASTUtils.getAttribute(annotation, "value"); - if (attribute.isPresent()) { - return ASTUtils.getExpressionValueAsString(attribute.get(), (a) -> {}); - } - else { - String beanName = type.getName().toString(); - return BeanUtils.getBeanNameFromType(beanName); - } - - } - private ITypeBinding getBeanType(TypeDeclaration type) { return type.resolveBinding(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java index a86633db0..aeed3a827 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Broadcom + * Copyright (c) 2024, 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 @@ -11,13 +11,19 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; +import java.util.Collection; 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.MethodDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; 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.boot.java.annotations.AnnotationAttributeProposal; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.java.IJavaProject; /** @@ -239,13 +245,28 @@ public class DependsOnCompletionProcessor implements AnnotationAttributeCompleti @Override public List getCompletionCandidates(IJavaProject project, ASTNode node) { + Collection beanNameFromCodeElement = getBeanNameFromSourceCodePosition(node); + return Arrays.stream(this.springIndex.getBeansOfProject(project.getElementName())) .map(bean -> bean.getName()) + .filter(beanName -> !beanNameFromCodeElement.contains(beanName)) .distinct() .map(beanName -> new AnnotationAttributeProposal(beanName)) .collect(Collectors.toList()); } - + private Collection getBeanNameFromSourceCodePosition(ASTNode node) { + ASTNode parent = node.getParent(); + if (parent instanceof MethodDeclaration method) { + Annotation beanAnnotation = ASTUtils.getBeanAnnotation(method); + return BeanUtils.getBeanNamesFromBeanAnnotation(beanAnnotation); + } + else if (parent instanceof TypeDeclaration type) { + Annotation componentAnnotation = ASTUtils.getAnnotation(type, Annotations.COMPONENT); + return List.of(BeanUtils.getBeanNameFromComponentAnnotation(componentAnnotation, type)); + } + + return List.of(); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index 610341272..c0d484f5e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2024 Pivotal, Inc. + * Copyright (c) 2017, 2025 Pivotal, 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 @@ -24,6 +24,7 @@ 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.ArrayInitializer; +import org.eclipse.jdt.core.dom.BodyDeclaration; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.FieldDeclaration; @@ -320,6 +321,10 @@ public class ASTUtils { } public static Annotation getBeanAnnotation(MethodDeclaration method) { + return getAnnotation(method, Annotations.BEAN); + } + + public static Annotation getAnnotation(BodyDeclaration method, String annotationType) { List modifiers = method.modifiers(); for (Object modifier : modifiers) { if (modifier instanceof Annotation) { @@ -327,7 +332,7 @@ public class ASTUtils { ITypeBinding typeBinding = annotation.resolveTypeBinding(); if (typeBinding != null) { String fqName = typeBinding.getQualifiedName(); - if (Annotations.BEAN.equals(fqName)) { + if (annotationType.equals(fqName)) { return annotation; } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java index d12683da2..61bc974f6 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java @@ -92,6 +92,11 @@ public class DependsOnCompletionProviderTest { assertCompletions("@DependsOn(<*>)", 2, "@DependsOn(\"bean1\"<*>)"); } + @Test + public void testDependsOnCompletionWithoutQuotesWithoutPrefixOnBeanMethod() throws Exception { + assertCompletionsOnBeanMethod("@DependsOn(<*>)", 2, "@DependsOn(\"bean1\"<*>)"); + } + // TODO: not yet working, needs more groundwork due to the parser skipping these non-valid parts of the AST // @Test // public void testDependsOnCompletionWithoutQuotesWithoutPrefixWithoutClosingBracket() throws Exception { @@ -202,6 +207,38 @@ public class DependsOnCompletionProviderTest { assertCompletions("@DependsOn({\"bean1\",<*>\"bean2\"})", 1, "@DependsOn({\"bean1\",\"bean3\",<*>\"bean2\"})"); } + @Test + public void testDependsOnCompletionExcludeDefaultBeanNameFromComponent() throws Exception { + Bean componentBean = new Bean("testDependsOnClass", "org.test.TestDependsOnClass", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false); + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, componentBean}); + + assertCompletions("@DependsOn(<*>)", 2, "@DependsOn(\"bean1\"<*>)"); + } + + @Test + public void testDependsOnCompletionExcludeExplicitBeanNameFromComponent() throws Exception { + Bean componentBeanWithName = new Bean("explicitBeanName", "org.test.TestDependsOnClass", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false); + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, componentBeanWithName}); + + assertCompletionsWithComponentBeanName("@DependsOn(<*>)", 2, "@DependsOn(\"bean1\"<*>)"); + } + + @Test + public void testDependsOnCompletionExcludeDefaultBeanNameFromBeanMethod() throws Exception { + Bean beanFromMethod = new Bean("beanFromMethod", "org.test.TestDependsOnClass", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false); + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, beanFromMethod}); + + assertCompletionsOnBeanMethod("@DependsOn(<*>)", 2, "@DependsOn(\"bean1\"<*>)"); + } + + @Test + public void testDependsOnCompletionExcludeExplicitBeanNameFromBeanMethod() throws Exception { + Bean beanFromMethodWithName = new Bean("beanFromMethodWithName", "org.test.TestDependsOnClass", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false); + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, beanFromMethodWithName}); + + assertCompletionsOnBeanMethodWithName("@DependsOn(<*>)", 2, "@DependsOn(\"bean1\"<*>)"); + } + private void assertCompletions(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { String editorContent = """ package org.test; @@ -239,5 +276,146 @@ public class DependsOnCompletionProviderTest { } } + private void assertCompletionsWithComponentBeanName(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { + String editorContent = """ + package org.test; + + import org.springframework.stereotype.Component; + import org.springframework.context.annotation.DependsOn; + + @Component("explicitBeanName") + """ + + completionLine + "\n" + + """ + public class TestDependsOnClass { + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri); + + List completions = editor.getCompletions(); + assertEquals(noOfExpectedCompletions, completions.size()); + + if (noOfExpectedCompletions > 0) { + editor.apply(completions.get(0)); + assertEquals(""" + package org.test; + + import org.springframework.stereotype.Component; + import org.springframework.context.annotation.DependsOn; + + @Component("explicitBeanName") + """ + expectedCompletedLine + "\n" + + """ + public class TestDependsOnClass { + } + """, editor.getText()); + } + } + + private void assertCompletionsOnBeanMethod(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { + String editorContent = """ + package org.test; + + import org.springframework.context.annotation.DependsOn; + import org.springframework.context.annotation.Configuration; + import org.springframework.context.annotation.Bean; + + @Configuration + public class TestDependsOnClass { + + @Bean + """ + + completionLine + "\n" + + """ + public Object beanFromMethod() { + return new Object(); + } + + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri); + + List completions = editor.getCompletions(); + assertEquals(noOfExpectedCompletions, completions.size()); + + if (noOfExpectedCompletions > 0) { + editor.apply(completions.get(0)); + assertEquals(""" + package org.test; + + import org.springframework.context.annotation.DependsOn; + import org.springframework.context.annotation.Configuration; + import org.springframework.context.annotation.Bean; + + @Configuration + public class TestDependsOnClass { + + @Bean + """ + + expectedCompletedLine + "\n" + + """ + public Object beanFromMethod() { + return new Object(); + } + + } + """, editor.getText()); + } + } + + private void assertCompletionsOnBeanMethodWithName(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { + String editorContent = """ + package org.test; + + import org.springframework.context.annotation.DependsOn; + import org.springframework.context.annotation.Configuration; + import org.springframework.context.annotation.Bean; + + @Configuration + public class TestDependsOnClass { + + @Bean("beanFromMethodWithName") + """ + + completionLine + "\n" + + """ + public Object beanFromMethod() { + return new Object(); + } + + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri); + + List completions = editor.getCompletions(); + assertEquals(noOfExpectedCompletions, completions.size()); + + if (noOfExpectedCompletions > 0) { + editor.apply(completions.get(0)); + assertEquals(""" + package org.test; + + import org.springframework.context.annotation.DependsOn; + import org.springframework.context.annotation.Configuration; + import org.springframework.context.annotation.Bean; + + @Configuration + public class TestDependsOnClass { + + @Bean("beanFromMethodWithName") + """ + + expectedCompletedLine + "\n" + + """ + public Object beanFromMethod() { + return new Object(); + } + + } + """, editor.getText()); + } + } + }