This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
The following commit(s) were added to refs/heads/master by this push: new e0e3e690 JEXL-413: added option and tests; e0e3e690 is described below commit e0e3e690fa86312896ca313f61a10321fc24f899 Author: Henri Biestro <hbies...@cloudera.com> AuthorDate: Sun Nov 12 12:33:41 2023 -0800 JEXL-413: added option and tests; --- .../org/apache/commons/jexl3/JexlException.java | 5 +- .../java/org/apache/commons/jexl3/JexlOptions.java | 23 +++++++- .../org/apache/commons/jexl3/internal/Engine.java | 3 ++ .../apache/commons/jexl3/internal/Interpreter.java | 17 +++--- .../commons/jexl3/internal/InterpreterBase.java | 10 ++++ .../org/apache/commons/jexl3/Issues400Test.java | 61 +++++++++++++++++++--- 6 files changed, 104 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlException.java b/src/main/java/org/apache/commons/jexl3/JexlException.java index a6f10ec6..c60b7296 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlException.java +++ b/src/main/java/org/apache/commons/jexl3/JexlException.java @@ -504,7 +504,9 @@ public class JexlException extends RuntimeException { /** The variable is already declared. */ REDEFINED, /** The variable has a null value. */ - NULLVALUE; + NULLVALUE, + /** THe variable is const and an attempt is made to assign it*/ + CONST; /** * Stringifies the variable issue. @@ -515,6 +517,7 @@ public class JexlException extends RuntimeException { switch(this) { case NULLVALUE : return VARQUOTE + var + "' is null"; case REDEFINED : return VARQUOTE + var + "' is already defined"; + case CONST : return VARQUOTE + var + "' is const"; case UNDEFINED : default: return VARQUOTE + var + "' is undefined"; } diff --git a/src/main/java/org/apache/commons/jexl3/JexlOptions.java b/src/main/java/org/apache/commons/jexl3/JexlOptions.java index 66fcaa90..79f51d1c 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlOptions.java +++ b/src/main/java/org/apache/commons/jexl3/JexlOptions.java @@ -42,6 +42,8 @@ import org.apache.commons.jexl3.internal.Engine; * @since 3.2 */ public final class JexlOptions { + /** The const capture bit. */ + private static final int CONST_CAPTURE = 8; /** The shared instance bit. */ private static final int SHARED = 7; /** The local shade bit. */ @@ -60,7 +62,7 @@ public final class JexlOptions { private static final int CANCELLABLE = 0; /** The flag names ordered. */ private static final String[] NAMES = { - "cancellable", "strict", "silent", "safe", "lexical", "antish", "lexicalShade", "sharedInstance" + "cancellable", "strict", "silent", "safe", "lexical", "antish", "lexicalShade", "sharedInstance", "constCapture" }; /** Default mask .*/ private static int DEFAULT = 1 /*<< CANCELLABLE*/ | 1 << STRICT | 1 << ANTISH | 1 << SAFE; @@ -299,6 +301,25 @@ public final class JexlOptions { } } + /** + * Sets whether lambda captured-variables are const or not. + * <p> + * When disabled, lambda-captured variables are implicitly converted to read-write local variable (let), + * when enabled, those are implicitly converted to read-only local variables (const). + * </p> + * @param flag true to enable, false to disable + */ + public void setConstCapture(final boolean flag) { + flags = set(CONST_CAPTURE, flags, true); + } + + /** + * @return true if lambda captured-variables are const, false otherwise + */ + public boolean isConstCapture() { + return isSet(CONST_CAPTURE, flags); + } + /** * Sets the arithmetic math context. * @param mcontext the context diff --git a/src/main/java/org/apache/commons/jexl3/internal/Engine.java b/src/main/java/org/apache/commons/jexl3/internal/Engine.java index f381fff2..fa304700 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java @@ -411,6 +411,9 @@ public class Engine extends JexlEngine { if (scriptFeatures.isLexicalShade()) { opts.setLexicalShade(true); } + if (scriptFeatures.supportsConstCapture()) { + opts.setConstCapture(true); + } } if (script != null) { // process script pragmas if any 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 2f99a7de..61b5bed8 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -1486,13 +1486,18 @@ public class Interpreter extends InterpreterBase { if (left instanceof ASTIdentifier) { variable = (ASTIdentifier) left; symbol = variable.getSymbol(); - if (symbol >= 0 && (variable.isLexical() || options.isLexical())) { - if (variable instanceof ASTVar) { - if (!defineVariable((ASTVar) variable, block)) { - return redefinedVariable(variable, variable.getName()); + if (symbol >= 0) { + if (variable.isLexical() || options.isLexical()) { + if (variable instanceof ASTVar) { + if (!defineVariable((ASTVar) variable, block)) { + return redefinedVariable(variable, variable.getName()); + } + } else if (variable.isShaded() && (variable.isLexical() || options.isLexicalShade())) { + return undefinedVariable(variable, variable.getName()); } - } else if (variable.isShaded() && (variable.isLexical() || options.isLexicalShade())) { - return undefinedVariable(variable, variable.getName()); + } + if (variable.isCaptured() && options.isConstCapture()) { + return constVariable(variable, variable.getName()); } } } else { diff --git a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java index 55461117..967b393c 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java +++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java @@ -476,6 +476,16 @@ public abstract class InterpreterBase extends ParserVisitor { return variableError(node, var, VariableIssue.REDEFINED); } + /** + * Triggered when a captured variable is const and assignment is attempted. + * @param node the node where the error originated from + * @param var the variable name + * @return throws JexlException if strict and not silent, null otherwise + */ + protected Object constVariable(final JexlNode node, final String var) { + return variableError(node, var, VariableIssue.CONST); + } + /** * Check if a null evaluated expression is protected by a ternary expression. * <p> diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index b01492d8..af1d565b 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -235,25 +235,72 @@ public class Issues400Test { @Test public void test412() { - Map<Object,Object> ctl = new HashMap<>(); + final Map<Object, Object> ctl = new HashMap<>(); ctl.put("one", 1); ctl.put("two", 2); - String fnsrc = "function f(x) { x }\n" + + final String fnsrc = "function f(x) { x }\n" + "let one = 'one', two = 'two';\n" + "{ one : f(1), two:f(2) }"; final JexlContext jc = new MapContext(); - final String[] sources = { - fnsrc - }; final JexlEngine jexl = new JexlBuilder().create(); try { final JexlScript e = jexl.createScript(fnsrc); final Object o = e.execute(jc); Assert.assertTrue(o instanceof Map); - Map<?,?> map = (Map<?, ?>) o; + Map<?, ?> map = (Map<?, ?>) o; Assert.assertEquals(map, ctl); - } catch(JexlException.Parsing xparse) { + } catch (JexlException.Parsing xparse) { Assert.fail(fnsrc + " : " + xparse.getMessage()); } } + + @Test + public void test413a() { + final JexlBuilder builder = new JexlBuilder(); + final JexlEngine jexl = builder.create(); + final JexlScript script = jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z"); + final Number result = (Number) script.execute(null, 12); + Assert.assertEquals(54, result); + } + + @Test + public void test413b() { + final JexlBuilder builder = new JexlBuilder(); + final JexlOptions options = builder.options(); + options.setConstCapture(true); + options.setLexical(true); + final JexlEngine jexl = builder.create(); + final JexlScript script = jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z"); + try { + final Number result = (Number) script.execute(null, 12); + Assert.fail("c should be const"); + } catch(JexlException.Variable xvar) { + Assert.assertEquals("c", xvar.getVariable()); + } + } + + @Test + public void test413c() { + final JexlBuilder builder = new JexlBuilder(); + final JexlEngine jexl = builder.create(); + final JexlScript script = jexl.createScript("#pragma jexl.options '+constCapture'\nvar c = 42; var f = y -> c += y; f(z)", "z"); + try { + Number result = (Number) script.execute(null, 12); + Assert.fail("c should be const"); + } catch(JexlException.Variable xvar) { + Assert.assertEquals("c", xvar.getVariable()); + } + } + + @Test + public void test413d() { + final JexlBuilder builder = new JexlBuilder().features(new JexlFeatures().constCapture(true)); + final JexlEngine jexl = builder.create(); + try { + final JexlScript script = jexl.createScript("var c = 42; var f = y -> c += y; f(z)", "z"); + Assert.fail("c should be const"); + } catch(JexlException.Parsing xvar) { + Assert.assertTrue(xvar.getMessage().contains("const")); + } + } }