This is an automated email from the ASF dual-hosted git repository.

henrib pushed a commit to branch JEXL-442
in repository https://gitbox.apache.org/repos/asf/commons-jexl.git


The following commit(s) were added to refs/heads/JEXL-442 by this push:
     new 483bfe74 JEXL-442: parse Jxlt (interpolation) based nodes at script 
parsing time; - pass the scope down Jxlt parsing to solve local variables; - 
enabled 'child' parser to allow sharing of scopes between the outer script and 
the inner interpolation expressions;
483bfe74 is described below

commit 483bfe74b92efd32f0b9cfe85db9ca16bdffd716
Author: Henrib <hbies...@gmail.com>
AuthorDate: Sun Jul 27 11:52:20 2025 +0200

    JEXL-442: parse Jxlt (interpolation) based nodes at script parsing time;
    - pass the scope down Jxlt parsing to solve local variables;
    - enabled 'child' parser to allow sharing of scopes between the outer 
script and the inner interpolation expressions;
---
 .../org/apache/commons/jexl3/internal/Engine.java  |   5 +-
 .../org/apache/commons/jexl3/internal/Frame.java   |  21 +--
 .../apache/commons/jexl3/internal/Interpreter.java |  64 +--------
 .../org/apache/commons/jexl3/internal/Scope.java   |  26 ++--
 .../commons/jexl3/internal/TemplateEngine.java     |  42 +++---
 .../commons/jexl3/internal/TemplateScript.java     |   8 +-
 .../jexl3/parser/ASTIdentifierAccessJxlt.java      |  20 ++-
 .../commons/jexl3/parser/ASTJxltLiteral.java       |  29 ++--
 .../org/apache/commons/jexl3/parser/JexlNode.java  |  19 +--
 .../apache/commons/jexl3/parser/JexlParser.java    | 152 ++++++++++++---------
 .../commons/jexl3/parser/JexlScriptParser.java     |  14 ++
 .../org/apache/commons/jexl3/parser/Parser.jjt     |  14 +-
 .../apache/commons/jexl3/PropertyAccessTest.java   |  22 ++-
 13 files changed, 213 insertions(+), 223 deletions(-)

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 519e4811..b6a7adb0 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java
@@ -796,7 +796,7 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
      * @throws JexlException if any error occurred during parsing
      */
     protected ASTJexlScript parse(final JexlInfo info, final boolean expr, 
final String src, final Scope scope) {
-        return parse(info, expr? this.expressionFeatures : 
this.scriptFeatures, src, scope);
+        return parser.jxltParse(info, expr? this.expressionFeatures : 
this.scriptFeatures, src, scope);
     }
 
     /**
@@ -852,14 +852,13 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
      * @param info the JexlInfo
      * @param expr whether to parse an expression or a script
      * @param src the source to parse
-     * @param scope the scope, may be null
+     * @param scope the scope, maybe null
      * @return the parsed tree
      */
     protected ASTJexlScript jxltParse(final JexlInfo info, final boolean expr, 
final String src, final Scope scope) {
         return parse(info, expr, src, scope);
     }
 
-
     /**
      * Processes jexl.module.ns pragma.
      *
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Frame.java 
b/src/main/java/org/apache/commons/jexl3/internal/Frame.java
index 430388ea..eed58905 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Frame.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Frame.java
@@ -24,8 +24,6 @@ import java.util.concurrent.atomic.AtomicReference;
  * @since 3.0
  */
 public class Frame {
-    /** The parent frame. */
-    private final Frame parent;
     /** The scope. */
     private final Scope scope;
     /** The actual stack frame. */
@@ -39,8 +37,7 @@ public class Frame {
      * @param r the stack frame
      * @param c the number of curried parameters
      */
-    protected Frame(final Frame f, Scope s, final Object[] r, final int c) {
-        parent = f;
+    protected Frame(Scope s, final Object[] r, final int c) {
         scope = s;
         stack = r;
         curried = c;
@@ -80,7 +77,7 @@ public class Frame {
      * @return a new instance of frame
      */
     Frame newFrame(final Scope s, final Object[] r, final int c) {
-        return new Frame(this, s, r, c);
+        return new Frame(s, r, c);
     }
 
     /**
@@ -119,14 +116,6 @@ public class Frame {
         return scope;
     }
 
-    /**
-     * Gets the parent frame.
-     * @return the parent frame or null if this is the root frame
-     */
-    public Frame getParent() {
-        return parent;
-    }
-
     /**
      * Gets this script unbound parameters, i.e. parameters not bound through 
curry().
      * @return the parameter names
@@ -170,13 +159,13 @@ public class Frame {
  * Pass-by-reference frame.
  */
 class ReferenceFrame extends Frame {
-    ReferenceFrame(final Frame f, final Scope s, final Object[] r, final int 
c) {
-        super(f, s, r, c);
+    ReferenceFrame(final Scope s, final Object[] r, final int c) {
+        super(s, r, c);
     }
 
     @Override
     Frame newFrame(final Scope s, final Object[] r, final int c) {
-        return new ReferenceFrame(this, s, r, c);
+        return new ReferenceFrame(s, r, c);
     }
 
     @Override
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 0381fb44..36ee9b9a 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
@@ -28,7 +28,6 @@ import org.apache.commons.jexl3.JexlArithmetic;
 import org.apache.commons.jexl3.JexlContext;
 import org.apache.commons.jexl3.JexlEngine;
 import org.apache.commons.jexl3.JexlException;
-import org.apache.commons.jexl3.JexlInfo;
 import org.apache.commons.jexl3.JexlOperator;
 import org.apache.commons.jexl3.JexlOptions;
 import org.apache.commons.jexl3.JexlScript;
@@ -161,7 +160,7 @@ public class Interpreter extends InterpreterBase {
             final int symbol = methodIdentifier.getSymbol();
             methodName = methodIdentifier.getName();
             functor = null;
-            // is it a global or local variable ?
+            // is it a global or local variable?
             if (target == context) {
                 if (frame != null && frame.has(symbol)) {
                     functor = frame.get(symbol);
@@ -192,7 +191,7 @@ public class Interpreter extends InterpreterBase {
         // solving the call site
         final CallDispatcher call = new CallDispatcher(node, cacheable);
         try {
-            // do we have a  cached version method/function name ?
+            // do we have a cached version method/function name?
             final Object eval = call.tryEval(target, methodName, argv);
             if (JexlEngine.TRY_FAILED != eval) {
                 return eval;
@@ -254,11 +253,11 @@ public class Interpreter extends InterpreterBase {
                         return ((JexlMethod) functor).invoke(target, argv);
                     }
                     final String mCALL = "call";
-                    // may be a generic callable, try a 'call' method
+                    // maybe a generic callable, try a 'call' method
                     if (call.isTargetMethod(functor, mCALL, argv)) {
                         return call.eval(mCALL);
                     }
-                    // functor is a var, may be method is a global one ?
+                    // functor is a var, may be method is a global one?
                     if (isavar) {
                         if (call.isContextMethod(methodName, argv)) {
                             return call.eval(methodName);
@@ -373,27 +372,6 @@ public class Interpreter extends InterpreterBase {
         return node.isSafe() ? null : unsolvableProperty(jxltNode, 
jxltNode.getExpressionSource(), true, cause);
     }
 
-    /**
-     * Evaluates a JxltHandle node.
-     * <p>This parses and stores the JXLT template if necessary (upon first 
execution)</p>
-     * @param node the node
-     * @return the JXLT template evaluation.
-     * @param <NODE> the node type
-     */
-    public  <NODE extends JexlNode & JexlNode.JxltHandle> void 
parseJxltHandle(final NODE node) {
-        if (node.getExpression() == null) {
-            final TemplateEngine jxlt = jexl.jxlt();
-            JexlInfo info = node.jexlInfo();
-            if (this.block != null) {
-                info = new JexlNode.Info(node, info);
-            }
-            Scope newScope = new Scope(frame != null ? frame.getScope() : 
null);
-            final JxltEngine.Expression expr = jxlt.parseExpression(info, 
node.getExpressionSource(), newScope);
-            node.setExpression(expr);
-            node.setScope(newScope);
-        }
-    }
-
     /**
      * Evaluates a JxltHandle node.
      * <p>This parses and stores the JXLT template if necessary (upon first 
execution)</p>
@@ -403,27 +381,9 @@ public class Interpreter extends InterpreterBase {
      */
     private <NODE extends JexlNode & JexlNode.JxltHandle> Object 
evalJxltHandle(final NODE node) {
         JxltEngine.Expression expr = node.getExpression();
-        if (expr == null) {
-            final TemplateEngine jxlt = jexl.jxlt();
-            JexlInfo info = node.jexlInfo();
-            if (this.block != null) {
-                info = new JexlNode.Info(node, info);
-            }
-            Scope newScope = new Scope(frame != null ? frame.getScope() : 
null);
-            expr = jxlt.parseExpression(info, node.getExpressionSource(), 
newScope);
-            node.setExpression(expr);
-            node.setScope(newScope);
-        }
         // internal classes to evaluate in context
         if (expr instanceof TemplateEngine.TemplateExpression) {
-            Scope exprScope = node.getScope();
-            String[] symbols = exprScope.getSymbols();
-            Object[] values = new Object[symbols.length];
-            for (int v = 0; v < values.length; ++v) {
-                values[v] = evalSymbol(symbols[v]);
-            }
-            Frame exprFrame = new Frame(frame, exprScope, values, 0);
-            final Object eval = ((TemplateEngine.TemplateExpression) 
expr).evaluate(context, exprFrame, options);
+            final Object eval = ((TemplateEngine.TemplateExpression) 
expr).evaluate(context, frame, options);
             if (eval != null) {
                 final String inter = eval.toString();
                 if (options.isStrictInterpolation()) {
@@ -436,20 +396,6 @@ public class Interpreter extends InterpreterBase {
         return null;
     }
 
-    Object evalSymbol(String symbol) {
-        Frame f = frame;
-        while (f != null) {
-            Scope scope = f.getScope();
-            Integer index = scope.getSymbol(symbol);
-            if (index != null && f.has(index)) {
-                Object value = f.get(index);
-                return value;
-            }
-            f = f.getParent();
-        }
-        return null;
-    }
-
     /**
      * Executes an assignment with an optional side effect operator.
      * @param node     the node
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Scope.java 
b/src/main/java/org/apache/commons/jexl3/internal/Scope.java
index 35ba4a5d..b90d3840 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Scope.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Scope.java
@@ -67,7 +67,7 @@ public final class Scope {
      */
     private Map<String, Integer> namedVariables;
     /**
-     * The map of local captured variables to parent scope variables, ie 
closure.
+     * The map of local captured variables to parent scope variables, i.e., 
closure.
      */
     private Map<Integer, Integer> capturedVariables;
     /**
@@ -131,8 +131,8 @@ public final class Scope {
             newFrame = frame.newFrame(this, arguments, 0);
         } else {
             newFrame = ref
-            ? new ReferenceFrame(frame, this, arguments, 0)
-            : new Frame(frame, this, arguments, 0);
+            ? new ReferenceFrame(this, arguments, 0)
+            : new Frame(this, arguments, 0);
         }
         return newFrame.assign(args);
     }
@@ -140,7 +140,7 @@ public final class Scope {
     /**
      * Declares a parameter.
      * <p>
-     * This method creates an new entry in the symbol map.
+     * This method creates a new entry in the symbol map.
      * </p>
      * @param param the parameter name
      * @return the register index storing this variable
@@ -161,7 +161,7 @@ public final class Scope {
     /**
      * Declares a local variable.
      * <p>
-     * This method creates an new entry in the symbol map.
+     * This method creates a new entry in the symbol map.
      * </p>
      * @param varName the variable name
      * @return the register index storing this variable
@@ -196,7 +196,7 @@ public final class Scope {
     }
 
     /**
-     * Gets the captured index of a given symbol, ie the target index of a 
symbol in a child scope.
+     * Gets the captured index of a given symbol, i.e., the target index of a 
symbol in a child scope.
      * @param symbol the symbol index
      * @return the target symbol index or null if the symbol is not captured
      */
@@ -213,7 +213,7 @@ public final class Scope {
     }
 
     /**
-     * Gets the index of a captured symbol, ie the source index of a symbol in 
a parent scope.
+     * Gets the index of a captured symbol, i.e., the source index of a symbol 
in a parent scope.
      * @param symbol the symbol index
      * @return the source symbol index or -1 if the symbol is not captured
      */
@@ -223,7 +223,7 @@ public final class Scope {
     }
 
     /**
-     * Gets this script captured symbols names, i.e. local variables defined 
in outer scopes and used
+     * Gets this script captured symbols names, i.e., local variables defined 
in outer scopes and used
      * by this scope.
      * @return the captured names
      */
@@ -262,7 +262,7 @@ public final class Scope {
     }
 
     /**
-     * Gets this script parameters, i.e. symbols assigned before creating 
local variables.
+     * Gets this script parameters, i.e., symbols assigned before creating 
local variables.
      * @return the parameter names
      */
     public String[] getParameters() {
@@ -295,8 +295,8 @@ public final class Scope {
     }
 
     /**
-     * Checks whether an identifier is a local variable or argument, ie a 
symbol.
-     * If this fails, look in parents for symbol that can be captured.
+     * Checks whether an identifier is a local variable or argument, i.e., a 
symbol.
+     * If this fails, look within parents for a symbol that can be captured.
      * @param name the symbol name
      * @return the symbol index
      */
@@ -305,7 +305,7 @@ public final class Scope {
     }
 
     /**
-     * Checks whether an identifier is a local variable or argument, ie a 
symbol.
+     * Checks whether an identifier is a local variable or argument, i.e., a 
symbol.
      * @param name the symbol name
      * @param capture whether solving by capturing a parent symbol is allowed
      * @return the symbol index
@@ -330,7 +330,7 @@ public final class Scope {
     }
 
     /**
-     * Gets this script symbols names, i.e. parameters and local variables.
+     * Gets this script symbols names, i.e., parameters and local variables.
      * @return the symbol names
      */
     public String[] getSymbols() {
diff --git 
a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java 
b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
index 73a9ad4b..64e91583 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
@@ -47,7 +47,7 @@ public final class TemplateEngine extends JxltEngine {
      * Abstract the source fragments, verbatim or immediate typed text blocks.
      */
     static final class Block {
-        /** The type of block, verbatim or directive. */
+        /** The type of block: verbatim or directive. */
         private final BlockType type;
         /** The block start line info. */
         private final int line;
@@ -109,7 +109,7 @@ public final class TemplateEngine extends JxltEngine {
     enum BlockType {
         /** Block is to be output "as is" but may be a unified expression. */
         VERBATIM,
-        /** Block is a directive, ie a fragment of JEXL code. */
+        /** Block is a directive, i.e., a fragment of JEXL code. */
         DIRECTIVE
     }
 
@@ -149,7 +149,7 @@ public final class TemplateEngine extends JxltEngine {
             for (final TemplateExpression expr : exprs) {
                 value = expr.evaluate(interpreter);
                 if (value != null) {
-                    strb.append(value.toString());
+                    strb.append(value);
                 }
             }
             return strb.toString();
@@ -234,7 +234,7 @@ public final class TemplateEngine extends JxltEngine {
         @Override
         public StringBuilder asString(final StringBuilder strb) {
             if (value != null) {
-                strb.append(value.toString());
+                strb.append(value);
             }
             return strb;
         }
@@ -310,7 +310,7 @@ public final class TemplateEngine extends JxltEngine {
         }
 
         /**
-         * Builds an TemplateExpression from a source, performs checks.
+         * Builds a TemplateExpression from a source, performs checks.
          * @param el     the unified el instance
          * @param source the source TemplateExpression
          * @return an TemplateExpression
@@ -524,15 +524,15 @@ public final class TemplateEngine extends JxltEngine {
     private enum ParseState {
         /** Parsing a constant. */
         CONST,
-        /** Parsing after $ . */
+        /** Parsing after <code>$</code> . */
         IMMEDIATE0,
-        /** Parsing after # . */
+        /** Parsing after <code>#</code> . */
         DEFERRED0,
-        /** Parsing after ${ . */
+        /** Parsing after <code>${</code> . */
         IMMEDIATE1,
-        /** Parsing after #{ . */
+        /** Parsing after <code>#{</code> . */
         DEFERRED1,
-        /** Parsing after \ . */
+        /** Parsing after <code>\</code> . */
         ESCAPE
     }
 
@@ -578,7 +578,7 @@ public final class TemplateEngine extends JxltEngine {
          * @param context the context storing global variables
          * @param options flags and properties that can alter the evaluation 
behavior.
          * @return the expression value
-         * @throws JexlException
+         * @throws JexlException if expression evaluation fails
          */
         protected final Object evaluate(final JexlContext context, final Frame 
frame, final JexlOptions options) {
             try {
@@ -668,7 +668,7 @@ public final class TemplateEngine extends JxltEngine {
          * @param context the context storing global variables
          * @param opts flags and properties that can alter the evaluation 
behavior.
          * @return the expression value
-         * @throws JexlException
+         * @throws JexlException if expression preparation fails
          */
         protected final TemplateExpression prepare(final JexlContext context, 
final Frame frame, final JexlOptions opts) {
             try {
@@ -693,7 +693,7 @@ public final class TemplateEngine extends JxltEngine {
             asString(strb);
             if (source != this) {
                 strb.append(" /*= ");
-                strb.append(source.toString());
+                strb.append(source);
                 strb.append(" */");
             }
             return strb.toString();
@@ -748,7 +748,7 @@ public final class TemplateEngine extends JxltEngine {
         strb.append(action);
         if (expr != null) {
             strb.append(" '");
-            strb.append(expr.toString());
+            strb.append(expr);
             strb.append("'");
         }
         final Throwable cause = xany.getCause();
@@ -827,7 +827,7 @@ public final class TemplateEngine extends JxltEngine {
     /** The first character for deferred expressions. */
     final char deferredChar;
 
-    /** Whether expressions can use JEXL script or only expressions (ie, no 
for, var, etc). */
+    /** Whether expressions can use JEXL script or only expressions (i.e., no 
for, var, etc). */
     final boolean noscript;
 
     /**
@@ -863,13 +863,17 @@ public final class TemplateEngine extends JxltEngine {
 
     @Override
     public JxltEngine.Expression createExpression(final JexlInfo jexlInfo, 
final String expression) {
+        return createExpression(jexlInfo, expression, null);
+    }
+
+    public JxltEngine.Expression createExpression(final JexlInfo jexlInfo, 
final String expression, final Scope scope) {
         final JexlInfo info = jexlInfo == null ?  jexl.createInfo() : jexlInfo;
         Exception xuel = null;
         TemplateExpression stmt = null;
         try {
             stmt = cache.get(expression);
             if (stmt == null) {
-                stmt = parseExpression(info, expression, null);
+                stmt = parseExpression(info, expression, scope);
                 cache.put(expression, stmt);
             }
         } catch (final JexlException xjexl) {
@@ -1115,7 +1119,7 @@ public final class TemplateEngine extends JxltEngine {
      * @param source the source reader
      * @return the list of blocks
      */
-    protected List<Block> readTemplate(final String prefix, final Reader 
source) {
+     List<Block> readTemplate(final String prefix, final Reader source) {
         final ArrayList<Block> blocks = new ArrayList<>();
         final BufferedReader reader;
         if (source instanceof BufferedReader) {
@@ -1159,7 +1163,7 @@ public final class TemplateEngine extends JxltEngine {
                     // still a directive
                     strb.append(line.subSequence(prefixLen, line.length()));
                 }
-            } else if (type == BlockType.VERBATIM) {
+            } else { //if (type == BlockType.VERBATIM) {
                 // switch to directive if necessary
                 prefixLen = startsWith(line, prefix);
                 if (prefixLen >= 0) {
@@ -1191,7 +1195,7 @@ public final class TemplateEngine extends JxltEngine {
      * @param pattern  the pattern to match at start of sequence
      * @return the first position after end of pattern if it matches, -1 
otherwise
      */
-    protected int startsWith(final CharSequence sequence, final CharSequence 
pattern) {
+    int startsWith(final CharSequence sequence, final CharSequence pattern) {
         final int length = sequence.length();
         int s = 0;
         while (s < length && Character.isSpaceChar(sequence.charAt(s))) {
diff --git 
a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java 
b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java
index 3db934e7..a210a56e 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java
@@ -180,7 +180,13 @@ public final class TemplateScript implements 
JxltEngine.Template {
                 // no node info means this verbatim is surrounded by comments 
markers;
                 // expr at this index is never called
                 if (ji != null) {
-                    te = jxlt.parseExpression(ji, block.getBody(), 
scopeOf(ji));
+                    Scope es = scopeOf(ji);
+                    if (es == null) {
+                        // if the scope is null, it means the verbatim is at 
the top level
+                        // of the script, so we use the script scope
+                        es = scope;
+                    }
+                    te = jxlt.parseExpression(ji, block.getBody(), es);
                 } else {
                     te = jxlt.new ConstantExpression(block.getBody(), null);
                 }
diff --git 
a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java 
b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java
index 212cc496..59f9972d 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java
@@ -17,15 +17,16 @@
 
 package org.apache.commons.jexl3.parser;
 
+import org.apache.commons.jexl3.JexlEngine;
 import org.apache.commons.jexl3.JxltEngine;
 import org.apache.commons.jexl3.internal.Scope;
+import org.apache.commons.jexl3.internal.TemplateEngine;
 
 /**
  * x.`expr`.
  */
 public class ASTIdentifierAccessJxlt extends ASTIdentifierAccess implements 
JexlNode.JxltHandle {
     protected transient JxltEngine.Expression jxltExpression;
-    private Scope scope;
 
     ASTIdentifierAccessJxlt(final int id) {
         super(id);
@@ -51,9 +52,16 @@ public class ASTIdentifierAccessJxlt extends 
ASTIdentifierAccess implements Jexl
         jxltExpression = tp;
     }
 
-    @Override
-    public Scope getScope() { return scope; }
-
-    @Override
-    public void setScope(final Scope scope) {  this.scope = scope; }
+    public void setIdentifier(final String src, final Scope scope) {
+        super.setIdentifier(src);
+        if (src != null && !src.isEmpty()) {
+            JexlEngine jexl = JexlEngine.getThreadEngine();
+            if (jexl != null) {
+                JxltEngine jxlt = jexl.createJxltEngine();
+                if (jxlt instanceof TemplateEngine) {
+                  this.jxltExpression = ((TemplateEngine) 
jxlt).createExpression(jexlInfo(), src, scope);
+                }
+            }
+        }
+    }
 }
diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java 
b/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java
index 1e52d487..fda769c5 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java
@@ -16,18 +16,18 @@
  */
 package org.apache.commons.jexl3.parser;
 
+import org.apache.commons.jexl3.JexlEngine;
 import org.apache.commons.jexl3.JxltEngine;
 import org.apache.commons.jexl3.internal.Scope;
+import org.apache.commons.jexl3.internal.TemplateEngine;
 
-public final class ASTJxltLiteral extends JexlNode implements 
JexlNode.JxltHandle{
+public final class ASTJxltLiteral extends JexlNode implements 
JexlNode.JxltHandle {
     /** Serial uid.*/
     private static final long serialVersionUID = 1L;
     /** The actual literal value. */
     private String literal;
     /** The expression (parsed). */
     private transient JxltEngine.Expression jxltExpression;
-    /** The scope of the expression. */
-    private Scope scope;
 
     /**
      * Creates a Jxlt literal node.
@@ -42,16 +42,6 @@ public final class ASTJxltLiteral extends JexlNode 
implements JexlNode.JxltHandl
         return literal;
     }
 
-    @Override
-    public Scope getScope() {
-        return scope;
-    }
-
-    @Override
-    public void setScope(final Scope scope) {
-        this.scope = scope;
-    }
-
     @Override
     public JxltEngine.Expression getExpression() {
         return jxltExpression;
@@ -75,8 +65,17 @@ public final class ASTJxltLiteral extends JexlNode 
implements JexlNode.JxltHandl
         this.jxltExpression = e;
     }
 
-    void setLiteral(final String literal) {
-        this.literal = literal;
+    void setLiteral(final String src, final Scope scope) {
+        this.literal = src;
+        if (src != null && !src.isEmpty()) {
+            JexlEngine jexl = JexlEngine.getThreadEngine();
+            if (jexl != null) {
+                JxltEngine jxlt = jexl.createJxltEngine();
+                if (jxlt instanceof TemplateEngine) {
+                  this.jxltExpression = ((TemplateEngine) 
jxlt).createExpression(jexlInfo(), src, scope);
+                }
+            }
+        }
     }
 
     @Override
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 dc75ab2e..9c60b382 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java
@@ -21,7 +21,6 @@ import org.apache.commons.jexl3.JexlCache;
 import org.apache.commons.jexl3.JexlContext.NamespaceFunctor;
 import org.apache.commons.jexl3.JexlInfo;
 import org.apache.commons.jexl3.JxltEngine;
-import org.apache.commons.jexl3.internal.Scope;
 import org.apache.commons.jexl3.introspection.JexlMethod;
 import org.apache.commons.jexl3.introspection.JexlPropertyGet;
 import org.apache.commons.jexl3.introspection.JexlPropertySet;
@@ -59,18 +58,6 @@ public abstract class JexlNode extends SimpleNode implements 
JexlCache.Reference
          * @param expr a TemplateEngine.TemplateExpression instance
          */
         void setExpression(JxltEngine.Expression expr);
-
-        /**
-         * Sets the scope of the expression.
-         * @param scope the scope
-         */
-        void setScope(Scope scope);
-
-        /**
-         * Gets the scope of the expression.
-         * @return the scope
-         */
-        Scope getScope();
     }
 
     @Override
@@ -142,7 +129,7 @@ public abstract class JexlNode extends SimpleNode 
implements JexlCache.Reference
      */
     private static final long serialVersionUID = 1L;
 
-    // line + column encoded: up to 4096 columns (ie 20 bits for line + 12 
bits for column)
+    // line + column encoded: up to 4096 columns (i.e., 20 bits for line + 12 
bits for column)
     private int lc = -1;
 
     public JexlNode(final int id) {
@@ -288,7 +275,7 @@ public abstract class JexlNode extends SimpleNode 
implements JexlCache.Reference
         for(int s = 0; s < nsiblings; ++s) {
             final JexlNode sibling = parent.jjtGetChild(s);
             if (sibling == this) {
-                // the next chid offset of this nodes parent
+                // the next child offset of this node_s parent
                 rhs = s + 1;
                 break;
             }
@@ -350,7 +337,7 @@ public abstract class JexlNode extends SimpleNode 
implements JexlCache.Reference
         if (lc >= 0) {
             final int c = lc & 0xfff;
             final int l = lc >> 0xc;
-            // at least an info with line/column number
+            // at least an info instance with line/column number
             return info != null ? info.at(info.getLine() + l - 1, c) : new 
JexlInfo(name, l, c);
         }
         // weird though; no jjSetFirstToken(...) ever called?
diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java 
b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java
index c378770e..da33ecbe 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java
@@ -32,6 +32,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.commons.jexl3.JexlEngine;
 import org.apache.commons.jexl3.JexlException;
@@ -183,7 +184,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * <p>Each parameter is associated with a register and is materialized
      * as an offset in the registers array used during evaluation.</p>
      */
-    protected Scope scope;
+    protected final AtomicReference<Scope> scopeReference;
     /**
      * When parsing inner functions/lambda, need to stack the scope (sic).
      */
@@ -246,7 +247,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
     /**
      * The current lexical block.
      */
-    protected LexicalUnit block;
+    protected AtomicReference<LexicalUnit> blockReference;
     /**
      * Stack of lexical blocks.
      */
@@ -255,6 +256,10 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * The map of lexical to functional blocks.
      */
     protected final Map<LexicalUnit, Scope> blockScopes;
+    /**
+     * The parent parser if any.
+     */
+    protected final JexlParser parent;
 
     /**
      * Creates a new parser.
@@ -263,11 +268,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * </p>
      */
     protected JexlParser() {
-        featureController = new FeatureController(JexlEngine.DEFAULT_FEATURES);
-        scopes = new ArrayDeque<>();
-        loopCounts = new ArrayDeque<>();
-        blocks = new ArrayDeque<>();
-        blockScopes = new IdentityHashMap<>();
+        this(null);
     }
 
     /**
@@ -279,19 +280,31 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
     protected JexlParser(JexlParser parser) {
         this.info = null;
         this.source = null;
-        featureController = parser.featureController;
-        scope = parser.scope;
-        scopes = parser.scopes;
-        pragmas = parser.pragmas;
-        namespaces = parser.namespaces;
-        loopCount = parser.loopCount;
-        loopCounts = parser.loopCounts;
-        block = parser.block;
-        blocks = parser.blocks;
-        blockScopes = parser.blockScopes;
-        fqcnResolver = parser.fqcnResolver;
-        imports = parser.imports;
-        autoSemicolon = parser.autoSemicolon;
+        if (parser != null) {
+            parent = parser;
+            featureController = parser.featureController;
+            scopeReference = parser.scopeReference;
+            scopes = parser.scopes;
+            pragmas = parser.pragmas;
+            namespaces = parser.namespaces;
+            loopCount = parser.loopCount;
+            loopCounts = parser.loopCounts;
+            blockReference = parser.blockReference;
+            blocks = parser.blocks;
+            blockScopes = parser.blockScopes;
+            fqcnResolver = parser.fqcnResolver;
+            imports = parser.imports;
+            autoSemicolon = parser.autoSemicolon;
+        } else {
+            parent = null;
+            featureController = new 
FeatureController(JexlEngine.DEFAULT_FEATURES);
+            scopeReference = new AtomicReference<>();
+            blockReference = new AtomicReference<>();
+            scopes = new ArrayDeque<>();
+            loopCounts = new ArrayDeque<>();
+            blocks = new ArrayDeque<>();
+            blockScopes = new IdentityHashMap<>();
+        }
     }
 
     /**
@@ -458,6 +471,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * @return the image
      */
     protected String checkVariable(final ASTIdentifier identifier, final 
String name) {
+        final Scope scope = scopeReference.get();
         if (scope != null) {
             final Integer symbol = scope.getSymbol(name);
             if (symbol != null) {
@@ -467,7 +481,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
                     // captured are declared in all cases
                     identifier.setCaptured(true);
                 } else {
-                    LexicalUnit unit = block;
+                    LexicalUnit unit = blockReference.get();
                     declared = unit.hasSymbol(symbol);
                     // one of the lexical blocks above should declare it
                     if (!declared) {
@@ -508,18 +522,20 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
     protected void cleanup(final JexlFeatures features) {
         info = null;
         source = null;
-        scope = null;
-        scopes.clear();
-        pragmas = null;
-        namespaces = null;
-        fqcnResolver = null;
-        imports.clear();
-        loopCounts.clear();
-        loopCount = 0;
-        blocks.clear();
-        block = null;
-        blockScopes.clear();
-        setFeatures(features);
+        if (parent == null ) {
+            scopeReference.set(null);
+            scopes.clear();
+            pragmas = null;
+            namespaces = null;
+            fqcnResolver = null;
+            imports.clear();
+            loopCounts.clear();
+            loopCount = 0;
+            blocks.clear();
+            blockReference.set(null);
+            blockScopes.clear();
+            setFeatures(features);
+        }
     }
 
     /**
@@ -540,8 +556,10 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
     protected void declareFunction(final ASTVar variable, final Token token) {
         final String name = token.image;
         // function foo() ... <=> const foo = ()->...
+        Scope scope = scopeReference.get();
         if (scope == null) {
             scope = new Scope(null);
+            scopeReference.set(scope);
         }
         final int symbol = scope.declareVariable(name);
         variable.setSymbol(symbol, name);
@@ -552,6 +570,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
         // function is const fun...
         if (declareSymbol(symbol)) {
             scope.addLexical(symbol);
+            LexicalUnit block = blockReference.get();
             block.setConstant(symbol);
         } else {
             if (getFeatures().isLexical()) {
@@ -576,12 +595,15 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
         if (!allowVariable(identifier)) {
             throwFeatureException(JexlFeatures.LOCAL_VAR, token);
         }
+        Scope scope = scopeReference.get();
         if (scope == null) {
             scope = new Scope(null, (String[]) null);
+            scopeReference.set(scope);
         }
         final int symbol = scope.declareParameter(identifier);
         // not sure how declaring a parameter could fail...
         // lexical feature error
+        LexicalUnit block = blockReference.get();
         if (!block.declareSymbol(symbol)) {
             if (lexical || getFeatures().isLexical()) {
                 final JexlInfo xinfo = info.at(token.beginLine, 
token.beginColumn);
@@ -661,6 +683,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
                 break;
             }
         }
+        LexicalUnit block = blockReference.get();
         return block == null || block.declareSymbol(symbol);
     }
 
@@ -680,8 +703,10 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
         if (!allowVariable(name)) {
             throwFeatureException(JexlFeatures.LOCAL_VAR, token);
         }
+        Scope scope = scopeReference.get();
         if (scope == null) {
             scope = new Scope(null);
+            scopeReference.set(scope);
         }
         final int symbol = scope.declareVariable(name);
         variable.setSymbol(symbol, name);
@@ -701,6 +726,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
         } else if (lexical) {
             scope.addLexical(symbol);
             if (constant) {
+                LexicalUnit block = blockReference.get();
                 block.setConstant(symbol);
             }
         }
@@ -722,7 +748,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * @return the named register map
      */
     protected Scope getScope() {
-        return scope;
+        return scopeReference.get();
     }
 
     /**
@@ -736,7 +762,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * @return the named register map
      */
     protected LexicalUnit getUnit() {
-        return block;
+        return blockReference.get();
     }
     /**
      * Default implementation does nothing but is overridden by generated code.
@@ -755,10 +781,11 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      */
     private boolean isConstant(final int symbol) {
         if (symbol >= 0) {
+            LexicalUnit block = blockReference.get();
             if (block != null && block.hasSymbol(symbol)) {
                 return block.isConstant(symbol);
             }
-            Scope blockScope = blockScopes.get(block);
+            Scope blockScope = blockScopes.get(blockReference.get());
             int lexical = symbol;
             for (final LexicalUnit unit : blocks) {
                 final Scope unitScope = blockScopes.get(unit);
@@ -865,6 +892,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * @return true if a variable with that name was declared
      */
     protected boolean isVariable(final String name) {
+        Scope scope = scopeReference.get();
         return scope != null && scope.getSymbol(name) != null;
     }
 
@@ -906,6 +934,7 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
             }
             final ASTJexlScript script = (ASTJexlScript) node;
             // reaccess in case local variables have been declared
+            Scope scope = scopeReference.get();
             if (script.getScope() != scope) {
                 script.setScope(scope);
             }
@@ -929,23 +958,21 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
         // heavy check
         featureController.controlNode(node);
     }
-//
-//    JxltEngine.Expression parseJxlt(final String src) {
-//        if (src != null && !src.isEmpty()) {
-//            JexlEngine jexl = JexlEngine.getThreadEngine();
-//            if (jexl != null) {
-//                JxltEngine jxlt = jexl.createJxltEngine();
-//                if (jxlt instanceof TemplateEngine) {
-//                    JxltEngine.Expression jxltExpression = ((TemplateEngine) 
jxlt).createExpression(null, src);
-//                    return jxltExpression;
-//                }
-//            }
-//        }
-//        return null;
-//    }
 
     /**
-     * Called by parser at beginning of node construction.
+     * Parses an embedded Jexl expression within an interpolation node.
+     * <p>This creates a sub-parser that shares the scopes and of the parent 
parser.</p>
+     * @param info the JexlInfo
+     * @param src the source to parse
+     * @return the parsed tree
+     */
+    @Override
+    public ASTJexlScript jxltParse(JexlInfo info, JexlFeatures features, 
String src, Scope scope) {
+        return new Parser(this).parse(info, features, src, scope);
+    }
+
+    /**
+     * Called by parser at the beginning of a node construction.
      * @param node the node
      */
     protected void jjtreeOpenNodeScope(final JexlNode node) {
@@ -975,11 +1002,8 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * Pops back to previous local variable scope.
      */
     protected void popScope() {
-        if (!scopes.isEmpty()) {
-            scope = scopes.pop();
-        } else {
-            scope = null;
-        }
+        Scope scope = scopes.isEmpty() ? null : scopes.pop();
+        scopeReference.set(scope);
         if (!loopCounts.isEmpty()) {
             loopCount = loopCounts.pop();
         }
@@ -990,26 +1014,26 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * @param unit restores the previous lexical scope
      */
     protected void popUnit(final LexicalUnit unit) {
+        LexicalUnit block = blockReference.get();
         if (block == unit){
             blockScopes.remove(unit);
-            if (!blocks.isEmpty()) {
-                block = blocks.pop();
-            } else {
-                block = null;
-            }
+            blockReference.set(blocks.isEmpty()? null : blocks.pop());
         }
     }
 
     /**
      * Create a new local variable scope and push it as current.
      */
-    protected void pushScope() {
+    protected Scope pushScope() {
+        Scope scope = scopeReference.get();
         if (scope != null) {
             scopes.push(scope);
         }
         scope = new Scope(scope, (String[]) null);
+        scopeReference.set(scope);
         loopCounts.push(loopCount);
         loopCount = 0;
+        return scope;
     }
 
     /**
@@ -1017,11 +1041,13 @@ public abstract class JexlParser extends StringParser 
implements JexlScriptParse
      * @param unit the new lexical unit
      */
     protected void pushUnit(final LexicalUnit unit) {
+        Scope scope = scopeReference.get();
         blockScopes.put(unit, scope);
+        LexicalUnit block = blockReference.get();
         if (block != null) {
             blocks.push(block);
         }
-        block = unit;
+        blockReference.set(unit);
     }
 
     protected void pushLoop() {
diff --git 
a/src/main/java/org/apache/commons/jexl3/parser/JexlScriptParser.java 
b/src/main/java/org/apache/commons/jexl3/parser/JexlScriptParser.java
index 80277455..d857e200 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/JexlScriptParser.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/JexlScriptParser.java
@@ -38,4 +38,18 @@ public interface JexlScriptParser {
    */
   ASTJexlScript parse(final JexlInfo info, final JexlFeatures features, final 
String src, final Scope scope);
 
+  /**
+   * Parses an embedded JXLT script or expression, an interpolation expression.
+   *
+   * @param info      information structure
+   * @param features  the set of parsing features
+   * @param src      the expression to parse
+   * @param scope     the script frame
+   * @return the parsed tree
+   * @throws JexlException if any error occurred during parsing
+   */
+  default ASTJexlScript jxltParse(JexlInfo info, JexlFeatures features, String 
src, Scope scope) {
+    return parse(info, features, src, scope);
+  }
+
 }
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 be51f942..edc6e234 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt
+++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt
@@ -70,12 +70,14 @@ public final class Parser extends JexlParser
     public ASTJexlScript parse(JexlInfo jexlInfo, JexlFeatures jexlFeatures, 
String jexlSrc, Scope jexlScope) {
         JexlFeatures previous = getFeatures();
         try {
-            setFeatures(jexlFeatures);
             // lets do the 'Unique Init' in here to be safe - it's a pain to 
remember
             this.info = jexlInfo != null? jexlInfo : new JexlInfo();
             this.source = jexlSrc;
-            this.pragmas = null;
-            this.scope = jexlScope;
+            if (this.parent == null) {
+                setFeatures(jexlFeatures);
+                this.pragmas = null;
+            }
+            this.scopeReference.set(jexlScope);
             token_source.comparatorNames = 
jexlFeatures.supportsComparatorNames();
             ReInit(jexlSrc);
             ASTJexlScript script = jexlFeatures.supportsScript()
@@ -1061,7 +1063,7 @@ void JxltLiteral() #JxltLiteral :
 }
 {
    t=<JXLT_LITERAL>
-   { jjtThis.setLiteral(src = Parser.buildString(t.image, true)); }
+   { jjtThis.setLiteral(src = Parser.buildString(t.image, true), getScope()); }
 }
 
 void RegexLiteral() :
@@ -1218,7 +1220,7 @@ void IdentifierAccess() #void :
     |
         t=<STRING_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, 
true)); } #IdentifierAccess
     |
-        t=<JXLT_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, 
true)); } #IdentifierAccessJxlt
+        t=<JXLT_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, 
true), getScope()); } #IdentifierAccessJxlt
     )
     |
     <QDOT> (
@@ -1226,7 +1228,7 @@ void IdentifierAccess() #void :
     |
         t=<STRING_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, 
true)); } #IdentifierAccessSafe
     |
-        t=<JXLT_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, 
true)); } #IdentifierAccessSafeJxlt
+        t=<JXLT_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, 
true), getScope()); } #IdentifierAccessSafeJxlt
     )
 }
 
diff --git a/src/test/java/org/apache/commons/jexl3/PropertyAccessTest.java 
b/src/test/java/org/apache/commons/jexl3/PropertyAccessTest.java
index d1338841..a1531e25 100644
--- a/src/test/java/org/apache/commons/jexl3/PropertyAccessTest.java
+++ b/src/test/java/org/apache/commons/jexl3/PropertyAccessTest.java
@@ -17,8 +17,10 @@
 package org.apache.commons.jexl3;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -346,17 +348,25 @@ class PropertyAccessTest extends JexlTestCase {
         result = script.execute(ctx, "querty");
         assertEquals("oops", result);
 
-        // fails with jxlt & lenient navigation
+        // parsing fails with jxlt & lenient navigation
         stmt = "(x)->{ x?.`c${la--ss` ?? 'oops' }";
-        script = engine.createScript(stmt);
-        result = script.execute(ctx, "querty");
-        assertEquals("oops", result);
+        try {
+            script = engine.createScript(stmt);
+            result = script.execute(ctx, "querty");
+        } catch (final JexlException xany) {
+            assertNotNull(xany.getMessage());
+            assertTrue(xany.getMessage().contains("c${la--ss"));
+        }
 
-        // fails with jxlt & strict navigation
+        // parsing fails with jxlt & strict navigation
         stmt = "(x)->{ x.`c${la--ss` ?? 'oops' }";
+        try {
         script = engine.createScript(stmt);
         result = script.execute(ctx, "querty");
-        assertEquals("oops", result);
+        } catch (final JexlException xany) {
+            assertNotNull(xany.getMessage());
+            assertTrue(xany.getMessage().contains("c${la--ss"));
+        }
     }
 
     @Test

Reply via email to