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

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


The following commit(s) were added to refs/heads/master by this push:
     new c27b0f5e JEXL: code cleanup; - remove final parameters from method 
declarations (no body); - set/get attributes caching was broken; - try and 
please PMD and spotbugs;
c27b0f5e is described below

commit c27b0f5e0760ed0a5c4a7715ff090f006f340eb2
Author: Henrib <[email protected]>
AuthorDate: Mon Feb 16 18:28:56 2026 +0100

    JEXL: code cleanup;
    - remove final parameters from method declarations (no body);
    - set/get attributes caching was broken;
    - try and please PMD and spotbugs;
---
 RELEASE-NOTES.txt                                  |  43 +++-
 src/changes/changes.xml                            |   1 +
 src/main/config/findbugs-exclude-filter.xml        |  30 ++-
 src/main/config/pmd.xml                            |   3 +
 .../org/apache/commons/jexl3/JexlArithmetic.java   |   9 +-
 .../org/apache/commons/jexl3/JexlOperator.java     |  36 +--
 .../org/apache/commons/jexl3/internal/Engine.java  |  14 +-
 .../org/apache/commons/jexl3/internal/Frame.java   |  10 +-
 .../commons/jexl3/internal/InterpreterBase.java    |  67 +++---
 .../commons/jexl3/internal/TemplateDebugger.java   |   2 +-
 .../commons/jexl3/internal/TemplateEngine.java     | 254 ++++++++++-----------
 .../jexl3/internal/introspection/Uberspect.java    |   3 +-
 .../jexl3/introspection/JexlPermissions.java       |  12 +-
 .../apache/commons/jexl3/parser/ParserVisitor.java |  34 +--
 .../org/apache/commons/jexl3/Issues400Test.java    |   1 -
 .../internal/introspection/PermissionsTest.java    |  40 ++++
 src/test/java/org/example/Pair.java                |  38 +++
 17 files changed, 365 insertions(+), 232 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 90ea4f06..8861c1a3 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -1,27 +1,24 @@
-Apache Commons JEXL 3.6.2 Release Notes
----------------------------------------
+Apache Commons JEXL 3.6.3 Release Notes
+=======================================
 
-The Apache Commons JEXL team is pleased to announce the release of Apache 
Commons JEXL 3.6.2.
+The Apache Commons JEXL team is pleased to announce the release of Apache 
Commons JEXL 3.6.3.
 
 Introduction
-------------
+============
 
 Apache Commons JEXL is a library that enables scripting features in Java 
applications and frameworks..
 
 This is a feature and maintenance release. Java 8 or later is required.
 
 
-Fixed Bugs
-----------
+Bugs Fixed in 3.6.3:
+====================
 
-o JEXL-455:  Tokenization error with multiline expressions. Thanks to Vincent 
Bussol. 
-o JEXL-454:  Switch NaN case not found. Thanks to Vincent Bussol. 
-o JEXL-453:  Finally clause is not evaluated. Thanks to Vincent Bussol. 
+o JEXL-456:  Change in template parser behavior.
 
-Changes
--------
 
-o          Bump org.apache.commons:commons-parent from 93 to 96. Thanks to 
Gary Gregory. 
+Changes in 3.6.3:
+=================
 
 
 Historical list of changes: 
https://commons.apache.org/proper/commons-jexl/changes.html
@@ -34,6 +31,28 @@ https://commons.apache.org/proper/commons-jexl/
 Download page: https://commons.apache.org/proper/commons-jexl/download_jexl.cgi
 
 
+========================================================================================================================
+Release 3.6.2
+========================================================================================================================
+
+Version 3.6.2 is a minor release.
+
+Compatibility with previous releases
+====================================
+Version 3.6.2 is source and binary compatible with 3.6.
+
+
+Bugs Fixed in 3.6.2:
+====================
+o JEXL-455:  Tokenization error with multiline expressions.
+o JEXL-454:  Switch NaN case not found.
+o JEXL-453:  Finally clause is not evaluated.
+
+Changes in 3.6.2:
+=================
+o          Bump org.apache.commons:commons-parent from 93 to 96. Thanks to 
Gary Gregory.
+
+
 
========================================================================================================================
 Release 3.6.1
 
========================================================================================================================
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 86823fe3..9b65913d 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -30,6 +30,7 @@
         <release version="3.6.3" date="YYYY-MM-DD"
                  description="This is a feature and maintenance release. Java 
8 or later is required.">
             <!-- FIX -->
+            <action dev="henrib" type="fix" issue="JEXL-456" due-to="Vincent 
Bussol">Change in template parser behavior.</action>
             <!-- ADD -->
             <!-- UPDATE -->
         </release>
diff --git a/src/main/config/findbugs-exclude-filter.xml 
b/src/main/config/findbugs-exclude-filter.xml
index d781bd17..d0005c7c 100644
--- a/src/main/config/findbugs-exclude-filter.xml
+++ b/src/main/config/findbugs-exclude-filter.xml
@@ -55,26 +55,44 @@
     <Match>
         <Class name="org.apache.commons.jexl3.parser.SimpleNode"/>
     </Match>
+    <Match>
+        <Class name="org.apache.commons.jexl3.parser.ASTCaseStatement"/>
+    </Match>
+    <Match>
+        <Class name="org.apache.commons.jexl3.parser.ASTSwitchStatement"/>
+    </Match>
+    <Match>
+        <Class name="org.apache.commons.jexl3.parser.ASTIdentifierAccessJxlt"/>
+    </Match>
     <Match>
         <Class name="org.apache.commons.jexl3.JexlBuilder"/>
-        <Bug code="EI2,EI"></Bug>
+        <Bug code="EI2,EI"/>
     </Match>
     <Match>
         <Package name="org.apache.commons.jexl3.internal"/>
-        <Bug code="EI2,EI"></Bug>
+        <Bug code="EI2,EI"/>
     </Match>
     <Match>
         <Package name="org.apache.commons.jexl3.introspection.internal"/>
-        <Bug code="EI2,EI"></Bug>
+        <Bug code="EI2,EI"/>
     </Match>
     <Match>
         <Package name="org.apache.commons.jexl3.parser"/>
-        <Bug code="EI2,EI"></Bug>
+        <Bug code="EI2,EI"/>
     </Match>
     <Match>
-        <Class name="org.apache.commons.jexl3.parser.ASTCaseStatement"/>
+        <Bug pattern="EI_EXPOSE_REP"/>
     </Match>
     <Match>
-        <Class name="org.apache.commons.jexl3.parser.ASTSwitchStatement"/>
+        <Bug pattern="EI_EXPOSE_REP2"/>
+    </Match>
+    <Match>
+        <Bug pattern="NP_BOOLEAN_RETURN_NULL"/>
+    </Match>
+    <Match>
+        <Bug pattern="CT_CONSTRUCTOR_THROW"/>
+    </Match>
+    <Match>
+        <Bug pattern="ES_COMPARING_STRING_WITH_EQ"/>
     </Match>
 </FindBugsFilter>
diff --git a/src/main/config/pmd.xml b/src/main/config/pmd.xml
index e2ecd9c3..c21230a2 100644
--- a/src/main/config/pmd.xml
+++ b/src/main/config/pmd.xml
@@ -23,6 +23,9 @@ limitations under the License.
     <description>
         JEXL custom rules
     </description>
+    <exclude-pattern>.*/JexlContext.java</exclude-pattern>
+    <exclude-pattern>.*/JexlUberspect.java</exclude-pattern>
+    <exclude-pattern>.*/parser/.*</exclude-pattern>
 
     <rule ref="category/java/bestpractices.xml">
         <!-- Often arguments and parameters array, not semantically varargs -->
diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java 
b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java
index f2d36eac..cfa3b986 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java
@@ -240,6 +240,9 @@ public class JexlArithmetic {
                }
             } catch (final NoSuchMethodException xany) {
                 arithmeticClass = arithmeticClass.getSuperclass();
+            } catch (final SecurityException xany) {
+                // ignore
+                break;
             }
         }
         return false;
@@ -1624,7 +1627,7 @@ public class JexlArithmetic {
     @Deprecated
     public JexlArithmetic options(final JexlEngine.Options options) {
         if (options != null) {
-            final boolean isstrict = Boolean.TRUE == 
options.isStrictArithmetic() || isStrict();
+            final boolean strict = 
Boolean.TRUE.equals(options.isStrictArithmetic()) || isStrict();
             MathContext bigdContext = options.getArithmeticMathContext();
             if (bigdContext == null) {
                 bigdContext = getMathContext();
@@ -1633,10 +1636,10 @@ public class JexlArithmetic {
             if (bigdScale == Integer.MIN_VALUE) {
                 bigdScale = getMathScale();
             }
-            if (isstrict != isStrict()
+            if (strict != isStrict()
                 || bigdScale != getMathScale()
                 || bigdContext != getMathContext()) {
-                return createWithOptions(isstrict, bigdContext, bigdScale);
+                return createWithOptions(strict, bigdContext, bigdScale);
             }
         }
         return this;
diff --git a/src/main/java/org/apache/commons/jexl3/JexlOperator.java 
b/src/main/java/org/apache/commons/jexl3/JexlOperator.java
index bae950b5..29e66940 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlOperator.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlOperator.java
@@ -622,10 +622,10 @@ public enum JexlOperator {
          * @return JexlEngine.TRY_FAILED if no operation was performed,
          *         the value to use as the side effect argument otherwise
          */
-        Object tryAssignOverload(final JexlCache.Reference node,
-                                 final JexlOperator operator,
-                                 final Consumer<Object> assign,
-                                 final Object... args);
+        Object tryAssignOverload(JexlCache.Reference node,
+                                 JexlOperator operator,
+                                 Consumer<Object> assign,
+                                 Object... args);
 
         /**
          * Calculate the {@code size} of various types:
@@ -637,7 +637,7 @@ public enum JexlOperator {
          * @param object the object to get the size of
          * @return the evaluation result
          */
-        Object size(final JexlCache.Reference node, final Object object);
+        Object size(JexlCache.Reference node, Object object);
 
         /**
          * Check for emptiness of various types: Collection, Array, Map, 
String, and anything that has a boolean isEmpty()
@@ -649,7 +649,7 @@ public enum JexlOperator {
          * @param object the object to check the emptiness of
          * @return the evaluation result
          */
-        Object empty(final JexlCache.Reference node, final Object object);
+        Object empty(JexlCache.Reference node, Object object);
 
         /**
          * The 'match'/'in' operator implementation.
@@ -665,10 +665,10 @@ public enum JexlOperator {
          * @param left  the right operand
          * @return true if left matches right, false otherwise
          */
-        boolean contains(final JexlCache.Reference node,
-                         final JexlOperator operator,
-                         final Object left,
-                         final Object right);
+        boolean contains(JexlCache.Reference node,
+                         JexlOperator operator,
+                         Object left,
+                         Object right);
 
         /**
          * The 'startsWith' operator implementation.
@@ -680,10 +680,10 @@ public enum JexlOperator {
          * @param right    the right operand
          * @return true if left starts with right, false otherwise
          */
-        boolean startsWith(final JexlCache.Reference node,
-                           final JexlOperator operator,
-                           final Object left,
-                           final Object right);
+        boolean startsWith(JexlCache.Reference node,
+                           JexlOperator operator,
+                           Object left,
+                           Object right);
 
         /**
          * The 'endsWith' operator implementation.
@@ -695,9 +695,9 @@ public enum JexlOperator {
          * @param right    the right operand
          * @return true if left ends with right, false otherwise
          */
-        boolean endsWith(final JexlCache.Reference node,
-                         final JexlOperator operator,
-                         final Object left,
-                         final Object right);
+        boolean endsWith(JexlCache.Reference node,
+                         JexlOperator operator,
+                         Object left,
+                         Object right);
     }
 }
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 f5bb1477..952094fa 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java
@@ -360,6 +360,13 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
      * @param conf the builder
      */
     public Engine(final JexlBuilder conf) {
+        // core properties:
+        final JexlUberspect uber = conf.uberspect() == null
+            ? getUberspect(conf.logger(), conf.strategy(), conf.permissions())
+            : conf.uberspect();
+        if (uber == null) {
+            throw new IllegalArgumentException("uberspect cannot be null");
+        }
         // options:
         this.options = conf.options().copy();
         this.strict = options.isStrict();
@@ -370,13 +377,6 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
         this.debug = option(conf.debug(), true);
         this.collectMode = conf.collectMode();
         this.stackOverflow = conf.stackOverflow() > 0? conf.stackOverflow() : 
Integer.MAX_VALUE;
-        // core properties:
-        final JexlUberspect uber = conf.uberspect() == null
-                ? getUberspect(conf.logger(), conf.strategy(), 
conf.permissions())
-                : conf.uberspect();
-        if (uber == null) {
-            throw new IllegalArgumentException("uberspect cannot be null");
-        }
         final ClassLoader loader = conf.loader();
         if (loader != null) {
             uber.setClassLoader(loader);
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 b55090ad..dbfaf212 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Frame.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Frame.java
@@ -43,14 +43,14 @@ public class Frame {
      * @param c the number of curried parameters
      */
     protected Frame(final Scope s, final Object[] r, final int c) {
-        scope = s;
-        stack = r;
-        curried = c;
-        final String[] symbols = scope.getSymbols();
+        final String[] symbols = s.getSymbols();
         if (symbols.length != r.length) {
             throw new IllegalArgumentException("Scope and stack frame size 
mismatch: "
-                    + symbols.length + " != " + r.length);
+                + symbols.length + " != " + r.length);
         }
+        scope = s;
+        stack = r;
+        curried = c;
     }
 
     /**
diff --git 
a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java 
b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
index e67dfe60..20a824bb 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
@@ -617,24 +617,29 @@ public abstract class InterpreterBase extends 
ParserVisitor {
             throw new JexlException(node, "object is null");
         }
         cancelCheck(node);
-        final JexlOperator operator = node != null && node.jjtGetParent() 
instanceof ASTArrayAccess
-                ? JexlOperator.ARRAY_GET : JexlOperator.PROPERTY_GET;
-        final Object result = operators.tryOverload(node, operator, object, 
attribute);
-        if (result != JexlEngine.TRY_FAILED) {
-            return result;
-        }
         Exception xcause = null;
         try {
-            // attempt to reuse last executor cached in volatile JexlNode.value
-            if (node != null && cache) {
-                final Object cached = node.jjtGetValue();
-                if (cached instanceof JexlPropertyGet) {
-                    final JexlPropertyGet vg = (JexlPropertyGet) cached;
-                    final Object value = vg.tryInvoke(object, attribute);
-                    if (!vg.tryFailed(value)) {
-                        return value;
+            final JexlOperator operator;
+            if (node != null) {
+                // attempt to reuse last executor cached in volatile 
JexlNode.value
+                if (cache) {
+                    final Object cached = node.jjtGetValue();
+                    if (cached instanceof JexlPropertyGet) {
+                        final JexlPropertyGet vg = (JexlPropertyGet) cached;
+                        final Object value = vg.tryInvoke(object, attribute);
+                        if (!vg.tryFailed(value)) {
+                            return value;
+                        }
                     }
                 }
+                // try operator overload
+                operator = node.jjtGetParent() instanceof ASTArrayAccess ? 
JexlOperator.ARRAY_GET : JexlOperator.PROPERTY_GET;
+                final Object result = operators.tryOverload(node, operator, 
object, attribute);
+                if (result != JexlEngine.TRY_FAILED) {
+                    return result;
+                }
+            } else {
+                operator = JexlOperator.PROPERTY_GET;
             }
             // resolve that property
             final List<JexlUberspect.PropertyResolver> resolvers = 
uberspect.getResolvers(operator, object);
@@ -985,25 +990,31 @@ public abstract class InterpreterBase extends 
ParserVisitor {
      */
     protected void setAttribute(final Object object, final Object attribute, 
final Object value, final JexlNode node) {
         cancelCheck(node);
-        final JexlOperator operator = node != null && node.jjtGetParent() 
instanceof ASTArrayAccess
-                                      ? JexlOperator.ARRAY_SET : 
JexlOperator.PROPERTY_SET;
-        final Object result = operators.tryOverload(node, operator, object, 
attribute, value);
-        if (result != JexlEngine.TRY_FAILED) {
-            return;
-        }
         Exception xcause = null;
         try {
-            // attempt to reuse last executor cached in volatile JexlNode.value
-            if (node != null && cache) {
-                final Object cached = node.jjtGetValue();
-                if (cached instanceof JexlPropertySet) {
-                    final JexlPropertySet setter = (JexlPropertySet) cached;
-                    final Object eval = setter.tryInvoke(object, attribute, 
value);
-                    if (!setter.tryFailed(eval)) {
-                        return;
+            final JexlOperator operator;
+            if (node != null) {
+                // attempt to reuse last executor cached in volatile 
JexlNode.value
+                if (cache) {
+                    final Object cached = node.jjtGetValue();
+                    if (cached instanceof JexlPropertySet) {
+                        final JexlPropertySet setter = (JexlPropertySet) 
cached;
+                        final Object eval = setter.tryInvoke(object, 
attribute, value);
+                        if (!setter.tryFailed(eval)) {
+                            return;
+                        }
                     }
                 }
+                // try operator overload
+                operator = node.jjtGetParent() instanceof ASTArrayAccess ? 
JexlOperator.ARRAY_SET : JexlOperator.PROPERTY_SET;
+                final Object result = operators.tryOverload(node, operator, 
object, attribute, value);
+                if (result != JexlEngine.TRY_FAILED) {
+                    return;
+                }
+            } else {
+                operator = JexlOperator.PROPERTY_SET;
             }
+            // resolve that property
             final List<JexlUberspect.PropertyResolver> resolvers = 
uberspect.getResolvers(operator, object);
             JexlPropertySet vs = uberspect.getPropertySet(resolvers, object, 
attribute, value);
             // if we can't find an exact match, narrow the value argument and 
try again
diff --git 
a/src/main/java/org/apache/commons/jexl3/internal/TemplateDebugger.java 
b/src/main/java/org/apache/commons/jexl3/internal/TemplateDebugger.java
index 383ae2c8..fdb660a7 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/TemplateDebugger.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateDebugger.java
@@ -317,7 +317,7 @@ public class TemplateDebugger extends Debugger {
             case COMPOSITE:
                 r = visit((CompositeExpression) expr, data);
                 break;
-            default:
+            default: // in case of new state in the future
                 r = null;
         }
         return r;
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 28b11a08..d3dc7e88 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
@@ -99,7 +99,7 @@ public final class TemplateEngine extends JxltEngine {
          * @param prefix the line prefix (immediate or deferred)
          */
         void toString(final StringBuilder strb, final String prefix) {
-            if (BlockType.VERBATIM.equals(type)) {
+            if (BlockType.VERBATIM == type) {
                 strb.append(body);
             } else {
                 final Iterator<CharSequence> lines = readLines(new 
StringReader(body));
@@ -997,145 +997,145 @@ public final class TemplateEngine extends JxltEngine {
         for (int column = 0; column < size; ++column) {
             final char c = expr.charAt(column);
             switch (state) {
-            case CONST:
-                if (c == immediateChar) {
-                    state = ParseState.IMMEDIATE0;
-                } else if (c == deferredChar) {
-                    inested = column;
-                    state = ParseState.DEFERRED0;
-                } else if (c == '\\') {
-                    state = ParseState.ESCAPE;
-                } else {
-                    // do buildup expr
-                    strb.append(c);
-                }
-                break;
-            case IMMEDIATE0: // $
-                if (c != '{') {
-                    // revert to CONST
-                    strb.append(immediateChar);
-                    state = ParseState.CONST;
-                    // 'unread' the current character
-                    column -= 1;
-                    continue;
-                }
-                state = ParseState.IMMEDIATE1;
-                // if chars in buffer, create constant
-                if (strb.length() > 0) {
-                    final TemplateExpression cexpr = new 
ConstantExpression(strb.toString(), null);
-                    builder.add(cexpr);
-                    strb.delete(0, Integer.MAX_VALUE);
-                }
-                break;
-            case DEFERRED0: // #
-                if (c != '{') {
-                    // revert to CONST
-                    strb.append(deferredChar);
-                    state = ParseState.CONST;
-                    // 'unread' the current character
-                    column -= 1;
-                    continue;
-                }
-                state = ParseState.DEFERRED1;
-                // if chars in buffer, create constant
-                if (strb.length() > 0) {
-                    final TemplateExpression cexpr = new 
ConstantExpression(strb.toString(), null);
-                    builder.add(cexpr);
-                    strb.delete(0, Integer.MAX_VALUE);
-                }
-                break;
-            case IMMEDIATE1: // ${...
-                if (c == '}') {
-                    if (immediate1 > 0) {
-                        immediate1 -= 1;
-                        strb.append(c);
+                case CONST:
+                    if (c == immediateChar) {
+                        state = ParseState.IMMEDIATE0;
+                    } else if (c == deferredChar) {
+                        inested = column;
+                        state = ParseState.DEFERRED0;
+                    } else if (c == '\\') {
+                        state = ParseState.ESCAPE;
                     } else {
-                        // materialize the immediate expr
-                        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);
-                        builder.add(iexpr);
-                        strb.delete(0, Integer.MAX_VALUE);
+                        // do buildup expr
+                        strb.append(c);
+                    }
+                    break;
+                case IMMEDIATE0: // $
+                    if (c != '{') {
+                        // revert to CONST
+                        strb.append(immediateChar);
                         state = ParseState.CONST;
+                        // 'unread' the current character
+                        column -= 1;
+                        continue;
                     }
-                } else if (!isIgnorable(c)) {
-                    if (c == '{') {
-                        immediate1 += 1;
+                    state = ParseState.IMMEDIATE1;
+                    // if chars in buffer, create constant
+                    if (strb.length() > 0) {
+                        final TemplateExpression cexpr = new 
ConstantExpression(strb.toString(), null);
+                        builder.add(cexpr);
+                        strb.delete(0, Integer.MAX_VALUE);
                     }
-                    // do buildup expr
-                    column = append(strb, expr, column, c);
-                }
-                break;
-            case DEFERRED1: // #{...
-                // skip inner strings - for '}' -
-                // nested immediate in deferred; need to balance count of '{' 
& '}'
-                // closing '}'
-                switch (c) {
-                    case '"':
-                    case '\'':
-                        strb.append(c);
-                        column = StringParser.readString(strb, expr, column + 
1, c);
-                        continue;
-                    case '{':
-                        if (expr.charAt(column - 1) == immediateChar) {
-                            inner1 += 1;
-                            strb.deleteCharAt(strb.length() - 1);
-                            nested = true;
-                        } else {
-                            deferred1 += 1;
-                            strb.append(c);
-                        }
+                    break;
+                case DEFERRED0: // #
+                    if (c != '{') {
+                        // revert to CONST
+                        strb.append(deferredChar);
+                        state = ParseState.CONST;
+                        // 'unread' the current character
+                        column -= 1;
                         continue;
-                    case '}':
-                        // balance nested immediate
-                        if (deferred1 > 0) {
-                            deferred1 -= 1;
+                    }
+                    state = ParseState.DEFERRED1;
+                    // if chars in buffer, create constant
+                    if (strb.length() > 0) {
+                        final TemplateExpression cexpr = new 
ConstantExpression(strb.toString(), null);
+                        builder.add(cexpr);
+                        strb.delete(0, Integer.MAX_VALUE);
+                    }
+                    break;
+                case IMMEDIATE1: // ${...
+                    if (c == '}') {
+                        if (immediate1 > 0) {
+                            immediate1 -= 1;
                             strb.append(c);
-                        } else if (inner1 > 0) {
-                            inner1 -= 1;
                         } else {
-                            // materialize the nested/deferred expr
+                            // materialize the immediate expr
                             final String src = escapeString(strb);
                             final JexlInfo srcInfo = info.at(lineno, column);
-                            TemplateExpression dexpr;
-                            if (nested) {
-                                dexpr = new NestedExpression(
-                                        escapeString(expr.substring(inested, 
column + 1)),
-                                        jexl.jxltParse(srcInfo, noscript, src, 
scope),
-                                        scope);
-                            } else {
-                                dexpr = new DeferredExpression(
-                                        src,
-                                        jexl.jxltParse(srcInfo, noscript, src, 
scope));
-                            }
-                            builder.add(dexpr);
+                            final TemplateExpression iexpr = new 
ImmediateExpression(src,
+                                jexl.jxltParse(srcInfo, noscript, src, scope), 
null);
+                            builder.add(iexpr);
                             strb.delete(0, Integer.MAX_VALUE);
-                            nested = false;
                             state = ParseState.CONST;
                         }
-                        break;
-                    default:
-                        if (!isIgnorable(c)) {
-                            // do buildup expr
-                            column = append(strb, expr, column, c);
+                    } else if (!isIgnorable(c)) {
+                        if (c == '{') {
+                            immediate1 += 1;
                         }
-                        break;
-                }
-                break;
-            case ESCAPE:
-                if (c == deferredChar) {
-                    strb.append(deferredChar);
-                } else if (c == immediateChar) {
-                    strb.append(immediateChar);
-                } else {
-                    strb.append('\\');
-                    strb.append(c);
-                }
-                state = ParseState.CONST;
-                break;
-            default: // in case we ever add new unified expression type
-                throw new UnsupportedOperationException("unexpected unified 
expression type");
+                        // do buildup expr
+                        column = append(strb, expr, column, c);
+                    }
+                    break;
+                case DEFERRED1: // #{...
+                    // skip inner strings - for '}' -
+                    // nested immediate in deferred; need to balance count of 
'{' & '}'
+                    // closing '}'
+                    switch (c) {
+                        case '"':
+                        case '\'':
+                            strb.append(c);
+                            column = StringParser.readString(strb, expr, 
column + 1, c);
+                            continue;
+                        case '{':
+                            if (expr.charAt(column - 1) == immediateChar) {
+                                inner1 += 1;
+                                strb.deleteCharAt(strb.length() - 1);
+                                nested = true;
+                            } else {
+                                deferred1 += 1;
+                                strb.append(c);
+                            }
+                            continue;
+                        case '}':
+                            // balance nested immediate
+                            if (deferred1 > 0) {
+                                deferred1 -= 1;
+                                strb.append(c);
+                            } else if (inner1 > 0) {
+                                inner1 -= 1;
+                            } else {
+                                // materialize the nested/deferred expr
+                                final String src = escapeString(strb);
+                                final JexlInfo srcInfo = info.at(lineno, 
column);
+                                TemplateExpression dexpr;
+                                if (nested) {
+                                    dexpr = new NestedExpression(
+                                            
escapeString(expr.substring(inested, column + 1)),
+                                            jexl.jxltParse(srcInfo, noscript, 
src, scope),
+                                            scope);
+                                } else {
+                                    dexpr = new DeferredExpression(
+                                            src,
+                                            jexl.jxltParse(srcInfo, noscript, 
src, scope));
+                                }
+                                builder.add(dexpr);
+                                strb.delete(0, Integer.MAX_VALUE);
+                                nested = false;
+                                state = ParseState.CONST;
+                            }
+                            break;
+                        default:
+                            if (!isIgnorable(c)) {
+                                // do buildup expr
+                                column = append(strb, expr, column, c);
+                            }
+                            break;
+                    }
+                    break;
+                case ESCAPE:
+                    if (c == deferredChar) {
+                        strb.append(deferredChar);
+                    } else if (c == immediateChar) {
+                        strb.append(immediateChar);
+                    } else {
+                        strb.append('\\');
+                        strb.append(c);
+                    }
+                    state = ParseState.CONST;
+                    break;
+                default: // in case we ever add new unified expression type // 
NOPMD
+                    throw new UnsupportedOperationException("unexpected 
unified expression type");
             }
             if (c == '\n') {
                 lineno += 1;
diff --git 
a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java 
b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java
index 610120e4..d8d2dbf0 100644
--- 
a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java
+++ 
b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java
@@ -99,11 +99,12 @@ public class Uberspect implements JexlUberspect {
      * @param perms the introspector permissions
      */
     public Uberspect(final Log runtimeLogger, final 
JexlUberspect.ResolverStrategy sty, final JexlPermissions perms) {
+        final ClassLoader cl = getClass().getClassLoader();
         logger = runtimeLogger == null ? LogFactory.getLog(JexlEngine.class) : 
runtimeLogger;
         strategy = sty == null ? JexlUberspect.JEXL_STRATEGY : sty;
         permissions = perms == null ? JexlPermissions.RESTRICTED : perms;
         ref = new SoftReference<>(null);
-        loader = new SoftReference<>(getClass().getClassLoader());
+        loader = new SoftReference<>(cl);
         operatorMap = new ConcurrentHashMap<>();
         version = new AtomicInteger();
     }
diff --git 
a/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java 
b/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java
index a70424ef..d84817b5 100644
--- a/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java
+++ b/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java
@@ -339,7 +339,7 @@ public interface JexlPermissions {
      * @return true if JEXL is allowed to introspect, false otherwise
      * @since 3.3
      */
-    boolean allow(final Class<?> clazz);
+    boolean allow(Class<?> clazz);
 
     /**
      * Checks whether a constructor allows JEXL introspection.
@@ -350,7 +350,7 @@ public interface JexlPermissions {
      * @return true if JEXL is allowed to introspect, false otherwise
      * @since 3.3
      */
-    boolean allow(final Constructor<?> ctor);
+    boolean allow(Constructor<?> ctor);
 
     /**
      * Checks whether a field explicitly disallows JEXL introspection.
@@ -360,7 +360,7 @@ public interface JexlPermissions {
      * @return true if JEXL is allowed to introspect, false otherwise
      * @since 3.3
      */
-    boolean allow(final Field field);
+    boolean allow(Field field);
 
     /**
      * Checks whether a method allows JEXL introspection.
@@ -372,7 +372,7 @@ public interface JexlPermissions {
      * @return true if JEXL is allowed to introspect, false otherwise
      * @since 3.3
      */
-    boolean allow(final Method method);
+    boolean allow(Method method);
 
     /**
      * Checks whether a package allows JEXL introspection.
@@ -383,7 +383,7 @@ public interface JexlPermissions {
      * @return true if JEXL is allowed to introspect, false otherwise
      * @since 3.3
      */
-    boolean allow(final Package pack);
+    boolean allow(Package pack);
 
     /**
      * Compose these permissions with a new set.
@@ -394,7 +394,7 @@ public interface JexlPermissions {
      * @param src the new constraints
      * @return the new permissions
      */
-    JexlPermissions compose(final String... src);
+    JexlPermissions compose(String... src);
 
     /**
      * Checks that a class is valid for permission check.
diff --git a/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java 
b/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java
index 44e7c497..bf089df3 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java
@@ -61,17 +61,17 @@ public abstract class ParserVisitor {
 
     protected abstract Object visit(ASTConstructorNode node, Object data);
 
-    protected abstract Object visit(ASTSwitchStatement node, final Object 
data);
+    protected abstract Object visit(ASTSwitchStatement node, Object data);
 
-    protected abstract Object visit(ASTCaseStatement node, final Object data);
+    protected abstract Object visit(ASTCaseStatement node, Object data);
 
-    protected abstract Object visit(ASTSwitchExpression node, final Object 
data);
+    protected abstract Object visit(ASTSwitchExpression node, Object data);
 
-    protected abstract Object visit(ASTCaseExpression node, final Object data);
+    protected abstract Object visit(ASTCaseExpression node, Object data);
 
     protected abstract Object visit(ASTContinue node, Object data);
 
-    protected abstract Object visit(ASTDecrementGetNode node, final Object 
data);
+    protected abstract Object visit(ASTDecrementGetNode node, Object data);
 
     protected abstract Object visit(ASTDefineVars node, Object data);
 
@@ -99,9 +99,9 @@ public abstract class ParserVisitor {
 
     protected abstract Object visit(ASTGENode node, Object data);
 
-    protected abstract Object visit(ASTGetDecrementNode node, final Object 
data);
+    protected abstract Object visit(ASTGetDecrementNode node, Object data);
 
-    protected abstract Object visit(ASTGetIncrementNode node, final Object 
data);
+    protected abstract Object visit(ASTGetIncrementNode node, Object data);
 
     protected abstract Object visit(ASTGTNode node, Object data);
 
@@ -111,9 +111,9 @@ public abstract class ParserVisitor {
 
     protected abstract Object visit(ASTIfStatement node, Object data);
 
-    protected abstract Object visit(ASTIncrementGetNode node, final Object 
data);
+    protected abstract Object visit(ASTIncrementGetNode node, Object data);
 
-    protected abstract Object visit(final ASTInstanceOf node, final Object 
data);
+    protected abstract Object visit(ASTInstanceOf node, Object data);
 
     protected abstract Object visit(ASTJexlScript node, Object data);
 
@@ -139,7 +139,7 @@ public abstract class ParserVisitor {
 
     protected abstract Object visit(ASTNEWNode node, Object data);
 
-    protected abstract Object visit(final ASTNotInstanceOf node, final Object 
data);
+    protected abstract Object visit(ASTNotInstanceOf node, Object data);
 
     protected abstract Object visit(ASTNotNode node, Object data);
 
@@ -155,7 +155,7 @@ public abstract class ParserVisitor {
 
     protected abstract Object visit(ASTOrNode node, Object data);
 
-    protected abstract Object visit(final ASTQualifiedIdentifier node, final 
Object data);
+    protected abstract Object visit(ASTQualifiedIdentifier node, Object data);
 
     protected abstract Object visit(ASTRangeNode node, Object data);
 
@@ -181,21 +181,21 @@ public abstract class ParserVisitor {
 
     protected abstract Object visit(ASTSetOrNode node, Object data);
 
-    protected abstract Object visit(ASTSetShiftLeftNode node, final Object 
data);
+    protected abstract Object visit(ASTSetShiftLeftNode node, Object data);
 
-    protected abstract Object visit(ASTSetShiftRightNode node, final Object 
data);
+    protected abstract Object visit(ASTSetShiftRightNode node, Object data);
 
-    protected abstract Object visit(ASTSetShiftRightUnsignedNode node, final 
Object data);
+    protected abstract Object visit(ASTSetShiftRightUnsignedNode node, Object 
data);
 
     protected abstract Object visit(ASTSetSubNode node, Object data);
 
     protected abstract Object visit(ASTSetXorNode node, Object data);
 
-    protected abstract Object visit(ASTShiftLeftNode node, final Object data);
+    protected abstract Object visit(ASTShiftLeftNode node, Object data);
 
-    protected abstract Object visit(ASTShiftRightNode node, final Object data);
+    protected abstract Object visit(ASTShiftRightNode node, Object data);
 
-    protected abstract Object visit(ASTShiftRightUnsignedNode node, final 
Object data);
+    protected abstract Object visit(ASTShiftRightUnsignedNode node, Object 
data);
 
     protected abstract Object visit(ASTSizeFunction node, Object data);
 
diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java 
b/src/test/java/org/apache/commons/jexl3/Issues400Test.java
index 280fd6d6..8b2a44ba 100644
--- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java
+++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java
@@ -1157,6 +1157,5 @@ public class Issues400Test {
         template.evaluate(null, writer);
         Assertions.assertEquals("42", writer.toString());
     }
-
 }
 
diff --git 
a/src/test/java/org/apache/commons/jexl3/internal/introspection/PermissionsTest.java
 
b/src/test/java/org/apache/commons/jexl3/internal/introspection/PermissionsTest.java
index 73f30511..37a8145a 100644
--- 
a/src/test/java/org/apache/commons/jexl3/internal/introspection/PermissionsTest.java
+++ 
b/src/test/java/org/apache/commons/jexl3/internal/introspection/PermissionsTest.java
@@ -29,6 +29,7 @@ import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
@@ -46,6 +47,8 @@ import org.apache.commons.jexl3.MapContext;
 import org.apache.commons.jexl3.internal.introspection.nojexlpackage.Invisible;
 import org.apache.commons.jexl3.introspection.JexlPermissions;
 import org.apache.commons.jexl3.introspection.JexlUberspect;
+import org.example.Pair;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -494,4 +497,41 @@ class PermissionsTest {
         found = Permissions.wildcardAllow(wildcards, "com.google.spexl");
         assertFalse(found);
     }
+
+    public static class Scheme {
+        public Pair cons(Object first, Object second) {
+            return new Pair(first, second);
+        }
+    }
+
+    @Test
+    void testPair0() {
+        final Map<String, Object> funcs = new HashMap<>();
+        funcs.put("lisp", new Scheme());
+        final JexlPermissions permissions = 
JexlPermissions.RESTRICTED.compose("org.example.*");
+        final JexlEngine jexl = new 
JexlBuilder().cache(8).permissions(permissions).namespaces(funcs).create();
+        String src = "let p = lisp:cons(17, 25); p.car + p.cdr;";
+        JexlScript script = jexl.createScript(src);
+        Assertions.assertEquals(42, script.execute(null));
+        Assertions.assertEquals(42, script.execute(null));
+        src = "(p, x, y) -> { p.car = x; p.cdr = y; }";
+        Pair p = new Pair(-1, -41);
+        script = jexl.createScript(src);
+        Assertions.assertNotNull(script.execute(null, p, 22, 20));
+        Assertions.assertEquals(22, p.car);
+        Assertions.assertEquals(20, p.cdr);
+        Assertions.assertNotNull(script.execute(null, p, 18, 24));
+        Assertions.assertEquals(18, p.car);
+        Assertions.assertEquals(24, p.cdr);
+    }
+
+    @Test
+    void testPair1() {
+        final Map<String, Object> funcs = new HashMap<>();
+        final JexlPermissions permissions = 
JexlPermissions.RESTRICTED.compose("org.example.*");
+        final JexlEngine jexl = new 
JexlBuilder().cache(8).permissions(permissions).namespaces(funcs).create();
+        final String src = "import org.example.Pair; new Pair(17, 25);";
+        final JexlScript script = jexl.createScript(src);
+        Assertions.assertThrows(JexlException.class, ()-> 
script.execute(null));
+    }
 }
diff --git a/src/test/java/org/example/Pair.java 
b/src/test/java/org/example/Pair.java
new file mode 100644
index 00000000..ae690b48
--- /dev/null
+++ b/src/test/java/org/example/Pair.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.example;
+
+import org.apache.commons.jexl3.annotations.NoJexl;
+
+public class Pair {
+  public Object car;
+  public Object cdr;
+
+  @NoJexl
+  public Pair(Object car, Object cdr) {
+    this.car = car;
+    this.cdr = cdr;
+  }
+
+  public static Pair cons(Number car, Number cdr) {
+    return new Pair(car, cdr);
+  }
+
+  public static Pair cons(Object car, Object cdr) {
+    return new Pair(car, cdr);
+  }
+}


Reply via email to