From 899b7372e4cce7e6a1f2cdb97849801bec437d6c Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Wed, 13 Nov 2024 15:48:12 +0100 Subject: [PATCH] GH-1405: take setter injection into account while indexing injection points --- .../ide/vscode/boot/java/utils/ASTUtils.java | 158 ++++++++++++------ .../test/SpringMetamodelIndexerBeansTest.java | 48 ++++++ .../test/NamedReferencesProviderTest.java | 16 +- .../injections/SetterInjectionService.java | 35 ++++ 4 files changed, 194 insertions(+), 63 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/SetterInjectionService.java 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 389ae80cf..5f1847153 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 @@ -412,6 +412,63 @@ public class ASTUtils { public static InjectionPoint[] findInjectionPoints(MethodDeclaration method, TextDocument doc) throws BadLocationException { List result = new ArrayList<>(); + findInjectionPoints(method, doc, result, false); + + return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS; + } + + public static InjectionPoint[] findInjectionPoints(TypeDeclaration type, TextDocument doc) throws BadLocationException { + List result = new ArrayList<>(); + + findInjectionPoints(type.getMethods(), doc, result); + findInjectionPoints(type.getFields(), doc, result); + + return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS; + } + + private static void findInjectionPoints(MethodDeclaration[] methods, TextDocument doc, List result) throws BadLocationException { + int constructorCount = 0; + + // special rule that if there is a single constructor, it doesn't have to have an autowired or inject annotation on it + MethodDeclaration singleConstructor = null; + + for (MethodDeclaration method : methods) { + if (method.isConstructor()) { + constructorCount++; + singleConstructor = method; + } + } + + if (constructorCount == 1) { + findInjectionPoints(singleConstructor, doc, result, false); + } + + // look for all methods with annotations (whether constructors or regular methods) + for (MethodDeclaration method : methods) { + findInjectionPoints(method, doc, result, true); + } + } + + public static void findInjectionPoints(MethodDeclaration method, TextDocument doc, List result, boolean checkForAnnotation) throws BadLocationException { + + Collection annotationsOnMethod = getAnnotations(method); + + if (checkForAnnotation) { + boolean isAutowired = false; + + for (Annotation annotation : annotationsOnMethod) { + String qualifiedName = annotation.resolveTypeBinding().getQualifiedName(); + if (Annotations.AUTOWIRED.equals(qualifiedName) + || Annotations.INJECT_JAVAX.equals(qualifiedName) + || Annotations.INJECT_JAKARTA.equals(qualifiedName)) { + isAutowired = true; + } + } + + if (!isAutowired) { + return; + } + } List parameters = method.parameters(); for (Object object : parameters) { @@ -427,70 +484,65 @@ public class ASTUtils { Location location = new Location(doc.getUri(), range); - AnnotationMetadata[] annotations = getAnnotationsMetadata(getAnnotations(variable), doc); + List allAnnotations = new ArrayList<>(); + allAnnotations.addAll(annotationsOnMethod); + allAnnotations.addAll(getAnnotations(variable)); + + AnnotationMetadata[] annotations = getAnnotationsMetadata(allAnnotations, doc); result.add(new InjectionPoint(name, type, location, annotations)); } } - - return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS; } - - public static InjectionPoint[] findInjectionPoints(TypeDeclaration type, TextDocument doc) throws BadLocationException { - List result = new ArrayList<>(); - MethodDeclaration[] methods = type.getMethods(); - for (MethodDeclaration method : methods) { - if (method.isConstructor()) { - result.addAll(Arrays.asList(ASTUtils.findInjectionPoints(method, doc))); + private static void findInjectionPoints(FieldDeclaration[] fields, TextDocument doc, List result) throws BadLocationException { + for (FieldDeclaration field : fields) { + findInjectionPoints(field, doc, result); + } + } + + private static void findInjectionPoints(FieldDeclaration field, TextDocument doc, List result) throws BadLocationException { + boolean autowiredField = false; + + List fieldAnnotations = new ArrayList<>(); + + List modifiers = field.modifiers(); + for (Object modifier : modifiers) { + if (modifier instanceof Annotation) { + Annotation annotation = (Annotation) modifier; + fieldAnnotations.add(annotation); + + String qualifiedName = annotation.resolveTypeBinding().getQualifiedName(); + if (Annotations.AUTOWIRED.equals(qualifiedName) + || Annotations.INJECT_JAVAX.equals(qualifiedName) + || Annotations.INJECT_JAKARTA.equals(qualifiedName) + || Annotations.VALUE.equals(qualifiedName)) { + autowiredField = true; + } } } - FieldDeclaration[] fields = type.getFields(); - for (FieldDeclaration field : fields) { - - boolean autowiredField = false; - - List fieldAnnotations = new ArrayList<>(); - - List modifiers = field.modifiers(); - for (Object modifier : modifiers) { - if (modifier instanceof Annotation) { - Annotation annotation = (Annotation) modifier; - fieldAnnotations.add(annotation); - - String qualifiedName = annotation.resolveTypeBinding().getQualifiedName(); - if (Annotations.AUTOWIRED.equals(qualifiedName) - || Annotations.INJECT_JAVAX.equals(qualifiedName) - || Annotations.INJECT_JAKARTA.equals(qualifiedName) - || Annotations.VALUE.equals(qualifiedName)) { - autowiredField = true; - } - } - } - - if (autowiredField) { - List fragments = field.fragments(); - for (Object fragment : fragments) { - if (fragment instanceof VariableDeclarationFragment) { - VariableDeclarationFragment varFragment = (VariableDeclarationFragment) fragment; - String fieldName = varFragment.getName().toString(); - - DocumentRegion region = ASTUtils.nodeRegion(doc, varFragment.getName()); - Range range = doc.toRange(region); - Location fieldLocation = new Location(doc.getUri(), range); - - String fieldType = field.getType().resolveBinding().getQualifiedName(); - - AnnotationMetadata[] annotationsMetadata = getAnnotationsMetadata(fieldAnnotations, doc); - - result.add(new InjectionPoint(fieldName, fieldType, fieldLocation, annotationsMetadata)); - } - } - } + if (!autowiredField) { + return; } - return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS; + List fragments = field.fragments(); + for (Object fragment : fragments) { + if (fragment instanceof VariableDeclarationFragment) { + VariableDeclarationFragment varFragment = (VariableDeclarationFragment) fragment; + String fieldName = varFragment.getName().toString(); + + DocumentRegion region = ASTUtils.nodeRegion(doc, varFragment.getName()); + Range range = doc.toRange(region); + Location fieldLocation = new Location(doc.getUri(), range); + + String fieldType = field.getType().resolveBinding().getQualifiedName(); + + AnnotationMetadata[] annotationsMetadata = getAnnotationsMetadata(fieldAnnotations, doc); + + result.add(new InjectionPoint(fieldName, fieldType, fieldLocation, annotationsMetadata)); + } + } } public static AnnotationMetadata[] getAnnotationsMetadata(Collection annotations, TextDocument doc) { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java index bebafeafb..0aa970246 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java @@ -35,6 +35,7 @@ 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.boot.java.Annotations; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.protocol.spring.AnnotationAttributeValue; import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; @@ -186,6 +187,53 @@ public class SpringMetamodelIndexerBeansTest { assertEquals(ip2Location, injectionPoints[1].getLocation()); } + @Test + void testSetterInjectionPointsFromConstructor() { + Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "setterInjectionService"); + assertEquals(1, beans.length); + + String docUri = directory.toPath().resolve("src/main/java/org/test/injections/SetterInjectionService.java").toUri().toString(); + + InjectionPoint[] injectionPoints = beans[0].getInjectionPoints(); + assertEquals(2, injectionPoints.length); + + assertEquals("bean1", injectionPoints[0].getName()); + assertEquals("org.test.BeanClass1", injectionPoints[0].getType()); + assertEquals(new Location(docUri, new Range(new Position(21, 33), new Position(21, 38))), injectionPoints[0].getLocation()); + + AnnotationMetadata[] point1Annotations = injectionPoints[0].getAnnotations(); + assertEquals(2, point1Annotations.length); + + assertEquals(Annotations.AUTOWIRED, point1Annotations[0].getAnnotationType()); + assertEquals(new Location(docUri, new Range(new Position(19, 1), new Position(19, 11))), point1Annotations[0].getLocation()); + assertEquals(0, point1Annotations[0].getAttributes().size()); + + assertEquals(Annotations.QUALIFIER, point1Annotations[1].getAnnotationType()); + assertEquals(new Location(docUri, new Range(new Position(20, 1), new Position(20, 41))), point1Annotations[1].getLocation()); + assertEquals(1, point1Annotations[1].getAttributes().size()); + assertEquals(1, point1Annotations[1].getAttributes().get("value").length); + assertEquals("setter-injection-qualifier", point1Annotations[1].getAttributes().get("value")[0].getName()); + assertEquals(new Location(docUri, new Range(new Position(20, 12), new Position(20, 40))), point1Annotations[1].getAttributes().get("value")[0].getLocation()); + + assertEquals("bean2", injectionPoints[1].getName()); + assertEquals("org.test.BeanClass2", injectionPoints[1].getType()); + assertEquals(new Location(docUri, new Range(new Position(26, 83), new Position(26, 88))), injectionPoints[1].getLocation()); + + AnnotationMetadata[] point2Annotations = injectionPoints[1].getAnnotations(); + assertEquals(2, point2Annotations.length); + assertEquals(Annotations.AUTOWIRED, point2Annotations[0].getAnnotationType()); + assertEquals(new Location(docUri, new Range(new Position(25, 1), new Position(25, 11))), point2Annotations[0].getLocation()); + assertEquals(0, point2Annotations[0].getAttributes().size()); + + assertEquals(Annotations.QUALIFIER, point2Annotations[1].getAnnotationType()); + assertEquals(new Location(docUri, new Range(new Position(26, 22), new Position(26, 71))), point2Annotations[1].getLocation()); + assertEquals(1, point2Annotations[1].getAttributes().size()); + assertEquals(1, point2Annotations[1].getAttributes().get("value").length); + assertEquals("setter-injection-qualifier-on-param", point2Annotations[1].getAttributes().get("value")[0].getName()); + assertEquals(new Location(docUri, new Range(new Position(26, 33), new Position(26, 70))), point2Annotations[1].getAttributes().get("value")[0].getLocation()); + + } + @Test void testBeanInjectionPointsFromAutowiredFields() { Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "autowiredInjectionService"); 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 index 774c662e5..2059bffdf 100644 --- 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 @@ -18,12 +18,12 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.ArrayUtils; 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.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -58,10 +58,8 @@ public class NamedReferencesProviderTest { private File directory; private IJavaProject project; private Bean bean1; - private Bean bean2; private String tempJavaDocUri1; - private String tempJavaDocUri2; private String tempJavaDocUri; @BeforeEach @@ -78,12 +76,11 @@ public class NamedReferencesProviderTest { 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}); + + Bean[] beans = ArrayUtils.add(springIndex.getBeansOfProject(project.getElementName()), bean1); + springIndex.updateBeans(project.getElementName(), beans); } @Test @@ -111,7 +108,6 @@ public class NamedReferencesProviderTest { } @Test - @Disabled // TODO: need to include setter injection in spring index as a first step, then resurrect this test case public void testNamedRefersToOtherNamedValues() throws Exception { Editor editor = harness.newEditor(LanguageId.JAVA, """ package org.test; @@ -124,11 +120,11 @@ public class NamedReferencesProviderTest { 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))); + new Range(new Position(27, 45), new Position(27, 61))); 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))); + new Range(new Position(27, 45), new Position(27, 61))); List references = editor.getReferences(); assertEquals(2, references.size()); diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/SetterInjectionService.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/SetterInjectionService.java new file mode 100644 index 000000000..0d0496d4f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/SetterInjectionService.java @@ -0,0 +1,35 @@ +package org.test.injections; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.test.BeanClass1; +import org.test.BeanClass2; + +@Service +public class SetterInjectionService { + + private BeanClass1 bean1; + private BeanClass2 bean2; + + private BeanClass1 somethingElse; + + public SetterInjectionService() { + } + + @Autowired + @Qualifier("setter-injection-qualifier") + public void setBean1(BeanClass1 bean1) { + this.bean1 = bean1; + } + + @Autowired + public void setBean2(@Qualifier("setter-injection-qualifier-on-param") BeanClass2 bean2) { + this.bean2 = bean2; + } + + public void setSomethingElse(BeanClass1 somethingElseNotInjected) { + this.somethingElse = somethingElseNotInjected; + } + +}