This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch JEXL-477 in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
commit bf440a3fcd8e5aa48ba79315d732d89ced40fe49 Author: henrib <[email protected]> AuthorDate: Thu Nov 12 15:19:16 2026 +0100 JEXL-477: Parse interpolation strings with the features of their script The expressions embedded in an interpolation string or an interpolated property name were parsed with the features of the engine. The parser now hands its current features to the interpolation it creates, so that a script created with its own features has them enforced, and its declared antish names solved, inside interpolations; cached interpolation expressions are keyed by those features. Co-Authored-By: Claude <[email protected]> --- src/changes/changes.xml | 1 + .../org/apache/commons/jexl3/internal/Engine.java | 15 ++++- .../commons/jexl3/internal/TemplateEngine.java | 66 ++++++++++++++++++---- .../jexl3/parser/ASTIdentifierAccessJxlt.java | 5 +- .../commons/jexl3/parser/ASTJxltLiteral.java | 5 +- .../apache/commons/jexl3/parser/JexlParser.java | 6 +- .../org/apache/commons/jexl3/parser/Parser.jjt | 6 +- .../commons/jexl3/JxltFeatureEnforcementTest.java | 38 +++++++++++++ 8 files changed, 122 insertions(+), 20 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b2550e67..f17fcf17 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -29,6 +29,7 @@ <body> <release version="3.7.2" date="YYYY-MM-DD" description="This is a feature and maintenance release. Java 8 or later is required."> <!-- FIX --> + <action dev="henrib" due-to="Claude" type="fix" issue="JEXL-477">Interpolation strings are parsed with the engine features instead of those of their script.</action> <action dev="henrib" due-to="Claude" type="fix" issue="JEXL-475">A jexl.options pragma modifies the engine options when evaluated with a plain context.</action> <!-- ADD --> <action dev="henrib" due-to="Claude" type="add" issue="JEXL-476">Add the safeUndefined option (JexlOptions.setSafeUndefined(boolean), JexlBuilder.safeUndefined(boolean)) to restrict safe navigation to null, undefined variables, properties and methods remaining errors; safe access operators treat properties, methods and array accesses alike.</action> 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 5a8aeafb..adfba15c 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java @@ -902,8 +902,21 @@ public class Engine extends JexlEngine implements JexlUberspect.ConstantResolver * @return The parsed tree */ protected ASTJexlScript jxltParse(final JexlInfo info, final boolean expr, final String src, final Scope scope) { + return jxltParse(info, expr ? this.expressionFeatures : this.scriptFeatures, src, scope); + } + + /** + * Parses a Jexl expression or script. + * + * @param info The JexlInfo + * @param features The features to parse with + * @param src The source to parse + * @param scope The scope, maybe null + * @return The parsed tree + */ + protected ASTJexlScript jxltParse(final JexlInfo info, final JexlFeatures features, final String src, final Scope scope) { synchronized(parsing) { - return parser.jxltParse(info, expr ? this.expressionFeatures : this.scriptFeatures, src, scope); + return parser.jxltParse(info, features, src, scope); } } 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 36d0bba7..836347c7 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java @@ -506,15 +506,19 @@ public final class TemplateEngine extends JxltEngine { */ final class NestedExpression extends JexlBasedExpression { private final Scope scope; + private final JexlFeatures features; /** * Creates a nested unified expression. * * @param expr The unified expression as a string * @param node The unified expression as an AST + * @param sc The scope + * @param ftrs The features to parse the prepared expression with */ - NestedExpression(final CharSequence expr, final JexlNode node, final Scope sc) { + NestedExpression(final CharSequence expr, final JexlNode node, final Scope sc, final JexlFeatures ftrs) { super(expr, node, null); this.scope = sc; + this.features = ftrs; if (source != this) { throw new IllegalArgumentException("Nested TemplateExpression cannot have a source"); } @@ -544,7 +548,7 @@ public final class TemplateEngine extends JxltEngine { @Override protected TemplateExpression prepare(final Interpreter interpreter) { final String value = interpreter.interpret(node).toString(); - final JexlNode dnode = jexl.jxltParse(node.jexlInfo(), noscript, value, scope); + final JexlNode dnode = jexl.jxltParse(node.jexlInfo(), features, value, scope); return new ImmediateExpression(value, dnode, this); } } @@ -912,16 +916,30 @@ public final class TemplateEngine extends JxltEngine { } public JxltEngine.Expression createExpression(final JexlInfo jexlInfo, final String expression, final Scope scope) { + return createExpression(jexlInfo, expression, scope, null); + } + + /** + * Creates an expression that is embedded in a script, an interpolation. + * + * @param jexlInfo The source info + * @param expression The expression source + * @param scope The scope + * @param ftrs The features of the script the expression is embedded in, null for those of the engine + * @return The expression + */ + public JxltEngine.Expression createExpression(final JexlInfo jexlInfo, final String expression, final Scope scope, + final JexlFeatures ftrs) { final JexlInfo info = jexlInfo == null ? jexl.createInfo() : jexlInfo; Exception xuel = null; TemplateExpression stmt = null; - final JexlFeatures features = noscript ? jexl.expressionFeatures : jexl.scriptFeatures; + final JexlFeatures features = features(ftrs); // do not cache interpolation expression, they are stored in AST node final boolean cached = cache != null && expression.length() < jexl.cacheThreshold; final JexlEngine je = Engine.putThreadEngine(jexl); try { if (!cached) { - stmt = parseExpression(info, expression, scope); + stmt = parseExpression(info, expression, scope, features); } else { final Source source = new Source(features, Scope.getSymbolsMap(scope), expression); final Object c = cache.get(source); @@ -929,7 +947,7 @@ public final class TemplateEngine extends JxltEngine { if (stmt != null) { return stmt; } - stmt = parseExpression(info, expression, scope); + stmt = parseExpression(info, expression, scope, features); cache.put(source, stmt); } } catch (final JexlException xjexl) { @@ -987,7 +1005,35 @@ public final class TemplateEngine extends JxltEngine { * @return The unified expression instance * @throws JexlException if an error occurs during parsing */ - TemplateExpression parseExpression(final JexlInfo info, final String expr, final Scope scope) { // CSOFF: MethodLength + TemplateExpression parseExpression(final JexlInfo info, final String expr, final Scope scope) { + return parseExpression(info, expr, scope, features(null)); + } + + /** + * Gets the features used to parse the JEXL expressions of a unified expression. + * + * @param ftrs The features of the script the unified expression is embedded in, null for those of the engine + * @return The features + */ + private JexlFeatures features(final JexlFeatures ftrs) { + if (ftrs == null) { + return noscript ? jexl.expressionFeatures : jexl.scriptFeatures; + } + return noscript && ftrs.supportsScript() ? new JexlFeatures(ftrs).script(false) : ftrs; + } + + /** + * Parses a unified expression. + * + * @param info The source info + * @param expr The string expression + * @param scope The template scope + * @param features The features to parse the JEXL expressions with + * @return The unified expression instance + * @throws JexlException if an error occurs during parsing + */ + TemplateExpression parseExpression(final JexlInfo info, final String expr, final Scope scope, + final JexlFeatures features) { // CSOFF: MethodLength final int size = expr.length(); final ExpressionBuilder builder = new ExpressionBuilder(0); final StringBuilder strb = new StringBuilder(size); @@ -1058,7 +1104,7 @@ public final class TemplateEngine extends JxltEngine { final String src = escapeString(strb); final JexlInfo srcInfo = info.at(lineno, column); final TemplateExpression iexpr = new ImmediateExpression(src, - jexl.jxltParse(srcInfo, noscript, src, scope), null); + jexl.jxltParse(srcInfo, features, src, scope), null); builder.add(iexpr); strb.delete(0, Integer.MAX_VALUE); state = ParseState.CONST; @@ -1106,12 +1152,12 @@ public final class TemplateEngine extends JxltEngine { if (nested) { dexpr = new NestedExpression( escapeString(expr.substring(inested, column + 1)), - jexl.jxltParse(srcInfo, noscript, src, scope), - scope); + jexl.jxltParse(srcInfo, features, src, scope), + scope, features); } else { dexpr = new DeferredExpression( src, - jexl.jxltParse(srcInfo, noscript, src, scope)); + jexl.jxltParse(srcInfo, features, src, scope)); } builder.add(dexpr); strb.delete(0, Integer.MAX_VALUE); 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 dd3e6f16..fc751d18 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java @@ -17,6 +17,7 @@ package org.apache.commons.jexl3.parser; +import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.JxltEngine; import org.apache.commons.jexl3.internal.Scope; @@ -50,8 +51,8 @@ public class ASTIdentifierAccessJxlt extends ASTIdentifierAccess implements Jexl jxltExpression = tp; } - public void setIdentifier(final String src, final Scope scope) { + public void setIdentifier(final String src, final Scope scope, final JexlFeatures features) { super.setIdentifier(src); - this.jxltExpression = JexlParser.parseInterpolation(jexlInfo(), src, scope); + this.jxltExpression = JexlParser.parseInterpolation(jexlInfo(), src, scope, features); } } 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 18c220c2..9b432a5e 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java @@ -16,6 +16,7 @@ */ package org.apache.commons.jexl3.parser; +import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.JxltEngine; import org.apache.commons.jexl3.internal.Scope; @@ -68,9 +69,9 @@ public final class ASTJxltLiteral extends JexlNode implements JexlNode.JxltHandl this.jxltExpression = e; } - void setLiteral(final String src, final Scope scope) { + void setLiteral(final String src, final Scope scope, final JexlFeatures features) { this.literal = src; - this.jxltExpression = JexlParser.parseInterpolation(jexlInfo(), src, scope); + this.jxltExpression = JexlParser.parseInterpolation(jexlInfo(), src, scope, features); } @Override 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 0e529277..2f6dbb8f 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java @@ -1170,16 +1170,18 @@ public abstract class JexlParser extends StringParser implements JexlScriptParse * @param info The JexlInfo * @param src The source to parse * @param scope The scope + * @param features The features of the script the expression is embedded in * @return The expression */ - static JxltEngine.Expression parseInterpolation(final JexlInfo info, final String src, final Scope scope) { + static JxltEngine.Expression parseInterpolation(final JexlInfo info, final String src, final Scope scope, + final JexlFeatures features) { final JexlEngine jexl = JexlEngine.getThreadEngine(); if (jexl != null) { // interpolation uses default $ and # as expression markers; // the cache size is negative to reuse the engine cache final JxltEngine jxlt = jexl.createJxltEngine(true, -1, '$', '#'); if (jxlt instanceof TemplateEngine) { - return ((TemplateEngine) jxlt).createExpression(info, src, scope); + return ((TemplateEngine) jxlt).createExpression(info, src, scope, features); } } throw new IllegalStateException("engine is not a accessible"); 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 a0e56ae9..fb98ecf9 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -1102,7 +1102,7 @@ void JxltLiteral() #JxltLiteral : } { t=<JXLT_LITERAL> - { jjtThis.setLiteral(src = Parser.buildString(t.image, true), getScope()); } + { jjtThis.setLiteral(src = Parser.buildString(t.image, true), getScope(), getFeatures()); } } void RegexLiteral() : @@ -1259,7 +1259,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), getScope()); } #IdentifierAccessJxlt + t=<JXLT_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, true), getScope(), getFeatures()); } #IdentifierAccessJxlt ) | <QDOT> ( @@ -1267,7 +1267,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), getScope()); } #IdentifierAccessSafeJxlt + t=<JXLT_LITERAL> { jjtThis.setIdentifier(Parser.buildString(t.image, true), getScope(), getFeatures()); } #IdentifierAccessSafeJxlt ) } diff --git a/src/test/java/org/apache/commons/jexl3/JxltFeatureEnforcementTest.java b/src/test/java/org/apache/commons/jexl3/JxltFeatureEnforcementTest.java index d4e6d90b..35469109 100644 --- a/src/test/java/org/apache/commons/jexl3/JxltFeatureEnforcementTest.java +++ b/src/test/java/org/apache/commons/jexl3/JxltFeatureEnforcementTest.java @@ -22,6 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collections; import org.junit.jupiter.api.Test; @@ -79,4 +81,40 @@ class JxltFeatureEnforcementTest { t.evaluate(new MapContext(), strw); assertEquals("hello 1\nhello 2\n", strw.toString()); } + + @Test + void testInterpolationHonorsScriptFeatures() { + final JexlFeatures permissive = new JexlFeatures(); + final JexlFeatures restricted = new JexlFeatures().methodCall(false); + final JexlContext ctxt = new MapContext(); + ctxt.set("s", "abc"); + final String src = "`${s.length()}`"; + // the features of the script are more restrictive than those of the engine + final JexlEngine jexl = new JexlBuilder().features(permissive).create(); + assertEquals("3", jexl.createScript(src).execute(ctxt).toString()); + assertThrows(JexlException.Feature.class, () -> jexl.createScript(restricted, null, "s.length()")); + for (final String istr : Arrays.asList(src, "`#{s.length()}`", "s.`${s.length()}`", "s?.`${s.length()}`")) { + final JxltEngine.Exception xjxlt = assertThrows(JxltEngine.Exception.class, + () -> jexl.createScript(restricted, null, istr), + "interpolation must honor the features of its script: " + istr); + assertInstanceOf(JexlException.Feature.class, xjxlt.getCause()); + } + // the features of the script are more permissive than those of the engine + final JexlEngine rjexl = new JexlBuilder().features(restricted).create(); + assertThrows(JxltEngine.Exception.class, () -> rjexl.createScript(src)); + assertEquals("3", rjexl.createScript(permissive, null, src).execute(ctxt).toString()); + assertThrows(JxltEngine.Exception.class, () -> rjexl.createScript(src)); + } + + @Test + void testInterpolationHonorsDeclaredAntish() { + final JexlFeatures declared = new JexlFeatures().antishTest(JexlFeatures.namePatterns(Collections.singletonList("x.y"))); + final JexlContext ctxt = new MapContext(); + ctxt.set("x.y", 42); + // the antish variable is declared by the features of the script, not by those of the engine + final JexlEngine jexl = new JexlBuilder().strict(true).create(); + assertEquals(42, jexl.createScript(declared, null, "x.y").execute(ctxt)); + assertEquals("42", jexl.createScript(declared, null, "`${x.y}`").execute(ctxt).toString()); + assertEquals("42", jexl.createScript(declared, null, "`#{x.y}`").execute(ctxt).toString()); + } }
