This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch JEXL-367 in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
commit 46273739be4fc04456c0655475e0944e50a385b1 Author: henrib <hen...@apache.org> AuthorDate: Thu May 5 00:06:59 2022 +0200 JEXL-367: adding fat-arrow vs thin-arrow lambda syntax; add named function syntax; --- .../org/apache/commons/jexl3/JexlFeatures.java | 50 ++++++++++++++++++- .../apache/commons/jexl3/internal/Debugger.java | 58 +++++++++++++++++++--- .../apache/commons/jexl3/internal/Interpreter.java | 10 +++- .../apache/commons/jexl3/parser/JexlParser.java | 40 ++++++++++++++- .../org/apache/commons/jexl3/parser/Parser.jjt | 27 +++++++--- .../org/apache/commons/jexl3/Issues300Test.java | 12 +++++ 6 files changed, 178 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlFeatures.java b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java index 15d07e72..06ed52a5 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlFeatures.java +++ b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java @@ -59,7 +59,8 @@ public final class JexlFeatures { private static final String[] F_NAMES = { "register", "reserved variable", "local variable", "assign/modify", "global assign/modify", "array reference", "create instance", "loop", "function", - "method call", "set/map/array literal", "pragma", "annotation", "script", "lexical", "lexicalShade" + "method call", "set/map/array literal", "pragma", "annotation", "script", "lexical", "lexicalShade", + "thin-arrow", "fat-arrow" }; /** Registers feature ordinal. */ private static final int REGISTER = 0; @@ -93,6 +94,10 @@ public final class JexlFeatures { public static final int LEXICAL = 14; /** Lexical shade feature ordinal. */ public static final int LEXICAL_SHADE = 15; + /** Fat-arrow lambda syntax. */ + public static final int THIN_ARROW = 16; + /** Fat-arrow lambda syntax. */ + public static final int FAT_ARROW = 17; /** * Creates an all-features-enabled instance. @@ -109,7 +114,8 @@ public final class JexlFeatures { | (1L << STRUCTURED_LITERAL) | (1L << PRAGMA) | (1L << ANNOTATION) - | (1L << SCRIPT); + | (1L << SCRIPT) + | (1L << THIN_ARROW); reservedNames = Collections.emptySet(); nameSpaces = TEST_STR_FALSE; } @@ -433,6 +439,46 @@ public final class JexlFeatures { return getFeature(LAMBDA); } + /** + * Sets whether thin-arrow lambda syntax is enabled. + * <p> + * When disabled, parsing a script/expression using syntactic thin-arrow (->) + * will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures thinArrow(final boolean flag) { + setFeature(THIN_ARROW, flag); + return this; + } + + /** + * @return true if thin-arrow lambda syntax is enabled, false otherwise + */ + public boolean supportsThinArrow() { + return getFeature(THIN_ARROW); + } + + /** + * Sets whether fat-arrow lambda syntax is enabled. + * <p> + * When disabled, parsing a script/expression using syntactic fat-arrow (=>) + * will throw a parsing exception. + * @param flag true to enable, false to disable + * @return this features instance + */ + public JexlFeatures fatArrow(final boolean flag) { + setFeature(FAT_ARROW, flag); + return this; + } + + /** + * @return true if fat-arrow lambda syntax is enabled, false otherwise + */ + public boolean supportsFatArrow() { + return getFeature(FAT_ARROW); + } + /** * Sets whether pragma constructs are enabled. * <p> diff --git a/src/main/java/org/apache/commons/jexl3/internal/Debugger.java b/src/main/java/org/apache/commons/jexl3/internal/Debugger.java index 106a060f..6617c3db 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Debugger.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Debugger.java @@ -18,6 +18,7 @@ package org.apache.commons.jexl3.internal; import org.apache.commons.jexl3.JexlExpression; +import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.JexlInfo; import org.apache.commons.jexl3.JexlScript; import org.apache.commons.jexl3.parser.*; @@ -47,6 +48,8 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { protected int indent = 2; /** accept() relative depth. */ protected int depth = Integer.MAX_VALUE; + /** Arrow symbol. */ + protected String arrow = "->"; /** * Creates a Debugger. @@ -68,6 +71,36 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { depth = Integer.MAX_VALUE; } + /** + * Tries (hard) to find the features used to parse a node. + * @param node the node + * @return the features or null + */ + protected JexlFeatures getFeatures(JexlNode node) { + JexlNode walk = node; + while(walk != null) { + if (walk instanceof ASTJexlScript) { + ASTJexlScript script = (ASTJexlScript) walk; + return script.getFeatures(); + } + walk = walk.jjtGetParent(); + } + return null; + } + + /** + * Sets the arrow style (fat or thin) depending on features. + * @param node the node to start seeking features from. + */ + protected void setArrowSymbol(JexlNode node) { + JexlFeatures features = getFeatures(node); + if (features != null && features.supportsFatArrow() && !features.supportsThinArrow()) { + arrow = "=>"; + } else { + arrow = "->"; + } + } + /** * Position the debugger on the root of an expression. * @param jscript the expression @@ -75,7 +108,8 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { */ public boolean debug(final JexlExpression jscript) { if (jscript instanceof Script) { - return debug(((Script) jscript).script); + Script script = (Script) jscript; + return debug(script.script); } return false; } @@ -87,7 +121,8 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { */ public boolean debug(final JexlScript jscript) { if (jscript instanceof Script) { - return debug(((Script) jscript).script); + Script script = (Script) jscript; + return debug(script.script); } return false; } @@ -111,6 +146,7 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { start = 0; end = 0; indentLevel = 0; + setArrowSymbol(node); if (node != null) { builder.setLength(0); cause = node; @@ -144,6 +180,7 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { start = 0; end = 0; indentLevel = 0; + setArrowSymbol(node); if (node != null) { builder.setLength(0); cause = node; @@ -675,15 +712,22 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { @Override protected Object visit(final ASTJexlScript node, Object arg) { Object data = arg; + boolean named = false; // if lambda, produce parameters if (node instanceof ASTJexlLambda) { final ASTJexlLambda lambda = (ASTJexlLambda) node; final JexlNode parent = node.jjtGetParent(); // use lambda syntax if not assigned boolean expr = isLambdaExpr(lambda); - final boolean named = parent instanceof ASTAssignment; - if (named && !expr) { + named = node.jjtGetChild(0) instanceof ASTVar; + final boolean assigned = parent instanceof ASTAssignment || named; + if (assigned && !expr) { builder.append("function"); + if (named) { + ASTVar avar = (ASTVar) node.jjtGetChild(0); + builder.append(' '); + builder.append(avar.getName()); + } } builder.append('('); final String[] params = lambda.getParameters(); @@ -695,11 +739,11 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { } } builder.append(')'); - if (named && !expr) { + if (assigned && !expr) { // block follows builder.append(' '); } else { - builder.append("->"); + builder.append(arrow); // add a space if lambda expr otherwise block follows if (expr) { builder.append(' '); @@ -711,7 +755,7 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { if (num == 1 && !(node instanceof ASTJexlLambda)) { data = accept(node.jjtGetChild(0), data); } else { - for (int i = 0; i < num; ++i) { + for (int i = named? 1 : 0; i < num; ++i) { final JexlNode child = node.jjtGetChild(i); acceptStatement(child, data); } diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index 19358524..8c3e7351 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -983,7 +983,15 @@ public class Interpreter extends InterpreterBase { @Override protected Object visit(final ASTJexlScript script, final Object data) { if (script instanceof ASTJexlLambda && !((ASTJexlLambda) script).isTopLevel()) { - return new Closure(this, (ASTJexlLambda) script); + Closure closure = new Closure(this, (ASTJexlLambda) script); + // if the function is named, assign in the local frame + JexlNode child0 = script.jjtGetChild(0); + if (child0 instanceof ASTVar) { + ASTVar var = (ASTVar) child0; + this.visit(var, data); + frame.set(var.getSymbol(), closure); + } + return closure; } block = new LexicalFrame(frame, block).defineArgs(); try { diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java index 709695f2..990c4c57 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java @@ -20,8 +20,8 @@ import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlException; import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.JexlInfo; -import org.apache.commons.jexl3.internal.Scope; import org.apache.commons.jexl3.internal.LexicalScope; +import org.apache.commons.jexl3.internal.Scope; import java.io.BufferedReader; import java.io.IOException; @@ -381,6 +381,27 @@ public abstract class JexlParser extends StringParser { return block == null || block.declareSymbol(symbol); } + /** + * Declares a local function. + * @param variable the identifier used to declare + * @param token the variable name toekn + */ + protected void declareFunction(final ASTVar variable, final Token token, Scope scope) { + final String name = token.image; + final int symbol = scope.declareVariable(name); + variable.setSymbol(symbol, name); + if (scope.isCapturedSymbol(symbol)) { + variable.setCaptured(true); + } + // lexical feature error + if (!declareSymbol(symbol)) { + if (getFeatures().isLexical()) { + throw new JexlException(variable, name + ": variable is already declared"); + } + variable.setRedefined(true); + } + } + /** * Declares a local variable. * <p> This method creates an new entry in the symbol map. </p> @@ -600,6 +621,23 @@ public abstract class JexlParser extends StringParser { featureController.controlNode(node); } + /** + * Check fat vs thin arrow syntax feature. + * @param token the arrow token + */ + protected void checkLambda(Token token) { + final String arrow = token.image; + if ("->".equals(arrow)) { + if (!getFeatures().supportsThinArrow()) { + throwFeatureException(JexlFeatures.THIN_ARROW, token); + } + return; + } + if ("=>".equals(arrow) && !getFeatures().supportsFatArrow()) { + throwFeatureException(JexlFeatures.FAT_ARROW, token); + } + } + /** * Throws Ambiguous exception. * <p>Seeks the end of the ambiguous statement to recover. diff --git a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt index 05eca5ad..b25f529d 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -136,7 +136,8 @@ TOKEN_MGR_DECLS : { | < FALSE : "false" > { popDot(); } | < RETURN : "return" > { popDot(); } | < FUNCTION : "function" > { popDot(); } - | < LAMBDA : "->" | "=>" > { popDot(); } + | < LAMBDA : "->" > { popDot(); } + | < FATARROW : "=>" > { popDot(); } | < BREAK : "break" > { popDot(); } | < CONTINUE : "continue" > { popDot(); } | < PRAGMA : "#pragma" > { popDot(); } @@ -453,6 +454,13 @@ void DeclareVar(boolean lexical, boolean constant) #Var : { t=<IDENTIFIER> { declareVariable(jjtThis, t, lexical, constant); } } +void DeclareFunction(Scope scope) #Var : +{ + Token t; +} +{ + t=<IDENTIFIER> { declareFunction(jjtThis, t, scope); } +} void Pragma() #void : { @@ -878,23 +886,26 @@ void ParametersLookahead() #void : {} void LambdaLookahead() #void : {} { - <FUNCTION> ParametersLookahead() + <FUNCTION> (<IDENTIFIER>)? ParametersLookahead() | - ParametersLookahead() <LAMBDA> + ParametersLookahead() (<LAMBDA> | <FATARROW>) | - <IDENTIFIER> <LAMBDA> + <IDENTIFIER> (<LAMBDA> | <FATARROW>) } void Lambda() #JexlLambda : { - pushScope(); + Token arrow; + Token name; + Scope scope = getFrame(); + pushFrame(); } { - { pushUnit(jjtThis); } <FUNCTION> Parameters() ( LOOKAHEAD(3) Block() | Expression()) { popUnit(jjtThis); } + { pushUnit(jjtThis); } <FUNCTION> (LOOKAHEAD(<IDENTIFIER>) DeclareFunction(scope))? Parameters() ( LOOKAHEAD(3) Block() | Expression()) { popUnit(jjtThis); } | - { pushUnit(jjtThis); } Parameters() <LAMBDA> ( LOOKAHEAD(3) Block() | Expression()) { popUnit(jjtThis); } + { pushUnit(jjtThis); } Parameters() (arrow=<LAMBDA> | arrow=<FATARROW>) ( LOOKAHEAD(3) Block() | Expression()) { checkLambda(arrow); popUnit(jjtThis); } | - { pushUnit(jjtThis); } Parameter() <LAMBDA> ( LOOKAHEAD(3) Block() | Expression()) { popUnit(jjtThis); } + { pushUnit(jjtThis); } Parameter() (arrow=<LAMBDA> | arrow=<FATARROW>)( LOOKAHEAD(3) Block() | Expression()) { checkLambda(arrow); popUnit(jjtThis); } } diff --git a/src/test/java/org/apache/commons/jexl3/Issues300Test.java b/src/test/java/org/apache/commons/jexl3/Issues300Test.java index 7cdf7b15..c5de60bd 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues300Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues300Test.java @@ -768,4 +768,16 @@ public class Issues300Test { Object result = script.execute(null); return result; } + + + @Test public void test367() { + String text = "var toto; function foo(x) { x }; var tata = 3; foo(3)"; + JexlEngine jexl = new JexlBuilder().safe(true).create(); + JexlScript script = jexl.createScript(text); + Object result = script.execute(null); + Assert.assertEquals(3, result); + String s0 = script.getParsedText(); + String s1 = script.getSourceText(); + Assert.assertNotEquals(s0, s1); + } }