You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/08/29 14:17:36 UTC
(groovy) 01/02: GROOVY-12319: Add test suite for Groovy compatibility with Java generics
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch generics-tck
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit de3bf992596fd60b660598d46a7b8b9cc1a156b1
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Aug 29 08:42:17 2026 +0000
GROOVY-12319: Add test suite for Groovy compatibility with Java generics
---
.../generics/GenericsJavaCompatibilityTest.groovy | 1700 ++++++++++++++++++++
1 file changed, 1700 insertions(+)
diff --git a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
new file mode 100644
index 0000000000..e996753a80
--- /dev/null
+++ b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
@@ -0,0 +1,1700 @@
+/*
+ * 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 gls.generics
+
+import groovy.transform.CompileStatic
+import org.apache.groovy.util.JavaShell
+import org.apache.groovy.util.JavaShellCompilationException
+import org.codehaus.groovy.control.CompilationFailedException
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
+import org.junit.jupiter.api.Test
+
+import java.lang.reflect.GenericArrayType
+import java.lang.reflect.InvocationTargetException
+import java.lang.reflect.Method
+import java.lang.reflect.ParameterizedType
+import java.lang.reflect.Type
+import java.lang.reflect.TypeVariable
+import java.lang.reflect.WildcardType
+
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Static Groovy ({@code @CompileStatic}) vs javac generics compatibility suite.
+ *
+ * Each case compiles the same source twice: javac via {@link JavaShell}, and
+ * Groovy through a class loader that always applies {@link CompileStatic}.
+ * Dynamic Groovy is out of scope. Both sides must agree on compile success or
+ * failure and, when {@code test()} can run, on its result.
+ *
+ * Bytecode instruction streams, constant pools and synthetic GroovyObject
+ * machinery are not compared. Those encodings change between JDK and Groovy
+ * releases. Compatibility of the generic type system is checked at the stable
+ * reflective Signature surface ({@link Class#getTypeParameters()},
+ * {@link Class#getGenericSuperclass()}, {@link Method#getGenericReturnType()},
+ * and so on) plus observable compile / run behaviour.
+ */
+final class GenericsJavaCompatibilityTest {
+
+ // =========================================================================
+ // Category 1: Generic Class & Interface Declarations and Type Bounds
+ // =========================================================================
+
+ @Test
+ void testGenericClassSingleTypeParameter() {
+ final String className = 'gls.generics.test.SingleTypeParam'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class SingleTypeParam {
+ public static class Box<T> {
+ private T val;
+ public Box(T val) { this.val = val; }
+ public T getVal() { return val; }
+ public void setVal(T val) { this.val = val; }
+ }
+ public static String test() {
+ Box<String> box = new Box<>("Apache Groovy");
+ box.setVal(box.getVal() + " Generics");
+ return box.getVal();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'Apache Groovy Generics', true)
+ }
+
+ @Test
+ void testGenericClassMultipleTypeParameters() {
+ final String className = 'gls.generics.test.MultipleTypeParams'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class MultipleTypeParams {
+ public static class Pair<K, V> {
+ private final K key;
+ private final V value;
+ public Pair(K key, V value) {
+ this.key = key;
+ this.value = value;
+ }
+ public K getKey() { return key; }
+ public V getValue() { return value; }
+ public String format() { return "" + key + "=" + value; }
+ }
+ public static String test() {
+ Pair<String, Integer> pair = new Pair<>("score", 100);
+ return pair.format();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'score=100', true)
+ }
+
+ @Test
+ void testGenericInterfaceImplementation() {
+ final String className = 'gls.generics.test.GenericInterfaceImpl'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class GenericInterfaceImpl {
+ public interface Transformer<I, O> {
+ O transform(I input);
+ }
+ public static class StringLengthTransformer implements Transformer<String, Integer> {
+ @Override
+ public Integer transform(String input) {
+ return input == null ? 0 : input.length();
+ }
+ }
+ public static int test() {
+ Transformer<String, Integer> t = new StringLengthTransformer();
+ return t.transform("GenericsCompatibility");
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 21, true)
+ }
+
+ @Test
+ void testSingleUpperBound() {
+ final String className = 'gls.generics.test.SingleUpperBound'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class SingleUpperBound {
+ public static class NumberHolder<T extends Number> {
+ private final T number;
+ public NumberHolder(T number) { this.number = number; }
+ public double asDouble() { return number.doubleValue(); }
+ public long asLong() { return number.longValue(); }
+ }
+ public static double test() {
+ NumberHolder<Integer> intHolder = new NumberHolder<>(42);
+ NumberHolder<Double> doubleHolder = new NumberHolder<>(3.14d);
+ return intHolder.asDouble() + doubleHolder.asDouble();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 45.14d, true)
+ }
+
+ @Test
+ void testMultipleBounds() {
+ final String className = 'gls.generics.test.MultipleBounds'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.io.Serializable;
+ public class MultipleBounds {
+ public static class MultiBoundContainer<T extends Number & Comparable<T> & Serializable> {
+ private final T first;
+ private final T second;
+ public MultiBoundContainer(T first, T second) {
+ this.first = first;
+ this.second = second;
+ }
+ public int compare() {
+ return first.compareTo(second);
+ }
+ public double sum() {
+ return first.doubleValue() + second.doubleValue();
+ }
+ }
+ public static double test() {
+ MultiBoundContainer<Integer> container = new MultiBoundContainer<>(10, 20);
+ int cmp = container.compare();
+ return container.sum() + cmp; // 30 + (-1) = 29
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 29.0d, true)
+ }
+
+ @Test
+ void testRecursiveTypeBoundsFBounded() {
+ final String className = 'gls.generics.test.RecursiveBounds'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class RecursiveBounds {
+ public static <T extends Comparable<T>> T findMax(List<T> list) {
+ if (list == null || list.isEmpty()) return null;
+ T max = list.get(0);
+ for (int i = 1; i < list.size(); i++) {
+ T item = list.get(i);
+ if (item.compareTo(max) > 0) {
+ max = item;
+ }
+ }
+ return max;
+ }
+ public static String test() {
+ List<String> names = Arrays.asList("alpha", "zebra", "beta", "gamma");
+ return findMax(names);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'zebra')
+ }
+
+ // =========================================================================
+ // Category 2: Generic Methods & Constructors
+ // =========================================================================
+
+ @Test
+ void testGenericMethodInference() {
+ final String className = 'gls.generics.test.MethodInference'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class MethodInference {
+ public static <T> List<T> makePairList(T first, T second) {
+ List<T> list = new ArrayList<>();
+ list.add(first);
+ list.add(second);
+ return list;
+ }
+ public static int test() {
+ List<Integer> numbers = makePairList(10, 20);
+ List<String> strings = makePairList("foo", "bar");
+ return numbers.size() + strings.size();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 4)
+ }
+
+ @Test
+ void testGenericMethodExplicitTypeArguments() {
+ final String className = 'gls.generics.test.ExplicitTypeArgs'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ExplicitTypeArgs {
+ public static class Helper {
+ public static <T> T identity(T val) {
+ return val;
+ }
+ public <E> String stringify(E val) {
+ return String.valueOf(val);
+ }
+ }
+ public static String test() {
+ String a = Helper.<String>identity("Explicit");
+ Helper h = new Helper();
+ String b = h.<Integer>stringify(123);
+ return a + ":" + b;
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'Explicit:123')
+ }
+
+ @Test
+ void testGenericConstructors() {
+ final String className = 'gls.generics.test.GenericConstructors'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class GenericConstructors {
+ public static class TypeHolder {
+ private final String typeName;
+ public <T> TypeHolder(T val) {
+ this.typeName = val.getClass().getSimpleName();
+ }
+ public String getTypeName() { return typeName; }
+ }
+ public static String test() {
+ TypeHolder h1 = new TypeHolder("text");
+ TypeHolder h2 = new TypeHolder(42);
+ return h1.getTypeName() + "-" + h2.getTypeName();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'String-Integer')
+ }
+
+ @Test
+ void testGenericVarargs() {
+ final String className = 'gls.generics.test.GenericVarargs'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class GenericVarargs {
+ @SafeVarargs
+ public static <T> List<T> collect(T... items) {
+ List<T> list = new ArrayList<>();
+ for (T item : items) {
+ list.add(item);
+ }
+ return list;
+ }
+ public static int test() {
+ List<Integer> list = collect(1, 2, 3, 4, 5);
+ int sum = 0;
+ for (int n : list) {
+ sum += n;
+ }
+ return sum;
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 15)
+ }
+
+ // =========================================================================
+ // Category 3: Wildcards & PECS
+ // =========================================================================
+
+ @Test
+ void testUnboundedWildcard() {
+ final String className = 'gls.generics.test.UnboundedWildcard'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class UnboundedWildcard {
+ public static int countElements(Collection<?> col) {
+ int c = 0;
+ for (Object o : col) {
+ if (o != null) c++;
+ }
+ return c;
+ }
+ public static int test() {
+ List<String> strList = Arrays.asList("a", "b", "c");
+ List<Integer> intList = Arrays.asList(1, 2, null, 4);
+ return countElements(strList) + countElements(intList);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 6)
+ }
+
+ @Test
+ void testUpperBoundedWildcardProducer() {
+ final String className = 'gls.generics.test.UpperBoundedWildcard'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class UpperBoundedWildcard {
+ public static double sumOfList(List<? extends Number> list) {
+ double s = 0.0;
+ for (Number n : list) {
+ s += n.doubleValue();
+ }
+ return s;
+ }
+ public static double test() {
+ List<Integer> li = Arrays.asList(1, 2, 3);
+ List<Double> ld = Arrays.asList(1.5d, 2.5d);
+ return sumOfList(li) + sumOfList(ld);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 10.0d)
+ }
+
+ @Test
+ void testLowerBoundedWildcardConsumer() {
+ final String className = 'gls.generics.test.LowerBoundedWildcard'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class LowerBoundedWildcard {
+ public static void addNumbers(List<? super Integer> list) {
+ for (int i = 1; i <= 3; i++) {
+ list.add(i);
+ }
+ }
+ public static int test() {
+ List<Integer> intList = new ArrayList<>();
+ List<Number> numList = new ArrayList<>();
+ List<Object> objList = new ArrayList<>();
+ addNumbers(intList);
+ addNumbers(numList);
+ addNumbers(objList);
+ return intList.size() + numList.size() + objList.size();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 9)
+ }
+
+ @Test
+ void testWildcardCaptureHelper() {
+ final String className = 'gls.generics.test.WildcardCapture'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class WildcardCapture {
+ private static <T> void swapHelper(List<T> list, int i, int j) {
+ T temp = list.get(i);
+ list.set(i, list.get(j));
+ list.set(j, temp);
+ }
+ public static void swap(List<?> list, int i, int j) {
+ swapHelper(list, i, j);
+ }
+ public static String test() {
+ List<String> list = new ArrayList<>(Arrays.asList("first", "second"));
+ swap(list, 0, 1);
+ return list.get(0) + "-" + list.get(1);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'second-first')
+ }
+
+ // =========================================================================
+ // Category 4: Diamond Operator <> & Type Inference
+ // =========================================================================
+
+ @Test
+ void testDiamondOperatorWithNestedGenerics() {
+ final String className = 'gls.generics.test.NestedDiamond'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class NestedDiamond {
+ public static int test() {
+ Map<String, List<Map<Integer, String>>> complexMap = new HashMap<>();
+ List<Map<Integer, String>> innerList = new ArrayList<>();
+ Map<Integer, String> innerMap = new HashMap<>();
+ innerMap.put(1, "one");
+ innerList.add(innerMap);
+ complexMap.put("root", innerList);
+ return complexMap.get("root").get(0).get(1).length();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 3)
+ }
+
+ @Test
+ void testDiamondAnonymousInnerClass() {
+ final String className = 'gls.generics.test.DiamondAnonymous'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class DiamondAnonymous {
+ public interface Processor<T> {
+ T process(T val);
+ }
+ public static String test() {
+ Processor<String> p = new Processor<>() {
+ @Override
+ public String process(String val) {
+ return "Processed: " + val;
+ }
+ };
+ return p.process("Data");
+ }
+ }
+ '''
+ // Groovy cannot use diamond <> with anonymous inner classes; keep the rest identical.
+ compileAndCompare(className, javaSrc, toGroovy(javaSrc.replace('new Processor<>()', 'new Processor<String>()')), 'Processed: Data', false)
+ }
+
+ @Test
+ void testTargetTypeInference() {
+ final String className = 'gls.generics.test.TargetTypeInference'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class TargetTypeInference {
+ public static <T> List<T> empty() {
+ return Collections.emptyList();
+ }
+ public static int test() {
+ List<String> sList = empty();
+ List<Integer> iList = empty();
+ return sList.size() + iList.size();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 0)
+ }
+
+ // =========================================================================
+ // Category 5: Covariance, Bridge Methods & Overriding
+ // =========================================================================
+
+ @Test
+ void testCovariantReturnAndBridgeMethod() {
+ final String className = 'gls.generics.test.BridgeAndCovariance'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class BridgeAndCovariance {
+ public interface Supplier<T> {
+ T get();
+ }
+ public static class StringSupplier implements Supplier<String> {
+ @Override
+ public String get() {
+ return "Hello from Bridge";
+ }
+ }
+ public static String test() {
+ StringSupplier specific = new StringSupplier();
+ Supplier<String> genericRef = specific;
+ @SuppressWarnings("rawtypes")
+ Supplier rawRef = specific;
+ return genericRef.get() + "|" + rawRef.get();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'Hello from Bridge|Hello from Bridge', true)
+ }
+
+ // =========================================================================
+ // Category 6: Raw Types & Interoperability
+ // =========================================================================
+
+ @Test
+ void testRawTypeInterop() {
+ final String className = 'gls.generics.test.RawTypeInterop'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class RawTypeInterop {
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public static int test() {
+ List<String> typedList = new ArrayList<>();
+ typedList.add("a");
+ typedList.add("b");
+ List rawList = typedList;
+ rawList.add("c");
+ return rawList.size();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 3)
+ }
+
+ // =========================================================================
+ // Category 7: Generic Exceptions & Parametric Throwables
+ // =========================================================================
+
+ @Test
+ void testGenericThrowsClause() {
+ final String className = 'gls.generics.test.GenericThrows'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.io.IOException;
+ public class GenericThrows {
+ public interface Action<E extends Exception> {
+ void execute() throws E;
+ }
+ public static <E extends Exception> void runAction(Action<E> action) throws E {
+ action.execute();
+ }
+ public static String test() {
+ final StringBuilder sb = new StringBuilder();
+ try {
+ runAction(new Action<IOException>() {
+ @Override
+ public void execute() throws IOException {
+ sb.append("executed");
+ throw new IOException("io-fail");
+ }
+ });
+ } catch (IOException e) {
+ sb.append(":caught-").append(e.getMessage());
+ }
+ return sb.toString();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'executed:caught-io-fail')
+ }
+
+ // =========================================================================
+ // Category 8: Strict Negative Verification (Compile-time & Runtime)
+ // =========================================================================
+
+ @Test
+ void testNegativeGenericSubclassOfThrowable() {
+ // JLS 8.1.2: a generic class may not extend Throwable. javac rejects this;
+ // static Groovy currently accepts the equivalent source, so only the Java side is asserted.
+ final String className = 'gls.generics.test.GenEx'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class GenEx<T> extends Exception {
+ private T info;
+ }
+ '''
+ JavaShell js = new JavaShell()
+ Throwable javaEx = shouldFail(JavaShellCompilationException) {
+ js.compile(className, javaSrc)
+ }
+ assert javaEx.message.contains("generic class may not extend 'java.lang.Throwable'") ||
+ javaEx.message.contains("a generic class may not extend java.lang.Throwable")
+ }
+
+ @Test
+ void testNegativeTypeArityMismatch() {
+ final String className = 'gls.generics.test.ArityMismatch'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class ArityMismatch {
+ public List<String, Integer> list;
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "wrong number of type arguments", "supplied with 2 type parameters")
+ }
+
+ @Test
+ void testNegativeMethodParamArityMismatch() {
+ final String className = 'gls.generics.test.MethodArityMismatch'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.Map;
+ public class MethodArityMismatch {
+ public void process(Map<String> map) {}
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "wrong number of type arguments", "supplied with 1 type parameter")
+ }
+
+ @Test
+ void testNegativeGenericInvarianceAssignment() {
+ final String className = 'gls.generics.test.InvarianceFail'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class InvarianceFail {
+ public static void test() {
+ List<Number> nums = new ArrayList<Integer>();
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "incompatible types", "Cannot assign")
+ }
+
+ @Test
+ void testNegativeMultipleBoundsMismatch() {
+ final String className = 'gls.generics.test.MultiBoundFail'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class MultiBoundFail {
+ public static class Holder<T extends Number & Comparable<T>> {}
+ public static void test() {
+ Holder<Object> h = new Holder<Object>();
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "not within bounds", "not a valid substitute for the bounded parameter")
+ }
+
+ @Test
+ void testNegativeWildcardInExtendsClause() {
+ final String className = 'gls.generics.test.WildcardSuper'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ public class WildcardSuper extends ArrayList<? extends Number> {
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "unexpected type", "A supertype may not specify a wildcard type")
+ }
+
+ @Test
+ void testNegativeParametricTypeForNonGenericClass() {
+ final String className = 'gls.generics.test.NonGenericParameterized'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.Date;
+ public class NonGenericParameterized {
+ public static void test() {
+ Object o = new Date<String>();
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "does not take parameters", "which takes no parameters")
+ }
+
+ @Test
+ void testNegativeDuplicateGenericInterfaceInheritance() {
+ final String className = 'gls.generics.test.InterfaceConflict'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class InterfaceConflict implements Comparable<String>, Comparable<Integer> {
+ public int compareTo(String o) { return 0; }
+ public int compareTo(Integer o) { return 0; }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "different arguments", "Duplicate interfaces in implements list")
+ }
+
+ @Test
+ void testNegativeMethodArgumentTypeMismatch() {
+ final String className = 'gls.generics.test.CallMismatch'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.*;
+ public class CallMismatch {
+ public static void test() {
+ List<String> list = new ArrayList<>();
+ list.add(123);
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "incompatible types", "Cannot call")
+ }
+
+ @Test
+ void testNegativeHeapPollutionClassCastException() {
+ final String className = 'gls.generics.test.HeapPollution'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class HeapPollution {
+ public interface Sink<T> {
+ void accept(T item);
+ }
+ public static class IntegerSink implements Sink<Integer> {
+ private Integer value;
+ @Override
+ public void accept(Integer item) {
+ this.value = item;
+ }
+ }
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public static void invokeRaw(Sink sink, Object item) {
+ sink.accept(item);
+ }
+ public static void test() {
+ invokeRaw(new IntegerSink(), "not an integer");
+ }
+ }
+ '''
+ assertNegativeRuntime(className, javaSrc, ClassCastException.class)
+ }
+
+ // =========================================================================
+ // Category 9: Inheritance, enclosing types, and bound forwarding
+ // =========================================================================
+
+ @Test
+ void testGenericClassExtendsParameterizedType() {
+ final String className = 'gls.generics.test.ExtendsParameterized'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ExtendsParameterized {
+ public static class Holder<T> {
+ private final T value;
+ public Holder(T value) { this.value = value; }
+ public T getValue() { return value; }
+ }
+ public static class Named extends Holder<String> {
+ public Named(String value) { super(value); }
+ }
+ public static String test() {
+ Holder<String> h = new Named("ok");
+ return h.getValue();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok', true)
+ }
+
+ @Test
+ void testTypeParameterForwardedToSuperclass() {
+ final String className = 'gls.generics.test.ForwardTypeParam'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ForwardTypeParam {
+ public static class Holder<T> {
+ private T value;
+ public void set(T value) { this.value = value; }
+ public T get() { return value; }
+ }
+ public static class Box<T> extends Holder<T> {
+ }
+ public static String test() {
+ Box<String> box = new Box<>();
+ box.set("fwd");
+ return box.get();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'fwd', true)
+ }
+
+ @Test
+ void testGenericInterfaceExtendsGenericInterface() {
+ final String className = 'gls.generics.test.GenericIfaceExtends'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class GenericIfaceExtends {
+ public interface Src<T> {
+ T get();
+ }
+ public interface SrcNum extends Src<Number> {
+ }
+ public static class Impl implements SrcNum {
+ @Override
+ public Number get() { return Integer.valueOf(7); }
+ }
+ public static int test() {
+ SrcNum s = new Impl();
+ return s.get().intValue();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 7, true)
+ }
+
+ @Test
+ void testNonStaticInnerClassUsesEnclosingTypeParameter() {
+ final String className = 'gls.generics.test.EnclosingTypeParam'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class EnclosingTypeParam<T> {
+ public class Inner {
+ private final T value;
+ Inner(T value) { this.value = value; }
+ public T getValue() { return value; }
+ }
+ public Inner make(T value) { return new Inner(value); }
+ public static String test() {
+ EnclosingTypeParam<String> outer = new EnclosingTypeParam<String>();
+ return outer.make("inner").getValue();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'inner')
+ }
+
+ @Test
+ void testStaticNestedClassHasIndependentTypeParameters() {
+ final String className = 'gls.generics.test.StaticNestedIndependent'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class StaticNestedIndependent {
+ public static class Outer<T> {
+ public static class Cell<U> {
+ private final U value;
+ public Cell(U value) { this.value = value; }
+ public U getValue() { return value; }
+ }
+ }
+ public static String test() {
+ Outer.Cell<Integer> cell = new Outer.Cell<>(42);
+ return String.valueOf(cell.getValue());
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, '42', true)
+ }
+
+ @Test
+ void testTypeParameterUsedAsBoundOfAnother() {
+ final String className = 'gls.generics.test.TypeParamAsBound'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class TypeParamAsBound {
+ public static <T, U extends T> U pick(U value) {
+ return value;
+ }
+ public static String test() {
+ CharSequence cs = pick("bound");
+ return cs.toString();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'bound')
+ }
+
+ @Test
+ void testForwardReferencedTypeParameterBounds() {
+ final String className = 'gls.generics.test.ForwardRefBounds'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class ForwardRefBounds {
+ public static class Foo<T extends List<X>, X extends Number> {
+ private final T list;
+ public Foo(T list) { this.list = list; }
+ public double first() { return list.get(0).doubleValue(); }
+ }
+ public static double test() {
+ ArrayList<Integer> nums = new ArrayList<Integer>();
+ nums.add(Integer.valueOf(9));
+ Foo<ArrayList<Integer>, Integer> foo = new Foo<>(nums);
+ return foo.first();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 9.0d, true)
+ }
+
+ @Test
+ void testFBoundedSelfType() {
+ final String className = 'gls.generics.test.FBoundedSelfType'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class FBoundedSelfType {
+ public abstract static class Entity<T extends Entity<T>> {
+ public abstract T self();
+ public T fluent() { return self(); }
+ }
+ public static class User extends Entity<User> {
+ @Override
+ public User self() { return this; }
+ public String name() { return "user"; }
+ }
+ public static String test() {
+ return new User().fluent().name();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'user', true)
+ }
+
+ @Test
+ void testIntersectionBoundUsedAsBothTypes() {
+ final String className = 'gls.generics.test.IntersectionDispatch'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class IntersectionDispatch {
+ public static <T extends Number & Comparable<T>> String describe(T value) {
+ return value.getClass().getSimpleName() + ":" + value.intValue() + ":" + value.compareTo(value);
+ }
+ public static String test() {
+ return describe(Integer.valueOf(5));
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'Integer:5:0')
+ }
+
+ @Test
+ void testIndependentBoundedTypeParameters() {
+ final String className = 'gls.generics.test.IndependentBoundedParams'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class IndependentBoundedParams {
+ public static class Mix<A extends CharSequence, B extends Number> {
+ public String mix(A a, B b) {
+ return a.toString() + b.intValue();
+ }
+ }
+ public static String test() {
+ Mix<String, Integer> mix = new Mix<>();
+ return mix.mix("n", Integer.valueOf(3));
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'n3', true)
+ }
+
+ // =========================================================================
+ // Category 10: Generic methods on generic types, arrays as type arguments
+ // =========================================================================
+
+ @Test
+ void testGenericMethodOnGenericClass() {
+ final String className = 'gls.generics.test.GenericMethodOnGenericClass'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.function.Function;
+ public class GenericMethodOnGenericClass {
+ public static class Box<T> {
+ private final T value;
+ public Box(T value) { this.value = value; }
+ public <U> U map(Function<T, U> mapper) {
+ return mapper.apply(value);
+ }
+ }
+ public static int test() {
+ Box<String> box = new Box<>("abcd");
+ return box.map(s -> s.length());
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 4)
+ }
+
+ @Test
+ void testArrayAsTypeArgument() {
+ final String className = 'gls.generics.test.ArrayAsTypeArg'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class ArrayAsTypeArg {
+ public static int test() {
+ List<String[]> list = new ArrayList<String[]>();
+ list.add(new String[] {"a", "bb"});
+ return list.get(0)[1].length();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 2)
+ }
+
+ @Test
+ void testChainedGenericMethodInference() {
+ final String className = 'gls.generics.test.ChainedInference'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ChainedInference {
+ public static <T> T id(T value) {
+ return value;
+ }
+ public static String test() {
+ return id(id("ok"));
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok')
+ }
+
+ @Test
+ void testOverrideGenericMethodWithMatchingBounds() {
+ final String className = 'gls.generics.test.OverrideMatchingBounds'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class OverrideMatchingBounds {
+ public interface Rec<T extends Rec<T>> {
+ }
+ public static class Base {
+ public <T extends Rec<T>> String tag() { return "base"; }
+ }
+ public static class Sub extends Base {
+ @Override
+ public <U extends Rec<U>> String tag() { return "sub"; }
+ }
+ public static String test() {
+ Base b = new Sub();
+ return b.tag();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'sub')
+ }
+
+ // =========================================================================
+ // Category 11: PECS, nested wildcards, function variance
+ // =========================================================================
+
+ @Test
+ void testPecsCollectionsCopy() {
+ final String className = 'gls.generics.test.PecsCopy'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.Collections;
+ import java.util.List;
+ public class PecsCopy {
+ public static String test() {
+ List<Integer> src = Arrays.asList(1, 2, 3);
+ List<Number> dst = new ArrayList<Number>();
+ dst.add(null);
+ dst.add(null);
+ dst.add(null);
+ Collections.copy(dst, src);
+ return dst.get(0).intValue() + "," + dst.get(2).intValue();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, '1,3')
+ }
+
+ @Test
+ void testNestedWildcards() {
+ final String className = 'gls.generics.test.NestedWildcards'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.List;
+ public class NestedWildcards {
+ public static int sizeOf(List<? extends List<? extends Number>> lists) {
+ int n = 0;
+ for (List<? extends Number> list : lists) {
+ n += list.size();
+ }
+ return n;
+ }
+ public static int test() {
+ List<List<Integer>> data = new ArrayList<List<Integer>>();
+ data.add(Arrays.asList(1, 2));
+ data.add(Arrays.asList(3));
+ return sizeOf(data);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 3)
+ }
+
+ @Test
+ void testComparatorSuperBoundSort() {
+ final String className = 'gls.generics.test.ComparatorSuperBound'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.Comparator;
+ import java.util.List;
+ public class ComparatorSuperBound {
+ public static String test() {
+ List<Integer> list = new ArrayList<Integer>();
+ list.add(Integer.valueOf(3));
+ list.add(Integer.valueOf(1));
+ list.add(Integer.valueOf(2));
+ Comparator<Number> byInt = (a, b) -> Integer.compare(a.intValue(), b.intValue());
+ list.sort(byInt);
+ return list.get(0) + "," + list.get(1) + "," + list.get(2);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, '1,2,3')
+ }
+
+ @Test
+ void testFunctionWildcardVariance() {
+ final String className = 'gls.generics.test.FunctionVariance'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.function.Function;
+ public class FunctionVariance {
+ public static <T, R> R apply(Function<? super T, ? extends R> f, T value) {
+ return f.apply(value);
+ }
+ public static int test() {
+ Function<Object, Integer> f = o -> String.valueOf(o).length();
+ return apply(f, "xyz");
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 3)
+ }
+
+ @Test
+ void testLowerBoundedWildcardGetReturnsObject() {
+ final String className = 'gls.generics.test.LowerBoundGet'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class LowerBoundGet {
+ public static String test() {
+ List<? super Integer> list = new ArrayList<Number>();
+ list.add(Integer.valueOf(8));
+ Object value = list.get(0);
+ return String.valueOf(value);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, '8')
+ }
+
+ @Test
+ void testWildcardAllowsAddingNull() {
+ final String className = 'gls.generics.test.WildcardAddNull'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class WildcardAddNull {
+ public static int test() {
+ List<?> list = new ArrayList<String>();
+ list.add(null);
+ return list.size();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 1)
+ }
+
+ // =========================================================================
+ // Category 12: Diamond target typing, unchecked conversion, raw assignment
+ // =========================================================================
+
+ @Test
+ void testDiamondInferredFromMethodArgument() {
+ final String className = 'gls.generics.test.DiamondMethodArg'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class DiamondMethodArg {
+ public static int len(List<String> list) {
+ return list.size();
+ }
+ public static int test() {
+ return len(new ArrayList<>());
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 0)
+ }
+
+ @Test
+ void testUncheckedCastToParameterizedType() {
+ final String className = 'gls.generics.test.UncheckedCast'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.Arrays;
+ import java.util.List;
+ public class UncheckedCast {
+ @SuppressWarnings("unchecked")
+ public static String test() {
+ List<?> wild = Arrays.asList("cast");
+ List<String> typed = (List<String>) wild;
+ return typed.get(0);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'cast')
+ }
+
+ @Test
+ void testRawTypeAssignedToParameterizedType() {
+ final String className = 'gls.generics.test.RawToParameterized'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class RawToParameterized {
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public static int test() {
+ List raw = new ArrayList();
+ raw.add("x");
+ List<String> typed = raw;
+ return typed.get(0).length();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 1)
+ }
+
+ @Test
+ void testMapEntryParameterizedInnerInterface() {
+ final String className = 'gls.generics.test.MapEntryInner'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.HashMap;
+ import java.util.Iterator;
+ import java.util.Map;
+ public class MapEntryInner {
+ public static String test() {
+ Map<String, Integer> map = new HashMap<>();
+ map.put("k", Integer.valueOf(3));
+ Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
+ Map.Entry<String, Integer> e = it.next();
+ return e.getKey() + e.getValue();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'k3')
+ }
+
+ // =========================================================================
+ // Category 13: Covariant override, conditional lub, records, instanceof
+ // =========================================================================
+
+ @Test
+ void testCovariantOverrideInGenericClassHierarchy() {
+ final String className = 'gls.generics.test.CovariantClassOverride'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class CovariantClassOverride {
+ public static class Parent<T> {
+ public T id(T value) { return value; }
+ }
+ public static class Child extends Parent<String> {
+ @Override
+ public String id(String value) { return value + "!"; }
+ }
+ public static String test() {
+ Parent<String> p = new Child();
+ return p.id("ok");
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok!', true)
+ }
+
+ @Test
+ void testConditionalExpressionLubsClassLiterals() {
+ final String className = 'gls.generics.test.ConditionalLub'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ConditionalLub {
+ public static String test() {
+ boolean cond = "x".length() == 1;
+ Class<?> cls = cond ? Integer.class : Double.class;
+ CharSequence cs = cond ? "hello" : new StringBuilder("hello");
+ return cls.getSimpleName() + cs.toString();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'Integerhello')
+ }
+
+ @Test
+ void testGenericRecord() {
+ final String className = 'gls.generics.test.GenericRecordPair'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class GenericRecordPair {
+ public static record Pair<K, V>(K key, V value) {
+ }
+ public static String test() {
+ Pair<String, Integer> pair = new Pair<>("a", Integer.valueOf(1));
+ return pair.key() + pair.value();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'a1', true)
+ }
+
+ @Test
+ void testReifiableInstanceofUnboundedWildcard() {
+ final String className = 'gls.generics.test.ReifiableInstanceof'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class ReifiableInstanceof {
+ public static boolean test() {
+ Object list = new ArrayList<String>();
+ Object cls = ReifiableInstanceof.class.getClass();
+ return (list instanceof List<?>) && (cls instanceof Class<?>);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, true)
+ }
+
+ @Test
+ void testDeclaredGenericSignaturesMatch() {
+ final String className = 'gls.generics.test.SignatureCatalog'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class SignatureCatalog {
+ public static class Box<T> {
+ public T value;
+ public Box(T value) { this.value = value; }
+ public T getValue() { return value; }
+ }
+ public static class Pair<K extends Number, V extends CharSequence> {
+ public final K k;
+ public final V v;
+ public Pair(K k, V v) { this.k = k; this.v = v; }
+ }
+ public static class Bounded<T extends Number & Comparable<T>> {
+ public int compare(T a, T b) { return a.compareTo(b); }
+ }
+ public static class Sub extends Box<String> {
+ public Sub(String value) { super(value); }
+ }
+ public List<? extends Number> extendsBound;
+ public List<? super Integer> superBound;
+ public List<?> unbounded;
+ public static <E> E identity(E value) { return value; }
+ public static String test() {
+ Box<String> box = new Box<>("x");
+ Pair<Integer, String> pair = new Pair<>(Integer.valueOf(1), "a");
+ Bounded<Integer> bounded = new Bounded<>();
+ Sub sub = new Sub("s");
+ return identity(box.getValue()) + pair.v + bounded.compare(Integer.valueOf(2), Integer.valueOf(3)) + sub.getValue();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'xa-1s', true)
+ }
+
+ // =========================================================================
+ // Category 14: Additional negative cases (JLS restrictions both sides share)
+ // =========================================================================
+
+ @Test
+ void testNegativeNonReifiableInstanceof() {
+ final String className = 'gls.generics.test.NonReifiableInstanceof'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class NonReifiableInstanceof {
+ public static boolean test(Object o) {
+ return o instanceof List<String>;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+ }
+
+ @Test
+ void testNegativeInstanceofParameterizedClassLiteral() {
+ final String className = 'gls.generics.test.InstanceofClassParam'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class InstanceofClassParam {
+ public static boolean test() {
+ return InstanceofClassParam.class.getClass() instanceof Class<InstanceofClassParam>;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be converted", "Cannot perform instanceof")
+ }
+
+ @Test
+ void testNegativeTypeParameterReferencedFromStaticNestedClass() {
+ final String className = 'gls.generics.test.StaticNestedOuterTp'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class StaticNestedOuterTp<T> {
+ static class Inner {
+ T field;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "non-static type variable", "unable to resolve class T")
+ }
+
+ @Test
+ void testNegativeTypeParameterStaticField() {
+ final String className = 'gls.generics.test.StaticTypeParamField'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class StaticTypeParamField<T> {
+ static T field;
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "non-static type variable", "unable to resolve class T")
+ }
+
+ @Test
+ void testNegativeWildcardWriteThroughExtends() {
+ final String className = 'gls.generics.test.WildcardWriteExtends'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class WildcardWriteExtends {
+ public static void test(List<? extends Number> list) {
+ list.add(Integer.valueOf(1));
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "incompatible types", "Cannot call")
+ }
+
+ // =========================================================================
+ // Test Harness Helper Methods
+ // =========================================================================
+
+ /**
+ * Static Groovy view of {@code javaSrc}: Java annotation array initializers become
+ * Groovy lists. Semicolons and the rest of the Java syntax are kept. CompileStatic
+ * is applied by {@link #newStaticGroovyClassLoader()}, not by rewriting the source.
+ */
+ private static String toGroovy(String javaSrc) {
+ javaSrc.replaceAll(/@SuppressWarnings\(\{([^}]*)\}\)/, '@SuppressWarnings([$1])')
+ }
+
+ /**
+ * Groovy class loader that always compiles with {@link CompileStatic}.
+ * Dynamic Groovy is not part of this suite.
+ */
+ private static GroovyClassLoader newStaticGroovyClassLoader() {
+ CompilerConfiguration config = new CompilerConfiguration()
+ config.addCompilationCustomizers(new ASTTransformationCustomizer(CompileStatic))
+ new GroovyClassLoader(GenericsJavaCompatibilityTest.classLoader, config)
+ }
+
+ /**
+ * Compiles {@code javaSrc} with javac and the {@link #toGroovy} view with static
+ * Groovy, then asserts matching {@code test()} results. When {@code compareSignatures}
+ * is true, user-declared generic signatures are compared through reflection.
+ */
+ private void assertPositive(String className, String javaSrc, Object expectedResult, boolean compareSignatures = false) {
+ compileAndCompare(className, javaSrc, toGroovy(javaSrc), expectedResult, compareSignatures)
+ }
+
+ private void compileAndCompare(String className, String javaSrc, String groovySrc, Object expectedResult, boolean compareSignatures) {
+ JavaShell js = new JavaShell()
+ Class<?> javaClass = js.compile(className, javaSrc)
+ Object javaResult = javaClass.getMethod('test').invoke(null)
+
+ Class<?> groovyClass = newStaticGroovyClassLoader().parseClass(groovySrc)
+ Object groovyResult = groovyClass.getMethod('test').invoke(null)
+
+ assert javaResult == groovyResult: "Execution result mismatch between Java ($javaResult) and Groovy ($groovyResult)"
+ if (expectedResult != null) {
+ assert javaResult == expectedResult: "Java result ($javaResult) does not match expected ($expectedResult)"
+ assert groovyResult == expectedResult: "Groovy result ($groovyResult) does not match expected ($expectedResult)"
+ }
+ if (compareSignatures) {
+ assertUserGenericDeclarations(javaClass, groovyClass)
+ }
+ }
+
+ /**
+ * Compares user-visible generic declarations of {@code javaClass} and {@code groovyClass}.
+ * GroovyObject, synthetic members and extra Groovy nested types are ignored so the check
+ * stays stable across compiler revisions.
+ */
+ private static void assertUserGenericDeclarations(Class<?> javaClass, Class<?> groovyClass) {
+ assertTypeParametersEqual(javaClass, groovyClass)
+ assertGenericSuperclassEqual(javaClass, groovyClass)
+ assertUserGenericInterfacesEqual(javaClass, groovyClass)
+ assertMatchingMethodsGeneric(javaClass, groovyClass)
+ assertMatchingFieldsGeneric(javaClass, groovyClass)
+
+ for (Class<?> javaNested : javaClass.getDeclaredClasses()) {
+ String simple = javaNested.getSimpleName()
+ if (simple == null || simple.isEmpty() || simple.matches(/\d+/)) {
+ continue
+ }
+ Class<?> groovyNested = groovyClass.getDeclaredClasses().find { it.simpleName == simple }
+ assert groovyNested != null: "Groovy is missing nested type ${javaClass.name}\$${simple}"
+ assertUserGenericDeclarations(javaNested, groovyNested)
+ }
+ }
+
+ private static void assertTypeParametersEqual(Class<?> javaClass, Class<?> groovyClass) {
+ TypeVariable<?>[] javaParams = javaClass.getTypeParameters()
+ TypeVariable<?>[] groovyParams = groovyClass.getTypeParameters()
+ assert javaParams.length == groovyParams.length:
+ "${javaClass.simpleName} type-parameter arity Java=${javaParams.length} Groovy=${groovyParams.length}"
+ for (int i = 0; i < javaParams.length; i++) {
+ assert javaParams[i].name == groovyParams[i].name:
+ "${javaClass.simpleName} type-parameter name Java=${javaParams[i].name} Groovy=${groovyParams[i].name}"
+ List<String> javaBounds = javaParams[i].bounds.collect { typeDesc(it) }
+ List<String> groovyBounds = groovyParams[i].bounds.collect { typeDesc(it) }
+ assert javaBounds == groovyBounds:
+ "${javaClass.simpleName}<${javaParams[i].name}> bounds Java=${javaBounds} Groovy=${groovyBounds}"
+ }
+ }
+
+ private static void assertGenericSuperclassEqual(Class<?> javaClass, Class<?> groovyClass) {
+ String javaSuper = typeDesc(javaClass.getGenericSuperclass())
+ String groovySuper = typeDesc(groovyClass.getGenericSuperclass())
+ assert javaSuper == groovySuper:
+ "${javaClass.simpleName} generic superclass Java=${javaSuper} Groovy=${groovySuper}"
+ }
+
+ private static void assertUserGenericInterfacesEqual(Class<?> javaClass, Class<?> groovyClass) {
+ List<String> javaIfaces = userGenericInterfaces(javaClass)
+ List<String> groovyIfaces = userGenericInterfaces(groovyClass)
+ assert javaIfaces == groovyIfaces:
+ "${javaClass.simpleName} generic interfaces Java=${javaIfaces} Groovy=${groovyIfaces}"
+ }
+
+ private static List<String> userGenericInterfaces(Class<?> type) {
+ type.getGenericInterfaces().findAll { iface ->
+ String raw = rawName(iface)
+ raw != 'groovy.lang.GroovyObject' && raw != 'groovy.lang.GroovyObjectSupport'
+ }.collect { typeDesc(it) }
+ }
+
+ private static void assertMatchingMethodsGeneric(Class<?> javaClass, Class<?> groovyClass) {
+ javaClass.getDeclaredMethods().findAll { m ->
+ !m.synthetic && !m.bridge && m.name != '<clinit>' && !m.name.contains('$')
+ }.each { javaMethod ->
+ def groovyMethod = groovyClass.getDeclaredMethods().find { gm ->
+ !gm.bridge && gm.name == javaMethod.name && Arrays.equals(gm.parameterTypes, javaMethod.parameterTypes)
+ }
+ if (groovyMethod == null) {
+ return
+ }
+ String javaSig = genericMethodDesc(javaMethod)
+ String groovySig = genericMethodDesc(groovyMethod)
+ assert javaSig == groovySig:
+ "${javaClass.simpleName}.${javaMethod.name} generic signature Java=${javaSig} Groovy=${groovySig}"
+ }
+ }
+
+ private static void assertMatchingFieldsGeneric(Class<?> javaClass, Class<?> groovyClass) {
+ javaClass.getDeclaredFields().findAll { f ->
+ !f.synthetic && !f.name.contains('$') && f.name != 'metaClass'
+ }.each { javaField ->
+ def groovyField = groovyClass.getDeclaredFields().find { it.name == javaField.name }
+ if (groovyField == null) {
+ return
+ }
+ String javaType = typeDesc(javaField.genericType)
+ String groovyType = typeDesc(groovyField.genericType)
+ assert javaType == groovyType:
+ "${javaClass.simpleName}.${javaField.name} generic type Java=${javaType} Groovy=${groovyType}"
+ }
+ }
+
+ private static String genericMethodDesc(Method method) {
+ String typeParams = method.typeParameters.length == 0 ? '' :
+ '<' + method.typeParameters.collect { tv ->
+ List<String> bounds = tv.bounds.collect { typeDesc(it) }
+ bounds == ['java.lang.Object'] ? tv.name : tv.name + ' extends ' + bounds.join(' & ')
+ }.join(', ') + '> '
+ String params = method.genericParameterTypes.collect { typeDesc(it) }.join(', ')
+ return typeParams + typeDesc(method.genericReturnType) + ' ' + method.name + '(' + params + ')'
+ }
+
+ private static String rawName(Type type) {
+ if (type instanceof Class) {
+ return ((Class<?>) type).name
+ }
+ if (type instanceof ParameterizedType) {
+ return rawName(((ParameterizedType) type).rawType)
+ }
+ return typeDesc(type)
+ }
+
+ /**
+ * Canonical, JDK-stable description of a reflective {@link Type}. Uses binary
+ * class names and type-variable names rather than {@code Type.toString()}, which
+ * has changed wording across JDK releases.
+ */
+ private static String typeDesc(Type type) {
+ if (type == null) {
+ return 'null'
+ }
+ if (type instanceof Class) {
+ Class<?> c = (Class<?>) type
+ if (c.isArray()) {
+ return typeDesc(c.componentType) + '[]'
+ }
+ return c.name
+ }
+ if (type instanceof TypeVariable) {
+ return ((TypeVariable<?>) type).name
+ }
+ if (type instanceof ParameterizedType) {
+ ParameterizedType p = (ParameterizedType) type
+ Type[] typeArgs = p.actualTypeArguments
+ String raw = typeDesc(p.rawType)
+ if (typeArgs == null || typeArgs.length == 0) {
+ return raw
+ }
+ return raw + '<' + typeArgs.collect { typeDesc(it) }.join(', ') + '>'
+ }
+ if (type instanceof WildcardType) {
+ WildcardType w = (WildcardType) type
+ Type[] lower = w.lowerBounds
+ if (lower != null && lower.length > 0) {
+ return '? super ' + typeDesc(lower[0])
+ }
+ Type[] upper = w.upperBounds
+ if (upper != null && upper.length > 0 && upper[0] != Object) {
+ return '? extends ' + typeDesc(upper[0])
+ }
+ return '?'
+ }
+ if (type instanceof GenericArrayType) {
+ return typeDesc(((GenericArrayType) type).genericComponentType) + '[]'
+ }
+ return String.valueOf(type)
+ }
+
+ /**
+ * Verifies that javac and static Groovy both reject invalid generics code.
+ */
+ private void assertNegativeCompile(String className, String javaSrc, String javaDiagnosticSub, String groovyDiagnosticSub) {
+ JavaShell js = new JavaShell()
+ Throwable javaEx = shouldFail(JavaShellCompilationException) {
+ js.compile(className, javaSrc)
+ }
+ assert javaEx.message.contains(javaDiagnosticSub): "Expected Java diagnostic to contain '$javaDiagnosticSub', but got: ${javaEx.message}"
+
+ Throwable groovyEx = shouldFail(CompilationFailedException) {
+ newStaticGroovyClassLoader().parseClass(toGroovy(javaSrc))
+ }
+ assert groovyEx.message.contains(groovyDiagnosticSub): "Expected Groovy diagnostic to contain '$groovyDiagnosticSub', but got: ${groovyEx.message}"
+ }
+
+ /**
+ * Verifies runtime negative behavior (e.g. heap pollution / ClassCastException on bridge method invocation).
+ */
+ private void assertNegativeRuntime(String className, String javaSrc, Class<? extends Throwable> expectedException) {
+ JavaShell js = new JavaShell()
+ Class<?> javaClass = js.compile(className, javaSrc)
+ InvocationTargetException javaInvocationEx = shouldFail(InvocationTargetException) {
+ javaClass.getMethod('test').invoke(null)
+ }
+ assert expectedException.isInstance(javaInvocationEx.getTargetException()):
+ "Java expected target exception ${expectedException.name}, but got: ${javaInvocationEx.getTargetException()}"
+
+ Class<?> groovyClass = newStaticGroovyClassLoader().parseClass(toGroovy(javaSrc))
+ InvocationTargetException groovyInvocationEx = shouldFail(InvocationTargetException) {
+ groovyClass.getMethod('test').invoke(null)
+ }
+ assert expectedException.isInstance(groovyInvocationEx.getTargetException()):
+ "Groovy expected target exception ${expectedException.name}, but got: ${groovyInvocationEx.getTargetException()}"
+ }
+
+}