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

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 34fa727ce9 GROOVY-12017: Tuple equals should follow Groovy's List 
equals semantics
34fa727ce9 is described below

commit 34fa727ce9c6da7e4de83b30bbfbece91f8172f8
Author: Paul King <[email protected]>
AuthorDate: Mon May 18 11:18:46 2026 +1000

    GROOVY-12017: Tuple equals should follow Groovy's List equals semantics
---
 src/main/java/groovy/lang/Tuple.java               |  2 +-
 .../typehandling/DefaultTypeTransformation.java    |  8 +++++
 src/test/groovy/groovy/lang/TupleTest.java         | 37 ++++++++++++++++++++++
 3 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/src/main/java/groovy/lang/Tuple.java 
b/src/main/java/groovy/lang/Tuple.java
index b6204abdd1..bd37eebfb9 100644
--- a/src/main/java/groovy/lang/Tuple.java
+++ b/src/main/java/groovy/lang/Tuple.java
@@ -127,7 +127,7 @@ public class Tuple<E> extends AbstractList<E> implements 
Serializable, Cloneable
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
-        if (!(o instanceof Tuple that)) return false;
+        if (!(o instanceof List<?> that)) return false;
 
         int size = size();
         if (size != that.size()) return false;
diff --git 
a/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java
 
b/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java
index aa62aa7344..611e01c07d 100644
--- 
a/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java
+++ 
b/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java
@@ -1190,6 +1190,14 @@ public class DefaultTypeTransformation {
         if (left == right) return true;
         if (left == null) return right instanceof NullObject;
         if (right == null) return left instanceof NullObject;
+        // An object that is both Comparable and a List (e.g. Tuple) must use
+        // list equality here, consistent with how a plain (non-Comparable)
+        // List is compared, rather than taking the Comparable/compareTo
+        // short-circuit below; otherwise `tuple == [..]` would be asymmetric
+        // with `[..] == tuple` and inconsistent with Tuple.equals(List).
+        if (left instanceof List && right instanceof List) {
+            return DefaultGroovyMethods.equals((List) left, (List) right);
+        }
         if (left instanceof Comparable) {
             return compareToWithEqualityCheck(left, right, true) == 0;
         }
diff --git a/src/test/groovy/groovy/lang/TupleTest.java 
b/src/test/groovy/groovy/lang/TupleTest.java
index bfe3ecb3e6..12d39a3d13 100644
--- a/src/test/groovy/groovy/lang/TupleTest.java
+++ b/src/test/groovy/groovy/lang/TupleTest.java
@@ -21,6 +21,7 @@ package groovy.lang;
 import groovy.test.GroovyTestCase;
 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
 
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -293,4 +294,40 @@ public class TupleTest extends GroovyTestCase {
             assert false : e.getMessage();
         }
     }
+
+    // Regression coverage for making Tuple equality symmetric with List
+    // (broaden Tuple.equals from `instanceof Tuple` to `instanceof List`,
+    // keeping Groovy's number-aware element comparison consistent with
+    // DefaultGroovyMethods.equals(List, List)).
+    public void testTupleEqualsBehavesLikeList() throws Exception {
+        Tuple2<Integer, Integer> t = new Tuple2<>(1, 2);
+        List<Integer> list = Arrays.asList(1, 2);
+
+        // .equals() symmetric in both operand orders
+        assertEquals("Tuple2.equals(List)", t, list);
+        assertEquals("List.equals(Tuple2)", list, t);
+        // hashCode is already the List formula, so it stays consistent
+        assertEquals("hashCode parity with List", t.hashCode(), 
list.hashCode());
+
+        // Groovy == in both operand orders
+        assertScript(
+                "def t = new Tuple2(1, 2)\n" +
+                "assert t == [1, 2]\n" +
+                "assert [1, 2] == t\n" +
+                // number-aware elements, consistent with equals(List, List)
+                "assert t == [1L, 2L]\n" +
+                "assert [1L, 2L] == t\n");
+
+        // number-aware elements via .equals(), consistent with equals(List, 
List)
+        assertEquals("number-aware elements", t, Arrays.asList(1L, 2L));
+
+        // unchanged: Tuple vs Tuple
+        assertEquals("Tuple vs Tuple", new Tuple2<>(1, 2), new Tuple2<>(1, 2));
+        // empty tuple vs empty list
+        assertEquals("empty Tuple vs empty List", tuple(), Arrays.asList());
+
+        // guardrails: differing size / content are not equal
+        assertNotEquals(t, Arrays.asList(1, 2, 3), "size mismatch");
+        assertNotEquals(t, Arrays.asList(1, 9), "content mismatch");
+    }
 }

Reply via email to