GH-1494: add special index nodes for configuration property details

This commit is contained in:
Martin Lippert
2025-03-10 11:41:04 +01:00
parent 2935f6d737
commit eb982b5586
14 changed files with 370 additions and 10 deletions

View File

@@ -17,6 +17,7 @@ import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareLookup;
import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider;
import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider;
import org.springframework.ide.vscode.boot.java.beans.ConfigurationPropertiesSymbolProvider;
import org.springframework.ide.vscode.boot.java.beans.FeignClientSymbolProvider;
import org.springframework.ide.vscode.boot.java.data.DataRepositorySymbolProvider;
import org.springframework.ide.vscode.boot.java.events.EventListenerSymbolProvider;
@@ -34,10 +35,12 @@ public class SpringSymbolIndexerConfig {
RequestMappingSymbolProvider requestMappingSymbolProvider = new RequestMappingSymbolProvider();
BeansSymbolProvider beansSymbolProvider = new BeansSymbolProvider();
ComponentSymbolProvider componentSymbolProvider = new ComponentSymbolProvider();
RestrictedDefaultSymbolProvider restrictedDefaultSymbolProvider = new RestrictedDefaultSymbolProvider();
ConfigurationPropertiesSymbolProvider configPropsSymbolProvider = new ConfigurationPropertiesSymbolProvider();
DataRepositorySymbolProvider dataRepositorySymbolProvider = new DataRepositorySymbolProvider();
EventListenerSymbolProvider eventListenerSymbolProvider = new EventListenerSymbolProvider();
RestrictedDefaultSymbolProvider restrictedDefaultSymbolProvider = new RestrictedDefaultSymbolProvider();
providers.put(Annotations.SPRING_REQUEST_MAPPING, requestMappingSymbolProvider);
providers.put(Annotations.SPRING_GET_MAPPING, requestMappingSymbolProvider);
providers.put(Annotations.SPRING_POST_MAPPING, requestMappingSymbolProvider);
@@ -49,6 +52,7 @@ public class SpringSymbolIndexerConfig {
providers.put(Annotations.COMPONENT, componentSymbolProvider);
providers.put(Annotations.NAMED_JAKARTA, componentSymbolProvider);
providers.put(Annotations.NAMED_JAVAX, componentSymbolProvider);
providers.put(Annotations.CONFIGURATION_PROPERTIES, configPropsSymbolProvider);
providers.put(Annotations.PROFILE, restrictedDefaultSymbolProvider);

View File

@@ -28,6 +28,8 @@ public class Annotations {
public static final String COMPONENT = "org.springframework.stereotype.Component";
public static final String CONFIGURATION = "org.springframework.context.annotation.Configuration";
public static final String CONTROLLER = "org.springframework.stereotype.Controller";
public static final String CONFIGURATION_PROPERTIES = "org.springframework.boot.context.properties.ConfigurationProperties";
public static final String REPOSITORY = "org.springframework.stereotype.Repository";
public static final String REPOSITORY_DEFINITION = "org.springframework.data.repository.RepositoryDefinition";

View File

@@ -39,8 +39,8 @@ public class BeanUtils {
return ASTUtils.getExpressionValueAsString(attribute.get(), (a) -> {});
}
else {
String beanName = type.getName().toString();
return BeanUtils.getBeanNameFromType(beanName);
String beanType = type.getName().toString();
return BeanUtils.getBeanNameFromType(beanType);
}
}

View File

@@ -135,11 +135,20 @@ public class ComponentSymbolProvider implements SymbolProvider {
indexEventListeners(beanDefinition, type, annotationType, metaAnnotations, context, doc);
indexEventListenerInterfaceImplementation(beanDefinition, type, context, doc);
indexRequestMappings(beanDefinition, type, annotationType, metaAnnotations, context, doc);
indexConfigurationProperties(beanDefinition, type, 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, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
if (annotationHierarchies.isAnnotatedWith(type.resolveBinding(), Annotations.CONFIGURATION_PROPERTIES)) {
ConfigurationPropertiesSymbolProvider.indexConfigurationProperties(beanDefinition, type, context, doc);
}
}
private void indexBeanMethods(Bean bean, TypeDeclaration type, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
if (bean.isConfiguration()) {
@@ -323,7 +332,7 @@ public class ComponentSymbolProvider implements SymbolProvider {
return null;
}
protected String beanLabel(String searchPrefix, String annotationTypeName, Collection<String> metaAnnotationNames, String beanName, String beanType) {
public static String beanLabel(String searchPrefix, String annotationTypeName, Collection<String> metaAnnotationNames, String beanName, String beanType) {
StringBuilder symbolLabel = new StringBuilder();
symbolLabel.append("@");
symbolLabel.append(searchPrefix);

View File

@@ -0,0 +1,72 @@
/*******************************************************************************
* Copyright (c) 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SymbolKind;
import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement;
import org.springframework.ide.vscode.commons.protocol.spring.SymbolElement;
/**
* @author Martin Lippert
*/
public class ConfigPropertyIndexElement extends AbstractSpringIndexElement implements SymbolElement {
private final String name;
private final String type;
private final Range range;
public ConfigPropertyIndexElement(String name, String type, Range range) {
this.name = name;
this.type = type;
this.range = range;
}
public String getName() {
return name;
}
public String getType() {
return type;
}
public Range getRange() {
return range;
}
@Override
public DocumentSymbol getDocumentSymbol() {
DocumentSymbol symbol = new DocumentSymbol();
symbol.setName(name + " (" + getShortTypeName() + ")");
symbol.setKind(SymbolKind.Property);
symbol.setRange(range);
symbol.setSelectionRange(range);
return symbol;
}
private String getShortTypeName() {
if (type != null) {
int i = type.lastIndexOf(".");
if (i >= 0) {
return type.substring(i + 1);
}
else {
return type;
}
}
return "";
}
}

View File

@@ -0,0 +1,148 @@
/*******************************************************************************
* Copyright (c) 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
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.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
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.InjectionPoint;
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;
/**
* @author Martin Lippert
*/
public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
private static final Logger log = LoggerFactory.getLogger(ConfigurationPropertiesSymbolProvider.class);
@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);
}
}
catch (BadLocationException e) {
log.error("", e);
}
}
protected void createSymbol(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);
boolean isComponentAnnotated = annotationHierarchies.isAnnotatedWith(typeBinding, Annotations.COMPONENT);
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,
Either.forLeft(location));
boolean isConfiguration = false; // otherwise, the ComponentSymbolProvider takes care of the bean definiton for this type
InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(type, doc);
Set<String> supertypes = new HashSet<>();
ASTUtils.findSupertypes(beanType, supertypes);
Collection<Annotation> annotationsOnType = ASTUtils.getAnnotations(type);
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, type, context, doc);
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
}
}
public static void indexConfigurationProperties(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
FieldDeclaration[] fields = type.getFields();
if (fields != null) {
for (FieldDeclaration field : fields) {
try {
Type fieldType = field.getType();
if (fieldType != null) {
@SuppressWarnings("unchecked")
List<VariableDeclarationFragment> fragments = field.fragments();
for (VariableDeclarationFragment fragment : fragments) {
SimpleName name = fragment.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

@@ -0,0 +1,105 @@
/*******************************************************************************
* Copyright (c) 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.index.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
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.beans.ConfigPropertyIndexElement;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Martin Lippert
*/
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
public class SpringIndexerConfigurationPropertiesTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringSymbolIndex indexer;
@Autowired private SpringMetamodelIndex springIndex;
private File directory;
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-configuration-properties-indexing/").toURI());
String projectDir = directory.toURI().toString();
// trigger project creation
projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
CompletableFuture<Void> initProject = indexer.waitOperation();
initProject.get(5, TimeUnit.SECONDS);
}
@Test
void testSimpleConfigPropertiesClass() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/com/example/configproperties/ConfigurationPropertiesExample.java").toUri().toString();
Bean[] beans = springIndex.getBeansOfDocument(docUri);
assertEquals(1, beans.length);
Bean configPropertiesComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("configurationPropertiesExample")).findFirst().get();
assertEquals("com.example.configproperties.ConfigurationPropertiesExample", configPropertiesComponentBean.getType());
List<SpringIndexElement> children = configPropertiesComponentBean.getChildren();
assertEquals(1, children.size());
ConfigPropertyIndexElement configPropElement = (ConfigPropertyIndexElement) children.get(0);
assertEquals("simpleConfigProp", configPropElement.getName());
assertEquals("java.lang.String", configPropElement.getType());
}
@Test
void testSimpleConfigPropertiesClassWithAdditionalConfigurationAnnotation() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/com/example/configproperties/ConfigurationPropertiesExampleWithConfigurationAnnotation.java").toUri().toString();
Bean[] beans = springIndex.getBeansOfDocument(docUri);
assertEquals(1, beans.length);
Bean configPropertiesComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("configurationPropertiesExampleWithConfigurationAnnotation")).findFirst().get();
assertEquals("com.example.configproperties.ConfigurationPropertiesExampleWithConfigurationAnnotation", configPropertiesComponentBean.getType());
List<SpringIndexElement> children = configPropertiesComponentBean.getChildren();
assertEquals(1, children.size());
ConfigPropertyIndexElement configPropElement = (ConfigPropertyIndexElement) children.get(0);
assertEquals("simpleConfigProp", configPropElement.getName());
assertEquals("java.lang.String", configPropElement.getType());
}
}

View File

@@ -0,0 +1,20 @@
package com.example.configproperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "com.example.config.prefix.simple")
public class ConfigurationPropertiesExampleWithConfigurationAnnotation {
private String simpleConfigProp = "default config value";
public ConfigurationPropertiesExampleWithConfigurationAnnotation(String simpleConfigProp) {
this.simpleConfigProp = simpleConfigProp;
}
public String getSimpleConfigProp() {
return simpleConfigProp;
}
}