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()
+ '''
+ }
+}