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 <[email protected]>
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();