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

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


The following commit(s) were added to refs/heads/GROOVY_4_0_X by this push:
     new 7ab97ea5bb GROOVY-12020: EnumSet.plus(element) loses EnumSet type 
under JPMS
7ab97ea5bb is described below

commit 7ab97ea5bba66ffd041cd79069cf0c6e970bc748
Author: Paul King <[email protected]>
AuthorDate: Thu May 28 11:05:34 2026 +1000

    GROOVY-12020: EnumSet.plus(element) loses EnumSet type under JPMS
    
    Under newer JDKs, ObjectUtil.cloneObject fails for EnumSet because the
    MethodHandles.Lookup, teleported into the concrete RegularEnumSet (a
    package-private subclass in java.base), lacks the access needed to
    unreflect the inherited public clone() method. The IllegalAccessException
    is swallowed by DefaultGroovyMethodsSupport.cloneObject's catch-all and
    cloneSimilarCollection falls back to LinkedHashSet, so
    
        EnumSet.of(A, B, C) + D
    
    now returns a LinkedHashSet instead of an EnumSet. Subsequent assignment
    to an EnumSet-typed variable or field then fails with a GroovyCastException.
    
    Fall back to plain reflective Method.invoke when the MethodHandle Lookup
    cannot access clazz. The clone method itself is public, so reflective
    invocation succeeds without needing --add-opens.
---
 .../java/org/apache/groovy/runtime/ObjectUtil.java | 13 ++++-
 src/test/groovy/bugs/Groovy12020.groovy            | 64 ++++++++++++++++++++++
 2 files changed, 74 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/apache/groovy/runtime/ObjectUtil.java 
b/src/main/java/org/apache/groovy/runtime/ObjectUtil.java
index 2aa59415fa..642e327a5b 100644
--- a/src/main/java/org/apache/groovy/runtime/ObjectUtil.java
+++ b/src/main/java/org/apache/groovy/runtime/ObjectUtil.java
@@ -73,9 +73,16 @@ public class ObjectUtil {
         }
 
         final Method cloneMethod = clazz.getMethod("clone");
-        final MethodHandle cloneMethodHandle = 
LOOKUP.in(clazz).unreflect(cloneMethod);
-
-        return (T) cloneMethodHandle.invokeWithArguments(object);
+        try {
+            final MethodHandle cloneMethodHandle = 
LOOKUP.in(clazz).unreflect(cloneMethod);
+            return (T) cloneMethodHandle.invokeWithArguments(object);
+        } catch (IllegalAccessException e) {
+            // GROOVY-12020: under JPMS the Lookup teleported into clazz can 
lack access
+            // when clazz is a package-private subclass in a sealed module 
(e.g.
+            // java.util.RegularEnumSet inside java.base). The public clone() 
method
+            // is still reachable via reflective invocation, so fall back to 
that.
+            return (T) cloneMethod.invoke(object);
+        }
     }
 
     /**
diff --git a/src/test/groovy/bugs/Groovy12020.groovy 
b/src/test/groovy/bugs/Groovy12020.groovy
new file mode 100644
index 0000000000..f50e522368
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy12020.groovy
@@ -0,0 +1,64 @@
+/*
+ *  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
+ *
+ *    http://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 groovy.bugs
+
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+final class Groovy12020 {
+
+    @Test // GROOVY-12020
+    void testEnumSetPlusReturnsEnumSet() {
+        assertScript '''
+            enum E { A, B, C, D }
+            EnumSet<E> first = EnumSet.of(E.A, E.B, E.C)
+            def sum = first + E.D
+            assert sum instanceof EnumSet
+            assert sum as List == [E.A, E.B, E.C, E.D]
+        '''
+    }
+
+    @Test // GROOVY-12020
+    void testEnumSetPlusInStaticInitializer() {
+        // Reporter's original shape: EnumSet field built from `another + 
element`
+        // under @CompileStatic, requiring an EnumSet-typed cast at the 
assignment.
+        assertScript '''
+            import groovy.transform.CompileStatic
+
+            enum EnumTest {
+                ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE
+
+                public static final EnumSet<EnumTest> firstSet  = 
EnumSet.of(ONE, TWO, THREE, FOUR, FIVE)
+                public static final EnumSet<EnumTest> secondSet = firstSet + 
SIX
+            }
+
+            @CompileStatic
+            class Application {
+                static void main(String[] args) {
+                    assert EnumTest.secondSet instanceof EnumSet
+                    assert EnumTest.secondSet as List == [EnumTest.ONE, 
EnumTest.TWO, EnumTest.THREE,
+                                                          EnumTest.FOUR, 
EnumTest.FIVE, EnumTest.SIX]
+                }
+            }
+
+            Application.main()
+        '''
+    }
+}

Reply via email to