GH-1429: do not include bean names from the same method or component
Fixes GH-1429
This commit is contained in:
@@ -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<Expression> 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<String> getBeanNamesFromBeanAnnotation(Annotation node) {
|
||||
Collection<StringLiteral> 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<StringLiteral> getBeanNameLiterals(Annotation node) {
|
||||
ImmutableList.Builder<StringLiteral> 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))) {
|
||||
|
||||
@@ -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<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
|
||||
@@ -165,7 +164,7 @@ public class BeansSymbolProvider extends AbstractSymbolProvider {
|
||||
}
|
||||
|
||||
protected Collection<Tuple2<String, DocumentRegion>> getBeanNames(Annotation node, TextDocument doc) {
|
||||
Collection<StringLiteral> beanNameNodes = getBeanNameLiterals(node);
|
||||
Collection<StringLiteral> beanNameNodes = BeanUtils.getBeanNameLiterals(node);
|
||||
|
||||
if (beanNameNodes != null && !beanNameNodes.isEmpty()) {
|
||||
ImmutableList.Builder<Tuple2<String,DocumentRegion>> namesAndRegions = ImmutableList.builder();
|
||||
@@ -204,16 +203,6 @@ public class BeansSymbolProvider extends AbstractSymbolProvider {
|
||||
return symbolLabel.toString();
|
||||
}
|
||||
|
||||
protected Collection<StringLiteral> getBeanNameLiterals(Annotation node) {
|
||||
ImmutableList.Builder<StringLiteral> 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();
|
||||
}
|
||||
|
||||
@@ -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<Expression> 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();
|
||||
}
|
||||
|
||||
@@ -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<AnnotationAttributeProposal> getCompletionCandidates(IJavaProject project, ASTNode node) {
|
||||
Collection<String> 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<String> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CompletionItem> 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<CompletionItem> 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<CompletionItem> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user