Skip to content

Commit

Permalink
GH-1397: find references to property keys from inside of annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
martinlippert committed Nov 8, 2024
1 parent 23ea2b2 commit ad0cb29
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,8 @@ protected ReferencesHandler createReferenceHandler(SimpleLanguageServer server,

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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,10 @@ public static InjectionPoint[] findInjectionPoints(TypeDeclaration type, TextDoc
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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@

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 {

Expand Down Expand Up @@ -103,7 +100,7 @@ public String extractPropertyKey(StringLiteral valueNode) {
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
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;
Expand All @@ -38,11 +41,16 @@
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;
Expand All @@ -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();
}

Expand All @@ -80,7 +91,7 @@ public List<? extends Location> provideReferences(CancelChecker cancelToken, IJa
if (node instanceof StringLiteral) {
String propertyKey = this.propertyExtractor.extractPropertyKey((StringLiteral) node);
if (propertyKey != null) {
return findReferencesFromPropertyFiles(propertyKey);
return findReferencesToPropertyKey(propertyKey);
}
}
}
Expand All @@ -90,6 +101,126 @@ public List<? extends Location> provideReferences(CancelChecker cancelToken, IJa

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();
Expand All @@ -112,7 +243,7 @@ public List<? extends Location> findReferencesFromPropertyFiles(String propertyK
.collect(Collectors.toList());
}
catch (Exception e) {
return null;
return Collections.emptyList();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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.boot.java.value.ValuePropertyReferencesProvider;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
Expand All @@ -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;
Expand All @@ -74,7 +76,7 @@ public void setup() throws Exception {

@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");
Expand All @@ -93,7 +95,7 @@ void testFindReferenceAtBeginningPropFile() throws Exception {

@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");
Expand All @@ -112,7 +114,7 @@ void testFindReferenceAtBeginningYMLFile() throws Exception {

@Test
void testFindReferenceWithinMultipleFiles() throws Exception {
ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(projectFinder);
ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(projectFinder, springIndex);

List<? extends Location> locations = provider.findReferencesFromPropertyFiles("appl1.prop");

Expand Down Expand Up @@ -163,7 +165,7 @@ private Location getLocation(List<? extends Location> locations, URI docURI) {
}

@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}\")";
Expand Down Expand Up @@ -196,4 +198,90 @@ public class TestDependsOnClass {
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());
}

}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
my.prop=test
my.prop=test
my.prop2=test
Original file line number Diff line number Diff line change
@@ -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 {

}
Loading

0 comments on commit ad0cb29

Please sign in to comment.