Property place holder support inside SPEL

This commit is contained in:
aboyko
2024-09-10 22:00:20 -04:00
parent 30abaec4d6
commit fca3fc0566
24 changed files with 2391 additions and 831 deletions

View File

@@ -19,7 +19,8 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemTy
public enum SpelProblemType implements ProblemType {
JAVA_SPEL_EXPRESSION_SYNTAX(ERROR, "SpEL parser raised a ParseException", "SpEL Expression Syntax");
JAVA_SPEL_EXPRESSION_SYNTAX(ERROR, "SpEL parser raised a ParseException", "SpEL Expression Syntax"),
PROPERTY_PLACE_HOLDER_SYNTAX(ERROR, "Property place holder raised a ParseException", "Property Place Holder Syntax");
private final ProblemSeverity defaultSeverity;
private String description;

View File

@@ -0,0 +1,35 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, Inc.
* 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, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import java.util.stream.Stream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.TerminalNode;
public class AntlrUtils {
public static Stream<Token> getAllLeafs(ParserRuleContext ctx) {
if (ctx.children == null) {
return Stream.empty();
}
return ctx.children.stream().flatMap(n -> {
if (n instanceof ParserRuleContext prc) {
return getAllLeafs(prc);
} else if (n instanceof TerminalNode tn) {
return Stream.of(tn.getSymbol());
}
return Stream.empty();
});
}
}

View File

@@ -26,7 +26,6 @@ import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ConsoleErrorListener;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;
@@ -201,7 +200,7 @@ public class MySqlSemanticTokens implements SemanticTokensDataProvider {
@Override
public void exitUdfFunctionCall(UdfFunctionCallContext fc) {
if (fc.fullId() != null) {
List<Token> ls = getAllLeafs(fc.fullId()).toList();
List<Token> ls = AntlrUtils.getAllLeafs(fc.fullId()).toList();
if (!ls.isEmpty()) {
semantics.put(ls.get(ls.size() - 1), "method");
}
@@ -211,13 +210,13 @@ public class MySqlSemanticTokens implements SemanticTokensDataProvider {
@Override
public void exitParameter(ParameterContext ctx) {
if (ctx.dottedId() != null) {
getAllLeafs(ctx.dottedId()).forEach(t -> semantics.put(t, "parameter"));
AntlrUtils.getAllLeafs(ctx.dottedId()).forEach(t -> semantics.put(t, "parameter"));
}
if (ctx.uid() != null) {
getAllLeafs(ctx.uid()).forEach(t -> semantics.put(t, "parameter"));
AntlrUtils.getAllLeafs(ctx.uid()).forEach(t -> semantics.put(t, "parameter"));
}
if (ctx.decimalLiteral() != null) {
getAllLeafs(ctx.decimalLiteral()).forEach(t -> semantics.put(t, "parameter"));
AntlrUtils.getAllLeafs(ctx.decimalLiteral()).forEach(t -> semantics.put(t, "parameter"));
}
}
@@ -278,17 +277,6 @@ public class MySqlSemanticTokens implements SemanticTokensDataProvider {
}
static Stream<Token> getAllLeafs(ParserRuleContext ctx) {
return ctx.children.stream().flatMap(n -> {
if (n instanceof ParserRuleContext prc) {
return getAllLeafs(prc);
} else if (n instanceof TerminalNode tn) {
return Stream.of(tn.getSymbol());
}
return Stream.empty();
});
}
}

View File

@@ -146,7 +146,7 @@ public class PostgreSqlSemanticTokens implements SemanticTokensDataProvider {
@Override
public void exitData_type(Data_typeContext dataType) {
if (dataType.identifier() != null) {
MySqlSemanticTokens.getAllLeafs(dataType.identifier())
AntlrUtils.getAllLeafs(dataType.identifier())
.filter(t -> t.getType() == PostgreSqlLexer.IDENTIFIER)
.forEach(t -> semantics.put(t, "type"));
}
@@ -155,11 +155,11 @@ public class PostgreSqlSemanticTokens implements SemanticTokensDataProvider {
@Override
public void exitParameter(ParameterContext param) {
if (param.identifier() != null) {
MySqlSemanticTokens.getAllLeafs(param.identifier()).forEach(t -> semantics.put(t, "parameter"));
AntlrUtils.getAllLeafs(param.identifier()).forEach(t -> semantics.put(t, "parameter"));
} else if (param.INTEGER_LITERAL() != null) {
semantics.put(param.INTEGER_LITERAL().getSymbol(), "parameter");
} else if (param.reserved_keyword() != null) {
MySqlSemanticTokens.getAllLeafs(param.reserved_keyword()).forEach(t -> semantics.put(t, "parameter"));
AntlrUtils.getAllLeafs(param.reserved_keyword()).forEach(t -> semantics.put(t, "parameter"));
}
}

View File

@@ -0,0 +1,161 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, Inc.
* 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, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.spel;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.antlr.v4.runtime.ANTLRErrorListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ConsoleErrorListener;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.AntlrUtils;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensDataProvider;
import org.springframework.ide.vscode.parser.placeholder.PropertyPlaceHolderBaseListener;
import org.springframework.ide.vscode.parser.placeholder.PropertyPlaceHolderLexer;
import org.springframework.ide.vscode.parser.placeholder.PropertyPlaceHolderParser;
import org.springframework.ide.vscode.parser.placeholder.PropertyPlaceHolderParser.DefaultValueContext;
import org.springframework.ide.vscode.parser.placeholder.PropertyPlaceHolderParser.KeyContext;
import org.springframework.ide.vscode.parser.placeholder.PropertyPlaceHolderParser.ValueContext;
public class PropertyPlaceHolderSemanticTokens implements SemanticTokensDataProvider {
private final Optional<Consumer<RecognitionException>> parseErrorHandler;
public PropertyPlaceHolderSemanticTokens(Optional<Consumer<RecognitionException>> parseErrorHandler) {
this.parseErrorHandler = parseErrorHandler;
}
@Override
public List<String> getTokenTypes() {
return List.of("property", "string", "operator");
}
@Override
public List<SemanticTokenData> computeTokens(String text, int initialOffset) {
PropertyPlaceHolderLexer lexer = new PropertyPlaceHolderLexer(CharStreams.fromString(text));
CommonTokenStream antlrTokens = new CommonTokenStream(lexer);
PropertyPlaceHolderParser parser = new PropertyPlaceHolderParser(antlrTokens);
Map<Token, String> semantics = new HashMap<>();
lexer.removeErrorListener(ConsoleErrorListener.INSTANCE);
parser.removeErrorListener(ConsoleErrorListener.INSTANCE);
List<SemanticTokenData> tokens = new ArrayList<>();
parser.addParseListener(new PropertyPlaceHolderBaseListener() {
@Override
public void exitKey(KeyContext ctx) {
// Remove all children nodes semantics as they will be replaced by a single token spanning multiple AST nodes
AntlrUtils.getAllLeafs(ctx).forEach(semantics::remove);
int start = ctx.getStart().getStartIndex() + initialOffset;
int end = start + ctx.getText().length();
tokens.add(new SemanticTokenData(start, end, "property", new String[0]));
}
@Override
public void exitDefaultValue(DefaultValueContext ctx) {
AntlrUtils.getAllLeafs(ctx).forEach(semantics::remove);
if (ctx.Colon() != null) {
semantics.put(ctx.Colon().getSymbol(), "operator");
}
if (ctx.value() != null) {
ValueContext valueCtx = ctx.value();
int start = valueCtx.getStart().getStartIndex() + initialOffset;
int end = start + valueCtx.getText().length();
tokens.add(new SemanticTokenData(start, end, "string", new String[0]));
}
}
@Override
public void visitTerminal(TerminalNode node) {
processTerminalNode(node);
}
@Override
public void visitErrorNode(ErrorNode node) {
processTerminalNode(node);
}
private void processTerminalNode(TerminalNode node) {
Token token = node.getSymbol();
switch (token.getType()) {
case PropertyPlaceHolderLexer.Space:
case PropertyPlaceHolderLexer.LineBreak:
case PropertyPlaceHolderLexer.EOF:
break;
case PropertyPlaceHolderLexer.Colon:
semantics.put(node.getSymbol(), "operator");
break;
default:
semantics.put(node.getSymbol(), "property");
}
}
});
parser.addErrorListener(new ANTLRErrorListener() {
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine,
String msg, RecognitionException e) {
parseErrorHandler.ifPresent(h -> h.accept(e));
}
@Override
public void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction,
ATNConfigSet configs) {
}
@Override
public void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex,
BitSet conflictingAlts, ATNConfigSet configs) {
}
@Override
public void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact,
BitSet ambigAlts, ATNConfigSet configs) {
}
});
parser.start();
tokens.addAll(semantics.entrySet().stream()
.map(e -> new SemanticTokenData(e.getKey().getStartIndex() + initialOffset,
e.getKey().getStartIndex() + e.getKey().getText().length() + initialOffset, e.getValue(),
new String[0]))
.collect(Collectors.toList()));
Collections.sort(tokens);
return tokens;
}
}

View File

@@ -10,20 +10,29 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.spel;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.ParseTreeListener;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.springframework.ide.vscode.boot.java.SpelProblemType;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.AntlrReconciler;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.parser.placeholder.PropertyPlaceHolderLexer;
import org.springframework.ide.vscode.parser.placeholder.PropertyPlaceHolderParser;
import org.springframework.ide.vscode.parser.spel.SpelLexer;
import org.springframework.ide.vscode.parser.spel.SpelParser;
public class SpelReconciler extends AntlrReconciler {
private boolean enabled;
private AntlrReconciler propertyHolderReconciler;
public SpelReconciler() {
super("SPEL", SpelParser.class, SpelLexer.class, "spelExpr", SpelProblemType.JAVA_SPEL_EXPRESSION_SYNTAX);
this.errorOnUnrecognizedTokens = false;
this.enabled = true;
this.propertyHolderReconciler = new AntlrReconciler("Place-Holder", PropertyPlaceHolderParser.class, PropertyPlaceHolderLexer.class, "start", SpelProblemType.PROPERTY_PLACE_HOLDER_SYNTAX);
}
public void setEnabled(boolean spelExpressionValidationEnabled) {
@@ -38,4 +47,42 @@ public class SpelReconciler extends AntlrReconciler {
super.reconcile(text, startPosition, problemCollector);
}
@Override
protected Parser createParser(String text, int startPosition, IProblemCollector problemCollector) throws Exception {
Parser parser = super.createParser(text, startPosition, problemCollector);
// Reconcile embedded SPEL
parser.addParseListener(new ParseTreeListener() {
private void processTerminal(TerminalNode node) {
if (node.getSymbol().getType() == SpelLexer.PROPERTY_PLACE_HOLDER) {
int placeHolderStartPosition = startPosition + node.getSymbol().getStartIndex() + 2;
String content = node.getSymbol().getText().substring(2, node.getSymbol().getText().length() - 1);
propertyHolderReconciler.reconcile(content, placeHolderStartPosition, problemCollector);
}
}
@Override
public void visitTerminal(TerminalNode node) {
processTerminal(node);
}
@Override
public void visitErrorNode(ErrorNode node) {
processTerminal(node);
}
@Override
public void enterEveryRule(ParserRuleContext ctx) {
}
@Override
public void exitEveryRule(ParserRuleContext ctx) {
}
});
return parser;
}
}

View File

@@ -14,8 +14,10 @@ import static org.springframework.ide.vscode.parser.spel.SpelLexer.*;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -50,9 +52,11 @@ import org.springframework.ide.vscode.parser.spel.SpelParserBaseListener;
public class SpelSemanticTokens implements SemanticTokensDataProvider {
private final Optional<Consumer<RecognitionException>> parseErrorHandler;
private final PropertyPlaceHolderSemanticTokens propertyPlaceHolderSemanticTokens;
public SpelSemanticTokens(Optional<Consumer<RecognitionException>> parseErrorHandler) {
this.parseErrorHandler = parseErrorHandler;
this.propertyPlaceHolderSemanticTokens = new PropertyPlaceHolderSemanticTokens(parseErrorHandler);
}
public SpelSemanticTokens() {
@@ -61,7 +65,9 @@ public class SpelSemanticTokens implements SemanticTokensDataProvider {
@Override
public List<String> getTokenTypes() {
return List.of("operator", "keyword", "type", "string", "number", "method", "property", "parameter");
LinkedHashSet<String> tokenTypes = new LinkedHashSet<>(List.of("operator", "keyword", "type", "string", "number", "method", "property", "parameter"));
tokenTypes.addAll(propertyPlaceHolderSemanticTokens.getTokenTypes());
return tokenTypes.stream().toList();
}
@Override
@@ -151,6 +157,9 @@ public class SpelSemanticTokens implements SemanticTokensDataProvider {
case DOUBLE_QUOTED_STRING:
semantics.put(node.getSymbol(), "string");
break;
case PROPERTY_PLACE_HOLDER:
tokens.addAll(computeTokensFromPropertyPlaceHolderNode(node, initialOffset));
break;
}
}
@@ -260,5 +269,24 @@ public class SpelSemanticTokens implements SemanticTokensDataProvider {
return tokens;
}
private Collection<? extends SemanticTokenData> computeTokensFromPropertyPlaceHolderNode(TerminalNode node,
int initialOffset) {
List<SemanticTokenData> placeHolderTokens = new ArrayList<>();
int startPosition = initialOffset + node.getSymbol().getStartIndex();
int placeHolderStartPosition = startPosition + 2;
int endPosition = startPosition + node.getText().length();
int placeHolderEndPosition = endPosition - 1;
// '${' operator
placeHolderTokens.add(new SemanticTokenData(startPosition, placeHolderStartPosition, "operator", new String[0]));
// Property Place Holder contents
placeHolderTokens.addAll(propertyPlaceHolderSemanticTokens.computeTokens(node.getText().substring(2, node.getText().length() - 1), placeHolderStartPosition));
// '}' operator
placeHolderTokens.add(new SemanticTokenData(placeHolderEndPosition, endPosition, "operator", new String[0]));
return placeHolderTokens;
}
}

View File

@@ -334,6 +334,12 @@
"label": "SpEL Expression Syntax",
"description": "SpEL parser raised a ParseException",
"defaultSeverity": "ERROR"
},
{
"code": "PROPERTY_PLACE_HOLDER_SYNTAX",
"label": "Property Place Holder Syntax",
"description": "Property place holder raised a ParseException",
"defaultSeverity": "ERROR"
}
]
},

View File

@@ -0,0 +1,59 @@
package org.springframework.ide.vscode.boot.java.spel;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
public class PropertyPlaceHolderSemanticTokensTest {
private PropertyPlaceHolderSemanticTokens provider = new PropertyPlaceHolderSemanticTokens(Optional.of(Assertions::fail));
@Test
void propertyWithDefault() {
List<SemanticTokenData> tokens = provider.computeTokens("server.port:5673", 0);
assertThat(tokens.size()).isEqualTo(3);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 11, "property", new String[0]));
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(11, 12, "operator", new String[0]));
assertThat(tokens.get(2)).isEqualTo(new SemanticTokenData(12, 16, "string", new String[0]));
}
@Test
void propertyOnly() {
List<SemanticTokenData> tokens = provider.computeTokens("server.port", 0);
assertThat(tokens.size()).isEqualTo(1);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 11, "property", new String[0]));
}
@Test
void propertyWithEmptyDefaulValue() {
List<SemanticTokenData> tokens = provider.computeTokens("server.port:", 0);
assertThat(tokens.size()).isEqualTo(3);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 11, "property", new String[0]));
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(11, 12, "operator", new String[0]));
assertThat(tokens.get(2)).isEqualTo(new SemanticTokenData(12, 12, "string", new String[0]));
}
@Test
void error_1() {
provider = new PropertyPlaceHolderSemanticTokens(Optional.empty());
List<SemanticTokenData> tokens = provider.computeTokens("server.:", 0);
assertThat(tokens.size()).isEqualTo(3);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 6, "property", new String[0]));
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(6, 7, "property", new String[0]));
assertThat(tokens.get(2)).isEqualTo(new SemanticTokenData(7, 8, "operator", new String[0]));
}
@Test
void error_2() {
provider = new PropertyPlaceHolderSemanticTokens(Optional.empty());
List<SemanticTokenData> tokens = provider.computeTokens("server.", 0);
assertThat(tokens.size()).isEqualTo(2);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 6, "property", new String[0]));
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(6, 7, "property", new String[0]));
}
}

View File

@@ -143,5 +143,17 @@ public class SpelSemanticTokensTest {
assertThat(tokens.get(8)).isEqualTo(new SemanticTokenData(21, 22, "operator", new String[0])); // )
}
@Test
void withPropertyPlaceHolder() {
List<SemanticTokenData> tokens = provider.computeTokens("${server.port:8080} == 8080", 0);
assertThat(tokens.size()).isEqualTo(7);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 2, "operator", new String[0])); // ${
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(2, 13, "property", new String[0])); // server.port
assertThat(tokens.get(2)).isEqualTo(new SemanticTokenData(13, 14, "operator", new String[0])); // :
assertThat(tokens.get(3)).isEqualTo(new SemanticTokenData(14, 18, "string", new String[0])); // 8080
assertThat(tokens.get(4)).isEqualTo(new SemanticTokenData(18, 19, "operator", new String[0])); // }
assertThat(tokens.get(5)).isEqualTo(new SemanticTokenData(20, 22, "operator", new String[0])); // ==
assertThat(tokens.get(6)).isEqualTo(new SemanticTokenData(23, 27, "number", new String[0])); // 8080
}
}

View File

@@ -12,6 +12,7 @@ package org.springframework.ide.vscode.boot.java.value.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.File;
import java.net.URI;
@@ -43,6 +44,7 @@ import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.editor.harness.PropertyIndexHarness;
import org.springframework.ide.vscode.boot.index.cache.IndexCache;
import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid;
import org.springframework.ide.vscode.boot.java.SpelProblemType;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine;
import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory;
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
@@ -330,7 +332,7 @@ public class ValueSpelExpressionValidationTest {
}
@Test
void testIgnoreSpelExpressionsWithPropertyPlaceholder() throws Exception {
void testSpelExpressionsWithPropertyPlaceholder_noErrors() throws Exception {
TextDocument doc = prepareDocument("@Value(\"onField\")", "@Value(value=\"#{${property.hello:false}}\")");
assertNotNull(doc);
@@ -340,6 +342,23 @@ public class ValueSpelExpressionValidationTest {
assertEquals(0, problems.size());
}
@Test
void testSpelExpressionsWithPropertyPlaceholder_withErrors() throws Exception {
TextDocument doc = prepareDocument("@Value(\"onField\")", "@Value(value=\"#{${property.}}\")");
assertNotNull(doc);
reconcileEngine.reconcile(doc, problemCollector);
List<ReconcileProblem> problems = problemCollector.getCollectedProblems();
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(199, problem.getOffset());
assertEquals(0, problem.getLength());
assertEquals(SpelProblemType.PROPERTY_PLACE_HOLDER_SYNTAX, problem.getType());
assertTrue(problem.getMessage().startsWith("Place-Holder:"));
}
private TextDocument prepareDocument(String selectedAnnotation, String annotationStatementBeforeTest) throws Exception {
String content = IOUtils.toString(new URI(docUri), StandardCharsets.UTF_8);