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);
+  }
 }

Reply via email to