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 589b0888 JEXL-404 : add syntax for safe array access ( ?[..] ); - update interpreter and debugger; - add test; - update syntax reference, release notes, changes; 589b0888 is described below commit 589b0888142541085714bbd7b0fc7e3615ae2727 Author: Henri Biestro <hbies...@cloudera.com> AuthorDate: Wed Aug 30 16:29:07 2023 +0200 JEXL-404 : add syntax for safe array access ( ?[..] ); - update interpreter and debugger; - add test; - update syntax reference, release notes, changes; --- RELEASE-NOTES.txt | 1 + src/changes/changes.xml | 3 + .../apache/commons/jexl3/internal/Debugger.java | 3 + .../apache/commons/jexl3/internal/Interpreter.java | 5 +- .../commons/jexl3/parser/ASTArrayAccess.java | 51 ++++++++++++++++ .../org/apache/commons/jexl3/parser/Parser.jjt | 12 ++-- src/site/xdoc/reference/syntax.xml | 6 ++ .../org/apache/commons/jexl3/Issues400Test.java | 67 ++++++++++++++++++++++ 8 files changed, 143 insertions(+), 5 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index d1d73bb2..197e2010 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -30,6 +30,7 @@ Version 3.3.1 is source and binary compatible with 3.3. New Features in 3.3.1: ==================== +* JEXL-404: Support array-access safe navigation (x?[y]) * JEXL-401: Captured variables should be read-only * JEXL-398: Allow 'trailing commas' or ellipsis while defining array, map and set literals diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 9caafabd..e292245a 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -29,6 +29,9 @@ <body> <release version="3.3.1" date="20YY-MM-DD"> <!-- ADD --> + <action dev="henrib" type="add" issue="JEXL-404" due-to="Xu Pengcheng"> + Support array-access safe navigation (x?[y]) + </action> <action dev="henrib" type="add" issue="JEXL-401"> Captured variables should be read-only </action> 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 ad65b2ba..75b23c89 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Debugger.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Debugger.java @@ -483,6 +483,9 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { protected Object visit(final ASTArrayAccess node, final Object data) { final int num = node.jjtGetNumChildren(); for (int i = 0; i < num; ++i) { + if (node.isSafeChild(i)) { + builder.append('?'); + } builder.append('['); accept(node.jjtGetChild(i), data); builder.append(']'); 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 05721831..5b6ffdf1 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -1111,7 +1111,10 @@ public class Interpreter extends InterpreterBase { for (int i = 0; i < numChildren; i++) { final JexlNode nindex = node.jjtGetChild(i); if (object == null) { - return unsolvableProperty(nindex, stringifyProperty(nindex), false, null); + // safe navigation access + return node.isSafeChild(i) + ? null + :unsolvableProperty(nindex, stringifyProperty(nindex), false, null); } final Object index = nindex.jjtAccept(this, null); cancelCheck(node); diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTArrayAccess.java b/src/main/java/org/apache/commons/jexl3/parser/ASTArrayAccess.java new file mode 100644 index 00000000..8cac2993 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTArrayAccess.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3.parser; + +/** + * Array access supporting (optional) safe notation. + */ +public class ASTArrayAccess extends JexlLexicalNode { + private static final long serialVersionUID = 1L; + /** Which children are accessed using a safe notation. + * Note that this does not really work after the 64th child. + * However, an expression like 'a?[b]?[c]?...?[b0]' with 64 terms is very unlikely + * to occur in real life and a bad idea anyhow. + */ + private long safe = 0; + + public ASTArrayAccess(final int id) { + super(id); + } + + public ASTArrayAccess(final Parser p, final int id) { + super(p, id); + } + + void setSafe(long s) { + this.safe = s; + } + + public boolean isSafeChild(int c) { + return (safe & (1L << c)) != 0; + } + + @Override + public Object jjtAccept(final ParserVisitor visitor, final Object data) { + return visitor.visit(this, data); + } +} 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 1e286d9f..6e49bf37 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -106,6 +106,7 @@ TOKEN_MGR_DECLS : { | < LCURLY : "{" > | < RCURLY : "}" > | < LBRACKET : "[" > + | < QLBRACKET : "?[" > | < RBRACKET : "]" > | < SEMICOL : ";" > | < COLON : ":" > @@ -418,7 +419,7 @@ void DoWhileStatement() : {} void ReturnStatement() : {} { - <RETURN> ( ExpressionStatement() )? + <RETURN> (LOOKAHEAD(2) ExpressionStatement() )? } void Continue() #Continue : { @@ -1014,14 +1015,17 @@ void IdentifierAccess() #void : ) } -void ArrayAccess() : {} +void ArrayAccess() : { + long safe = 0L; + int s = 0; + } { - (LOOKAHEAD(1) <LBRACKET> Expression() <RBRACKET>)+ + (LOOKAHEAD(2) (<LBRACKET>|<QLBRACKET> { safe |= (1 << s++); }) Expression() <RBRACKET>)+ { jjtThis.setSafe(safe); } } void MemberAccess() #void : {} { - LOOKAHEAD(<LBRACKET>) ArrayAccess() + LOOKAHEAD(<LBRACKET>|<QLBRACKET>) ArrayAccess() | LOOKAHEAD(<DOT>) IdentifierAccess() | diff --git a/src/site/xdoc/reference/syntax.xml b/src/site/xdoc/reference/syntax.xml index 83f3a40c..b21567d0 100644 --- a/src/site/xdoc/reference/syntax.xml +++ b/src/site/xdoc/reference/syntax.xml @@ -271,6 +271,12 @@ back-quoted interpolation strings as in <code>cal.`${dd.year}-${dd.month}-${dd.day}`</code>. These syntaxes are mixable with safe-access as in <code>foo.'b a r'?.quux</code> or <code> foo?.`${bar}`.quux</code>.</p> + <p>The safe-access array operator (as in <code>foo?[bar]</code>) provides the same behavior as + the safe-access operator and shortcuts any null or non-existent references + along the navigation path, allowing a safe-navigation free of errors. + In the previous expression, if 'foo' is null, the whole expression will evaluate as null. + Note that this can also be used in a chain as in <code>x?[y]?[z]</code>. + </p> <p>Access operators can be overloaded in <code>JexlArithmetic</code>, so that the operator behavior will differ depending on the type of the operator arguments</p> </td> diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index ea9d30cf..f760dae7 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -88,4 +88,71 @@ public class Issues400Test { } } } + + @Test + public void test404a() { + final JexlEngine jexl = new JexlBuilder() + .cache(64) + .strict(true) + .safe(false) + .create(); + Map<String,Object> a = Collections.singletonMap("b", 42); + // access is constant + for(String src : new String[]{ "a.b", "a?.b", "a['b']", "a?['b']", "a?.`b`"}) { + run404(jexl, src, a); + run404(jexl, src + ";", a); + } + // access is variable + for(String src : new String[]{ "a[b]", "a?[b]", "a?.`${b}`"}) { + run404(jexl, src, a, "b"); + run404(jexl, src + ";", a, "b"); + } + // add a 3rd access + Map<String,Object> b = Collections.singletonMap("c", 42); + a = Collections.singletonMap("b", b); + for(String src : new String[]{ "a[b].c", "a?[b]?['c']", "a?.`${b}`.c"}) { + run404(jexl, src, a, "b"); + } + } + + private static void run404(JexlEngine jexl, String src, Object...a) { + try { + JexlScript script = jexl.createScript(src, "a", "b"); + if (!src.endsWith(";")) { + Assert.assertEquals(script.getSourceText(), script.getParsedText()); + } + Object result = script.execute(null, a); + Assert.assertEquals(42, result); + } catch(JexlException.Parsing xparse) { + Assert.fail(src); + } + } + + @Test + public void test404b() { + final JexlEngine jexl = new JexlBuilder() + .cache(64) + .strict(true) + .safe(false) + .create(); + Map<String, Object> a = Collections.singletonMap("b", Collections.singletonMap("c", 42)); + JexlScript script; + Object result = -42; + script = jexl.createScript("a?['B']?['C']", "a"); + result = script.execute(null, a); + Assert.assertEquals(script.getSourceText(), script.getParsedText()); + Assert.assertEquals(null, result); + script = jexl.createScript("a?['b']?['C']", "a"); + Assert.assertEquals(script.getSourceText(), script.getParsedText()); + result = script.execute(null, a); + Assert.assertEquals(null, result); + script = jexl.createScript("a?['b']?['c']", "a"); + Assert.assertEquals(script.getSourceText(), script.getParsedText()); + result = script.execute(null, a); + Assert.assertEquals(42, result); + script = jexl.createScript("a?['B']?['C']?: 1042", "a"); + Assert.assertEquals(script.getSourceText(), script.getParsedText()); + result = script.execute(null, a); + Assert.assertEquals(1042, result); + } }