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