You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by he...@apache.org on 2021/05/31 17:38:58 UTC

[commons-jexl] branch master updated: JEXL-346: use namespace and local variable names to help disambiguate ternary expressions

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 438bc97  JEXL-346: use namespace and local variable names to help disambiguate ternary expressions
438bc97 is described below

commit 438bc977beecf51f922f5e02a7dac630911a511d
Author: henrib <he...@apache.org>
AuthorDate: Mon May 31 19:38:45 2021 +0200

    JEXL-346: use namespace and local variable names to help disambiguate ternary expressions
---
 .../apache/commons/jexl3/internal/Interpreter.java |  85 +--------
 .../commons/jexl3/internal/InterpreterBase.java    |  31 +++-
 .../org/apache/commons/jexl3/parser/JexlNode.java  |  26 ++-
 .../commons/jexl3/parser/OperatorController.java   | 191 +++++++++++++++++++++
 .../org/apache/commons/jexl3/parser/Parser.jjt     |  13 +-
 .../org/apache/commons/jexl3/ArithmeticTest.java   |  44 +++++
 .../org/apache/commons/jexl3/Issues300Test.java    |  46 ++++-
 7 files changed, 338 insertions(+), 98 deletions(-)

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 ab24f53..1d2c6d2 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
@@ -33,83 +33,7 @@ import org.apache.commons.jexl3.JxltEngine;
 import org.apache.commons.jexl3.introspection.JexlMethod;
 import org.apache.commons.jexl3.introspection.JexlPropertyGet;
 
-import org.apache.commons.jexl3.parser.ASTAddNode;
-import org.apache.commons.jexl3.parser.ASTAndNode;
-import org.apache.commons.jexl3.parser.ASTAnnotatedStatement;
-import org.apache.commons.jexl3.parser.ASTAnnotation;
-import org.apache.commons.jexl3.parser.ASTArguments;
-import org.apache.commons.jexl3.parser.ASTArrayAccess;
-import org.apache.commons.jexl3.parser.ASTArrayLiteral;
-import org.apache.commons.jexl3.parser.ASTAssignment;
-import org.apache.commons.jexl3.parser.ASTBitwiseAndNode;
-import org.apache.commons.jexl3.parser.ASTBitwiseComplNode;
-import org.apache.commons.jexl3.parser.ASTBitwiseOrNode;
-import org.apache.commons.jexl3.parser.ASTBitwiseXorNode;
-import org.apache.commons.jexl3.parser.ASTBlock;
-import org.apache.commons.jexl3.parser.ASTBreak;
-import org.apache.commons.jexl3.parser.ASTConstructorNode;
-import org.apache.commons.jexl3.parser.ASTContinue;
-import org.apache.commons.jexl3.parser.ASTDivNode;
-import org.apache.commons.jexl3.parser.ASTDoWhileStatement;
-import org.apache.commons.jexl3.parser.ASTEQNode;
-import org.apache.commons.jexl3.parser.ASTERNode;
-import org.apache.commons.jexl3.parser.ASTEWNode;
-import org.apache.commons.jexl3.parser.ASTEmptyFunction;
-import org.apache.commons.jexl3.parser.ASTExtendedLiteral;
-import org.apache.commons.jexl3.parser.ASTFalseNode;
-import org.apache.commons.jexl3.parser.ASTForeachStatement;
-import org.apache.commons.jexl3.parser.ASTFunctionNode;
-import org.apache.commons.jexl3.parser.ASTGENode;
-import org.apache.commons.jexl3.parser.ASTGTNode;
-import org.apache.commons.jexl3.parser.ASTIdentifier;
-import org.apache.commons.jexl3.parser.ASTIdentifierAccess;
-import org.apache.commons.jexl3.parser.ASTIdentifierAccessJxlt;
-import org.apache.commons.jexl3.parser.ASTIfStatement;
-import org.apache.commons.jexl3.parser.ASTJexlLambda;
-import org.apache.commons.jexl3.parser.ASTJexlScript;
-import org.apache.commons.jexl3.parser.ASTJxltLiteral;
-import org.apache.commons.jexl3.parser.ASTLENode;
-import org.apache.commons.jexl3.parser.ASTLTNode;
-import org.apache.commons.jexl3.parser.ASTMapEntry;
-import org.apache.commons.jexl3.parser.ASTMapLiteral;
-import org.apache.commons.jexl3.parser.ASTMethodNode;
-import org.apache.commons.jexl3.parser.ASTModNode;
-import org.apache.commons.jexl3.parser.ASTMulNode;
-import org.apache.commons.jexl3.parser.ASTNENode;
-import org.apache.commons.jexl3.parser.ASTNEWNode;
-import org.apache.commons.jexl3.parser.ASTNRNode;
-import org.apache.commons.jexl3.parser.ASTNSWNode;
-import org.apache.commons.jexl3.parser.ASTNotNode;
-import org.apache.commons.jexl3.parser.ASTNullLiteral;
-import org.apache.commons.jexl3.parser.ASTNullpNode;
-import org.apache.commons.jexl3.parser.ASTNumberLiteral;
-import org.apache.commons.jexl3.parser.ASTOrNode;
-import org.apache.commons.jexl3.parser.ASTRangeNode;
-import org.apache.commons.jexl3.parser.ASTReference;
-import org.apache.commons.jexl3.parser.ASTReferenceExpression;
-import org.apache.commons.jexl3.parser.ASTRegexLiteral;
-import org.apache.commons.jexl3.parser.ASTReturnStatement;
-import org.apache.commons.jexl3.parser.ASTSWNode;
-import org.apache.commons.jexl3.parser.ASTSetAddNode;
-import org.apache.commons.jexl3.parser.ASTSetAndNode;
-import org.apache.commons.jexl3.parser.ASTSetDivNode;
-import org.apache.commons.jexl3.parser.ASTSetLiteral;
-import org.apache.commons.jexl3.parser.ASTSetModNode;
-import org.apache.commons.jexl3.parser.ASTSetMultNode;
-import org.apache.commons.jexl3.parser.ASTSetOrNode;
-import org.apache.commons.jexl3.parser.ASTSetSubNode;
-import org.apache.commons.jexl3.parser.ASTSetXorNode;
-import org.apache.commons.jexl3.parser.ASTSizeFunction;
-import org.apache.commons.jexl3.parser.ASTStringLiteral;
-import org.apache.commons.jexl3.parser.ASTSubNode;
-import org.apache.commons.jexl3.parser.ASTTernaryNode;
-import org.apache.commons.jexl3.parser.ASTTrueNode;
-import org.apache.commons.jexl3.parser.ASTUnaryMinusNode;
-import org.apache.commons.jexl3.parser.ASTUnaryPlusNode;
-import org.apache.commons.jexl3.parser.ASTVar;
-import org.apache.commons.jexl3.parser.ASTWhileStatement;
-import org.apache.commons.jexl3.parser.JexlNode;
-import org.apache.commons.jexl3.parser.Node;
+import org.apache.commons.jexl3.parser.*;
 
 /**
  * An interpreter of JEXL syntax.
@@ -1224,12 +1148,11 @@ public class Interpreter extends InterpreterBase {
                 }
                 final String aname = ant != null ? ant.toString() : "?";
                 final boolean defined = isVariableDefined(frame, block, aname);
-                if (defined && !arithmetic.isStrict()) {
+                // defined but null; arg of a strict operator?
+                if (defined && (!arithmetic.isStrict() || !node.jjtGetParent().isStrictOperator())) {
                     return null;
                 }
-                if (!defined || !(node.jjtGetParent() instanceof ASTJexlScript)) {
-                    return unsolvableVariable(node, aname, !defined);
-                }
+                return unsolvableVariable(node, aname, !defined);
             }
         }
         return object;
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 8525856..8ecf0a4 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
@@ -298,25 +298,38 @@ public abstract class InterpreterBase extends ParserVisitor {
      */
     protected Object getVariable(final Frame frame, final LexicalScope block, final ASTIdentifier identifier) {
         final int symbol = identifier.getSymbol();
+        final String name = identifier.getName();
         // if we have a symbol, we have a scope thus a frame
         if (options.isLexicalShade() && identifier.isShaded()) {
-            return undefinedVariable(identifier, identifier.getName());
+            return undefinedVariable(identifier, name);
         }
+        // a local var ?
         if ((symbol >= 0) && frame.has(symbol)) {
             final Object value = frame.get(symbol);
+            // not out of scope with no lexical shade ?
             if (value != Scope.UNDEFINED) {
+                // null argument of an arithmetic operator ?
+                if (value == null && arithmetic.isStrict() && identifier.jjtGetParent().isStrictOperator()) {
+                    return unsolvableVariable(identifier, name, false); // defined but null
+                }
                 return value;
             }
         }
-        final String name = identifier.getName();
+        // consider global
         final Object value = context.get(name);
-        if (value == null && !context.has(name)) {
-            final boolean ignore = (isSafe()
-                    && (symbol >= 0
-                    || identifier.jjtGetParent() instanceof ASTAssignment))
-                    || (identifier.jjtGetParent() instanceof ASTReference);
-            if (!ignore) {
-                return unsolvableVariable(identifier, name, true); // undefined
+        // is it null ?
+        if (value == null) {
+            // is it defined ?
+            if (!context.has(name)) {
+                // not defined, ignore in some cases...
+                final boolean ignore =
+                        (isSafe() && (symbol >= 0 || identifier.jjtGetParent() instanceof ASTAssignment))
+                         || (identifier.jjtGetParent() instanceof ASTReference);
+                if (!ignore) {
+                    return undefinedVariable(identifier, name); // undefined
+                }
+            } else if (arithmetic.isStrict() && identifier.jjtGetParent().isStrictOperator()) {
+                return unsolvableVariable(identifier, name, false); // defined but null
             }
         }
         return value;
diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java b/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java
index aecb5c9..86652f4 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java
@@ -16,7 +16,9 @@
  */
 package org.apache.commons.jexl3.parser;
 
+import org.apache.commons.jexl3.JexlException;
 import org.apache.commons.jexl3.JexlInfo;
+import org.apache.commons.jexl3.internal.ScriptVisitor;
 import org.apache.commons.jexl3.introspection.JexlMethod;
 import org.apache.commons.jexl3.introspection.JexlPropertyGet;
 import org.apache.commons.jexl3.introspection.JexlPropertySet;
@@ -114,6 +116,26 @@ public abstract class JexlNode extends SimpleNode {
     }
 
     /**
+     * Checks whether this node is an operator.
+     *
+     * @return true if node is an operator node, false otherwise
+     */
+    public boolean isOperator() {
+        return OperatorController.INSTANCE.control(this, Boolean.TRUE);
+    }
+
+    /**
+     * Checks whether this node is an operator that accepts a null argument
+     * even when arithmetic is in strict mode.
+     * The sole cases are equals and not equals.
+     *
+     * @return true if node accepts null arguments, false otherwise
+     */
+    public boolean isStrictOperator() {
+        return OperatorController.INSTANCE.control(this, Boolean.FALSE);
+    }
+
+    /**
      * Whether this node is a constant node Its value can not change after the first evaluation and can be cached
      * indefinitely.
      *
@@ -259,9 +281,7 @@ public abstract class JexlNode extends SimpleNode {
         for (JexlNode walk = node.jjtGetParent(); walk != null; walk = walk.jjtGetParent()) {
             // protect only the condition part of the ternary
             if (walk instanceof ASTTernaryNode
-                || walk instanceof ASTNullpNode
-                || walk instanceof ASTEQNode
-                || walk instanceof ASTNENode) {
+                || walk instanceof ASTNullpNode) {
                 return node == walk.jjtGetChild(0);
             }
             if (!(walk instanceof ASTReference || walk instanceof ASTArrayAccess)) {
diff --git a/src/main/java/org/apache/commons/jexl3/parser/OperatorController.java b/src/main/java/org/apache/commons/jexl3/parser/OperatorController.java
new file mode 100644
index 0000000..90d0f3c
--- /dev/null
+++ b/src/main/java/org/apache/commons/jexl3/parser/OperatorController.java
@@ -0,0 +1,191 @@
+/*
+ * 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;
+
+import org.apache.commons.jexl3.JexlException;
+import org.apache.commons.jexl3.internal.ScriptVisitor;
+
+/**
+ * Checks if node is an operator node.
+ **/
+class OperatorController extends ScriptVisitor {
+    static final OperatorController INSTANCE  = new OperatorController();
+    /**
+     * Controls the operator.
+     * @param node the node
+     * @param safe whether we are checking for any or only null-unsafe operators
+     * @return true if node is (null-unsafe) operator
+     */
+    boolean control(final JexlNode node, Boolean safe) {
+        return Boolean.TRUE.equals(node.jjtAccept(this, safe));
+    }
+
+    @Override
+    protected Object visitNode(final JexlNode node, final Object data) {
+        return false;
+    }
+
+    @Override
+    protected Object visit(final ASTNotNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTAddNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSetAddNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTMulNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSetMultNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTModNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSetModNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTDivNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSetDivNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTBitwiseAndNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSetAndNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTBitwiseOrNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSetOrNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTBitwiseXorNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSetXorNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTBitwiseComplNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSubNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSetSubNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTEQNode node, final Object data) {
+        return data;
+    }
+
+    @Override
+    protected Object visit(final ASTNENode node, final Object data) {
+        return data;
+    }
+
+    @Override
+    protected Object visit(final ASTGTNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTGENode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTLTNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTLENode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTSWNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTNSWNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTEWNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTNEWNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTERNode node, final Object data) {
+        return true;
+    }
+
+    @Override
+    protected Object visit(final ASTNRNode node, final Object data) {
+        return true;
+    }
+}
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 fb5ae37..cb782e8 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt
+++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt
@@ -312,7 +312,7 @@ ASTJexlScript JexlScript(Scope frame) : {
 ASTJexlScript JexlExpression(Scope frame) #JexlScript : {
     jjtThis.setScope(frame);
 }
-{   
+{
    {
         pushUnit(jjtThis);
    }
@@ -350,7 +350,7 @@ void Statement() #void : {}
     | Continue()
     | Break()
     | Var()
-    | Pragma() 
+    | Pragma()
 }
 
 void Block() #Block : {}
@@ -507,8 +507,13 @@ void ConditionalExpression() #void : {}
 {
   ConditionalOrExpression()
   (
-    <QMARK> Expression() <COLON> Expression() #TernaryNode(3)
-  |
+    <QMARK> (LOOKAHEAD(<IDENTIFIER> <COLON>,
+                       { isVariable(getToken(1).image) || !isDeclaredNamespace(getToken(1).image) })
+                Identifier(true)
+             |
+                Expression()
+             ) <COLON> Expression() #TernaryNode(3)
+     |
     <ELVIS> Expression() #TernaryNode(2)
   |
     <NULLP> Expression() #NullpNode(2)
diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java
index 35f7c85..b1f4d82 100644
--- a/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java
+++ b/src/test/java/org/apache/commons/jexl3/ArithmeticTest.java
@@ -71,6 +71,29 @@ public class ArithmeticTest extends JexlTestCase {
         asserter.failExpression("left & right", ".*null.*");
         asserter.failExpression("left | right", ".*null.*");
         asserter.failExpression("left ^ right", ".*null.*");
+        asserter.failExpression("left < right", ".*null.*");
+        asserter.failExpression("left <= right", ".*null.*");
+        asserter.failExpression("left > right", ".*null.*");
+        asserter.failExpression("left >= right", ".*null.*");
+    }
+
+    @Test
+    public void testLeftNullOperand2() throws Exception {
+        asserter.setVariable("x.left", null);
+        asserter.setVariable("right", Integer.valueOf(8));
+        asserter.setStrict(true);
+        asserter.failExpression("x.left + right", ".*null.*");
+        asserter.failExpression("x.left - right", ".*null.*");
+        asserter.failExpression("x.left * right", ".*null.*");
+        asserter.failExpression("x.left / right", ".*null.*");
+        asserter.failExpression("x.left % right", ".*null.*");
+        asserter.failExpression("x.left & right", ".*null.*");
+        asserter.failExpression("x.left | right", ".*null.*");
+        asserter.failExpression("x.left ^ right", ".*null.*");
+        asserter.failExpression("x.left < right", ".*null.*");
+        asserter.failExpression("x.left <= right", ".*null.*");
+        asserter.failExpression("x.left > right", ".*null.*");
+        asserter.failExpression("x.left >= right", ".*null.*");
     }
 
     @Test
@@ -85,9 +108,30 @@ public class ArithmeticTest extends JexlTestCase {
         asserter.failExpression("left & right", ".*null.*");
         asserter.failExpression("left | right", ".*null.*");
         asserter.failExpression("left ^ right", ".*null.*");
+        asserter.failExpression("left < right", ".*null.*");
+        asserter.failExpression("left <= right", ".*null.*");
+        asserter.failExpression("left > right", ".*null.*");
+        asserter.failExpression("left >= right", ".*null.*");
     }
 
     @Test
+    public void testRightNullOperand2() throws Exception {
+        asserter.setVariable("left", Integer.valueOf(9));
+        asserter.setVariable("y.right", null);
+        asserter.failExpression("left + y.right", ".*null.*");
+        asserter.failExpression("left - y.right", ".*null.*");
+        asserter.failExpression("left * y.right", ".*null.*");
+        asserter.failExpression("left / y.right", ".*null.*");
+        asserter.failExpression("left % y.right", ".*null.*");
+        asserter.failExpression("left & y.right", ".*null.*");
+        asserter.failExpression("left | y.right", ".*null.*");
+        asserter.failExpression("left ^ y.right", ".*null.*");
+        asserter.failExpression("left < y.right", ".*null.*");
+        asserter.failExpression("left <= y.right", ".*null.*");
+        asserter.failExpression("left > y.right", ".*null.*");
+        asserter.failExpression("left >= y.right", ".*null.*");
+    }
+    @Test
     public void testNullOperands() throws Exception {
         asserter.setVariable("left", null);
         asserter.setVariable("right", null);
diff --git a/src/test/java/org/apache/commons/jexl3/Issues300Test.java b/src/test/java/org/apache/commons/jexl3/Issues300Test.java
index 68e73cd..f3267d4 100644
--- a/src/test/java/org/apache/commons/jexl3/Issues300Test.java
+++ b/src/test/java/org/apache/commons/jexl3/Issues300Test.java
@@ -257,7 +257,7 @@ public class Issues300Test {
 
     @Test
     public void test314() throws Exception {
-        final JexlEngine jexl = new JexlBuilder().strict(true).create();
+        final JexlEngine jexl = new JexlBuilder().strict(true).safe(false).create();
         final Map<String,Object> vars = new HashMap<String, Object>();
         final JexlContext ctxt = new VaContext(vars);
         JexlScript script;
@@ -549,4 +549,48 @@ public class Issues300Test {
             Assert.assertTrue(exception.getMessage().contains("VARIABLE"));
         }
     }
+
+    @Test
+    public void test331() throws Exception {
+        final JexlEngine jexl = new JexlBuilder().create();
+        final JexlContext ctxt = new MapContext();
+        JexlScript script;
+        Object result;
+        script = jexl.createScript("a + '\\n' + b", "a", "b");
+        result = script.execute(ctxt, "hello", "world");
+        Assert.assertTrue(result.toString().contains("\n"));
+    }
+
+    @Test
+    public void test347() throws Exception {
+        final String src = "A.B == 5";
+        JexlEngine jexl = new JexlBuilder().safe(true).create();
+        JexlScript script = jexl.createScript(src);
+        Object result = script.execute(null);
+        // safe navigation is lenient wrt null
+        Assert.assertFalse((Boolean)result);
+
+        jexl = new JexlBuilder().strict(true).safe(false).create();
+        JexlContext ctxt = new MapContext();
+        script = jexl.createScript(src);
+        // A and A.B undefined
+        try {
+            result = script.execute(ctxt);
+            Assert.fail("should only succeed with safe navigation");
+        } catch(JexlException xany) {
+            Assert.assertNotNull(xany);
+        }
+        // A is null, A.B is undefined
+        ctxt.set("A", null);
+        try {
+            result = script.execute(ctxt);
+            Assert.fail("should only succeed with safe navigation");
+        } catch(JexlException xany) {
+            Assert.assertNotNull(xany);
+        }
+        // A.B is null
+        ctxt.set("A.B", null);
+        result = script.execute(ctxt);
+        Assert.assertFalse((Boolean) result);
+    }
 }