Skip to content

Commit 613bab4

Browse files
committed
GH-1494: record types now supported in configuration properties indexing
1 parent 980f392 commit 613bab4

File tree

7 files changed

+186
-18
lines changed

7 files changed

+186
-18
lines changed

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanUtils.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Optional;
1515

1616
import org.eclipse.jdt.core.dom.ASTNode;
17+
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
1718
import org.eclipse.jdt.core.dom.Annotation;
1819
import org.eclipse.jdt.core.dom.Expression;
1920
import org.eclipse.jdt.core.dom.MethodDeclaration;
@@ -33,7 +34,7 @@ public class BeanUtils {
3334

3435
private static final String[] NAME_ATTRIBUTES = {"value", "name"};
3536

36-
public static String getBeanNameFromComponentAnnotation(Annotation annotation, TypeDeclaration type) {
37+
public static String getBeanNameFromComponentAnnotation(Annotation annotation, AbstractTypeDeclaration type) {
3738
Optional<Expression> attribute = ASTUtils.getAttribute(annotation, "value");
3839
if (attribute.isPresent()) {
3940
return ASTUtils.getExpressionValueAsString(attribute.get(), (a) -> {});

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java

+51-6
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
import java.util.stream.Stream;
2020

2121
import org.eclipse.jdt.core.dom.ASTVisitor;
22+
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
2223
import org.eclipse.jdt.core.dom.Annotation;
2324
import org.eclipse.jdt.core.dom.Expression;
2425
import org.eclipse.jdt.core.dom.IMethodBinding;
2526
import org.eclipse.jdt.core.dom.ITypeBinding;
2627
import org.eclipse.jdt.core.dom.MethodDeclaration;
2728
import org.eclipse.jdt.core.dom.MethodInvocation;
29+
import org.eclipse.jdt.core.dom.RecordDeclaration;
2830
import org.eclipse.jdt.core.dom.TypeDeclaration;
2931
import org.eclipse.lsp4j.Location;
3032
import org.eclipse.lsp4j.SymbolKind;
@@ -46,6 +48,7 @@
4648
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
4749
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
4850
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
51+
import org.springframework.ide.vscode.commons.protocol.spring.DefaultValues;
4952
import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint;
5053
import org.springframework.ide.vscode.commons.protocol.spring.SimpleSymbolElement;
5154
import org.springframework.ide.vscode.commons.util.BadLocationException;
@@ -63,8 +66,11 @@ public class ComponentSymbolProvider implements SymbolProvider {
6366
@Override
6467
public void addSymbols(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
6568
try {
66-
if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration) {
67-
createSymbol(node, annotationType, metaAnnotations, context, doc);
69+
if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration type) {
70+
createSymbol(type, node, annotationType, metaAnnotations, context, doc);
71+
}
72+
else if (node != null && node.getParent() != null && node.getParent() instanceof RecordDeclaration record) {
73+
createSymbol(record, node, annotationType, metaAnnotations, context, doc);
6874
}
6975
else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName())) {
7076
WorkspaceSymbol symbol = DefaultSymbolProvider.provideDefaultSymbol(node, doc);
@@ -77,15 +83,13 @@ else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName(
7783
}
7884
}
7985

80-
protected void createSymbol(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
86+
private void createSymbol(TypeDeclaration type, Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
8187
String annotationTypeName = annotationType.getName();
8288

8389
Collection<String> metaAnnotationNames = metaAnnotations.stream()
8490
.map(ITypeBinding::getName)
8591
.collect(Collectors.toList());
8692

87-
TypeDeclaration type = (TypeDeclaration) node.getParent();
88-
8993
String beanName = BeanUtils.getBeanNameFromComponentAnnotation(node, type);
9094
ITypeBinding beanType = type.resolveBinding();
9195

@@ -141,7 +145,48 @@ protected void createSymbol(Annotation node, ITypeBinding annotationType, Collec
141145
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
142146
}
143147

144-
private void indexConfigurationProperties(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
148+
private void createSymbol(RecordDeclaration record, Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
149+
String annotationTypeName = annotationType.getName();
150+
151+
Collection<String> metaAnnotationNames = metaAnnotations.stream()
152+
.map(ITypeBinding::getName)
153+
.collect(Collectors.toList());
154+
155+
String beanName = BeanUtils.getBeanNameFromComponentAnnotation(node, record);
156+
ITypeBinding beanType = record.resolveBinding();
157+
158+
Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()));
159+
160+
WorkspaceSymbol symbol = new WorkspaceSymbol(
161+
beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, beanType.getName()), SymbolKind.Interface,
162+
Either.forLeft(location));
163+
164+
boolean isConfiguration = Annotations.CONFIGURATION.equals(annotationType.getQualifiedName())
165+
|| metaAnnotations.stream().anyMatch(t -> Annotations.CONFIGURATION.equals(t.getQualifiedName()));
166+
167+
InjectionPoint[] injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS;
168+
169+
Set<String> supertypes = new HashSet<>();
170+
ASTUtils.findSupertypes(beanType, supertypes);
171+
172+
Collection<Annotation> annotationsOnType = ASTUtils.getAnnotations(record);
173+
174+
AnnotationMetadata[] annotations = Stream.concat(
175+
Arrays.stream(ASTUtils.getAnnotationsMetadata(annotationsOnType, doc))
176+
,
177+
metaAnnotations.stream()
178+
.map(an -> new AnnotationMetadata(an.getQualifiedName(), true, null, null)))
179+
.toArray(AnnotationMetadata[]::new);
180+
181+
Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName());
182+
183+
indexConfigurationProperties(beanDefinition, record, context, doc);
184+
185+
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
186+
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
187+
}
188+
189+
private void indexConfigurationProperties(Bean beanDefinition, AbstractTypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
145190
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
146191

147192
if (annotationHierarchies.isAnnotatedWith(type.resolveBinding(), Annotations.CONFIGURATION_PROPERTIES)) {

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigurationPropertiesSymbolProvider.java

+49-9
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
import java.util.stream.Collectors;
1919
import java.util.stream.Stream;
2020

21+
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
2122
import org.eclipse.jdt.core.dom.Annotation;
2223
import org.eclipse.jdt.core.dom.FieldDeclaration;
2324
import org.eclipse.jdt.core.dom.ITypeBinding;
25+
import org.eclipse.jdt.core.dom.RecordDeclaration;
2426
import org.eclipse.jdt.core.dom.SimpleName;
27+
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
2528
import org.eclipse.jdt.core.dom.Type;
2629
import org.eclipse.jdt.core.dom.TypeDeclaration;
2730
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
@@ -55,44 +58,44 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
5558
@Override
5659
public void addSymbols(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
5760
try {
58-
if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration) {
59-
createSymbol(node, annotationType, metaAnnotations, context, doc);
61+
if (node != null && node.getParent() != null) {
62+
if (node.getParent() instanceof AbstractTypeDeclaration abstractType) {
63+
createSymbolForType(abstractType, node, annotationType, metaAnnotations, context, doc);
64+
}
6065
}
6166
}
6267
catch (BadLocationException e) {
6368
log.error("", e);
6469
}
6570
}
6671

67-
protected void createSymbol(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
72+
protected void createSymbolForType(AbstractTypeDeclaration type, Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
6873
String annotationTypeName = annotationType.getName();
6974

7075
Collection<String> metaAnnotationNames = metaAnnotations.stream()
7176
.map(ITypeBinding::getName)
7277
.collect(Collectors.toList());
7378

74-
TypeDeclaration type = (TypeDeclaration) node.getParent();
7579
ITypeBinding typeBinding = type.resolveBinding();
7680

7781
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
7882
boolean isComponentAnnotated = annotationHierarchies.isAnnotatedWith(typeBinding, Annotations.COMPONENT);
7983

8084
if (!isComponentAnnotated) {
8185
String beanName = BeanUtils.getBeanNameFromType(type.getName().getFullyQualifiedName());
82-
ITypeBinding beanType = type.resolveBinding();
8386

8487
Location location = new Location(doc.getUri(), doc.toRange(type.getStartPosition(), type.getLength()));
8588

8689
WorkspaceSymbol symbol = new WorkspaceSymbol(
87-
ComponentSymbolProvider.beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, beanType.getName()), SymbolKind.Interface,
90+
ComponentSymbolProvider.beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, typeBinding.getName()), SymbolKind.Interface,
8891
Either.forLeft(location));
8992

9093
boolean isConfiguration = false; // otherwise, the ComponentSymbolProvider takes care of the bean definiton for this type
9194

9295
InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(type, doc);
9396

9497
Set<String> supertypes = new HashSet<>();
95-
ASTUtils.findSupertypes(beanType, supertypes);
98+
ASTUtils.findSupertypes(typeBinding, supertypes);
9699

97100
Collection<Annotation> annotationsOnType = ASTUtils.getAnnotations(type);
98101

@@ -103,7 +106,7 @@ protected void createSymbol(Annotation node, ITypeBinding annotationType, Collec
103106
.map(an -> new AnnotationMetadata(an.getQualifiedName(), true, null, null)))
104107
.toArray(AnnotationMetadata[]::new);
105108

106-
Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName());
109+
Bean beanDefinition = new Bean(beanName, typeBinding.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName());
107110

108111
indexConfigurationProperties(beanDefinition, type, context, doc);
109112

@@ -112,7 +115,16 @@ protected void createSymbol(Annotation node, ITypeBinding annotationType, Collec
112115
}
113116
}
114117

115-
public static void indexConfigurationProperties(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
118+
public static void indexConfigurationProperties(Bean beanDefinition, AbstractTypeDeclaration abstractType, SpringIndexerJavaContext context, TextDocument doc) {
119+
if (abstractType instanceof TypeDeclaration type) {
120+
indexConfigurationPropertiesForType(beanDefinition, type, context, doc);
121+
}
122+
else if (abstractType instanceof RecordDeclaration record) {
123+
indexConfigurationPropertiesForRecord(beanDefinition, record, context, doc);
124+
}
125+
}
126+
127+
public static void indexConfigurationPropertiesForType(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
116128

117129
FieldDeclaration[] fields = type.getFields();
118130
if (fields != null) {
@@ -145,4 +157,32 @@ public static void indexConfigurationProperties(Bean beanDefinition, TypeDeclara
145157

146158
}
147159

160+
public static void indexConfigurationPropertiesForRecord(Bean beanDefinition, RecordDeclaration record, SpringIndexerJavaContext context, TextDocument doc) {
161+
162+
@SuppressWarnings("unchecked")
163+
List<SingleVariableDeclaration> fields = record.recordComponents();
164+
165+
if (fields != null) {
166+
for (SingleVariableDeclaration field : fields) {
167+
try {
168+
Type fieldType = field.getType();
169+
if (fieldType != null) {
170+
171+
SimpleName name = field.getName();
172+
if (name != null) {
173+
174+
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, field);
175+
Range range = doc.toRange(nodeRegion);
176+
ConfigPropertyIndexElement configPropElement = new ConfigPropertyIndexElement(name.getFullyQualifiedName(), fieldType.resolveBinding().getQualifiedName(), range);
177+
178+
beanDefinition.addChild(configPropElement);
179+
}
180+
}
181+
} catch (BadLocationException e) {
182+
log.error("error identifying config property field", e);
183+
}
184+
}
185+
}
186+
187+
}
148188
}

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.stream.Stream;
2323

2424
import org.eclipse.jdt.core.dom.ASTNode;
25+
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
2526
import org.eclipse.jdt.core.dom.Annotation;
2627
import org.eclipse.jdt.core.dom.ArrayInitializer;
2728
import org.eclipse.jdt.core.dom.BodyDeclaration;
@@ -39,6 +40,7 @@
3940
import org.eclipse.jdt.core.dom.Name;
4041
import org.eclipse.jdt.core.dom.NormalAnnotation;
4142
import org.eclipse.jdt.core.dom.QualifiedName;
43+
import org.eclipse.jdt.core.dom.RecordDeclaration;
4244
import org.eclipse.jdt.core.dom.SimpleName;
4345
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
4446
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
@@ -284,10 +286,26 @@ public static List<Expression> expandExpressionsFromPotentialArray(Expression ex
284286
}
285287
}
286288

289+
public static Collection<Annotation> getAnnotations(AbstractTypeDeclaration abstractTypeDeclaration) {
290+
if (abstractTypeDeclaration instanceof TypeDeclaration typeDeclaration) {
291+
return getAnnotations(typeDeclaration);
292+
}
293+
else if (abstractTypeDeclaration instanceof RecordDeclaration recordDeclaration) {
294+
return getAnnotations(recordDeclaration);
295+
}
296+
else {
297+
return null;
298+
}
299+
}
300+
287301
public static Collection<Annotation> getAnnotations(TypeDeclaration typeDeclaration) {
288302
return getAnnotationsFromModifiers(typeDeclaration.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY));
289303
}
290304

305+
public static Collection<Annotation> getAnnotations(RecordDeclaration recordDeclaration) {
306+
return getAnnotationsFromModifiers(recordDeclaration.getStructuralProperty(RecordDeclaration.MODIFIERS2_PROPERTY));
307+
}
308+
291309
public static Collection<Annotation> getAnnotations(MethodDeclaration methodDeclaration) {
292310
return getAnnotationsFromModifiers(methodDeclaration.getStructuralProperty(MethodDeclaration.MODIFIERS2_PROPERTY));
293311
}
@@ -488,7 +506,16 @@ public static InjectionPoint[] findInjectionPoints(MethodDeclaration method, Tex
488506
return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS;
489507
}
490508

491-
public static InjectionPoint[] findInjectionPoints(TypeDeclaration type, TextDocument doc) throws BadLocationException {
509+
public static InjectionPoint[] findInjectionPoints(AbstractTypeDeclaration abstractType, TextDocument doc) throws BadLocationException {
510+
if (abstractType instanceof TypeDeclaration type) {
511+
return findInjectionPointsForType(type, doc);
512+
}
513+
else {
514+
return DefaultValues.EMPTY_INJECTION_POINTS;
515+
}
516+
}
517+
518+
public static InjectionPoint[] findInjectionPointsForType(TypeDeclaration type, TextDocument doc) throws BadLocationException {
492519
List<InjectionPoint> result = new ArrayList<>();
493520

494521
findInjectionPoints(type.getMethods(), doc, result);

Diff for: headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringIndexerConfigurationPropertiesTest.java

+47
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,51 @@ void testSimpleConfigPropertiesClassWithAdditionalConfigurationAnnotation() thro
102102
assertEquals("java.lang.String", configPropElement.getType());
103103
}
104104

105+
@Test
106+
void testSimpleConfigPropertiesRecord() throws Exception {
107+
String docUri = directory.toPath().resolve("src/main/java/com/example/configproperties/ConfigurationPropertiesWithRecords.java").toUri().toString();
108+
109+
Bean[] beans = springIndex.getBeansOfDocument(docUri);
110+
assertEquals(1, beans.length);
111+
112+
Bean configPropertiesComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("configurationPropertiesWithRecords")).findFirst().get();
113+
assertEquals("com.example.configproperties.ConfigurationPropertiesWithRecords", configPropertiesComponentBean.getType());
114+
115+
List<SpringIndexElement> children = configPropertiesComponentBean.getChildren();
116+
assertEquals(2, children.size());
117+
118+
ConfigPropertyIndexElement configPropElement1 = (ConfigPropertyIndexElement) children.get(0);
119+
assertEquals("name", configPropElement1.getName());
120+
assertEquals("java.lang.String", configPropElement1.getType());
121+
122+
ConfigPropertyIndexElement configPropElement2 = (ConfigPropertyIndexElement) children.get(1);
123+
assertEquals("duration", configPropElement2.getName());
124+
assertEquals("int", configPropElement2.getType());
125+
}
126+
127+
@Test
128+
void testSimpleConfigPropertiesRecordAndConfigurationAnnotation() throws Exception {
129+
String docUri = directory.toPath().resolve("src/main/java/com/example/configproperties/ConfigurationPropertiesWithRecordsAndConfigurationAnnotation.java").toUri().toString();
130+
131+
Bean[] beans = springIndex.getBeansOfDocument(docUri);
132+
assertEquals(1, beans.length);
133+
134+
Bean configPropertiesComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("configurationPropertiesWithRecordsAndConfigurationAnnotation")).findFirst().get();
135+
assertEquals("com.example.configproperties.ConfigurationPropertiesWithRecordsAndConfigurationAnnotation", configPropertiesComponentBean.getType());
136+
137+
List<SpringIndexElement> children = configPropertiesComponentBean.getChildren();
138+
assertEquals(2, children.size());
139+
140+
ConfigPropertyIndexElement configPropElement1 = (ConfigPropertyIndexElement) children.get(0);
141+
assertEquals("name", configPropElement1.getName());
142+
assertEquals("java.lang.String", configPropElement1.getType());
143+
144+
ConfigPropertyIndexElement configPropElement2 = (ConfigPropertyIndexElement) children.get(1);
145+
assertEquals("duration", configPropElement2.getName());
146+
assertEquals("int", configPropElement2.getType());
147+
148+
}
149+
150+
151+
105152
}

Diff for: headless-services/spring-boot-language-server/src/test/resources/test-projects/test-configuration-properties-indexing/src/main/java/com/example/configproperties/ConfigurationPropertiesExampleWithConfigurationAnnotation.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import org.springframework.context.annotation.Configuration;
55

66
@Configuration
7-
@ConfigurationProperties(prefix = "com.example.config.prefix.simple")
7+
@ConfigurationProperties(prefix = "com.example.config.prefix.simple2")
88
public class ConfigurationPropertiesExampleWithConfigurationAnnotation {
99

1010
private String simpleConfigProp = "default config value";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.example.configproperties;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
import org.springframework.context.annotation.Configuration;
5+
6+
@Configuration
7+
@ConfigurationProperties(prefix = "com.example.config.record.prefix2")
8+
public record ConfigurationPropertiesWithRecordsAndConfigurationAnnotation (String name, int duration) {}

0 commit comments

Comments
 (0)