GH-1494: record types now supported in configuration properties indexing

This commit is contained in:
Martin Lippert
2025-03-10 14:06:07 +01:00
parent 980f3926ee
commit 613bab4c7a
7 changed files with 186 additions and 18 deletions

View File

@@ -14,6 +14,7 @@ import java.util.Collection;
import java.util.Optional;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
@@ -33,7 +34,7 @@ public class BeanUtils {
private static final String[] NAME_ATTRIBUTES = {"value", "name"};
public static String getBeanNameFromComponentAnnotation(Annotation annotation, TypeDeclaration type) {
public static String getBeanNameFromComponentAnnotation(Annotation annotation, AbstractTypeDeclaration type) {
Optional<Expression> attribute = ASTUtils.getAttribute(annotation, "value");
if (attribute.isPresent()) {
return ASTUtils.getExpressionValueAsString(attribute.get(), (a) -> {});

View File

@@ -19,12 +19,14 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.SymbolKind;
@@ -46,6 +48,7 @@ import org.springframework.ide.vscode.boot.java.utils.DefaultSymbolProvider;
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.DefaultValues;
import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint;
import org.springframework.ide.vscode.commons.protocol.spring.SimpleSymbolElement;
import org.springframework.ide.vscode.commons.util.BadLocationException;
@@ -63,8 +66,11 @@ public class ComponentSymbolProvider implements SymbolProvider {
@Override
public void addSymbols(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
try {
if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration) {
createSymbol(node, annotationType, metaAnnotations, context, doc);
if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration type) {
createSymbol(type, node, annotationType, metaAnnotations, context, doc);
}
else if (node != null && node.getParent() != null && node.getParent() instanceof RecordDeclaration record) {
createSymbol(record, node, annotationType, metaAnnotations, context, doc);
}
else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName())) {
WorkspaceSymbol symbol = DefaultSymbolProvider.provideDefaultSymbol(node, doc);
@@ -77,15 +83,13 @@ public class ComponentSymbolProvider implements SymbolProvider {
}
}
protected void createSymbol(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
private void createSymbol(TypeDeclaration type, Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
String annotationTypeName = annotationType.getName();
Collection<String> metaAnnotationNames = metaAnnotations.stream()
.map(ITypeBinding::getName)
.collect(Collectors.toList());
TypeDeclaration type = (TypeDeclaration) node.getParent();
String beanName = BeanUtils.getBeanNameFromComponentAnnotation(node, type);
ITypeBinding beanType = type.resolveBinding();
@@ -141,7 +145,48 @@ public class ComponentSymbolProvider implements SymbolProvider {
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
}
private void indexConfigurationProperties(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
private void createSymbol(RecordDeclaration record, Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
String annotationTypeName = annotationType.getName();
Collection<String> metaAnnotationNames = metaAnnotations.stream()
.map(ITypeBinding::getName)
.collect(Collectors.toList());
String beanName = BeanUtils.getBeanNameFromComponentAnnotation(node, record);
ITypeBinding beanType = record.resolveBinding();
Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()));
WorkspaceSymbol symbol = new WorkspaceSymbol(
beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, beanType.getName()), SymbolKind.Interface,
Either.forLeft(location));
boolean isConfiguration = Annotations.CONFIGURATION.equals(annotationType.getQualifiedName())
|| metaAnnotations.stream().anyMatch(t -> Annotations.CONFIGURATION.equals(t.getQualifiedName()));
InjectionPoint[] injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS;
Set<String> supertypes = new HashSet<>();
ASTUtils.findSupertypes(beanType, supertypes);
Collection<Annotation> annotationsOnType = ASTUtils.getAnnotations(record);
AnnotationMetadata[] annotations = Stream.concat(
Arrays.stream(ASTUtils.getAnnotationsMetadata(annotationsOnType, doc))
,
metaAnnotations.stream()
.map(an -> new AnnotationMetadata(an.getQualifiedName(), true, null, null)))
.toArray(AnnotationMetadata[]::new);
Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName());
indexConfigurationProperties(beanDefinition, record, context, doc);
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
}
private void indexConfigurationProperties(Bean beanDefinition, AbstractTypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
if (annotationHierarchies.isAnnotatedWith(type.resolveBinding(), Annotations.CONFIGURATION_PROPERTIES)) {

View File

@@ -18,10 +18,13 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
@@ -55,8 +58,10 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
@Override
public void addSymbols(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
try {
if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration) {
createSymbol(node, annotationType, metaAnnotations, context, doc);
if (node != null && node.getParent() != null) {
if (node.getParent() instanceof AbstractTypeDeclaration abstractType) {
createSymbolForType(abstractType, node, annotationType, metaAnnotations, context, doc);
}
}
}
catch (BadLocationException e) {
@@ -64,14 +69,13 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
}
}
protected void createSymbol(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
protected void createSymbolForType(AbstractTypeDeclaration type, Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
String annotationTypeName = annotationType.getName();
Collection<String> metaAnnotationNames = metaAnnotations.stream()
.map(ITypeBinding::getName)
.collect(Collectors.toList());
TypeDeclaration type = (TypeDeclaration) node.getParent();
ITypeBinding typeBinding = type.resolveBinding();
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
@@ -79,12 +83,11 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
if (!isComponentAnnotated) {
String beanName = BeanUtils.getBeanNameFromType(type.getName().getFullyQualifiedName());
ITypeBinding beanType = type.resolveBinding();
Location location = new Location(doc.getUri(), doc.toRange(type.getStartPosition(), type.getLength()));
WorkspaceSymbol symbol = new WorkspaceSymbol(
ComponentSymbolProvider.beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, beanType.getName()), SymbolKind.Interface,
ComponentSymbolProvider.beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, typeBinding.getName()), SymbolKind.Interface,
Either.forLeft(location));
boolean isConfiguration = false; // otherwise, the ComponentSymbolProvider takes care of the bean definiton for this type
@@ -92,7 +95,7 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(type, doc);
Set<String> supertypes = new HashSet<>();
ASTUtils.findSupertypes(beanType, supertypes);
ASTUtils.findSupertypes(typeBinding, supertypes);
Collection<Annotation> annotationsOnType = ASTUtils.getAnnotations(type);
@@ -103,7 +106,7 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
.map(an -> new AnnotationMetadata(an.getQualifiedName(), true, null, null)))
.toArray(AnnotationMetadata[]::new);
Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName());
Bean beanDefinition = new Bean(beanName, typeBinding.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName());
indexConfigurationProperties(beanDefinition, type, context, doc);
@@ -112,7 +115,16 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
}
}
public static void indexConfigurationProperties(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
public static void indexConfigurationProperties(Bean beanDefinition, AbstractTypeDeclaration abstractType, SpringIndexerJavaContext context, TextDocument doc) {
if (abstractType instanceof TypeDeclaration type) {
indexConfigurationPropertiesForType(beanDefinition, type, context, doc);
}
else if (abstractType instanceof RecordDeclaration record) {
indexConfigurationPropertiesForRecord(beanDefinition, record, context, doc);
}
}
public static void indexConfigurationPropertiesForType(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
FieldDeclaration[] fields = type.getFields();
if (fields != null) {
@@ -145,4 +157,32 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
}
public static void indexConfigurationPropertiesForRecord(Bean beanDefinition, RecordDeclaration record, SpringIndexerJavaContext context, TextDocument doc) {
@SuppressWarnings("unchecked")
List<SingleVariableDeclaration> fields = record.recordComponents();
if (fields != null) {
for (SingleVariableDeclaration field : fields) {
try {
Type fieldType = field.getType();
if (fieldType != null) {
SimpleName name = field.getName();
if (name != null) {
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, field);
Range range = doc.toRange(nodeRegion);
ConfigPropertyIndexElement configPropElement = new ConfigPropertyIndexElement(name.getFullyQualifiedName(), fieldType.resolveBinding().getQualifiedName(), range);
beanDefinition.addChild(configPropElement);
}
}
} catch (BadLocationException e) {
log.error("error identifying config property field", e);
}
}
}
}
}

View File

@@ -22,6 +22,7 @@ import java.util.function.Consumer;
import java.util.stream.Stream;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.BodyDeclaration;
@@ -39,6 +40,7 @@ import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
@@ -284,10 +286,26 @@ public class ASTUtils {
}
}
public static Collection<Annotation> getAnnotations(AbstractTypeDeclaration abstractTypeDeclaration) {
if (abstractTypeDeclaration instanceof TypeDeclaration typeDeclaration) {
return getAnnotations(typeDeclaration);
}
else if (abstractTypeDeclaration instanceof RecordDeclaration recordDeclaration) {
return getAnnotations(recordDeclaration);
}
else {
return null;
}
}
public static Collection<Annotation> getAnnotations(TypeDeclaration typeDeclaration) {
return getAnnotationsFromModifiers(typeDeclaration.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY));
}
public static Collection<Annotation> getAnnotations(RecordDeclaration recordDeclaration) {
return getAnnotationsFromModifiers(recordDeclaration.getStructuralProperty(RecordDeclaration.MODIFIERS2_PROPERTY));
}
public static Collection<Annotation> getAnnotations(MethodDeclaration methodDeclaration) {
return getAnnotationsFromModifiers(methodDeclaration.getStructuralProperty(MethodDeclaration.MODIFIERS2_PROPERTY));
}
@@ -488,7 +506,16 @@ public class ASTUtils {
return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS;
}
public static InjectionPoint[] findInjectionPoints(TypeDeclaration type, TextDocument doc) throws BadLocationException {
public static InjectionPoint[] findInjectionPoints(AbstractTypeDeclaration abstractType, TextDocument doc) throws BadLocationException {
if (abstractType instanceof TypeDeclaration type) {
return findInjectionPointsForType(type, doc);
}
else {
return DefaultValues.EMPTY_INJECTION_POINTS;
}
}
public static InjectionPoint[] findInjectionPointsForType(TypeDeclaration type, TextDocument doc) throws BadLocationException {
List<InjectionPoint> result = new ArrayList<>();
findInjectionPoints(type.getMethods(), doc, result);

View File

@@ -102,4 +102,51 @@ public class SpringIndexerConfigurationPropertiesTest {
assertEquals("java.lang.String", configPropElement.getType());
}
@Test
void testSimpleConfigPropertiesRecord() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/com/example/configproperties/ConfigurationPropertiesWithRecords.java").toUri().toString();
Bean[] beans = springIndex.getBeansOfDocument(docUri);
assertEquals(1, beans.length);
Bean configPropertiesComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("configurationPropertiesWithRecords")).findFirst().get();
assertEquals("com.example.configproperties.ConfigurationPropertiesWithRecords", configPropertiesComponentBean.getType());
List<SpringIndexElement> children = configPropertiesComponentBean.getChildren();
assertEquals(2, children.size());
ConfigPropertyIndexElement configPropElement1 = (ConfigPropertyIndexElement) children.get(0);
assertEquals("name", configPropElement1.getName());
assertEquals("java.lang.String", configPropElement1.getType());
ConfigPropertyIndexElement configPropElement2 = (ConfigPropertyIndexElement) children.get(1);
assertEquals("duration", configPropElement2.getName());
assertEquals("int", configPropElement2.getType());
}
@Test
void testSimpleConfigPropertiesRecordAndConfigurationAnnotation() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/com/example/configproperties/ConfigurationPropertiesWithRecordsAndConfigurationAnnotation.java").toUri().toString();
Bean[] beans = springIndex.getBeansOfDocument(docUri);
assertEquals(1, beans.length);
Bean configPropertiesComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("configurationPropertiesWithRecordsAndConfigurationAnnotation")).findFirst().get();
assertEquals("com.example.configproperties.ConfigurationPropertiesWithRecordsAndConfigurationAnnotation", configPropertiesComponentBean.getType());
List<SpringIndexElement> children = configPropertiesComponentBean.getChildren();
assertEquals(2, children.size());
ConfigPropertyIndexElement configPropElement1 = (ConfigPropertyIndexElement) children.get(0);
assertEquals("name", configPropElement1.getName());
assertEquals("java.lang.String", configPropElement1.getType());
ConfigPropertyIndexElement configPropElement2 = (ConfigPropertyIndexElement) children.get(1);
assertEquals("duration", configPropElement2.getName());
assertEquals("int", configPropElement2.getType());
}
}

View File

@@ -4,7 +4,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "com.example.config.prefix.simple")
@ConfigurationProperties(prefix = "com.example.config.prefix.simple2")
public class ConfigurationPropertiesExampleWithConfigurationAnnotation {
private String simpleConfigProp = "default config value";

View File

@@ -0,0 +1,8 @@
package com.example.configproperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "com.example.config.record.prefix2")
public record ConfigurationPropertiesWithRecordsAndConfigurationAnnotation (String name, int duration) {}