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 d80b5724 JEXL: fix Closure hashCode/equals in the edge case of 
recursive functions;
d80b5724 is described below

commit d80b57246c49906c938ff34f9743bfb5a9bc5ae7
Author: henrib <[email protected]>
AuthorDate: Fri Feb 17 20:00:23 2023 +0100

    JEXL: fix Closure hashCode/equals in the edge case of recursive functions;
---
 .../org/apache/commons/jexl3/internal/Closure.java | 37 ++++++++++++++++++++++
 .../org/apache/commons/jexl3/internal/Frame.java   | 21 ++++++++++++
 .../java/org/apache/commons/jexl3/LambdaTest.java  | 19 +++++++++++
 3 files changed, 77 insertions(+)

diff --git a/src/main/java/org/apache/commons/jexl3/internal/Closure.java 
b/src/main/java/org/apache/commons/jexl3/internal/Closure.java
index 5f058a8e..0c836b94 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Closure.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Closure.java
@@ -20,6 +20,9 @@ import org.apache.commons.jexl3.JexlContext;
 import org.apache.commons.jexl3.JexlOptions;
 import org.apache.commons.jexl3.parser.ASTJexlLambda;
 
+import java.util.Arrays;
+import java.util.Objects;
+
 /**
  * A Script closure.
  */
@@ -59,6 +62,40 @@ public class Closure extends Script {
         options = closureOptions != null ? closureOptions.copy() :  null;
     }
 
+    @Override
+    public int hashCode() {
+        // CSOFF: Magic number
+        int hash = 17;
+        hash = 31 * hash + Objects.hashCode(jexl);
+        hash = 31 * hash + Objects.hashCode(source);
+        hash = 31 * hash + (frame != null ? 
Arrays.deepHashCode(frame.nocycleStack(this)) : 0);
+        // CSON: Magic number
+        return hash;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Closure other = (Closure) obj;
+        if (this.jexl != other.jexl) {
+            return false;
+        }
+        if (!Objects.equals(this.source, other.source)) {
+            return false;
+        }
+        if (this.frame == other.frame) {
+            return true;
+        }
+        return Arrays.deepEquals(
+                this.frame.nocycleStack(this),
+                other.frame.nocycleStack(other));
+    }
+
     @Override
     public String[] getUnboundParameters() {
         return frame.getUnboundParameters();
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 9101a95f..f34ef2a3 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Frame.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Frame.java
@@ -42,6 +42,27 @@ public final class Frame {
         curried = c;
     }
 
+    /**
+     * Replace any instance of a closure in this stack by its (fuzzy encoded) 
offset in it.
+     * <p>This is to avoid the cyclic dependency between the closure and its 
frame stack that
+     * may point back to it that occur with recursive function definitions.</p>
+     * @param closure the owning closure
+     * @return the cleaned-up stack or the stack itself (most of the time)
+     */
+    Object[] nocycleStack(Closure closure) {
+        Object[] ns = stack;
+        for(int i = 0; i < stack.length; ++i) {
+            if (stack[i] == closure) {
+                if (ns == stack) {
+                    ns = stack.clone();
+                }
+                // fuzz it a little
+                ns[i] = Closure.class.hashCode() + i;
+            }
+        }
+        return ns;
+    }
+
     /**
      * Gets this script unbound parameters, i.e. parameters not bound through 
curry().
      * @return the parameter names
diff --git a/src/test/java/org/apache/commons/jexl3/LambdaTest.java 
b/src/test/java/org/apache/commons/jexl3/LambdaTest.java
index fb9a5dfb..9f07b980 100644
--- a/src/test/java/org/apache/commons/jexl3/LambdaTest.java
+++ b/src/test/java/org/apache/commons/jexl3/LambdaTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.jexl3;
 
+import org.apache.commons.jexl3.internal.Closure;
 import org.apache.commons.jexl3.internal.Script;
 import org.junit.Assert;
 import org.junit.Test;
@@ -147,6 +148,24 @@ public class LambdaTest extends JexlTestCase {
         Assert.assertEquals(42, result);
     }
 
+    @Test
+    public void testCompareLambdaRecurse() throws Exception {
+        final JexlEngine jexl = createEngine();
+        final String factSrc = "function fact(x) { x < 2? 1 : x * fact(x - 1) 
}";
+        final JexlScript fact0 = jexl.createScript(factSrc);
+        final JexlScript fact1 = jexl.createScript(fact0.toString());
+        Assert.assertEquals(fact0, fact1);
+        Closure r0 = (Closure) fact0.execute(null);
+        Closure r1 = (Closure) fact1.execute(null);
+        Assert.assertEquals(720, r0.execute(null, 6));
+        Assert.assertEquals(720, r1.execute(null, 6));
+        Assert.assertEquals(r0, r1);
+        Assert.assertEquals(r1, r0);
+        // ensure we did not break anything through equals
+        Assert.assertEquals(720, r0.execute(null, 6));
+        Assert.assertEquals(720, r1.execute(null, 6));
+    }
+
     @Test
     public void testHoistLambda() {
         final JexlEngine jexl = createEngine();

Reply via email to