GH-1429: do not include bean names from the same method or component

Fixes GH-1429
This commit is contained in:
Martin Lippert
2025-01-22 10:12:48 +01:00
parent 2a11dbf58f
commit 2d86e093b0
6 changed files with 263 additions and 32 deletions

View File

@@ -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))) {

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}