Skip to content

Commit

Permalink
JEXL-404 : add syntax for safe array access ( ?[..] );
Browse files Browse the repository at this point in the history
- update interpreter and debugger;
- add test;
- update syntax reference, release notes, changes;
  • Loading branch information
Henri Biestro committed Aug 30, 2023
1 parent c64ee0d commit 589b088
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 5 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/apache/commons/jexl3/internal/Debugger.java
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,9 @@ protected Object visit(final ASTAndNode node, final Object data) {
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(']');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,10 @@ protected Object visit(final ASTArrayAccess node, final Object data) {
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);
Expand Down
51 changes: 51 additions & 0 deletions src/main/java/org/apache/commons/jexl3/parser/ASTArrayAccess.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 8 additions & 4 deletions src/main/java/org/apache/commons/jexl3/parser/Parser.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ TOKEN_MGR_DECLS : {
| < LCURLY : "{" >
| < RCURLY : "}" >
| < LBRACKET : "[" >
| < QLBRACKET : "?[" >
| < RBRACKET : "]" >
| < SEMICOL : ";" >
| < COLON : ":" >
Expand Down Expand Up @@ -418,7 +419,7 @@ void DoWhileStatement() : {}

void ReturnStatement() : {}
{
<RETURN> ( ExpressionStatement() )?
<RETURN> (LOOKAHEAD(2) ExpressionStatement() )?
}

void Continue() #Continue : {
Expand Down Expand Up @@ -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()
|
Expand Down
6 changes: 6 additions & 0 deletions src/site/xdoc/reference/syntax.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
67 changes: 67 additions & 0 deletions src/test/java/org/apache/commons/jexl3/Issues400Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,71 @@ public void test403() {
}
}
}

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

0 comments on commit 589b088

Please sign in to comment.