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