diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconciler.java index 06ff38e57..61fb0364a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconciler.java @@ -58,7 +58,7 @@ public class JdtCronReconciler implements JdtAstReconciler { if (value instanceof MemberValuePair) { MemberValuePair pair = (MemberValuePair) value; String name = pair.getName().getFullyQualifiedName(); - if (name != null && "cron".equals(name)) { + if (name != null && "cron".equals(name) && JdtCronSemanticTokensProvider.isCronExpression(pair.getValue())) { QueryJdtAstReconciler.reconcileExpression(cronReconciler, pair.getValue(), problemCollector); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProvider.java index 49d325057..040267c17 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProvider.java @@ -14,9 +14,12 @@ import java.util.List; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.TextBlock; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.JdtSemanticTokensProvider; import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider; @@ -68,7 +71,7 @@ public class JdtCronSemanticTokensProvider implements JdtSemanticTokensProvider if (value instanceof MemberValuePair) { MemberValuePair pair = (MemberValuePair) value; String name = pair.getName().getFullyQualifiedName(); - if (name != null && "cron".equals(name)) { + if (name != null && "cron".equals(name) && isCronExpression(pair.getValue())) { JdtDataQuerySemanticTokensProvider.computeTokensForExpression(tokensProvider, pair.getValue()).forEach(collector::accept); } } @@ -81,5 +84,20 @@ public class JdtCronSemanticTokensProvider implements JdtSemanticTokensProvider }; } + + public static boolean isCronExpression(Expression e) { + String value = null; + if (e instanceof StringLiteral sl) { + value = sl.getLiteralValue(); + } else if (e instanceof TextBlock tb) { + value = tb.getLiteralValue(); + } + value = value.trim(); + if (value.startsWith("#{") || value.startsWith("${")) { + // Either SPEL or Property Holder + return false; + } + return value != null; + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java index 041df1322..2259726ea 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java @@ -58,7 +58,9 @@ public final class AnnotationParamSpelExtractor { new AnnotationParamSpelExtractor(SPRING_POST_FILTER, "value", "", ""), new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, null, "", ""), - new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "", "") + new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "", ""), + + new AnnotationParamSpelExtractor(Annotations.SCHEDULED, "cron", "#{", "}"), }; @@ -125,10 +127,16 @@ public final class AnnotationParamSpelExtractor { private Optional fromStringLiteral(StringLiteral valueExp) { String value = valueExp.getEscapedValue(); value = value.substring(1, value.length() - 1); - if (value != null && value.startsWith(paramValuePrefix) && value.endsWith(paramValuePostfix)) { - String spelText = value.substring(paramValuePrefix.length(), value.length() - paramValuePostfix.length()); - int offset = valueExp.getStartPosition() + paramValuePrefix.length() + 1; - return Optional.of(new Snippet(spelText, offset)); + if (value != null) { + int startIdx = value.indexOf(paramValuePrefix); + if (startIdx >= 0) { + int endIdx = value.lastIndexOf(paramValuePostfix); + if (endIdx >= 0) { + String spelText = value.substring(startIdx + paramValuePrefix.length(), endIdx); + int offset = valueExp.getStartPosition() + startIdx + paramValuePrefix.length() + 1; + return Optional.of(new Snippet(spelText, offset)); + } + } } return Optional.empty(); } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronSemanticTokensTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronSemanticTokensTest.java index d034fe7a0..1b51b5529 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronSemanticTokensTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronSemanticTokensTest.java @@ -274,4 +274,5 @@ public class CronSemanticTokensTest { assertThat(tokens.get(11)).isEqualTo(new SemanticTokenData(24, 25, "operator", new String[0])); // - assertThat(tokens.get(12)).isEqualTo(new SemanticTokenData(25, 30, "enum", new String[0])); // MARCH } + } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconcilerTest.java index 9fa7b8bd0..ab2449acc 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconcilerTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconcilerTest.java @@ -87,6 +87,46 @@ public class JdtCronReconcilerTest { editor.assertProblems(); } + @Test + void noErrors_PropertyHolder() throws Exception { + String source = """ + package example.demo; + + import org.springframework.scheduling.annotation.Scheduled; + + public class A { + + @Scheduled(cron = " ${demo.cron} ") + void foo() {} + + } + """; + String docUri = directory.toPath().resolve("src/main/java/example/demo/A.java").toUri() + .toString(); + Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri); + editor.assertProblems(); + } + + @Test + void noErrors_SPEL() throws Exception { + String source = """ + package example.demo; + + import org.springframework.scheduling.annotation.Scheduled; + + public class A { + + @Scheduled(cron = " #{demo.cron} ") + void foo() {} + + } + """; + String docUri = directory.toPath().resolve("src/main/java/example/demo/A.java").toUri() + .toString(); + Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri); + editor.assertProblems(); + } + @Test void errorsReported_1() throws Exception { String source = """ diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProviderTest.java index 0b33a5a00..772cfb8d4 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProviderTest.java @@ -367,4 +367,56 @@ public class JdtCronSemanticTokensProviderTest { assertThat(token).isEqualTo(new SemanticTokenData(142, 147, "enum", new String[0])); assertThat(source.substring(token.start(), token.end())).isEqualTo("MARCH"); } + + @Test + void noTokens_SPEL() throws Exception { + String source = """ + package my.package + + import org.springframework.scheduling.annotation.Scheduled; + + public class A { + + @Scheduled(cron=" #{demo.cron} ") + void foo() {} + + } + """; + + String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/A.java").toUri() + .toASCIIString(); + CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "A.java", jp); + + assertThat(cu).isNotNull(); + + List tokens = computeTokens(cu); + + assertThat(tokens.size()).isEqualTo(0); + } + + @Test + void noTokens_PropertyHolder() throws Exception { + String source = """ + package my.package + + import org.springframework.scheduling.annotation.Scheduled; + + public class A { + + @Scheduled(cron=" ${demo.cron} ") + void foo() {} + + } + """; + + String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/A.java").toUri() + .toASCIIString(); + CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "A.java", jp); + + assertThat(cu).isNotNull(); + + List tokens = computeTokens(cu); + + assertThat(tokens.size()).isEqualTo(0); + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/spel/JdtSpelSemanticTokensProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/spel/JdtSpelSemanticTokensProviderTest.java index 9846cd993..787e7ce55 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/spel/JdtSpelSemanticTokensProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/spel/JdtSpelSemanticTokensProviderTest.java @@ -154,5 +154,43 @@ public class JdtSpelSemanticTokensProviderTest { assertThat(source.substring(token.start(), token.end())).isEqualTo("'[a-zA-Z\\s]+'"); } + + @Test + void leadingAndTrailingSpaces() throws Exception { + String source = """ + package my.package + + import org.springframework.scheduling.annotation.Scheduled; + + public class A { + + @Scheduled(cron=" #{demo.cron} ") + void foo() {} + + } + """; + + String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/A.java").toUri() + .toASCIIString(); + CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "A.java", jp); + + assertThat(cu).isNotNull(); + + List tokens = computeTokens(cu); + + assertThat(tokens.size()).isEqualTo(3); + + SemanticTokenData token = tokens.get(0); + assertThat(token).isEqualTo(new SemanticTokenData(121, 125, "variable", new String[0])); + assertThat(source.substring(token.start(), token.end())).isEqualTo("demo"); + + token = tokens.get(1); + assertThat(token).isEqualTo(new SemanticTokenData(125, 126, "operator", new String[0])); + assertThat(source.substring(token.start(), token.end())).isEqualTo("."); + + token = tokens.get(2); + assertThat(token).isEqualTo(new SemanticTokenData(126, 130, "property", new String[0])); + assertThat(source.substring(token.start(), token.end())).isEqualTo("cron"); + } }