From 85103b61dc8f2777b48c9a1ff1e55c7057f156f2 Mon Sep 17 00:00:00 2001 From: aboyko Date: Mon, 1 May 2023 12:39:54 -0400 Subject: [PATCH] Navigate to property value from @ConditionalOnProperty --- .../PropertyValueAnnotationDefProvider.java | 88 ++++++++++++++++--- ...ropertyValueAnnotationDefProviderTest.java | 69 ++++++++++++++- 2 files changed, 141 insertions(+), 16 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyValueAnnotationDefProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyValueAnnotationDefProvider.java index 9d08e5ce5..80caa4533 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyValueAnnotationDefProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyValueAnnotationDefProvider.java @@ -19,9 +19,11 @@ import java.util.Optional; 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.FieldDeclaration; +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.Location; import org.eclipse.lsp4j.LocationLink; @@ -43,6 +45,36 @@ import com.google.common.collect.ImmutableList.Builder; public class PropertyValueAnnotationDefProvider implements IJavaDefinitionProvider { private static final Logger log = LoggerFactory.getLogger(PropertyValueAnnotationDefProvider.class); + + private static final String PARAM_VALUE = "value"; + private static final String PARAM_NAME = "name"; + private static final String PARAM_PREFIX = "prefix"; + + private Map annotationToPropertyKeyExtractor = Map.of( + Annotations.VALUE, (a, p, v) -> { + if (a.isSingleMemberAnnotation()) { + return extractPropertyKey(v.getLiteralValue()); + } else if (a.isNormalAnnotation() && PARAM_VALUE.equals(p.getName().getIdentifier())) { + return extractPropertyKey(v.getLiteralValue()); + } + return null; + }, + Annotations.CONDITIONAL_ON_PROPERTY, (a, p, v) -> { + if (a.isSingleMemberAnnotation()) { + return v.getLiteralValue(); + } else if (a.isNormalAnnotation()) { + switch (p.getName().getIdentifier()) { + case PARAM_VALUE: + return v.getLiteralValue(); + case PARAM_NAME: + String prefix = extractAnnotationParameter(a, PARAM_PREFIX); + String name = v.getLiteralValue(); + return prefix != null && !prefix.isBlank() ? prefix + "." + name : name; + } + } + return null; + } + ); @Override public List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, @@ -52,13 +84,26 @@ public class PropertyValueAnnotationDefProvider implements IJavaDefinitionProvid String propertyKey = null; ASTNode parent = valueNode.getParent(); - if (parent instanceof Annotation && isApplicableValueAnnotation((Annotation) parent)) { - propertyKey = extractPropertyKey(valueNode.getLiteralValue()); + if (parent instanceof Annotation) { + Annotation a = (Annotation) parent; + IAnnotationBinding binding = a.resolveAnnotationBinding(); + if (binding != null && binding.getAnnotationType() != null) { + PropertyKeyExtractor propertyExtractor = annotationToPropertyKeyExtractor.get(binding.getAnnotationType().getQualifiedName()); + if (propertyExtractor != null) { + propertyKey = propertyExtractor.extract(a, null, valueNode); + } + } } else if (parent instanceof MemberValuePair - && "value".equals(((MemberValuePair) parent).getName().getIdentifier()) - && parent.getParent() instanceof Annotation - && isApplicableValueAnnotation((Annotation) parent.getParent())) { - propertyKey = extractPropertyKey(valueNode.getLiteralValue()); + && parent.getParent() instanceof Annotation) { + MemberValuePair pair = (MemberValuePair) parent; + Annotation a = (Annotation) parent.getParent(); + IAnnotationBinding binding = a.resolveAnnotationBinding(); + if (binding != null && binding.getAnnotationType() != null) { + PropertyKeyExtractor propertyExtractor = annotationToPropertyKeyExtractor.get(binding.getAnnotationType().getQualifiedName()); + if (propertyExtractor != null) { + propertyKey = propertyExtractor.extract(a, pair, valueNode); + } + } } if (propertyKey != null) { @@ -87,12 +132,6 @@ public class PropertyValueAnnotationDefProvider implements IJavaDefinitionProvid return Collections.emptyList(); } - private static boolean isApplicableValueAnnotation(Annotation a) { - IAnnotationBinding binding = a.resolveAnnotationBinding(); - return binding != null && Annotations.VALUE.equals(binding.getAnnotationType().getQualifiedName()) - && a.getParent() instanceof FieldDeclaration; - } - private List findValueReferences(IJavaProject project, String propertyKey, Map targetRanges) { Builder links = ImmutableList.builder(); IClasspathUtil.getClasspathResourcesFullPaths(project.getClasspath()).forEach(path -> { @@ -159,6 +198,25 @@ public class PropertyValueAnnotationDefProvider implements IJavaDefinitionProvid }); return links.build(); } + + @SuppressWarnings("unchecked") + private static String extractAnnotationParameter(Annotation a, String param) { + Expression value = null; + if (a.isSingleMemberAnnotation() && PARAM_VALUE.equals(param)) { + value = ((SingleMemberAnnotation) a).getValue(); + } else if (a.isNormalAnnotation()) { + for (MemberValuePair pair : (List) ((NormalAnnotation) a).values()) { + if (param.equals(pair.getName().getIdentifier())) { + value = pair.getValue(); + break; + } + } + } + if (value instanceof StringLiteral) { + return ((StringLiteral) value).getLiteralValue(); + } + return null; + } private static String extractPropertyKey(String s) { if (s.length() > 3 && (s.startsWith("${") || s.startsWith("#{")) && s.endsWith("}")) { @@ -166,5 +224,9 @@ public class PropertyValueAnnotationDefProvider implements IJavaDefinitionProvid } return null; } + + private interface PropertyKeyExtractor { + String extract(Annotation a, MemberValuePair pair, StringLiteral v); + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/PropertyValueAnnotationDefProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/PropertyValueAnnotationDefProviderTest.java index fa12bcab3..24d991bc7 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/PropertyValueAnnotationDefProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/PropertyValueAnnotationDefProviderTest.java @@ -72,7 +72,7 @@ public class PropertyValueAnnotationDefProviderTest { } @Test - void propertiesCase() throws Exception { + void propertiesCase_ValueAnnotation() throws Exception { Path propertiesFilePath = projectFile("src/main/resources/application.properties", "some.prop=5"); Editor editor = harness.newEditor(LanguageId.JAVA, """ package org.test; @@ -93,7 +93,7 @@ public class PropertyValueAnnotationDefProviderTest { } @Test - void yamlCase() throws Exception { + void yamlCase_ValueAnnotation() throws Exception { Path yamlFilePath = projectFile("src/main/resources/application.yml", """ some: prop: 5 @@ -117,7 +117,7 @@ public class PropertyValueAnnotationDefProviderTest { } @Test - void combinedCase() throws Exception { + void combinedCase_ValueAnnotation() throws Exception { Path propertiesFilePath = projectFile("src/main/resources/application.properties", "some.prop=5"); Path yamlFilePath = projectFile("src/main/resources/application.yml", """ some: @@ -144,4 +144,67 @@ public class PropertyValueAnnotationDefProviderTest { editor.assertLinkTargets("some.prop", List.of(expectedYamlLocation, expectedPropsLocation)); } + + @Test + void noValueCase_ConditionOnPropertyAnnotation() throws Exception { + Path propertiesFilePath = projectFile("src/main/resources/application.properties", "some.prop=5"); + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + + @ConditionalOnProperty("some.prop") + public class TestValueCompletion { + + private String value1; + }"""); + + LocationLink expectedLocation = new LocationLink(propertiesFilePath.toUri().toASCIIString(), + new Range(new Position(0, 0), new Position(0, 11)), new Range(new Position(0, 10), new Position(0, 11)), + new Range(new Position(4, 23), new Position(4, 34))); + + editor.assertLinkTargets("some.prop", List.of(expectedLocation)); + } + + @Test + void valueCase_ConditionOnPropertyAnnotation() throws Exception { + Path propertiesFilePath = projectFile("src/main/resources/application.properties", "some.prop=5"); + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + + @ConditionalOnProperty(value = "some.prop") + public class TestValueCompletion { + + private String value1; + }"""); + + LocationLink expectedLocation = new LocationLink(propertiesFilePath.toUri().toASCIIString(), + new Range(new Position(0, 0), new Position(0, 11)), new Range(new Position(0, 10), new Position(0, 11)), + new Range(new Position(4, 31), new Position(4, 42))); + + editor.assertLinkTargets("some.prop", List.of(expectedLocation)); + } + + @Test + void nameAndPrefixCase_ConditionOnPropertyAnnotation() throws Exception { + Path propertiesFilePath = projectFile("src/main/resources/application.properties", "some.prop=5"); + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + + @ConditionalOnProperty(prefix = "some", name = "prop") + public class TestValueCompletion { + + private String value1; + }"""); + + LocationLink expectedLocation = new LocationLink(propertiesFilePath.toUri().toASCIIString(), + new Range(new Position(0, 0), new Position(0, 11)), new Range(new Position(0, 10), new Position(0, 11)), + new Range(new Position(4, 47), new Position(4, 53))); + + editor.assertLinkTargets("prop", List.of(expectedLocation)); + } }