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
The following commit(s) were added to refs/heads/JEXL-367 by this push: new b7a89c98 JEXL-367: adding fat-arrow vs thin-arrow lambda syntax; add named function syntax; b7a89c98 is described below commit b7a89c985324f94376da51278c93cd347fe11ac8 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/JexlArithmetic.java | 6 +- .../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 | 25 ++++++--- .../org/apache/commons/jexl3/Issues300Test.java | 12 ++++ .../apache/commons/jexl3/jexl342/OptionalTest.java | 64 ++++++++++++++++++++++ 8 files changed, 244 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index b34d3678..9b0929f9 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -497,7 +497,7 @@ public class JexlArithmetic { } /** - * Given a Number, return back the value using the smallest type the result + * Given a Number, return the value using the smallest type the result * will fit into. * <p>This works hand in hand with parameter 'widening' in java * method calls, e.g. a call to substring(int,int) with an int and a long @@ -1390,8 +1390,8 @@ public class JexlArithmetic { return 0; } if (isNumberable(left) || isNumberable(right)) { - final long lhs = toLong(left); - final long rhs = toLong(right); + final long lhs = toLong(left instanceof String? Double.parseDouble((String) left) : left); + final long rhs = toLong(right instanceof String? Double.parseDouble((String) right) : right); if (lhs < rhs) { return -1; } 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 dcd6ea4b..4b089b4d 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 469e184d..acfd2c50 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -980,7 +980,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 d893a0fc..aea15f5a 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; @@ -376,6 +376,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> @@ -561,6 +582,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 94efc9d1..090ae4cd 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -135,7 +135,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(); } @@ -443,6 +444,13 @@ void DeclareVar(boolean lexical) #Var : { t=<IDENTIFIER> { declareVariable(jjtThis, t, lexical); } } +void DeclareFunction(Scope scope) #Var : +{ + Token t; +} +{ + t=<IDENTIFIER> { declareFunction(jjtThis, t, scope); } +} void Pragma() #void : { @@ -860,23 +868,26 @@ void Parameters() #void : {} void LambdaLookahead() #void : {} { - <FUNCTION> Parameters() + <FUNCTION> (<IDENTIFIER>)? Parameters() | - Parameters() <LAMBDA> + Parameters() (<LAMBDA> | <FATARROW>) | - Parameter() <LAMBDA> + Parameter() (<LAMBDA> | <FATARROW>) } void Lambda() #JexlLambda : { + 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); + } } diff --git a/src/test/java/org/apache/commons/jexl3/jexl342/OptionalTest.java b/src/test/java/org/apache/commons/jexl3/jexl342/OptionalTest.java index ea28e115..13d18a5f 100644 --- a/src/test/java/org/apache/commons/jexl3/jexl342/OptionalTest.java +++ b/src/test/java/org/apache/commons/jexl3/jexl342/OptionalTest.java @@ -16,18 +16,24 @@ */ package org.apache.commons.jexl3.jexl342; +import org.apache.commons.jexl3.JexlArithmetic; import org.apache.commons.jexl3.JexlBuilder; +import org.apache.commons.jexl3.JexlContext; import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlException; import org.apache.commons.jexl3.JexlInfo; import org.apache.commons.jexl3.JexlScript; +import org.apache.commons.jexl3.MapContext; import org.apache.commons.jexl3.introspection.JexlUberspect; import org.junit.Assert; import org.junit.Test; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; public class OptionalTest { @@ -45,6 +51,64 @@ public class OptionalTest { } } + public static class StreamContext extends MapContext { + public Stream map(Collection<Object> c, JexlScript s) { + JexlContext context = JexlEngine.getThreadContext(); + return c.stream().map(a->s.execute(context, a)); + } + public Object reduce(Stream<Object> stream, JexlScript script) { + return stream.reduce((identity, element)->{ + JexlContext context = JexlEngine.getThreadContext(); + return script.execute(context, identity, element); + }); + } + } + + @Test + public void testStream() { + String src = "[1, 2, 3, ...].map(x -> x * x).reduce((acc, x)->acc + x).intValue()"; + JexlBuilder builder = new JexlBuilder(); + JexlUberspect uber = builder.create().getUberspect(); + JexlEngine jexl = builder.uberspect(new ReferenceUberspect(uber)).safe(false).create(); + JexlInfo info = new JexlInfo("testStream", 1, 1); + MapContext context = new StreamContext(); + JexlScript script = jexl.createScript(src, "list"); + Object result = script.execute(context, Arrays.asList(1, 2, 3)); + Assert.assertEquals(14, result); + //Optional<?> result = (Optional<?>) script.execute(context, Arrays.asList(1, 2, 3)); + //Assert.assertEquals(14, result.get()); + } + + public static class OptionalArithmetic extends JexlArithmetic { + public OptionalArithmetic(boolean astrict) { + super(astrict); + } + public Object add(Optional<?> lhs, Optional<?> rhs) { + return add(lhs.get(), rhs.get()); + } + public Object add(Object lhs, Optional<?> rhs) { + return add(lhs, rhs.get()); + } + public Object add(Optional<?> lhs, Object rhs) { + return add(lhs, rhs); + } + } + + @Test + public void testOptionalArgs() { + JexlBuilder builder = new JexlBuilder(); + JexlArithmetic jexla = new OptionalArithmetic(true); + JexlUberspect uber = builder.create().getUberspect(); + JexlEngine jexl = builder.uberspect(new ReferenceUberspect(uber)).arithmetic(jexla).safe(false).create(); + JexlInfo info = new JexlInfo("testStream", 1, 1); + MapContext context = new StreamContext(); + String src = "x + x"; + JexlScript script = jexl.createScript(src, "x"); + Optional<Integer> x = Optional.of(21); + Object result = script.execute(context, x); + Assert.assertEquals(42, result); + } + @Test public void test342() { JexlBuilder builder = new JexlBuilder();