GH-1397: find references to property keys from inside of annotations

This commit is contained in:
Martin Lippert
2024-11-08 15:51:21 +01:00
parent 23ea2b2af9
commit ad0cb29968
10 changed files with 277 additions and 15 deletions

View File

@@ -318,8 +318,8 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
Map<String, ReferenceProvider> providers = new HashMap<>();
providers.put(Annotations.VALUE, new ValuePropertyReferencesProvider(projectFinder));
providers.put(Annotations.CONDITIONAL_ON_PROPERTY, new ValuePropertyReferencesProvider(projectFinder));
providers.put(Annotations.VALUE, new ValuePropertyReferencesProvider(projectFinder, index));
providers.put(Annotations.CONDITIONAL_ON_PROPERTY, new ValuePropertyReferencesProvider(projectFinder, index));
providers.put(Annotations.QUALIFIER, new QualifierReferencesProvider(index, symbolIndex));
providers.put(Annotations.NAMED_JAKARTA, new NamedReferencesProvider(index, symbolIndex));
providers.put(Annotations.NAMED_JAVAX, new NamedReferencesProvider(index, symbolIndex));

View File

@@ -449,7 +449,10 @@ public class ASTUtils {
fieldAnnotations.add(annotation);
String qualifiedName = annotation.resolveTypeBinding().getQualifiedName();
if (Annotations.AUTOWIRED.equals(qualifiedName) || Annotations.INJECT_JAVAX.equals(qualifiedName) || Annotations.INJECT_JAKARTA.equals(qualifiedName)) {
if (Annotations.AUTOWIRED.equals(qualifiedName)
|| Annotations.INJECT_JAVAX.equals(qualifiedName)
|| Annotations.INJECT_JAKARTA.equals(qualifiedName)
|| Annotations.VALUE.equals(qualifiedName)) {
autowiredField = true;
}
}

View File

@@ -15,16 +15,13 @@ import java.util.Map;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.lsp4j.LocationLink;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.commons.java.IJavaProject;
public class PropertyExtractor {
@@ -103,7 +100,7 @@ public class PropertyExtractor {
return null;
}
private String extractPropertyKey(String s) {
public static String extractPropertyKey(String s) {
if (s.length() > 3 && (s.startsWith("${") || s.startsWith("#{")) && s.endsWith("}")) {
return s.substring(2, s.length() - 1);
}

View File

@@ -19,8 +19,11 @@ import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -38,11 +41,16 @@ import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider;
import org.springframework.ide.vscode.boot.properties.BootPropertiesLanguageServerComponents;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
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.text.TextDocument;
import org.springframework.ide.vscode.commons.yaml.ast.YamlASTProvider;
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
@@ -65,9 +73,12 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider {
private final JavaProjectFinder projectFinder;
private final PropertyExtractor propertyExtractor;
private final SpringMetamodelIndex springIndex;
public ValuePropertyReferencesProvider(JavaProjectFinder projectFinder) {
public ValuePropertyReferencesProvider(JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) {
this.projectFinder = projectFinder;
this.springIndex = springIndex;
this.propertyExtractor = new PropertyExtractor();
}
@@ -80,7 +91,7 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider {
if (node instanceof StringLiteral) {
String propertyKey = this.propertyExtractor.extractPropertyKey((StringLiteral) node);
if (propertyKey != null) {
return findReferencesFromPropertyFiles(propertyKey);
return findReferencesToPropertyKey(propertyKey);
}
}
}
@@ -90,6 +101,126 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider {
return null;
}
public List<? extends Location> findReferencesToPropertyKey(String propertyKey) {
List<? extends Location> fromPropertyFiles = findReferencesFromPropertyFiles(propertyKey);
List<? extends Location> fromAnnotations = findReferencesFromAnnotations(propertyKey);
List<Location> result = new ArrayList<>();
result.addAll(fromPropertyFiles);
result.addAll(fromAnnotations);
return result;
}
public List<? extends Location> findReferencesFromAnnotations(String propertyKey) {
List<Location> result = new ArrayList<>();
Collection<? extends IJavaProject> allProjects = this.projectFinder.all();
for (IJavaProject project : allProjects) {
collectReferencesFromAnnotations(project, propertyKey, result);
}
return result;
}
private void collectReferencesFromAnnotations(IJavaProject project, String propertyKey, List<Location> result) {
Bean[] beans = springIndex.getBeansOfProject(project.getElementName());
if (beans != null) {
for (Bean bean : beans) {
collectReferencesFromAnnotations(bean, propertyKey, result);
}
}
}
private void collectReferencesFromAnnotations(Bean bean, String propertyKey, List<Location> result) {
AnnotationMetadata[] annotations = bean.getAnnotations();
for (AnnotationMetadata annotation : annotations) {
collectReferencesFromAnnotation(annotation, bean, propertyKey, result);
}
InjectionPoint[] injectionPoints = bean.getInjectionPoints();
for (InjectionPoint injectionPoint : injectionPoints) {
AnnotationMetadata[] annotationsFromInjectionPoint = injectionPoint.getAnnotations();
for (AnnotationMetadata annotation : annotationsFromInjectionPoint) {
collectReferencesFromAnnotation(annotation, injectionPoint, propertyKey, result);
}
}
}
private void collectReferencesFromAnnotation(AnnotationMetadata annotation, Bean bean, String propertyKey, List<Location> result) {
if (Annotations.VALUE.equals(annotation.getAnnotationType())) {
Map<String, String[]> attributes = annotation.getAttributes();
String[] values = attributes.get("value");
if (values != null && values.length > 0) {
for (String value : values) {
String extractedKey = PropertyExtractor.extractPropertyKey(value);
if (extractedKey != null && extractedKey.equals(propertyKey)) {
result.add(bean.getLocation());
}
}
}
}
else if (Annotations.CONDITIONAL_ON_PROPERTY.equals(annotation.getAnnotationType())) {
Map<String, String[]> attributes = annotation.getAttributes();
String[] prefixes = attributes.get("prefix");
String prefix = prefixes != null && prefixes.length == 1 ? prefixes[0] + "." : "";
String[] names = attributes.get("name");
if (names == null) {
names = attributes.get("value");
}
if (names != null) {
for (String name : names) {
String key = prefix + name;
if (key.equals(propertyKey)) {
result.add(bean.getLocation());
}
}
}
}
}
private void collectReferencesFromAnnotation(AnnotationMetadata annotation, InjectionPoint injectionPoint, String propertyKey, List<Location> result) {
if (Annotations.VALUE.equals(annotation.getAnnotationType())) {
Map<String, String[]> attributes = annotation.getAttributes();
String[] values = attributes.get("value");
if (values != null && values.length > 0) {
for (String value : values) {
String extractedKey = PropertyExtractor.extractPropertyKey(value);
if (extractedKey != null && extractedKey.equals(propertyKey)) {
result.add(injectionPoint.getLocation());
}
}
}
}
else if (Annotations.CONDITIONAL_ON_PROPERTY.equals(annotation.getAnnotationType())) {
Map<String, String[]> attributes = annotation.getAttributes();
String[] prefixes = attributes.get("prefix");
String prefix = prefixes != null && prefixes.length == 1 ? prefixes[0] + "." : "";
String[] names = attributes.get("name");
if (names == null) {
names = attributes.get("value");
}
if (names != null) {
for (String name : names) {
String key = prefix + name;
if (key.equals(propertyKey)) {
result.add(injectionPoint.getLocation());
}
}
}
}
}
public List<? extends Location> findReferencesFromPropertyFiles(String propertyKey) {
Collection<? extends IJavaProject> allProjects = this.projectFinder.all();
@@ -112,7 +243,7 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider {
.collect(Collectors.toList());
}
catch (Exception e) {
return null;
return Collections.emptyList();
}
}

View File

@@ -31,6 +31,7 @@ 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.Annotations;
import org.springframework.ide.vscode.boot.java.value.ValuePropertyReferencesProvider;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
@@ -50,6 +51,7 @@ public class ValuePropertyReferenceFinderTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringMetamodelIndex springIndex;
@Autowired private SpringSymbolIndex indexer;
private File directory;
@@ -74,7 +76,7 @@ public class ValuePropertyReferenceFinderTest {
@Test
void testFindReferenceAtBeginningPropFile() throws Exception {
ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(projectFinder);
ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(projectFinder, springIndex);
Path file = resourceDir.resolve("simple-case/application.properties");
List<? extends Location> locations = provider.findReferences(file, "test.property");
@@ -93,7 +95,7 @@ public class ValuePropertyReferenceFinderTest {
@Test
void testFindReferenceAtBeginningYMLFile() throws Exception {
ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(projectFinder);
ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(projectFinder, springIndex);
Path file = resourceDir.resolve("simple-yml/application.yml");
List<? extends Location> locations = provider.findReferences(file, "test.property");
@@ -112,7 +114,7 @@ public class ValuePropertyReferenceFinderTest {
@Test
void testFindReferenceWithinMultipleFiles() throws Exception {
ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(projectFinder);
ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(projectFinder, springIndex);
List<? extends Location> locations = provider.findReferencesFromPropertyFiles("appl1.prop");
@@ -163,7 +165,7 @@ public class ValuePropertyReferenceFinderTest {
}
@Test
void testFindReferencesToPropertyFromValueAnnotation() throws Exception {
void testFindReferencesToPropertyFromValueAnnotationAtName() throws Exception {
harness.getServer().getWorkspaceService().setWorkspaceFolders(List.of(new WorkspaceFolder(directory.toURI().toString())));
String completionLine = "@Value(\"${my.<*>prop}\")";
@@ -196,4 +198,90 @@ public class ValuePropertyReferenceFinderTest {
assertEquals(0, location.getRange().getEnd().getLine());
assertEquals(7, location.getRange().getEnd().getCharacter());
}
@Test
void testFindReferencesToPropertyFromValueAnnotationAtPrefix() throws Exception {
harness.getServer().getWorkspaceService().setWorkspaceFolders(List.of(new WorkspaceFolder(directory.toURI().toString())));
String completionLine = "@Value(\"${<*>my.prop}\")";
String editorContent = """
package org.test;
import
""" +
Annotations.VALUE + ";" +
"""
@Component
""" +
completionLine + "\n" +
"""
public class TestDependsOnClass {
}
""";
Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri);
List<? extends Location> references = editor.getReferences();
assertEquals(1, references.size());
Location location = references.get(0);
assertEquals(directory.toPath().resolve("src/main/java/application.properties").toUri().toString(), location.getUri());
assertEquals(0, location.getRange().getStart().getLine());
assertEquals(0, location.getRange().getStart().getCharacter());
assertEquals(0, location.getRange().getEnd().getLine());
assertEquals(7, location.getRange().getEnd().getCharacter());
}
@Test
void testFindReferenceForPropertiesUsedInAnnotations() throws Exception {
ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(projectFinder, springIndex);
List<? extends Location> locations = provider.findReferencesToPropertyKey("my.prop2");
assertNotNull(locations);
assertEquals(5, locations.size());
URI propertiesFile = directory.toPath().resolve("src/main/java/application.properties").toUri();
Location location = getLocation(locations, propertiesFile);
assertNotNull(location);
assertEquals(1, location.getRange().getStart().getLine());
assertEquals(0, location.getRange().getStart().getCharacter());
assertEquals(1, location.getRange().getEnd().getLine());
assertEquals(8, location.getRange().getEnd().getCharacter());
URI javaFile = directory.toPath().resolve("src/main/java/org/test/properties/PropertyUsageWithValue.java").toUri();
location = getLocation(locations, javaFile);
assertNotNull(location);
assertEquals(9, location.getRange().getStart().getLine());
assertEquals(16, location.getRange().getStart().getCharacter());
assertEquals(9, location.getRange().getEnd().getLine());
assertEquals(24, location.getRange().getEnd().getCharacter());
javaFile = directory.toPath().resolve("src/main/java/org/test/properties/PropertyUsageWithConditional.java").toUri();
location = getLocation(locations, javaFile);
assertNotNull(location);
assertEquals(5, location.getRange().getStart().getLine());
assertEquals(0, location.getRange().getStart().getCharacter());
assertEquals(5, location.getRange().getEnd().getLine());
assertEquals(10, location.getRange().getEnd().getCharacter());
javaFile = directory.toPath().resolve("src/main/java/org/test/properties/PropertyUsageWithConditionalAndArray.java").toUri();
location = getLocation(locations, javaFile);
assertNotNull(location);
assertEquals(5, location.getRange().getStart().getLine());
assertEquals(0, location.getRange().getStart().getCharacter());
assertEquals(5, location.getRange().getEnd().getLine());
assertEquals(10, location.getRange().getEnd().getCharacter());
javaFile = directory.toPath().resolve("src/main/java/org/test/properties/PropertyUsageWithConditionalWithArrayAndPrefix.java").toUri();
location = getLocation(locations, javaFile);
assertNotNull(location);
assertEquals(5, location.getRange().getStart().getLine());
assertEquals(0, location.getRange().getStart().getCharacter());
assertEquals(5, location.getRange().getEnd().getLine());
assertEquals(10, location.getRange().getEnd().getCharacter());
}
}

View File

@@ -0,0 +1,10 @@
package org.test.properties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty("my.prop2")
public class PropertyUsageWithConditional {
}

View File

@@ -0,0 +1,10 @@
package org.test.properties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty(name = {"my.prop2"})
public class PropertyUsageWithConditionalAndArray {
}

View File

@@ -0,0 +1,10 @@
package org.test.properties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty(prefix = "my", name = {"prop2"})
public class PropertyUsageWithConditionalWithArrayAndPrefix {
}

View File

@@ -0,0 +1,12 @@
package org.test.properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class PropertyUsageWithValue {
@Value("${my.prop2}")
private String someProp;
}