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 8e04c7d84..e4ea59042 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 @@ -21,9 +21,14 @@ 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 org.springframework.ide.vscode.commons.util.text.DocumentRegion; +import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableList; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + public class BeanUtils { private static final String[] NAME_ATTRIBUTES = {"value", "name"}; @@ -40,10 +45,10 @@ public class BeanUtils { } public static Collection getBeanNamesFromBeanAnnotation(Annotation node) { - Collection beanNameNodes = getBeanNameLiterals(node); + Collection beanNameNodes = getBeanNameExpressions(node); if (beanNameNodes != null && !beanNameNodes.isEmpty()) { - return beanNameNodes.stream().map(nameNode -> ASTUtils.getLiteralValue(nameNode)) + return beanNameNodes.stream().map(nameNode -> ASTUtils.getExpressionValueAsString(nameNode, v -> {})) .toList(); } else { @@ -56,14 +61,33 @@ public class BeanUtils { } } - 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)); - }); + public static Collection> getBeanNamesFromBeanAnnotationWithRegions(Annotation node, TextDocument doc) { + Collection beanNameNodes = getBeanNameExpressions(node); + + if (beanNameNodes != null && !beanNameNodes.isEmpty()) { + ImmutableList.Builder> namesAndRegions = ImmutableList.builder(); + for (Expression nameNode : beanNameNodes) { + String name = ASTUtils.getExpressionValueAsString(nameNode, v -> {}); + + DocumentRegion region = ASTUtils.nodeRegion(doc, nameNode); + if (nameNode instanceof StringLiteral) { + region = new DocumentRegion(region.getDocument(), region.getStart() + 1, region.getEnd() - 1); + } + namesAndRegions.add(Tuples.of(name, region)); + } + return namesAndRegions.build(); + } + else { + ASTNode parent = node.getParent(); + if (parent instanceof MethodDeclaration) { + MethodDeclaration method = (MethodDeclaration) parent; + return ImmutableList.of(Tuples.of( + method.getName().toString(), + ASTUtils.nameRegion(doc, node) + )); + } + return ImmutableList.of(); } - return literals.build(); } public static String getBeanNameFromType(String typeName) { @@ -83,4 +107,14 @@ public class BeanUtils { return BeanUtils.getBeanNameFromType(beanName); } + private static Collection getBeanNameExpressions(Annotation node) { + ImmutableList.Builder literals = ImmutableList.builder(); + for (String attrib : NAME_ATTRIBUTES) { + ASTUtils.getAttribute(node, attrib).ifPresent((valueExp) -> { + literals.addAll(ASTUtils.expandExpressionsFromPotentialArray(valueExp)); + }); + } + return literals.build(); + } + } 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 5309b53fd..46c83cfa0 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 @@ -24,7 +24,6 @@ import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.Modifier; import org.eclipse.jdt.core.dom.ParameterizedType; -import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.Type; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Location; @@ -50,10 +49,7 @@ import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.DocumentRegion; import org.springframework.ide.vscode.commons.util.text.TextDocument; -import com.google.common.collect.ImmutableList; - import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; /** * @author Martin Lippert @@ -95,7 +91,7 @@ public class BeansSymbolProvider extends AbstractSymbolProvider { ITypeBinding beanType = getBeanType(method); String markerString = getAnnotations(method); - for (Tuple2 nameAndRegion : getBeanNames(node, doc)) { + for (Tuple2 nameAndRegion : BeanUtils.getBeanNamesFromBeanAnnotationWithRegions(node, doc)) { try { Location location = new Location(doc.getUri(), doc.toRange(nameAndRegion.getT2())); @@ -167,30 +163,6 @@ public class BeansSymbolProvider extends AbstractSymbolProvider { } } - protected Collection> getBeanNames(Annotation node, TextDocument doc) { - Collection beanNameNodes = BeanUtils.getBeanNameLiterals(node); - - if (beanNameNodes != null && !beanNameNodes.isEmpty()) { - ImmutableList.Builder> namesAndRegions = ImmutableList.builder(); - for (StringLiteral nameNode : beanNameNodes) { - String name = ASTUtils.getLiteralValue(nameNode); - namesAndRegions.add(Tuples.of(name, ASTUtils.stringRegion(doc, nameNode))); - } - return namesAndRegions.build(); - } - else { - ASTNode parent = node.getParent(); - if (parent instanceof MethodDeclaration) { - MethodDeclaration method = (MethodDeclaration) parent; - return ImmutableList.of(Tuples.of( - method.getName().toString(), - ASTUtils.nameRegion(doc, node) - )); - } - return ImmutableList.of(); - } - } - public static String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) { StringBuilder symbolLabel = new StringBuilder(); symbolLabel.append('@'); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index 718020a4b..169c8319e 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 @@ -106,29 +106,34 @@ public class ASTUtils { } public static Optional getAttribute(Annotation annotation, String name) { - if (annotation != null) { - try { - if (annotation.isSingleMemberAnnotation() && name.equals("value")) { - SingleMemberAnnotation sma = (SingleMemberAnnotation) annotation; - return Optional.ofNullable(sma.getValue()); - } else if (annotation.isNormalAnnotation()) { - NormalAnnotation na = (NormalAnnotation) annotation; - Object attributeObjs = na.getStructuralProperty(NormalAnnotation.VALUES_PROPERTY); - if (attributeObjs instanceof List) { - for (Object atrObj : (List)attributeObjs) { - if (atrObj instanceof MemberValuePair) { - MemberValuePair mvPair = (MemberValuePair) atrObj; - if (name.equals(mvPair.getName().getIdentifier())) { - return Optional.ofNullable(mvPair.getValue()); - } + if (annotation == null) { + return Optional.empty(); + } + + try { + + if (annotation.isSingleMemberAnnotation() && name.equals("value")) { + SingleMemberAnnotation sma = (SingleMemberAnnotation) annotation; + return Optional.ofNullable(sma.getValue()); + + } else if (annotation.isNormalAnnotation()) { + NormalAnnotation na = (NormalAnnotation) annotation; + Object attributeObjs = na.getStructuralProperty(NormalAnnotation.VALUES_PROPERTY); + if (attributeObjs instanceof List) { + for (Object atrObj : (List)attributeObjs) { + if (atrObj instanceof MemberValuePair) { + MemberValuePair mvPair = (MemberValuePair) atrObj; + if (name.equals(mvPair.getName().getIdentifier())) { + return Optional.ofNullable(mvPair.getValue()); } } } } - } catch (Exception e) { - log.error("", e); } + } catch (Exception e) { + log.error("", e); } + return Optional.empty(); } @@ -206,13 +211,17 @@ public class ASTUtils { if (exp instanceof StringLiteral) { return getLiteralValue((StringLiteral) exp); } else if (exp instanceof Name) { + IBinding binding = ((Name) exp).resolveBinding(); if (binding != null && binding.getKind() == IBinding.VARIABLE) { + IVariableBinding varBinding = (IVariableBinding) binding; + ITypeBinding klass = varBinding.getDeclaringClass(); - if (klass!=null) { + if (klass != null) { dependencies.accept(klass); } + Object constValue = varBinding.getConstantValue(); if (constValue != null) { return constValue.toString(); @@ -265,6 +274,15 @@ public class ASTUtils { return ImmutableList.of(); } + @SuppressWarnings("unchecked") + public static List expandExpressionsFromPotentialArray(Expression exp) { + if (exp instanceof ArrayInitializer array) { + return ((List)array.expressions()); + } + else { + return List.of(exp); + } + } public static Collection getAnnotations(TypeDeclaration typeDeclaration) { return getAnnotationsFromModifiers(typeDeclaration.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY)); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java index 2a9a32776..db78c45b4 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java @@ -90,20 +90,35 @@ public class SpringIndexerBeansTest { // @Bean("implicitNamedBean") SpringIndexerHarness.symbol("implicitNamedBean", "@+ 'implicitNamedBean' (@Bean) BeanClass"), + // @Bean("implicit" + "NamedBean") + SpringIndexerHarness.symbol("\"implicit\" + \"NamedBean\" + \"Concatenated\"", "@+ 'implicitNamedBeanConcatenated' (@Bean) BeanClass"), + // @Bean(value="valueBean") SpringIndexerHarness.symbol("valueBean", "@+ 'valueBean' (@Bean) BeanClass"), + // @Bean(value="value" + "Bean") + SpringIndexerHarness.symbol("\"valueBean\" + \"Concatenated\"", "@+ 'valueBeanConcatenated' (@Bean) BeanClass"), + // @Bean(value= {"valueBean1", "valueBean2"}) SpringIndexerHarness.symbol("valueBean1", "@+ 'valueBean1' (@Bean) BeanClass"), SpringIndexerHarness.symbol("valueBean2", "@+ 'valueBean2' (@Bean) BeanClass"), + // @Bean(value= {"value" + "Bean1" + "Concatenated", "valueBean2" + "Concatenated"}) + SpringIndexerHarness.symbol("\"value\" + \"Bean1\" + \"Concatenated\"", "@+ 'valueBean1Concatenated' (@Bean) BeanClass"), + SpringIndexerHarness.symbol("\"valueBean2\" + \"Concatenated\"", "@+ 'valueBean2Concatenated' (@Bean) BeanClass"), + // @Bean(name="namedBean") SpringIndexerHarness.symbol("namedBean", "@+ 'namedBean' (@Bean) BeanClass"), // @Bean(name= {"namedBean1", "namedBean2"}) SpringIndexerHarness.symbol("namedBean1", "@+ 'namedBean1' (@Bean) BeanClass"), - SpringIndexerHarness.symbol("namedBean2", "@+ 'namedBean2' (@Bean) BeanClass") - ); + SpringIndexerHarness.symbol("namedBean2", "@+ 'namedBean2' (@Bean) BeanClass"), + + // @Bean(name= {"named" + "Bean1" + "Concatenated", "named" + "Bean2" + Constants.SAMPLE_CONSTANT}) + SpringIndexerHarness.symbol("\"named\" + \"Bean1\" + \"Concatenated\"", "@+ 'namedBean1Concatenated' (@Bean) BeanClass"), + SpringIndexerHarness.symbol("\"named\" + \"Bean2\" + Constants.SAMPLE_CONSTANT", "@+ 'namedBean2SampleConstant' (@Bean) BeanClass") + + ); } @Test diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SpecialConfiguration.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SpecialConfiguration.java index c24beecae..dd0a76082 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SpecialConfiguration.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SpecialConfiguration.java @@ -11,16 +11,31 @@ public class SpecialConfiguration { return new BeanClass(); } + @Bean("implicit" + "NamedBean" + "Concatenated") + public BeanClass specialBeanWithStringConcatenation() { + return new BeanClass(); + } + @Bean(value="valueBean") public BeanClass specialBean2() { return new BeanClass(); } + @Bean(value="valueBean" + "Concatenated") + public BeanClass specialBean2WithStringConcatenation() { + return new BeanClass(); + } + @Bean(value= {"valueBean1", "valueBean2"}) public BeanClass specialBean3() { return new BeanClass(); } + @Bean(value= {"value" + "Bean1" + "Concatenated", "valueBean2" + "Concatenated"}) + public BeanClass specialBean3WithStringConcatenation() { + return new BeanClass(); + } + @Bean(name="namedBean") public BeanClass specialBean4() { return new BeanClass(); @@ -31,4 +46,9 @@ public class SpecialConfiguration { return new BeanClass(); } + @Bean(name= {"named" + "Bean1" + "Concatenated", "named" + "Bean2" + Constants.SAMPLE_CONSTANT}) + public BeanClass specialBean5WithConcatenationAndConstant() { + return new BeanClass(); + } + }