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());
+    }
 }

Reply via email to