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/31 14:31:43 UTC
(groovy) branch generics-tck updated (1eea44ba3e -> d6ef127e47)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a change to branch generics-tck
in repository https://gitbox.apache.org/repos/asf/groovy.git
discard 1eea44ba3e GROOVY-12319: document JLS well-formedness breaks and fix remaining diagnostics
omit 86f1ab3e3c GROOVY-12319: clarify generics compile error messages
omit 9312a1335d GROOVY-12319: review follow-up for rare types and instanceof
omit 14d93f64ab GROOVY-12319: Java compatibility: remaining generic type syntax
omit 9c5176ac28 GROOVY-12319: Add test suite for Groovy compatibility with Java generics
add 6a3a99f54b GROOVY-12323: ChannelSelect: add claimable send offers (output guards)
new 30ac5fdbdc GROOVY-12319: Add test suite for Groovy compatibility with Java generics
new 58f0e99fd6 GROOVY-12319: Java compatibility: remaining generic type syntax
new 5a8cc8d1e2 GROOVY-12319: review follow-up for rare types and instanceof
new d5c8152c5c GROOVY-12319: clarify generics compile error messages
new d6ef127e47 GROOVY-12319: document JLS well-formedness breaks and fix remaining diagnostics
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (1eea44ba3e)
\
N -- N -- N refs/heads/generics-tck (d6ef127e47)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
src/main/java/groovy/concurrent/ChannelSelect.java | 260 +++++++++++++-----
.../groovy/runtime/async/DefaultAsyncChannel.java | 300 +++++++++++++--------
.../apache/groovy/runtime/async/SelectClaim.java | 193 +++++++++++++
src/spec/doc/_core-async-channels.adoc | 34 +++
.../groovy/concurrent/ChannelSelectTest.groovy | 271 +++++++++++++++++++
5 files changed, 887 insertions(+), 171 deletions(-)
create mode 100644 src/main/java/org/apache/groovy/runtime/async/SelectClaim.java
(groovy) 01/05: GROOVY-12319: Add test suite for Groovy compatibility with Java generics
Posted by su...@apache.org.
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 30ac5fdbdc6989c167346446059d306b4790a5a2
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()}"
+ }
+
+}
(groovy) 02/05: GROOVY-12319: Java compatibility: remaining generic type syntax
Posted by su...@apache.org.
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 58f0e99fd6089c0b9feec0662db55afcb7fb882d
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Aug 29 14:14:13 2026 +0000
GROOVY-12319: Java compatibility: remaining generic type syntax
---
src/antlr/GroovyParser.g4 | 17 +-
.../groovy/grape/GrabAnnotationTransformation.java | 2 +-
.../apache/groovy/parser/antlr4/AstBuilder.java | 116 +-
.../java/org/codehaus/groovy/ast/ClassNode.java | 63 +-
.../java/org/codehaus/groovy/ast/GenericsType.java | 2 +-
.../java/org/codehaus/groovy/ast/MethodNode.java | 13 +
.../groovy/ast/expr/ConstructorCallExpression.java | 66 +-
.../codehaus/groovy/ast/tools/GenericsUtils.java | 147 +-
.../org/codehaus/groovy/classgen/Verifier.java | 29 +-
.../groovy/classgen/asm/BytecodeHelper.java | 57 +-
.../codehaus/groovy/control/GenericsVisitor.java | 277 +++-
.../codehaus/groovy/control/ResolveVisitor.java | 13 +
.../groovy/control/StaticImportVisitor.java | 4 +-
.../transform/stc/StaticTypeCheckingSupport.java | 27 +-
.../transform/stc/StaticTypeCheckingVisitor.java | 216 ++-
.../groovy/transform/stc/TypeCheckingContext.java | 7 +-
src/spec/doc/core-operators.adoc | 7 +
src/spec/test/OperatorsTest.groovy | 15 +
src/test-resources/core/BreakingChange_01x.groovy | 4 +-
src/test-resources/core/BreakingChange_04x.groovy | 4 +-
src/test-resources/core/Expression_19.groovy | 8 +-
src/test/groovy/bugs/Groovy12319.groovy | 1227 ++++++++++++++++
src/test/groovy/bugs/Groovy5318.groovy | 5 +-
.../generics/GenericsJavaCompatibilityTest.groovy | 1495 +++++++++++++++++++-
.../groovy/gls/generics/GenericsUsageTest.groovy | 5 +-
.../groovy/gls/innerClass/InnerClassTest.groovy | 4 +-
.../stc/ArraysAndCollectionsSTCTest.groovy | 8 +-
.../stc/DefaultGroovyMethodsSTCTest.groovy | 2 +-
.../groovy/transform/stc/GenericsSTCTest.groovy | 7 +-
.../org/codehaus/groovy/ast/ClassNodeTest.java | 15 +
.../org/codehaus/groovy/ast/MethodNodeTest.groovy | 10 +
.../groovy/ast/tools/GenericsUtilsTest.groovy | 144 ++
.../groovy/classgen/BytecodeHelperTest.groovy | 50 +
.../console/ui/AstNodeToScriptAdapterTest.groovy | 10 +-
34 files changed, 3823 insertions(+), 253 deletions(-)
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 1db3a14fbb..403433c533 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -347,8 +347,9 @@ primitiveType
: BuiltInPrimitiveType
;
+// GROOVY-12319: JLS 4.5 "rare" types (Outer<T>.Inner after the outer type arguments)
referenceType
- : qualifiedClassName typeArguments?
+ : qualifiedClassName (typeArguments (DOT identifier typeArguments?)*)?
;
matchingType // see: instanceof / !instanceof type patterns (JEP 394)
@@ -374,7 +375,7 @@ options { baseContext = type; }
standardClassOrInterfaceType
options { baseContext = referenceType; }
- : qualifiedStandardClassName typeArguments?
+ : qualifiedStandardClassName (typeArguments (DOT className typeArguments?)*)?
;
typeArguments
@@ -990,7 +991,7 @@ pathElement returns [int t]
: NL*
(
DOT NL*
- ( NEW creator[1]
+ ( NEW nonWildcardTypeArguments? creator[1] // GROOVY-12319: outer.new <T>Inner(...)
{ $t = 6; }
|
// AT: foo.@bar selects the field (or attribute), not property
@@ -1086,9 +1087,10 @@ primary
identifier typeArguments? #identifierPrmrAlt
| literal #literalPrmrAlt
| gstring #gstringPrmrAlt
- | NEW NL* creator[0] #newPrmrAlt
- | THIS #thisPrmrAlt
- | SUPER #superPrmrAlt
+ // GROOVY-12319: JLS 15.9 / 8.8.7.1 constructor type arguments (new <T>C(), <T>this(), <T>super())
+ | NEW NL* nonWildcardTypeArguments? creator[0] #newPrmrAlt
+ | nonWildcardTypeArguments? THIS #thisPrmrAlt
+ | nonWildcardTypeArguments? SUPER #superPrmrAlt
| parExpression #parenPrmrAlt
| closureOrLambdaExpression #closureOrLambdaExpressionPrmrAlt
| list #listPrmrAlt
@@ -1213,7 +1215,8 @@ anonymousInnerClassDeclaration[int t]
createdName
: annotationsOpt
( primitiveType
- | qualifiedClassName typeArgumentsOrDiamond?
+ | // GROOVY-12319: JLS 15.9 ClassOrInterfaceTypeToInstantiate (new Outer<T>.Inner<U>(...))
+ qualifiedClassName (typeArgumentsOrDiamond (DOT identifier typeArgumentsOrDiamond?)*)?
)
;
diff --git a/src/main/java/groovy/grape/GrabAnnotationTransformation.java b/src/main/java/groovy/grape/GrabAnnotationTransformation.java
index e3398ecc6f..b628721b5a 100644
--- a/src/main/java/groovy/grape/GrabAnnotationTransformation.java
+++ b/src/main/java/groovy/grape/GrabAnnotationTransformation.java
@@ -473,7 +473,7 @@ public class GrabAnnotationTransformation extends ClassCodeVisitorSupport implem
// call stack — the stack-walk depth is tuned for the compile-time path, not the
// generated static-initializer path, and would overshoot into java.lang.reflect frames.
basicArgs.addMapEntryExpression(constX("classLoader"),
- callX(new ClassExpression(classNode), "getClassLoader"));
+ callX(new ClassExpression(classNode.getPlainNodeReference()), "getClassLoader"));
if (autoDownload != null) {
basicArgs.addMapEntryExpression(constX(AUTO_DOWNLOAD_SETTING), constX(autoDownload));
}
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 70fef04b72..97fcb45fe3 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -2878,6 +2878,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
} else if (asBoolean(ctx.creator())) {
CreatorContext creatorContext = ctx.creator();
creatorContext.putNodeMetaData(ENCLOSING_INSTANCE_EXPRESSION, baseExpr);
+ if (asBoolean(ctx.nonWildcardTypeArguments())) {
+ creatorContext.putNodeMetaData(PATH_EXPRESSION_BASE_EXPR_GENERICS_TYPES, this.visitNonWildcardTypeArguments(ctx.nonWildcardTypeArguments()));
+ }
return configureAST(this.visitCreator(creatorContext), ctx);
} else if (asBoolean(ctx.indexPropertyArgs())) { // e.g. list[1, 3, 5]
Tuple2<Token, Expression> tuple = this.visitIndexPropertyArgs(ctx.indexPropertyArgs());
@@ -2969,14 +2972,14 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
ctx);
}
- return configureAST(
- new ConstructorCallExpression(
- SUPER_STR.equals(baseExprText)
- ? ClassNode.SUPER
- : ClassNode.THIS,
- argumentsExpr
- ),
- ctx);
+ ConstructorCallExpression constructorCallExpression = new ConstructorCallExpression(
+ SUPER_STR.equals(baseExprText)
+ ? ClassNode.SUPER
+ : ClassNode.THIS,
+ argumentsExpr
+ );
+ constructorCallExpression.setGenericsTypes(baseExpr.getNodeMetaData(PATH_EXPRESSION_BASE_EXPR_GENERICS_TYPES));
+ return configureAST(constructorCallExpression, ctx);
}
MethodCallExpression methodCallExpression = this.createMethodCallExpression(baseExpr, argumentsExpr);
@@ -3861,17 +3864,28 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
@Override
public Expression visitNewPrmrAlt(final NewPrmrAltContext ctx) {
+ if (asBoolean(ctx.nonWildcardTypeArguments())) {
+ ctx.creator().putNodeMetaData(PATH_EXPRESSION_BASE_EXPR_GENERICS_TYPES, this.visitNonWildcardTypeArguments(ctx.nonWildcardTypeArguments()));
+ }
return configureAST(this.visitCreator(ctx.creator()), ctx);
}
@Override
public VariableExpression visitThisPrmrAlt(final ThisPrmrAltContext ctx) {
- return configureAST(new VariableExpression(ctx.THIS().getText()), ctx);
+ VariableExpression varExpr = configureAST(new VariableExpression(ctx.THIS().getText()), ctx);
+ if (asBoolean(ctx.nonWildcardTypeArguments())) {
+ varExpr.putNodeMetaData(PATH_EXPRESSION_BASE_EXPR_GENERICS_TYPES, this.visitNonWildcardTypeArguments(ctx.nonWildcardTypeArguments()));
+ }
+ return varExpr;
}
@Override
public VariableExpression visitSuperPrmrAlt(final SuperPrmrAltContext ctx) {
- return configureAST(new VariableExpression(ctx.SUPER().getText()), ctx);
+ VariableExpression varExpr = configureAST(new VariableExpression(ctx.SUPER().getText()), ctx);
+ if (asBoolean(ctx.nonWildcardTypeArguments())) {
+ varExpr.putNodeMetaData(PATH_EXPRESSION_BASE_EXPR_GENERICS_TYPES, this.visitNonWildcardTypeArguments(ctx.nonWildcardTypeArguments()));
+ }
+ return varExpr;
}
// } primary ---------------------------------------------------------------
@@ -3883,6 +3897,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
if (asBoolean(ctx.arguments())) { // create instance of class
Expression arguments = this.visitArguments(ctx.arguments());
Expression enclosingInstanceExpression = ctx.getNodeMetaData(ENCLOSING_INSTANCE_EXPRESSION);
+ GenericsType[] constructorGenerics = ctx.getNodeMetaData(PATH_EXPRESSION_BASE_EXPR_GENERICS_TYPES);
if (enclosingInstanceExpression != null) {
if (arguments instanceof ArgumentListExpression) {
@@ -3910,10 +3925,16 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
ConstructorCallExpression constructorCallExpression = new ConstructorCallExpression(anonymousInnerClassNode, arguments);
constructorCallExpression.setUsingAnonymousInnerClass(true);
+ if (constructorGenerics != null) {
+ constructorCallExpression.setGenericsTypes(constructorGenerics);
+ }
return configureAST(constructorCallExpression, ctx);
}
ConstructorCallExpression constructorCallExpression = new ConstructorCallExpression(classNode, arguments);
+ if (constructorGenerics != null) {
+ constructorCallExpression.setGenericsTypes(constructorGenerics);
+ }
return configureAST(constructorCallExpression, ctx);
}
@@ -3963,16 +3984,14 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
@Override
public ClassNode visitCreatedName(final CreatedNameContext ctx) {
- ClassNode classNode = null;
- if (asBoolean(ctx.qualifiedClassName())) {
- classNode = this.visitQualifiedClassName(ctx.qualifiedClassName());
- if (asBoolean(ctx.typeArgumentsOrDiamond())) {
- classNode.setGenericsTypes(
- this.visitTypeArgumentsOrDiamond(ctx.typeArgumentsOrDiamond()));
+ ClassNode classNode;
+ if (asBoolean(ctx.primitiveType())) {
+ classNode = configureAST(this.visitPrimitiveType(ctx.primitiveType()), ctx);
+ } else {
+ classNode = this.visitParameterizedClassType(ctx);
+ if (classNode != null) {
configureAST(classNode, ctx);
}
- } else if (asBoolean(ctx.primitiveType())) {
- classNode = configureAST(this.visitPrimitiveType(ctx.primitiveType()), ctx);
}
if (classNode == null) {
throw createParsingFailedException("Unsupported created name: " + ctx.getText(), ctx);
@@ -4647,29 +4666,52 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
@Override
public ClassNode visitReferenceType(final ReferenceTypeContext ctx) {
- ClassNode classNode;
- if (asBoolean(ctx.qualifiedClassName())) {
- if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR)) {
- ctx.qualifiedClassName().putNodeMetaData(IS_INSIDE_INSTANCEOF_EXPR, Boolean.TRUE);
- }
- classNode = this.visitQualifiedClassName(ctx.qualifiedClassName());
- } else {
- if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR)) {
- ctx.qualifiedStandardClassName().putNodeMetaData(IS_INSIDE_INSTANCEOF_EXPR, Boolean.TRUE);
- }
- classNode = this.visitQualifiedStandardClassName(ctx.qualifiedStandardClassName());
- }
+ return configureAST(this.visitParameterizedClassType(ctx), ctx);
+ }
- if (asBoolean(ctx.typeArguments())) {
- GenericsType[] generics = this.visitTypeArguments(ctx.typeArguments());
- classNode.setGenericsTypes(generics);
- if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR) && Arrays.stream(generics).anyMatch(gt ->
- !gt.isWildcard() || asBoolean(gt.getLowerBound()) || ArrayGroovyMethods.asBoolean(gt.getUpperBounds()))) { // GROOVY-11585
- throw this.createParsingFailedException("Cannot perform instanceof check against parameterized type " + classNode, ctx);
+ /**
+ * Builds a class type that may include JLS 4.5 rare types ({@code Outer<T>.Inner<U>}).
+ * Shared by {@code referenceType} (type arguments only) and {@code createdName}
+ * (type arguments or diamond).
+ */
+ private ClassNode visitParameterizedClassType(final GroovyParserRuleContext ctx) {
+ ClassNode classNode = null;
+ for (int i = 0, n = ctx.getChildCount(); i < n; i += 1) {
+ ParseTree child = ctx.getChild(i);
+ if (child instanceof QualifiedClassNameContext qcn) {
+ if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR)) {
+ qcn.putNodeMetaData(IS_INSIDE_INSTANCEOF_EXPR, Boolean.TRUE);
+ }
+ classNode = this.visitQualifiedClassName(qcn);
+ } else if (child instanceof QualifiedStandardClassNameContext qscn) {
+ if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR)) {
+ qscn.putNodeMetaData(IS_INSIDE_INSTANCEOF_EXPR, Boolean.TRUE);
+ }
+ classNode = this.visitQualifiedStandardClassName(qscn);
+ } else if (child instanceof TypeArgumentsContext tac) {
+ GenericsType[] generics = this.visitTypeArguments(tac);
+ classNode.setGenericsTypes(generics);
+ rejectParameterizedInstanceof(ctx, classNode, generics);
+ } else if (child instanceof TypeArgumentsOrDiamondContext tad) {
+ classNode.setGenericsTypes(this.visitTypeArgumentsOrDiamond(tad));
+ } else if (child instanceof IdentifierContext || child instanceof ClassNameContext) {
+ ClassNode outer = classNode;
+ ClassNode inner = ClassHelper.makeWithoutCaching(outer.getName() + '.' + child.getText());
+ if (!isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR)) {
+ inner = this.proxyClassNode(inner);
+ }
+ inner.setOuterClassType(outer); // GROOVY-10646, GROOVY-12319
+ classNode = configureAST(inner, (GroovyParserRuleContext) child);
}
}
+ return classNode;
+ }
- return configureAST(classNode, ctx);
+ private void rejectParameterizedInstanceof(final GroovyParserRuleContext ctx, final ClassNode classNode, final GenericsType[] generics) {
+ if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR) && Arrays.stream(generics).anyMatch(gt ->
+ !gt.isWildcard() || asBoolean(gt.getLowerBound()) || ArrayGroovyMethods.asBoolean(gt.getUpperBounds()))) { // GROOVY-11585
+ throw this.createParsingFailedException("Cannot perform instanceof check against parameterized type " + classNode, ctx);
+ }
}
@Override
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassNode.java b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
index ec0a56f7a1..7c7544190c 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
@@ -257,17 +257,15 @@ public class ClassNode extends AnnotatedNode {
}
/**
- * Constructs a non-primary array {@code ClassNode} when no real array class is available.
- * This internal constructor is used by {@code makeArray()} to create a synthetic array ClassNode.
- * The resulting node represents an array type with appropriate interfaces and modifiers.
- *
- * @param componentType the {@link ClassNode} representing the element type of the array
+ * Synthetic array {@code ClassNode} used by {@link #makeArray()} when no real
+ * array class is available. {@code componentType} is the element type.
*/
- private ClassNode(final ClassNode componentType) {
- this(componentType.getName() + "[]", ACC_ABSTRACT | ACC_FINAL | ACC_PUBLIC, ClassHelper.OBJECT_TYPE,
+ private static ClassNode syntheticArray(final ClassNode componentType) {
+ ClassNode node = new ClassNode(componentType.getName() + "[]", ACC_ABSTRACT | ACC_FINAL | ACC_PUBLIC, ClassHelper.OBJECT_TYPE,
new ClassNode[]{ClassHelper.CLONEABLE_TYPE, ClassHelper.SERIALIZABLE_TYPE}, MixinNode.EMPTY_ARRAY);
- this.componentType = componentType.redirect();
- this.isPrimaryNode = false;
+ node.componentType = componentType.redirect();
+ node.isPrimaryNode = false;
+ return node;
}
//--------------------------------------------------------------------------
@@ -339,6 +337,10 @@ public class ClassNode extends AnnotatedNode {
if (isArray()) {
n.componentType = redirect().getComponentType();
}
+ ClassNode outerType = getOuterClassType();
+ if (outerType != null) {
+ n.setOuterClassType(outerType);
+ }
return n;
}
@@ -503,6 +505,7 @@ public class ClassNode extends AnnotatedNode {
* @return the modifier flags as an integer bitmask
* @see #setModifiers(int)
* @see #isAbstract()
+ * @see #isStatic()
* @see #isInterface()
* @see #isEnum()
*/
@@ -1861,6 +1864,22 @@ faces: if (method == null && asBoolean(getInterfaces())) { // GROOVY-11323
return (getModifiers() & ACC_ABSTRACT) != 0;
}
+ /**
+ * Returns whether this {@link ClassNode} represents a static class.
+ * A class is static if the {@code ACC_STATIC} modifier flag is set in its modifiers.
+ * Static nested classes have no enclosing instance; they can be used without an instance of the enclosing class.
+ * This method checks the actual modifier bits, independent of the source code syntax.
+ *
+ * @return {@code true} if this class has the static modifier set; {@code false} otherwise
+ * @see #getModifiers()
+ * @see #setModifiers(int)
+ *
+ * @since 6.0.0
+ */
+ public boolean isStatic() {
+ return (getModifiers() & ACC_STATIC) != 0;
+ }
+
/**
* Returns whether this {@link ClassNode} represents an interface type.
* A class is an interface if the {@code ACC_INTERFACE} modifier flag is set in its modifiers.
@@ -1980,7 +1999,7 @@ faces: if (method == null && asBoolean(getInterfaces())) { // GROOVY-11323
// don't use the ClassHelper here!
node = new ClassNode(type, this);
} else {
- node = new ClassNode(this);
+ node = syntheticArray(this);
}
return node;
}
@@ -2034,6 +2053,28 @@ faces: if (method == null && asBoolean(getInterfaces())) { // GROOVY-11323
return result;
}
+ /**
+ * For a JLS 4.5 rare type {@code Outer<T>.Inner}, the parameterized enclosing
+ * type. Distinct from {@link #getOuterClass()}, which is the enclosing class
+ * of a nested class <em>declaration</em>.
+ *
+ * @return the parameterized enclosing type, or {@code null} if this is not a rare type
+ * @since 6.0.0
+ */
+ public ClassNode getOuterClassType() {
+ return getNodeMetaData("outer.class");
+ }
+
+ /**
+ * Records the parameterized enclosing type of a JLS 4.5 rare type.
+ *
+ * @param outer the parameterized {@code Outer<T>} node; {@code null} clears it
+ * @since 6.0.0
+ */
+ public void setOuterClassType(final ClassNode outer) {
+ putNodeMetaData("outer.class", outer);
+ }
+
/**
* @return the field on the outer class or {@code null} if this is not an inner class
*/
@@ -2247,6 +2288,7 @@ faces: if (method == null && asBoolean(getInterfaces())) { // GROOVY-11323
*
* @return {@code true} if this class is declared in a static method; {@code false} otherwise
* @see #setStaticClass(boolean)
+ * @see #isStatic()
* @see #isScriptBody()
*/
public boolean isStaticClass() {
@@ -2261,6 +2303,7 @@ faces: if (method == null && asBoolean(getInterfaces())) { // GROOVY-11323
*
* @param staticClass {@code true} to mark as defined in a static context; {@code false} otherwise
* @see #isStaticClass()
+ * @see #isStatic()
* @see #setScriptBody(boolean)
*/
public void setStaticClass(boolean staticClass) {
diff --git a/src/main/java/org/codehaus/groovy/ast/GenericsType.java b/src/main/java/org/codehaus/groovy/ast/GenericsType.java
index 3f3b019a75..ef573f5dc8 100644
--- a/src/main/java/org/codehaus/groovy/ast/GenericsType.java
+++ b/src/main/java/org/codehaus/groovy/ast/GenericsType.java
@@ -160,7 +160,7 @@ public class GenericsType extends ASTNode {
if (Modifier.isStatic(theType.getModifiers()) || theType.isInterface()) {
sb.append(parentClassNodeName);
} else {
- ClassNode outerClass = theType.getNodeMetaData("outer.class");
+ ClassNode outerClass = theType.getOuterClassType();
if (outerClass == null) outerClass = theType.getOuterClass();
sb.append(genericsBounds(outerClass, new HashSet<>()));
}
diff --git a/src/main/java/org/codehaus/groovy/ast/MethodNode.java b/src/main/java/org/codehaus/groovy/ast/MethodNode.java
index 51c6e068de..1a932116e8 100644
--- a/src/main/java/org/codehaus/groovy/ast/MethodNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/MethodNode.java
@@ -28,6 +28,7 @@ import static org.apache.groovy.ast.tools.ClassNodeUtils.formatTypeName;
import static org.apache.groovy.ast.tools.MethodNodeUtils.methodDescriptor;
import static org.codehaus.groovy.ast.tools.GenericsUtils.toGenericTypesString;
import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
+import static org.objectweb.asm.Opcodes.ACC_BRIDGE;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
@@ -342,6 +343,18 @@ public class MethodNode extends AnnotatedNode {
return (modifiers & (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED)) == 0;
}
+ /**
+ * Checks whether this method is a compiler-generated bridge.
+ * Bridge methods are adapters inserted for covariant overrides and
+ * generic type erasure.
+ *
+ * @return true if this method has the bridge modifier
+ * @since 6.0.0
+ */
+ public boolean isBridge() {
+ return (modifiers & ACC_BRIDGE) != 0;
+ }
+
/**
* Returns the list of checked exceptions declared in the method's throws clause.
*
diff --git a/src/main/java/org/codehaus/groovy/ast/expr/ConstructorCallExpression.java b/src/main/java/org/codehaus/groovy/ast/expr/ConstructorCallExpression.java
index 86bcbcb5af..f1b10ee43a 100644
--- a/src/main/java/org/codehaus/groovy/ast/expr/ConstructorCallExpression.java
+++ b/src/main/java/org/codehaus/groovy/ast/expr/ConstructorCallExpression.java
@@ -20,6 +20,7 @@ package org.codehaus.groovy.ast.expr;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.GroovyCodeVisitor;
/**
@@ -28,6 +29,8 @@ import org.codehaus.groovy.ast.GroovyCodeVisitor;
* as well as special constructor calls like {@code this()} and {@code super()} calls within constructors.
* The constructor is identified by the type being constructed, and the arguments are wrapped in a {@link TupleExpression}.
* May optionally use an anonymous inner class.
+ * Explicit constructor type arguments ({@code new <T>Box(t)}, {@code <T>this(t)}, {@code <T>super(t)})
+ * are stored separately from the constructed type's own type arguments.
*
* @see {@link MethodCall} for the method interface this class implements
* @see {@link TupleExpression} for argument representation
@@ -44,6 +47,10 @@ public class ConstructorCallExpression extends Expression implements MethodCall
* Whether this constructor call uses an anonymous inner class declaration.
*/
private boolean usesAnonymousInnerClass;
+ /**
+ * Explicit constructor type arguments, e.g. {@code new <String>Box("x")} or {@code <T>super(t)}.
+ */
+ private GenericsType[] genericsTypes;
/**
* Creates a constructor call expression.
@@ -71,6 +78,7 @@ public class ConstructorCallExpression extends Expression implements MethodCall
public Expression transformExpression(ExpressionTransformer transformer) {
ConstructorCallExpression answer = new ConstructorCallExpression(getType(), transformer.transform(arguments));
answer.setUsingAnonymousInnerClass(isUsingAnonymousInnerClass());
+ answer.setGenericsTypes(genericsTypes);
answer.setSourcePosition(this);
answer.copyNodeMetaData(this);
return answer;
@@ -98,15 +106,63 @@ public class ConstructorCallExpression extends Expression implements MethodCall
@Override
public String getText() {
- String text;
+ StringBuilder builder = new StringBuilder(64);
if (isSuperCall()) {
- text = "super ";
+ appendTypeArguments(builder);
+ builder.append("super ");
} else if (isThisCall()) {
- text = "this ";
+ appendTypeArguments(builder);
+ builder.append("this ");
} else {
- text = "new " + getType().toString(false);
+ builder.append("new ");
+ appendTypeArguments(builder);
+ builder.append(getType().toString(false));
+ }
+ return builder.append(getArguments().getText()).toString();
+ }
+
+ private void appendTypeArguments(final StringBuilder builder) {
+ if (!isUsingGenerics()) return;
+ builder.append('<');
+ boolean first = true;
+ for (GenericsType t : genericsTypes) {
+ if (!first) builder.append(", ");
+ else first = false;
+ builder.append(t);
}
- return text + getArguments().getText();
+ builder.append('>');
+ }
+
+ /**
+ * Returns the explicit constructor type arguments, if specified.
+ * Used for generic constructor invocations such as {@code new <String>Box("x")},
+ * {@code <T>this(t)} and {@code <T>super(t)}.
+ *
+ * @return an array of {@link GenericsType} parameters, or {@code null} if none are specified
+ * @since 6.0.0
+ */
+ public GenericsType[] getGenericsTypes() {
+ return genericsTypes;
+ }
+
+ /**
+ * Sets the explicit constructor type arguments.
+ *
+ * @param genericsTypes an array of {@link GenericsType} parameters; may be {@code null}
+ * @since 6.0.0
+ */
+ public void setGenericsTypes(final GenericsType[] genericsTypes) {
+ this.genericsTypes = genericsTypes;
+ }
+
+ /**
+ * Indicates whether explicit constructor type arguments are specified.
+ *
+ * @return {@code true} if type arguments are present; {@code false} otherwise
+ * @since 6.0.0
+ */
+ public boolean isUsingGenerics() {
+ return genericsTypes != null && genericsTypes.length > 0;
}
/**
diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GenericsUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GenericsUtils.java
index c4909f9608..4385234072 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GenericsUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GenericsUtils.java
@@ -20,6 +20,7 @@ package org.codehaus.groovy.ast.tools;
import groovy.lang.Tuple2;
import groovy.transform.stc.IncorrectTypeHintException;
+import groovy.transform.Internal;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassHelper;
@@ -573,7 +574,7 @@ public class GenericsUtils {
// class C<T extends Number> extends A<T,Object,String> { }
// the type "A<T,Object,String> -> A<X,Y,Z>" will produce [X:Number,Y:Object,Z:String]
- ClassNode oc = type.getNodeMetaData("outer.class"); // GROOVY-10646: outer class type parameters
+ ClassNode oc = type.getOuterClassType(); // GROOVY-10646: outer class type parameters
Map<String, ClassNode> newSpec = oc != null ? createGenericsSpec(oc, oldSpec) : new HashMap<>();
GenericsType[] gt = type.getGenericsTypes(), rgt = type.redirect().getGenericsTypes();
if (gt != null && rgt != null) {
@@ -584,6 +585,150 @@ public class GenericsUtils {
return newSpec;
}
+ /**
+ * Writes {@code genericsSpec} onto {@code type} as an explicit type-argument
+ * list, filling any missing parameter from its bound (or {@code Object}).
+ * Mutates {@code type} in place; callers must pass a per-use copy such as
+ * an anonymous class's diamond super type, not a shared redirect.
+ *
+ * @since 6.0.0
+ */
+ @Internal
+ public static void applyGenericsSpec(final ClassNode type, final Map<String, ClassNode> genericsSpec) {
+ GenericsType[] rgt = type.redirect().getGenericsTypes();
+ if (rgt == null || rgt.length == 0) return;
+ GenericsType[] newGt = new GenericsType[rgt.length];
+ for (int i = 0; i < rgt.length; i += 1) {
+ ClassNode resolved = genericsSpec.get(rgt[i].getName());
+ if (resolved == null) {
+ resolved = rgt[i].getUpperBounds() != null && rgt[i].getUpperBounds().length > 0
+ ? rgt[i].getUpperBounds()[0] : ClassHelper.OBJECT_TYPE;
+ }
+ newGt[i] = new GenericsType(resolved);
+ }
+ type.setGenericsTypes(newGt);
+ }
+
+ /**
+ * Recovers a {@link #createGenericsSpec(ClassNode, Map) generics specification}
+ * for {@code superType} from {@code impl}'s instance methods. Used for diamond
+ * anonymous classes (GROOVY-12319 / JEP 213): type arguments of {@code Super<>}
+ * are not written in source, so they are read back from overrides such as
+ * {@code String process(String)} for {@code T process(T)}.
+ * <p>
+ * The result uses the same {@code Map<String, ClassNode>} protocol as
+ * {@link #createGenericsSpec(ClassNode, Map)} and {@link #applyGenericsSpec(ClassNode, Map)}.
+ * Assignment / target typing remains the primary JLS 15.9.3 witness; this is
+ * the fallback used when the diamond is still empty.
+ *
+ * @since 6.0.0
+ */
+ @Internal
+ public static Map<String, ClassNode> inferGenericsSpecFromOverrides(final ClassNode impl, final ClassNode superType, final Map<String, ClassNode> oldSpec) {
+ Map<String, ClassNode> spec = new HashMap<>(oldSpec);
+ GenericsType[] rgt = superType.redirect().getGenericsTypes();
+ if (rgt == null || rgt.length == 0) return spec;
+
+ Set<String> typeParameterNames = new HashSet<>();
+ for (GenericsType gt : rgt) {
+ typeParameterNames.add(gt.getName());
+ }
+
+ Map<String, List<MethodNode>> declaredByName = new HashMap<>();
+ for (MethodNode declaredMethod : impl.getMethods()) {
+ if (!isPossibleOverride(declaredMethod)) continue;
+ declaredByName.computeIfAbsent(declaredMethod.getName(), k -> new ArrayList<>()).add(declaredMethod);
+ }
+ for (MethodNode superMethod : superType.getMethods()) {
+ if (!isPossibleOverride(superMethod)) continue;
+ List<MethodNode> candidates = declaredByName.get(superMethod.getName());
+ if (candidates == null) continue;
+ for (MethodNode declaredMethod : candidates) {
+ if (declaredMethod.getParameters().length != superMethod.getParameters().length) continue;
+ Map<String, ClassNode> extracted = new HashMap<>();
+ Parameter[] sp = superMethod.getParameters();
+ Parameter[] dp = declaredMethod.getParameters();
+ boolean plausibleOverride = true;
+ for (int i = 0; i < sp.length; i += 1) {
+ if (!extractOverrideConnection(dp[i].getType(), sp[i].getType(), extracted)) {
+ plausibleOverride = false;
+ break;
+ }
+ }
+ if (!plausibleOverride) continue;
+ extractOverrideConnection(declaredMethod.getReturnType(), superMethod.getReturnType(), extracted);
+ for (Map.Entry<String, ClassNode> entry : extracted.entrySet()) {
+ if (typeParameterNames.contains(entry.getKey())) {
+ spec.putIfAbsent(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ }
+ return spec;
+ }
+
+ /**
+ * Connects an override's actual type to a super-type parameter that may
+ * contain placeholders. Primitive types are boxed so they can appear as
+ * type arguments; {@link ClassHelper#getWrapper(ClassNode)} is not used
+ * because it redirects and would drop type arguments such as
+ * {@code List<String>}.
+ * <p>
+ * Only declared types that still mention a placeholder are considered, and
+ * only when {@code actual} is related to the erasure of {@code declared},
+ * so coincidental name-and-arity matches are skipped without catching
+ * {@link GroovyBugError}.
+ *
+ * @return {@code false} when the pair cannot be an override because the
+ * erasures are unrelated; {@code true} otherwise
+ */
+ private static boolean extractOverrideConnection(final ClassNode actual, final ClassNode declared, final Map<String, ClassNode> extracted) {
+ if (actual == null || declared == null) return true;
+ if (!declared.isGenericsPlaceHolder() && !hasUnresolvedGenerics(declared)) return true;
+ ClassNode boxed = ClassHelper.isPrimitiveType(actual) ? ClassHelper.getWrapper(actual) : actual;
+ ClassNode erasure = declared.redirect();
+ if (!declared.isGenericsPlaceHolder()
+ && !boxed.equals(erasure)
+ && !implementsInterfaceOrIsSubclassOf(boxed, erasure)
+ && !implementsInterfaceOrIsSubclassOf(erasure, boxed)) {
+ return false;
+ }
+ extractSuperClassGenerics(boxed, declared, extracted);
+ return true;
+ }
+
+ private static boolean isPossibleOverride(final MethodNode method) {
+ return !(method.isStatic() || method.isPrivate() || method.isBridge());
+ }
+
+ /**
+ * Diamond {@code <>} of an anonymous class is stored on its super type,
+ * or on an interface once {@code InnerClassVisitor} has moved an interface
+ * super type. Returns that node, or {@code null} if none still carries a
+ * diamond.
+ *
+ * @since 6.0.0
+ */
+ @Internal
+ public static ClassNode diamondTargetOfAnonymousClass(final ClassNode anonType) {
+ if (anonType == null) return null;
+ ClassNode sc = anonType.getUnresolvedSuperClass(false);
+ if (isEmptyDiamond(sc)) return sc;
+ ClassNode[] ifaces = anonType.getInterfaces();
+ if (ifaces != null) {
+ for (ClassNode iface : ifaces) {
+ if (isEmptyDiamond(iface)) return iface;
+ }
+ }
+ return null;
+ }
+
+ private static boolean isEmptyDiamond(final ClassNode type) {
+ if (type == null) return false;
+ GenericsType[] gt = type.getGenericsTypes();
+ return gt != null && gt.length == 0;
+ }
+
/**
* @since 2.4.1
*/
diff --git a/src/main/java/org/codehaus/groovy/classgen/Verifier.java b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
index 0d73045355..d948be8d36 100644
--- a/src/main/java/org/codehaus/groovy/classgen/Verifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
@@ -122,8 +122,10 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
import static org.codehaus.groovy.ast.tools.GenericsUtils.addMethodGenerics;
+import static org.codehaus.groovy.ast.tools.GenericsUtils.applyGenericsSpec;
import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpec;
import static org.codehaus.groovy.ast.tools.GenericsUtils.createGenericsSpec;
+import static org.codehaus.groovy.ast.tools.GenericsUtils.inferGenericsSpecFromOverrides;
import static org.codehaus.groovy.ast.tools.GenericsUtils.parameterizeType;
import static org.codehaus.groovy.ast.tools.PropertyNodeUtils.adjustPropertyModifiersForMethod;
import static org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys.STATIC_COMPILE_NODE;
@@ -1783,7 +1785,7 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
private void addCovariantMethods(final ClassNode classNode, final List<MethodNode> declaredMethods, final Map<String, MethodNode> interfaceMethods, final Map<String, MethodNode> methodsToAdd, final Map<String, ClassNode> oldGenericsSpec) {
ClassNode sc = classNode.getUnresolvedSuperClass(false);
if (sc != null) {
- Map<String, ClassNode> genericsSpec = createGenericsSpec(sc, oldGenericsSpec);
+ Map<String, ClassNode> genericsSpec = genericsSpecForSuperType(classNode, sc, oldGenericsSpec);
List<MethodNode> scMethods = sc.getMethods();
// add bridge methods in original class for overridden methods with changed signature
storeMissingCovariantMethods(declaredMethods, methodsToAdd, genericsSpec, scMethods);
@@ -1807,14 +1809,35 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
for (ClassNode anInterface : classNode.getInterfaces()) {
List<MethodNode> ifaceMethods = anInterface.getMethods();
- Map<String, ClassNode> genericsSpec = createGenericsSpec(anInterface, oldGenericsSpec);
+ Map<String, ClassNode> genericsSpec = genericsSpecForSuperType(classNode, anInterface, oldGenericsSpec);
storeMissingCovariantMethods(declaredMethods, methodsToAdd, genericsSpec, ifaceMethods);
addCovariantMethods(anInterface, declaredMethods, interfaceMethods, methodsToAdd, genericsSpec);
}
}
+ /**
+ * Diamond AIC types may still carry {@code Super<>} at classgen when no
+ * assignment target filled them. Recover the spec from overrides (GROOVY-12319)
+ * and write it onto {@code superType} so covariant bridges see the same
+ * parameterization as the anonymous body.
+ */
+ private static Map<String, ClassNode> genericsSpecForSuperType(final ClassNode classNode, final ClassNode superType, final Map<String, ClassNode> oldGenericsSpec) {
+ Map<String, ClassNode> genericsSpec = createGenericsSpec(superType, oldGenericsSpec);
+ GenericsType[] usage = superType.getGenericsTypes();
+ // Only an actual diamond (<>) is inferred here. A raw AIC of a generic
+ // super type (gt == null) must not pay override reconstruction.
+ if (classNode instanceof InnerClassNode && ((InnerClassNode) classNode).isAnonymous()
+ && usage != null && usage.length == 0) {
+ genericsSpec = inferGenericsSpecFromOverrides(classNode, superType, oldGenericsSpec);
+ if (!genericsSpec.isEmpty()) {
+ applyGenericsSpec(superType, genericsSpec);
+ }
+ }
+ return genericsSpec;
+ }
+
private static boolean isPossibleOverride(final MethodNode methodNode) {
- return (methodNode.getModifiers() & (ACC_BRIDGE | ACC_PRIVATE | ACC_STATIC)) == 0;
+ return !(methodNode.isBridge() || methodNode.isPrivate() || methodNode.isStatic());
}
private void storeMissingCovariantMethods(final List<MethodNode> declaredMethods, final Map<String, MethodNode> methodsToAdd, final Map<String, ClassNode> genericsSpec, final List<MethodNode> superClassMethods) {
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java b/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
index 1187065a13..0b12674e16 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
@@ -337,7 +337,10 @@ public class BytecodeHelper {
}
private static boolean hasGenerics(ClassNode type) {
- return type.isArray() ? hasGenerics(type.getComponentType()) : type.getGenericsTypes() != null;
+ if (type.isArray()) return hasGenerics(type.getComponentType());
+ if (type.getGenericsTypes() != null && type.getGenericsTypes().length > 0) return true;
+ ClassNode outer = type.getOuterClassType();
+ return outer != null && hasGenerics(outer);
}
/**
@@ -393,18 +396,13 @@ public class BytecodeHelper {
public static String getTypeGenericsSignature(ClassNode node) {
if (!usesGenericsInTypeSignature(node)) return null;
StringBuilder ret = new StringBuilder(100);
- ret.append(getTypeDescription(node.getPlainNodeReference(), false));
- addSubTypes(ret, node.getGenericsTypes(), "<", ">");
+ writeParameterizedClass(ret, node);
ret.append(";");
-
return ret.toString();
}
private static boolean usesGenericsInTypeSignature(ClassNode node) {
- if (!node.isUsingGenerics()) return false;
- if (hasGenerics(node)) return true;
-
- return false;
+ return hasGenerics(node);
}
/**
@@ -429,7 +427,7 @@ public class BytecodeHelper {
}
private static void getGenericsTypeSpec(StringBuilder ret, GenericsType[] genericsTypes) {
- if (genericsTypes == null) return;
+ if (genericsTypes == null || genericsTypes.length == 0) return;
ret.append('<');
for (GenericsType genericsType : genericsTypes) {
String name = genericsType.getName();
@@ -447,8 +445,11 @@ public class BytecodeHelper {
* @return the bounds signature, or {@code null} if none is needed
*/
public static String getGenericsBounds(ClassNode type) {
- GenericsType[] genericsTypes = type.getGenericsTypes();
- if (genericsTypes == null) return null;
+ if (type.isArray()) {
+ String component = getGenericsBounds(type.getComponentType());
+ return component == null ? null : "[" + component;
+ }
+ if (!hasGenerics(type)) return null;
StringBuilder ret = new StringBuilder(100);
if (type.isGenericsPlaceHolder()) {
addSubTypes(ret, type.getGenericsTypes(), "", "");
@@ -466,12 +467,41 @@ public class BytecodeHelper {
ret.append(printType.getGenericsTypes()[0].getName());
ret.append(";");
} else {
- ret.append(getTypeDescription(printType, false));
- addSubTypes(ret, printType.getGenericsTypes(), "<", ">");
+ writeParameterizedClass(ret, printType);
if (!isPrimitiveType(printType)) ret.append(";");
}
}
+ /**
+ * Writes a class type and its type arguments, using the JLS 4.5 nested form
+ * {@code LOuter<...>.Inner<...>} when an enclosing rare type is present.
+ */
+ private static void writeParameterizedClass(StringBuilder ret, ClassNode printType) {
+ ClassNode owner = printType.getOuterClassType();
+ if (owner != null) {
+ writeParameterizedClass(ret, owner);
+ ret.append('.');
+ ret.append(innerClassSimpleName(printType, owner));
+ addSubTypes(ret, printType.getGenericsTypes(), "<", ">");
+ return;
+ }
+ ret.append(getTypeDescription(printType, false));
+ addSubTypes(ret, printType.getGenericsTypes(), "<", ">");
+ }
+
+ private static String innerClassSimpleName(final ClassNode inner, final ClassNode owner) {
+ String innerName = inner.getName();
+ String ownerName = owner.getName();
+ if (innerName.startsWith(ownerName) && innerName.length() > ownerName.length()) {
+ char sep = innerName.charAt(ownerName.length());
+ if (sep == '.' || sep == '$') {
+ return innerName.substring(ownerName.length() + 1).replace('$', '.');
+ }
+ }
+ int dot = Math.max(innerName.lastIndexOf('.'), innerName.lastIndexOf('$'));
+ return dot < 0 ? innerName : innerName.substring(dot + 1);
+ }
+
private static void writeGenericsBounds(StringBuilder ret, GenericsType type, boolean writeInterfaceMarker) {
if (type.getUpperBounds() != null) {
ClassNode[] bounds = type.getUpperBounds();
@@ -487,6 +517,7 @@ public class BytecodeHelper {
private static void addSubTypes(StringBuilder ret, GenericsType[] types, String start, String end) {
if (types == null) return;
+ if (types.length == 0 && "<".equals(start)) return;
ret.append(start);
for (GenericsType type : types) {
if (type.getType().isArray()) {
diff --git a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
index 2c133d69ce..991cfdc548 100644
--- a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
@@ -19,6 +19,7 @@
package org.codehaus.groovy.control;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
+import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.GenericsType;
@@ -26,13 +27,21 @@ import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.ArrayExpression;
+import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.CastExpression;
+import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.syntax.Types;
import org.codehaus.groovy.transform.trait.Traits;
+import static org.codehaus.groovy.ast.ClassHelper.isObjectType;
+import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType;
+import static org.codehaus.groovy.ast.tools.GenericsUtils.diamondTargetOfAnonymousClass;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isUnboundedWildcard;
/**
@@ -42,6 +51,7 @@ import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isUnbo
* <li>class header (class and superclass declaration)</li>
* <li>arity of type parameters for fields, parameters, local variables</li>
* <li>invalid diamond {@code <>} usage</li>
+ * <li>JLS well-formedness of type arguments, bounds, array creation, and type parameters</li>
* </ul>
*/
public class GenericsVisitor extends ClassCodeVisitorSupport {
@@ -76,13 +86,20 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
*/
@Override
public void visitClass(final ClassNode node) {
+ checkTypeParameterBounds(node.getGenericsTypes());
+ if (node.getGenericsTypes() != null && node.getGenericsTypes().length > 0
+ && node.isDerivedFrom(ClassHelper.THROWABLE_TYPE)) {
+ addError("A generic class may not extend java.lang.Throwable", node);
+ }
+
ClassNode sc = node.getUnresolvedSuperClass(false);
if (checkWildcard(sc)) return;
boolean isAIC = node instanceof InnerClassNode && ((InnerClassNode) node).isAnonymous();
checkGenericsUsage(sc, sc.redirect(), isAIC ? Boolean.TRUE : null);
for (ClassNode face : node.getInterfaces()) {
- checkGenericsUsage(face);
+ checkWildcard(face); // JLS 8.1.5: superinterfaces may not specify a wildcard
+ checkGenericsUsage(face, face.redirect(), isAIC ? Boolean.TRUE : null);
}
visitObjectInitializerStatements(node);
@@ -109,6 +126,7 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
*/
@Override
protected void visitConstructorOrMethod(final MethodNode node, final boolean isConstructor) {
+ checkTypeParameterBounds(node.getGenericsTypes());
for (Parameter p : node.getParameters()) {
checkGenericsUsage(p.getType());
}
@@ -127,9 +145,21 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
@Override
public void visitConstructorCallExpression(final ConstructorCallExpression expression) {
ClassNode type = expression.getType();
+ if (type.isGenericsPlaceHolder()) {
+ addError("Cannot instantiate the type parameter " + type.getUnresolvedName(), expression);
+ }
+ if (hasWildcardTypeArgument(type)) {
+ addError("A constructor call may not specify a wildcard type", expression);
+ }
boolean isAIC = type instanceof InnerClassNode
&& ((InnerClassNode) type).isAnonymous();
checkGenericsUsage(type, type.redirect(), isAIC);
+ if (expression.isUsingGenerics()) {
+ ClassNode created = isAIC ? diamondTargetOfAnonymousClass(type) : type;
+ if (created != null && created.getGenericsTypes() != null && created.getGenericsTypes().length == 0) {
+ addError("Cannot use diamond <> together with constructor type arguments", expression);
+ }
+ }
super.visitConstructorCallExpression(expression);
}
@@ -159,11 +189,61 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
*/
@Override
public void visitArrayExpression(final ArrayExpression expression) {
+ ClassNode elementType = expression.getElementType();
+ if (!isReifiable(elementType)) {
+ addError("generic array creation", expression);
+ }
checkGenericsUsage(expression.getType());
super.visitArrayExpression(expression);
}
+ /**
+ * JLS 15.8.2: a class literal may not name a type variable.
+ */
+ @Override
+ public void visitClassExpression(final ClassExpression expression) {
+ ClassNode type = expression.getType();
+ if (type.isGenericsPlaceHolder()) {
+ addError("Cannot select from a type parameter " + type.getUnresolvedName(), expression);
+ }
+ super.visitClassExpression(expression);
+ }
+
+ /**
+ * Groovy represents the type operand of {@code instanceof} as a
+ * {@link ClassExpression}, but it is not a class literal. Skip it so
+ * {@link InstanceOfVerifier} can diagnose JLS 15.20.2.
+ */
+ @Override
+ public void visitBinaryExpression(final BinaryExpression expression) {
+ if (expression.getOperation().isA(Types.INSTANCEOF_OPERATOR)
+ && expression.getRightExpression() instanceof ClassExpression) {
+ expression.getLeftExpression().visit(this);
+ return;
+ }
+ super.visitBinaryExpression(expression);
+ }
+
+ /**
+ * JLS 4.5.2: a static member of a generic type must be referred to by the
+ * generic type name, not a parameterization such as {@code Cell<String>.id()}.
+ */
+ @Override
+ public void visitMethodCallExpression(final MethodCallExpression call) {
+ checkStaticMemberViaParameterizedType(call.getObjectExpression(), call);
+ super.visitMethodCallExpression(call);
+ }
+
+ /**
+ * JLS 4.5.2 also applies to static fields accessed as {@code Cell<String>.ID}.
+ */
+ @Override
+ public void visitPropertyExpression(final PropertyExpression expression) {
+ checkStaticMemberViaParameterizedType(expression.getObjectExpression(), expression);
+ super.visitPropertyExpression(expression);
+ }
+
/**
* Validates generic usage for cast target types.
*
@@ -178,6 +258,12 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
//--------------------------------------------------------------------------
+ private void checkStaticMemberViaParameterizedType(final Expression object, final Expression location) {
+ if (object instanceof ClassExpression && isParameterizedTypeUsage(object.getType())) {
+ addError("Cannot refer to a static member of a generic type through a parameterization", location);
+ }
+ }
+
private boolean checkWildcard(final ClassNode sn) {
boolean wildcard = false;
if (sn.getGenericsTypes() != null) {
@@ -199,6 +285,19 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
private void checkGenericsUsage(final ClassNode cn, final ClassNode rn, final Boolean isAIC) {
if (cn.isGenericsPlaceHolder()) return;
+ ClassNode oc = cn.getOuterClassType(); // GROOVY-12319: Outer<T>.Inner
+ if (oc != null) {
+ checkGenericsUsage(oc);
+ // JLS 4.5 / 6.5.5: a parameterized enclosing type may only select a
+ // non-static member type, and a generic member must be parameterized.
+ if (isParameterizedEnclosingType(oc)) {
+ if (isStaticMemberType(cn)) {
+ addError("Cannot refer to a static member of a generic type through a parameterization", cn);
+ } else if (cn.getGenericsTypes() == null && rn.getGenericsTypes() != null) {
+ addError("A raw member type may not be used with a parameterized enclosing type", cn);
+ }
+ }
+ }
GenericsType[] cnTypes = cn.getGenericsTypes();
// raw type usage is always allowed
if (cnTypes == null) return;
@@ -215,53 +314,167 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
}
// parameterize a type by using all the parameters only
if (cnTypes.length != rnTypes.length) {
- if (Boolean.FALSE.equals(isAIC) && cnTypes.length == 0) {
- return; // allow Diamond for non-AIC cases from CCE
+ if (isAIC != null && cnTypes.length == 0) {
+ return; // diamond on constructor calls, including anonymous classes (GROOVY-12319 / JEP 213)
}
- String message;
- if (Boolean.TRUE.equals(isAIC) && cnTypes.length == 0) {
- message = "Cannot use diamond <> with anonymous inner classes";
- } else {
- message = "The class " + cn.toString(false) + " (supplied with " + plural("type parameter", cnTypes.length) +
- ") refers to the class " + rn.toString(false) + " which takes " + plural("parameter", rnTypes.length);
- if (cnTypes.length == 0) {
- message += " (invalid Diamond <> usage?)";
- }
+ String message = "The class " + cn.toString(false) + " (supplied with " + plural("type parameter", cnTypes.length) +
+ ") refers to the class " + rn.toString(false) + " which takes " + plural("parameter", rnTypes.length);
+ if (cnTypes.length == 0) {
+ message += " (invalid Diamond <> usage?)";
}
addError(message, cn);
return;
}
for (int i = 0; i < cnTypes.length; i++) {
ClassNode cnType = cnTypes[i].getType();
- ClassNode rnType = rnTypes[i].getType();
+ if (isPrimitiveType(cnType)) {
+ addError("The type argument " + cnType.getName() + " is of primitive type; a reference type is required", cnTypes[i]);
+ continue;
+ }
// check nested type parameters
checkGenericsUsage(cnType);
- // check bounds: unbounded wildcard (aka "?") is universal substitute
- if (!isUnboundedWildcard(cnTypes[i])) {
- // check upper bound(s)
- ClassNode[] bounds = rnTypes[i].getUpperBounds();
-
- // first can be class or interface
- boolean valid = cnType.isDerivedFrom(rnType) || ((rnType.isInterface() || Traits.isTrait(rnType)) && cnType.implementsInterface(rnType));
-
- // subsequent bounds if present can be interfaces
- if (valid && bounds != null && bounds.length > 1) {
- for (int j = 1; j < bounds.length; j++) {
- ClassNode bound = bounds[j];
- if (!cnType.implementsInterface(bound)) {
- valid = false;
- break;
- }
- }
+ if (!isTypeArgumentWithinBounds(cnTypes[i], rnTypes[i])) {
+ addError("The type " + cnTypes[i].getName() + " is not a valid substitute for the bounded parameter <" + rnTypes[i] + ">", cnTypes[i]);
+ }
+ }
+ }
+
+ /**
+ * JLS 4.5: a type argument is within bounds of the corresponding type
+ * parameter. Unbounded {@code ?} is always within bounds. For
+ * {@code ? extends U} against {@code T extends B}, javac accepts the
+ * argument when {@code glb(U, B)} is well-formed (not an empty
+ * intersection of two classes). For {@code ? super L}, {@code L} must
+ * be a subtype of {@code B}.
+ */
+ private boolean isTypeArgumentWithinBounds(final GenericsType arg, final GenericsType param) {
+ if (isUnboundedWildcard(arg)) return true;
+ ClassNode[] paramBounds = param.getUpperBounds();
+ ClassNode firstBound = (paramBounds != null && paramBounds.length > 0)
+ ? paramBounds[0] : param.getType().redirect();
+
+ if (arg.isWildcard()) {
+ ClassNode lower = arg.getLowerBound();
+ if (lower != null) {
+ return isSubtypeOfBound(lower, firstBound);
+ }
+ ClassNode[] uppers = arg.getUpperBounds();
+ if (uppers != null && uppers.length > 0) {
+ return !areDisjointClassTypes(uppers[0], firstBound);
+ }
+ return true;
+ }
+
+ ClassNode cnType = arg.getType();
+ boolean valid = isSubtypeOfBound(cnType, firstBound);
+ if (valid && paramBounds != null && paramBounds.length > 1) {
+ for (int j = 1; j < paramBounds.length; j++) {
+ ClassNode bound = paramBounds[j];
+ if (!cnType.implementsInterface(bound)) {
+ return false;
}
+ }
+ }
+ return valid;
+ }
+
+ private static boolean isSubtypeOfBound(final ClassNode type, final ClassNode bound) {
+ if (bound == null || isObjectType(bound)) return true;
+ return type.isDerivedFrom(bound)
+ || ((bound.isInterface() || Traits.isTrait(bound)) && type.implementsInterface(bound));
+ }
- if (!valid) {
- addError("The type " + cnTypes[i].getName() + " is not a valid substitute for the bounded parameter <" + rnTypes[i] + ">", cnTypes[i]);
+ /**
+ * Two classes (not interfaces) whose glb would be empty: neither is a
+ * subtype of the other. {@code Object} is a universal upper bound.
+ */
+ private static boolean areDisjointClassTypes(final ClassNode a, final ClassNode b) {
+ if (a == null || b == null || isObjectType(a) || isObjectType(b)) return false;
+ if (a.isInterface() || b.isInterface() || Traits.isTrait(a) || Traits.isTrait(b)) {
+ return false;
+ }
+ return !a.isDerivedFrom(b) && !b.isDerivedFrom(a);
+ }
+
+ /**
+ * JLS 4.4: a class type or type variable may appear only as the first type
+ * of a bound; additional bounds must be interfaces.
+ */
+ private void checkTypeParameterBounds(final GenericsType[] typeParameters) {
+ if (typeParameters == null) return;
+ for (GenericsType tp : typeParameters) {
+ ClassNode[] bounds = tp.getUpperBounds();
+ if (bounds == null) continue;
+ for (int i = 1; i < bounds.length; i++) {
+ ClassNode bound = bounds[i];
+ if (bound != null && !bound.isInterface() && !Traits.isTrait(bound)) {
+ addError("Additional bounds of a type parameter must be interfaces", tp);
}
}
}
}
+ /**
+ * True when {@code type} is a parameterized usage ({@code Cell<String>}), as
+ * opposed to the generic type name ({@code Cell}) or the class declaration
+ * node (whose {@link ClassNode#getGenericsTypes()} are the formals).
+ */
+ private static boolean isParameterizedTypeUsage(final ClassNode type) {
+ GenericsType[] gt = type.getGenericsTypes();
+ return gt != null && gt.length > 0 && type.isRedirectNode() && !type.isGenericsPlaceHolder();
+ }
+
+ /**
+ * True when {@code type} carries type arguments, including wildcards
+ * ({@code Outer<?>}). Raw names ({@code Outer}) are not parameterized.
+ */
+ private static boolean isParameterizedEnclosingType(final ClassNode type) {
+ GenericsType[] gt = type.getGenericsTypes();
+ return gt != null && gt.length > 0 && !type.isGenericsPlaceHolder();
+ }
+
+ /**
+ * Nested interfaces and enums are implicitly static; otherwise the
+ * {@code ACC_STATIC} bit on the declaration is authoritative.
+ */
+ private static boolean isStaticMemberType(final ClassNode type) {
+ ClassNode declared = type.redirect();
+ return declared.isStatic()
+ || declared.isInterface()
+ || declared.isEnum();
+ }
+
+ private static boolean hasWildcardTypeArgument(final ClassNode type) {
+ GenericsType[] gt = type.getGenericsTypes();
+ if (gt == null) return false;
+ for (GenericsType t : gt) {
+ if (t.isWildcard()) return true;
+ }
+ ClassNode oc = type.getOuterClassType();
+ return oc != null && hasWildcardTypeArgument(oc);
+ }
+
+ /**
+ * JLS 4.7: types available in full at run time. Used for array creation
+ * (JLS 15.10.1), which requires a reifiable component type.
+ */
+ private static boolean isReifiable(ClassNode type) {
+ while (type.isArray()) {
+ type = type.getComponentType();
+ }
+ if (type.isGenericsPlaceHolder()) return false;
+ if (isPrimitiveType(type)) return true;
+ GenericsType[] gt = type.getGenericsTypes();
+ if (gt != null) {
+ if (gt.length == 0) return false;
+ for (GenericsType t : gt) {
+ if (!isUnboundedWildcard(t)) return false;
+ }
+ }
+ ClassNode oc = type.getOuterClassType();
+ return oc == null || isReifiable(oc);
+ }
+
private static String plural(final String string, final int count) {
return count + " " + (count == 1 ? string : string + "s");
}
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index 78a4a88db0..0bcb6e0d69 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -420,6 +420,11 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
if (type.isRedirectNode() || !type.isPrimaryClassNode()) {
visitTypeAnnotations(type); // JSR 308 support
}
+ ClassNode oc = type.getOuterClassType(); // GROOVY-10646, GROOVY-12319: Outer<T>.Inner
+ if (oc != null) {
+ resolveOrFail(oc, msg, node, preferImports);
+ resolveGenericsTypes(oc.getGenericsTypes());
+ }
if (preferImports && !type.isResolved() && !type.isPrimaryClassNode()) {
resolveGenericsTypes(type.getGenericsTypes());
if (resolveAliasFromModule(type)) return;
@@ -483,6 +488,13 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
protected boolean resolve(final ClassNode type, final boolean testModuleImports, final boolean testDefaultImports, final boolean testStaticInnerClasses) {
GenericsType[] genericsTypes = type.getGenericsTypes();
resolveGenericsTypes(genericsTypes);
+ ClassNode oc = type.getOuterClassType();
+ if (oc != null) {
+ if (!resolve(oc, testModuleImports, testDefaultImports, testStaticInnerClasses)) {
+ return false;
+ }
+ resolveGenericsTypes(oc.getGenericsTypes());
+ }
if (type.isPrimaryClassNode()) return true;
if (type.isArray()) {
@@ -1347,6 +1359,7 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
addError("You cannot create an instance from the abstract " + getDescription(cceType) + ".", cce);
}
}
+ resolveGenericsTypes(cce.getGenericsTypes());
return cce.transformExpression(this);
}
diff --git a/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java b/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java
index 8786567651..808c70219b 100644
--- a/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java
@@ -309,7 +309,7 @@ public class StaticImportVisitor extends ClassCodeExpressionTransformer {
}
}
} else if (staticWrtCurrent && isSuperExpression(object)) {
- Expression result = new MethodCallExpression(new ClassExpression(currentClass.getSuperClass()), method, args);
+ Expression result = new MethodCallExpression(new ClassExpression(currentClass.getSuperClass().getPlainNodeReference()), method, args);
result.setSourcePosition(mce);
return result;
}
@@ -416,7 +416,7 @@ public class StaticImportVisitor extends ClassCodeExpressionTransformer {
if (currentMethod != null && currentMethod.isStatic()
&& isSuperExpression(pe.getObjectExpression())) {
PropertyExpression pexp = new PropertyExpression(
- new ClassExpression(currentClass.getUnresolvedSuperClass()),
+ new ClassExpression(currentClass.getUnresolvedSuperClass().getPlainNodeReference()),
transform(pe.getProperty())
);
pexp.setSourcePosition(pe);
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
index bf6d37f088..1be2df4e32 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
@@ -1856,7 +1856,7 @@ public abstract class StaticTypeCheckingSupport {
} else if (type.equals(target)) {
extractGenericsConnections(connections, type.getGenericsTypes(), target.getGenericsTypes());
- extractGenericsConnections(connections, type.getNodeMetaData("outer.class"), target.getOuterClass()); //GROOVY-10646
+ extractGenericsConnections(connections, type.getOuterClassType(), target.getOuterClass()); //GROOVY-10646
} else if (implementsInterfaceOrIsSubclassOf(type, target)) {
ClassNode superClass = getNextSuperClass(type, target);
@@ -2021,7 +2021,7 @@ public abstract class StaticTypeCheckingSupport {
Optional.ofNullable(type.getOuterClass())
.filter(oc -> oc.getGenericsTypes()!=null)
.map(oc -> applyGenericsContext(ctx, spec, oc))
- .ifPresent(oc -> newType.putNodeMetaData("outer.class", oc));
+ .ifPresent(newType::setOuterClassType);
}
}
return new GenericsType(newType);
@@ -2078,6 +2078,9 @@ public abstract class StaticTypeCheckingSupport {
}
GenericsType[] gt = type.getGenericsTypes();
+ if (gt == null && type.isGenericsPlaceHolder()) {
+ gt = new GenericsType[]{type.asGenericsType()};
+ }
if (asBoolean(spec)) {
gt = applyGenericsContext(ctx, spec, gt);
}
@@ -2087,16 +2090,18 @@ public abstract class StaticTypeCheckingSupport {
return cn;
}
- if (!gt[0].isPlaceholder()) { // convert T to Type or Type<...>
- return getCombinedBoundType(gt[0]);
- }
+ if (gt != null && gt.length > 0) {
+ if (!gt[0].isPlaceholder()) { // convert T to Type or Type<...>
+ return getCombinedBoundType(gt[0]);
+ }
- if (type.getGenericsTypes()[0] != gt[0]) { // convert T to X
- ClassNode cn = make(gt[0].getName()) , erasure = getCombinedBoundType(gt[0]).redirect();
- cn.setGenericsPlaceHolder(true);
- cn.setGenericsTypes(gt);
- cn.setRedirect(erasure);
- return cn;
+ if (type.getGenericsTypes() == null || type.getGenericsTypes()[0] != gt[0]) { // convert T to X
+ ClassNode cn = make(gt[0].getName()), erasure = getCombinedBoundType(gt[0]).redirect();
+ cn.setGenericsPlaceHolder(true);
+ cn.setGenericsTypes(gt);
+ cn.setRedirect(erasure);
+ return cn;
+ }
}
return type; // nothing to do
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
index f74ee46f30..e31fb71edc 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -567,14 +567,20 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
typeCheckingContext.pushEnclosingClassNode(node);
Set<MethodNode> oldSet = typeCheckingContext.alreadyVisitedMethods;
typeCheckingContext.alreadyVisitedMethods = new LinkedHashSet<>();
-
- doWithTypeCheckingExtensions(node, super::visitClass);
- node.getInnerClasses().forEachRemaining(this::visitClass);
-
- typeCheckingContext.alreadyVisitedMethods = oldSet;
- typeCheckingContext.popEnclosingClassNode();
- if (type != null) {
- typeCheckingContext.popErrorCollector();
+ boolean oldStatic = typeCheckingContext.isInStaticContext;
+ if (node instanceof InnerClassNode && ((InnerClassNode) node).isAnonymous()) {
+ typeCheckingContext.isInStaticContext = false; // AIC body is an instance context
+ }
+ try {
+ doWithTypeCheckingExtensions(node, super::visitClass);
+ node.getInnerClasses().forEachRemaining(this::visitClass);
+ } finally {
+ typeCheckingContext.isInStaticContext = oldStatic;
+ typeCheckingContext.alreadyVisitedMethods = oldSet;
+ typeCheckingContext.popEnclosingClassNode();
+ if (type != null) {
+ typeCheckingContext.popErrorCollector();
+ }
}
node.putNodeMetaData(INFERRED_TYPE, node);
@@ -1418,10 +1424,18 @@ out: if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
*/
protected void inferDiamondType(final ConstructorCallExpression cce, final ClassNode lType) {
ClassNode cceType = cce.getType(), inferredType = lType;
+ boolean isAIC = cce.isUsingAnonymousInnerClass();
+ if (isAIC) {
+ ClassNode diamond = GenericsUtils.diamondTargetOfAnonymousClass(cce.getType());
+ if (diamond != null) cceType = diamond;
+ }
// check if constructor call expression makes use of the diamond operator
if (cceType.getGenericsTypes() != null && cceType.getGenericsTypes().length == 0) {
ArgumentListExpression argumentList = InvocationWriter.makeArgumentList(cce.getArguments());
ConstructorNode constructor = cce.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
+ if (isAIC && constructor != null && constructor.getDeclaringClass() != cceType.redirect()) {
+ constructor = matchingConstructor(cceType.redirect(), argumentList.getExpressions().size());
+ }
if (!argumentList.getExpressions().isEmpty() && constructor != null) {
ClassNode type = GenericsUtils.parameterizeType(cceType, cceType);
type = inferReturnTypeGenerics(type, constructor, argumentList);
@@ -1448,10 +1462,18 @@ out: if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
inferredType = getCombinedBoundType(inferredType.getGenericsTypes()[0]);
}
adjustGenerics(inferredType, cceType);
- storeType(cce, cceType);
+ storeType(cce, isAIC ? cce.getType() : cceType);
}
}
+ private static ConstructorNode matchingConstructor(final ClassNode type, final int argumentCount) {
+ if (type == null || type.isInterface()) return null;
+ for (ConstructorNode ctor : type.getDeclaredConstructors()) {
+ if (ctor.getParameters().length == argumentCount) return ctor;
+ }
+ return null;
+ }
+
private void adjustGenerics(final ClassNode source, final ClassNode target) {
GenericsType[] genericsTypes = source.getGenericsTypes();
if (genericsTypes == null) { // Map foo = new HashMap<>()
@@ -1963,6 +1985,7 @@ out: if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
if (leftExpressionType.isUsingGenerics()
&& !missesGenericsTypes(rightExpressionType)
&& !(rightExpression instanceof ClosureExpression) // GROOVY-10277
+ && !(rightExpression instanceof ConstructorCallExpression && ((ConstructorCallExpression) rightExpression).isUsingAnonymousInnerClass()) // GROOVY-12319: AIC type is the generated class, not the diamond super type
&& !isNullConstant(rightExpression) && !UNKNOWN_PARAMETER_TYPE.equals(rightExpressionType)
&& !GenericsUtils.buildWildcardType(leftExpressionType).isCompatibleWith(wrapTypeIfNecessary(rightExpressionType)))
addStaticTypeError("Incompatible generic argument types. Cannot assign " + prettyPrintType(rightExpressionType) + " to: " + prettyPrintType(leftExpressionType), rightExpression);
@@ -3089,65 +3112,95 @@ out: if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
typeCheckingContext.getEnclosingClosure().addReturnType(returnType);
}
- /** {@inheritDoc} */
@Override
public void visitConstructorCallExpression(final ConstructorCallExpression call) {
- if (!extension.beforeMethodCall(call)) {
- ClassNode receiver;
- if (call.isThisCall()) {
- receiver = makeThis();
- } else if (call.isSuperCall()) {
- receiver = makeSuper();
- } else {
- receiver = call.getType();
- }
- Expression arguments = call.getArguments();
- ArgumentListExpression argumentList = InvocationWriter.makeArgumentList(arguments);
+ typeCheckingContext.pushEnclosingMethodCall(call);
+ try {
+ if (!extension.beforeMethodCall(call)) {
+ ClassNode receiver;
+ if (call.isThisCall()) {
+ receiver = makeThis();
+ } else if (call.isSuperCall()) {
+ receiver = makeSuper();
+ } else {
+ receiver = call.getType();
+ }
+ Expression arguments = call.getArguments();
+ ArgumentListExpression argumentList = InvocationWriter.makeArgumentList(arguments);
- // visit functional arguments *after* method has been chosen
- visitMethodCallArguments(receiver, argumentList, false, null);
- final ClassNode[] argumentTypes = getArgumentTypes(argumentList);
+ // visit functional arguments *after* method has been chosen
+ visitMethodCallArguments(receiver, argumentList, false, null);
+ final ClassNode[] argumentTypes = getArgumentTypes(argumentList);
- MethodNode ctor;
- if (looksLikeNamedArgConstructor(receiver, argumentTypes)
- && findMethod(receiver, "<init>", argumentTypes).isEmpty()
- && findMethod(receiver, "<init>", init(argumentTypes)).size() == 1) {
- ctor = typeCheckMapConstructor(call, receiver, arguments);
- } else {
- ctor = findMethodOrFail(call, receiver, "<init>", argumentTypes);
- visitMethodCallArguments(receiver, argumentList, true, ctor);
+ MethodNode ctor;
+ if (looksLikeNamedArgConstructor(receiver, argumentTypes)
+ && findMethod(receiver, "<init>", argumentTypes).isEmpty()
+ && findMethod(receiver, "<init>", init(argumentTypes)).size() == 1) {
+ ctor = typeCheckMapConstructor(call, receiver, arguments);
+ } else {
+ ctor = findMethodOrFail(call, receiver, "<init>", argumentTypes);
+ visitMethodCallArguments(receiver, argumentList, true, ctor);
+ if (ctor != null) {
+ Parameter[] parameters = ctor.getParameters();
+ GenericsType[] typeParameters = ctor.getDeclaringClass().getGenericsTypes();
+ if (typeParameters != null) { // GROOVY-10283, GROOVY-10316, GROOVY-10482, GROOVY-10624, GROOVY-10698
+ Map<GenericsTypeName, GenericsType> context = extractGenericsConnectionsFromArguments(typeParameters, parameters, argumentList, receiver.getGenericsTypes());
+ if (!context.isEmpty()) parameters = Arrays.stream(parameters).map(p -> new Parameter(applyGenericsContext(context, p.getType()), p.getName())).toArray(Parameter[]::new);
+ }
+ resolvePlaceholdersFromImplicitTypeHints(argumentTypes, argumentList, parameters);
+ typeCheckMethodsWithGenericsOrFail(receiver, argumentTypes, ctor, call);
+ checkForbiddenSpreadArgument(argumentList, parameters);
+ } else {
+ checkForbiddenSpreadArgument(argumentList);
+ }
+ }
if (ctor != null) {
- Parameter[] parameters = ctor.getParameters();
- GenericsType[] typeParameters = ctor.getDeclaringClass().getGenericsTypes();
- if (typeParameters != null) { // GROOVY-10283, GROOVY-10316, GROOVY-10482, GROOVY-10624, GROOVY-10698
- Map<GenericsTypeName, GenericsType> context = extractGenericsConnectionsFromArguments(typeParameters, parameters, argumentList, receiver.getGenericsTypes());
- if (!context.isEmpty()) parameters = Arrays.stream(parameters).map(p -> new Parameter(applyGenericsContext(context, p.getType()), p.getName())).toArray(Parameter[]::new);
+ storeTargetMethod(call, ctor);
+ if (call.isUsingGenerics()) {
+ GenericsType[] typeParameters = ctor.getGenericsTypes();
+ if (typeParameters != null && typeParameters.length != call.getGenericsTypes().length) {
+ addStaticTypeError("Incorrect number of constructor type arguments; required " + typeParameters.length, call);
+ }
}
- resolvePlaceholdersFromImplicitTypeHints(argumentTypes, argumentList, parameters);
- typeCheckMethodsWithGenericsOrFail(receiver, argumentTypes, ctor, call);
- checkForbiddenSpreadArgument(argumentList, parameters);
- } else {
- checkForbiddenSpreadArgument(argumentList);
}
}
- if (ctor != null) storeTargetMethod(call, ctor);
- }
-
- // GROOVY-9327: check for AIC in STC method with non-STC enclosing class
- if (call.isUsingAnonymousInnerClass()) {
- Set<MethodNode> methods = typeCheckingContext.methodsToBeVisited;
- if (!methods.isEmpty()) { // indicates specific methods have STC
- typeCheckingContext.methodsToBeVisited = Collections.emptySet();
+ // GROOVY-9327: check for AIC in STC method with non-STC enclosing class
+ if (call.isUsingAnonymousInnerClass()) {
ClassNode anonType = call.getType();
- visitClass(anonType); // visit anon. inner class inline with method
- anonType.putNodeMetaData(StaticTypeCheckingVisitor.class, Boolean.TRUE);
+ ClassNode diamondTarget = GenericsUtils.diamondTargetOfAnonymousClass(anonType);
+ if (diamondTarget != null && diamondTarget.getGenericsTypes() != null && diamondTarget.getGenericsTypes().length == 0) {
+ BinaryExpression enclosingBinaryExpression = typeCheckingContext.getEnclosingBinaryExpression();
+ ClassNode lType = null;
+ if (enclosingBinaryExpression != null && isAssignment(enclosingBinaryExpression.getOperation().getType())) {
+ lType = getType(enclosingBinaryExpression.getLeftExpression());
+ }
+ if (lType != null) { // assignment target type is the primary JLS 15.9.3 witness
+ inferDiamondType(call, lType);
+ }
+ if (diamondTarget.getGenericsTypes() != null && diamondTarget.getGenericsTypes().length == 0) {
+ // no target yet (or it was not enough): recover T from overrides so the AIC body is checked against a filled diamond
+ Map<String, ClassNode> spec = GenericsUtils.inferGenericsSpecFromOverrides(anonType, diamondTarget, Collections.emptyMap());
+ if (!spec.isEmpty()) {
+ GenericsUtils.applyGenericsSpec(diamondTarget, spec);
+ }
+ }
+ }
+ Set<MethodNode> methods = typeCheckingContext.methodsToBeVisited;
+ if (!methods.isEmpty()) { // indicates specific methods have STC
+ typeCheckingContext.methodsToBeVisited = Collections.emptySet();
+
+ visitClass(anonType); // visit anon. inner class inline with method
+ anonType.putNodeMetaData(StaticTypeCheckingVisitor.class, Boolean.TRUE);
- typeCheckingContext.methodsToBeVisited = methods;
+ typeCheckingContext.methodsToBeVisited = methods;
+ }
}
- }
- extension.afterMethodCall(call);
+ extension.afterMethodCall(call);
+ } finally {
+ typeCheckingContext.popEnclosingMethodCall();
+ }
}
private boolean looksLikeNamedArgConstructor(final ClassNode receiver, final ClassNode[] argumentTypes) {
@@ -3941,7 +3994,22 @@ out: if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
boolean typeParametersResolved = false;
// first check for explicit type arguments
if (isConstructor) {
- typeParametersResolved = hasTypeArguments;
+ Expression emc = typeCheckingContext.getEnclosingMethodCall();
+ if (emc instanceof ConstructorCallExpression cce) {
+ GenericsType[] typeArguments = cce.getGenericsTypes();
+ if (typeArguments != null) {
+ GenericsType[] methodTypeParameters = method.getGenericsTypes();
+ if (methodTypeParameters != null && methodTypeParameters.length == typeArguments.length) {
+ typeParametersResolved = true;
+ for (int i = 0; i < methodTypeParameters.length; i += 1) {
+ context.put(new GenericsTypeName(methodTypeParameters[i].getName()), typeArguments[i]);
+ }
+ }
+ }
+ }
+ if (!typeParametersResolved) {
+ typeParametersResolved = hasTypeArguments;
+ }
} else {
Expression emc = typeCheckingContext.getEnclosingMethodCall();
if (emc instanceof MethodCallExpression mce) {
@@ -4178,6 +4246,10 @@ out: if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
if (arguments == call.getArguments() || InvocationWriter.makeArgumentList(arguments).getExpressions().stream().anyMatch(arg ->
arg instanceof ClosureExpression && DefaultGroovyMethods.contains(InvocationWriter.makeArgumentList(call.getArguments()), arg)))
typeArguments = call.getGenericsTypes();
+ } else if (emc instanceof ConstructorCallExpression call) {
+ if (arguments == call.getArguments() || InvocationWriter.makeArgumentList(arguments).getExpressions().stream().anyMatch(arg ->
+ arg instanceof ClosureExpression && DefaultGroovyMethods.contains(InvocationWriter.makeArgumentList(call.getArguments()), arg)))
+ typeArguments = call.getGenericsTypes();
}
returnType = inferReturnTypeGenerics(receiver, methodNode, arguments, typeArguments);
@@ -4572,9 +4644,9 @@ out: if (mn.size() != 1) {
case "getThisObject":
returnType = makeThis();
}
- } else if (isUsingGenericsOrIsArrayUsingGenerics(returnType)) {
+ } else if (returnType.isGenericsPlaceHolder() || isUsingGenericsOrIsArrayUsingGenerics(returnType)) {
ClassNode irtg = inferReturnTypeGenerics(chosenReceiver.getType(), targetMethod, callArguments, call.getGenericsTypes());
- if (irtg != null && implementsInterfaceOrIsSubclassOf(irtg, returnType))
+ if (irtg != null && (returnType.isGenericsPlaceHolder() || implementsInterfaceOrIsSubclassOf(irtg, returnType)))
returnType = irtg;
}
// GROOVY-7106, GROOVY-7274, GROOVY-8909, GROOVY-8961, GROOVY-9734, GROOVY-9844, GROOVY-9915, et al.
@@ -6295,6 +6367,13 @@ trying: for (ClassNode[] signature : signatures) {
protected ClassNode getType(final ASTNode node) {
ClassNode type = node.getNodeMetaData(INFERRED_TYPE);
if (type != null) {
+ if (node instanceof VariableExpression vexp && vexp.getOriginType() != null && vexp.getOriginType().getOuterClassType() != null) {
+ if (type.getOuterClassType() == null) {
+ type = type.getPlainNodeReference();
+ type.setGenericsTypes(vexp.getOriginType().getGenericsTypes());
+ type.setOuterClassType(vexp.getOriginType().getOuterClassType());
+ }
+ }
return type;
}
@@ -6319,7 +6398,15 @@ trying: for (ClassNode[] signature : signatures) {
return fieldType;
}
if (variable != vexp && variable instanceof VariableExpression) {
- return getType((VariableExpression) variable);
+ type = getType((VariableExpression) variable);
+ ClassNode originType = ((VariableExpression) variable).getOriginType();
+ if (originType != null && originType.getOuterClassType() != null && type != null
+ && type.getOuterClassType() == null) {
+ type = type.getPlainNodeReference();
+ type.setGenericsTypes(originType.getGenericsTypes());
+ type.setOuterClassType(originType.getOuterClassType());
+ }
+ return type;
}
if (variable instanceof Parameter parameter) {
if (getTemporaryTypesForExpression(vexp).isEmpty()) { // not instanceof
@@ -7054,6 +7141,17 @@ out: for (ClassNode type : todo) {
}
}
result = extractPlaceHolders(receiver, declaring);
+ ClassNode oc = receiver.getOuterClassType();
+ if (oc == null && receiver.redirect() != null) {
+ oc = receiver.redirect().getOuterClassType();
+ }
+ if (oc == null && argument instanceof VariableExpression vexp && vexp.getOriginType() != null) {
+ oc = vexp.getOriginType().getOuterClassType();
+ }
+ if (oc != null) {
+ Map<GenericsTypeName, GenericsType> outerPlaceHolders = extractPlaceHolders(oc, oc.redirect());
+ outerPlaceHolders.forEach(result::putIfAbsent); // inner type parameters win over outer
+ }
if (!result.isEmpty()) Optional.ofNullable(method.getGenericsTypes()).ifPresent(methodGenerics ->
Arrays.stream(methodGenerics).map(gt -> new GenericsTypeName(gt.getName())).forEach(result::remove)); // GROOVY-10322
}
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingContext.java b/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingContext.java
index 94e461f500..0b798ca866 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingContext.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingContext.java
@@ -23,6 +23,7 @@ import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
@@ -383,13 +384,13 @@ public class TypeCheckingContext {
/**
* Pushes a method call into the method call stack.
*
- * @param call the call expression to be pushed, either a {@link MethodCallExpression} or a {@link StaticMethodCallExpression}
+ * @param call the call expression to be pushed, either a {@link MethodCallExpression}, {@link StaticMethodCallExpression}, or {@link ConstructorCallExpression}
*/
public void pushEnclosingMethodCall(final Expression call) {
- if (call instanceof MethodCallExpression || call instanceof StaticMethodCallExpression) {
+ if (call instanceof MethodCallExpression || call instanceof StaticMethodCallExpression || call instanceof ConstructorCallExpression) {
enclosingMethodCalls.addFirst(call);
} else {
- throw new IllegalArgumentException("Expression must be a method call or a static method call");
+ throw new IllegalArgumentException("Expression must be a method call, a static method call or a constructor call");
}
}
diff --git a/src/spec/doc/core-operators.adoc b/src/spec/doc/core-operators.adoc
index 207d1a5e06..944a164325 100644
--- a/src/spec/doc/core-operators.adoc
+++ b/src/spec/doc/core-operators.adoc
@@ -909,6 +909,13 @@ same name in Java 7. It is used to indicate that generic types should be inferre
include::../test/OperatorsTest.groovy[tags=diamond_op,indent=0]
----
+As of GROOVY-12319, diamond may also be used when creating an anonymous inner class, matching Java 9 (JEP 213):
+
+[source,groovy]
+----
+include::../test/OperatorsTest.groovy[tags=diamond_op_aic,indent=0]
+----
+
In dynamic Groovy, this is totally unused. In statically type checked Groovy, it is also optional since the Groovy
type checker performs type inference whether this operator is present or not.
diff --git a/src/spec/test/OperatorsTest.groovy b/src/spec/test/OperatorsTest.groovy
index e8ed79159e..92d5e2f4d9 100644
--- a/src/spec/test/OperatorsTest.groovy
+++ b/src/spec/test/OperatorsTest.groovy
@@ -743,6 +743,21 @@ assert function(*args,5,6) == 26
// end::diamond_op[]
}
+ @Test
+ void testDiamondOperatorOnAnonymousInnerClass() {
+ assertScript '''
+ // tag::diamond_op_aic[]
+ interface Processor<T> {
+ T process(T val)
+ }
+ Processor<String> p = new Processor<>() {
+ String process(String val) { val.toUpperCase() }
+ }
+ assert p.process('ok') == 'OK'
+ // end::diamond_op_aic[]
+ '''
+ }
+
@Test
void testCallOperator() {
assertScript '''
diff --git a/src/test-resources/core/BreakingChange_01x.groovy b/src/test-resources/core/BreakingChange_01x.groovy
index 33bb86d0b6..c23a070075 100644
--- a/src/test-resources/core/BreakingChange_01x.groovy
+++ b/src/test-resources/core/BreakingChange_01x.groovy
@@ -28,7 +28,7 @@ assert b.x == 1
a<String> b2 = new a<String>()
assert b2.x == 1
-a<String>[] b3 = new a<String>[0]
+a<String>[] b3 = new a[0]
assert b3.length == 0
core.a b4 = new a()
@@ -37,6 +37,6 @@ assert b4.x == 1
core.a<String> b5 = new a<String>()
assert b5.x == 1
-core.a<String>[] b6 = new a<String>[0]
+core.a<String>[] b6 = new a[0]
assert b6.length == 0
diff --git a/src/test-resources/core/BreakingChange_04x.groovy b/src/test-resources/core/BreakingChange_04x.groovy
index 477932b17c..0773a3914f 100644
--- a/src/test-resources/core/BreakingChange_04x.groovy
+++ b/src/test-resources/core/BreakingChange_04x.groovy
@@ -28,7 +28,7 @@ b2 = new B<String>()
assert b2 instanceof B
B<String>[] b3
-b3 = new B<String>[0]
+b3 = new B[0]
assert b3 instanceof B[]
core.B b4
@@ -40,5 +40,5 @@ b5 = new B<String>()
assert b5 instanceof B
core.B<String>[] b6
-b6 = new B<String>[0]
+b6 = new B[0]
assert b6 instanceof B[]
\ No newline at end of file
diff --git a/src/test-resources/core/Expression_19.groovy b/src/test-resources/core/Expression_19.groovy
index 3f893a614b..dd1bff2586 100644
--- a/src/test-resources/core/Expression_19.groovy
+++ b/src/test-resources/core/Expression_19.groovy
@@ -28,14 +28,14 @@ int[][] b = new int[length()][2 * 8];
ArrayList[] c = new ArrayList[10];
ArrayList[][] cc = new ArrayList[10][size()];
java.util.ArrayList[] d = new java.util.ArrayList[10];
-ArrayList[] e = new ArrayList<String>[10];
-java.util.ArrayList[] f = new java.util.ArrayList<String>[10];
-java.util.ArrayList[] g = new java.util.ArrayList<String>[size()];
+ArrayList[] e = new ArrayList<?>[10];
+java.util.ArrayList[] f = new java.util.ArrayList<?>[10];
+java.util.ArrayList[] g = new java.util.ArrayList<?>[size()];
int[][] h = new int[10][];
int[][][] i = new int[10][][];
ArrayList[][] j = new ArrayList[10][];
-ArrayList[][] k = new ArrayList<String>[10][];
+ArrayList[][] k = new ArrayList<?>[10][];
def bb = new A.B();
def bb2 = new A.B[0];
diff --git a/src/test/groovy/bugs/Groovy12319.groovy b/src/test/groovy/bugs/Groovy12319.groovy
new file mode 100644
index 0000000000..2af644c6da
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy12319.groovy
@@ -0,0 +1,1227 @@
+/*
+ * 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 bugs
+
+import org.codehaus.groovy.ast.ClassHelper
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.FieldNode
+import org.codehaus.groovy.ast.GenericsType
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
+import org.codehaus.groovy.ast.expr.ConstantExpression
+import org.codehaus.groovy.ast.expr.ConstructorCallExpression
+import org.codehaus.groovy.control.CompilationFailedException
+import org.codehaus.groovy.control.GenericsVisitor
+import org.codehaus.groovy.control.SourceUnit
+import org.junit.jupiter.api.Test
+import org.objectweb.asm.Opcodes
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+final class Groovy12319 {
+
+ // =========================================================================
+ // 1. Diamond <> on anonymous inner classes
+ // =========================================================================
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_Basic() {
+ assertScript '''
+ interface Processor<T> {
+ T process(T val)
+ }
+
+ Processor<String> p = new Processor<>() {
+ @Override
+ String process(String val) {
+ return val.toUpperCase()
+ }
+ }
+
+ assert p.process("hello") == "HELLO"
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_ExtendingClass() {
+ assertScript '''
+ abstract class BaseHolder<T> {
+ T value
+ BaseHolder(T value) {
+ this.value = value
+ }
+ abstract T transform()
+ }
+
+ BaseHolder<Integer> holder = new BaseHolder<>(42) {
+ @Override
+ Integer transform() {
+ return value * 2
+ }
+ }
+
+ assert holder.transform() == 84
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_MultipleTypeParams() {
+ assertScript '''
+ interface BiTransformer<T, U, R> {
+ R transform(T t, U u)
+ }
+
+ BiTransformer<String, Integer, String> transformer = new BiTransformer<>() {
+ @Override
+ String transform(String s, Integer i) {
+ return s + ":" + i
+ }
+ }
+
+ assert transformer.transform("score", 100) == "score:100"
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_CompileStatic() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+
+ @CompileStatic
+ class Tester {
+ interface Handler<T> {
+ T handle(T input)
+ }
+
+ static String run() {
+ Handler<String> h = new Handler<>() {
+ @Override
+ String handle(String input) {
+ return "handled " + input
+ }
+ }
+ return h.handle("data")
+ }
+ }
+
+ assert Tester.run() == "handled data"
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_TypeChecked() {
+ assertScript '''
+ import groovy.transform.TypeChecked
+
+ @TypeChecked
+ class Tester {
+ static class Box<T> {
+ T item
+ Box(T item) { this.item = item }
+ T get() { return item }
+ }
+
+ static Integer run() {
+ Box<Integer> b = new Box<>(123) {
+ @Override
+ Integer get() {
+ return super.get() + 1
+ }
+ }
+ return b.get()
+ }
+ }
+
+ assert Tester.run() == 124
+ '''
+ }
+
+ // =========================================================================
+ // 2. Qualified parameterized inner types ("rare" types)
+ // =========================================================================
+
+ @Test
+ void testQualifiedParameterizedInnerType_VariableDeclaration() {
+ assertScript '''
+ class Outer<T> {
+ class Inner<U> {
+ T t
+ U u
+ Inner(T t, U u) {
+ this.t = t
+ this.u = u
+ }
+ String display() {
+ return "" + t + "-" + u
+ }
+ }
+ }
+
+ Outer<String>.Inner<Integer> x = new Outer<String>().new Inner<Integer>("val", 99)
+ assert x.display() == "val-99"
+ '''
+ }
+
+ @Test
+ void testQualifiedParameterizedInnerType_FieldsAndMethods() {
+ assertScript '''
+ class Outer<T> {
+ class Inner<U> {
+ T t
+ U u
+ Inner(T t, U u) { this.t = t; this.u = u }
+ }
+ }
+
+ class Service {
+ Outer<String>.Inner<Integer> field
+
+ Outer<String>.Inner<Integer> echo(Outer<String>.Inner<Integer> param) {
+ return param
+ }
+ }
+
+ Outer<String> outer = new Outer<>()
+ Outer<String>.Inner<Integer> inner = outer.new Outer.Inner<Integer>("abc", 123)
+ Service s = new Service(field: inner)
+ assert s.echo(s.field).t == "abc"
+ assert s.echo(s.field).u == 123
+ '''
+ }
+
+ @Test
+ void testQualifiedParameterizedInnerType_NestedGenerics() {
+ assertScript '''
+ class Outer<T> {
+ class Inner<U> {
+ T t; U u
+ Inner(T t, U u) { this.t = t; this.u = u }
+ }
+ }
+
+ List<Outer<String>.Inner<Integer>> list = new ArrayList<>()
+ Outer<String> o = new Outer<>()
+ list.add(o.new Outer.Inner<Integer>("item", 1))
+ assert list.size() == 1
+ assert list[0].t == "item"
+ assert list[0].u == 1
+ '''
+ }
+
+ @Test
+ void testQualifiedParameterizedInnerType_CompileStatic() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+
+ class Outer<T> {
+ class Inner<U> {
+ T t
+ U u
+ Inner(T t, U u) {
+ this.t = t
+ this.u = u
+ }
+ T getT() { return t }
+ U getU() { return u }
+ }
+ }
+
+ @CompileStatic
+ class Tester {
+ static String test() {
+ Outer<String> outer = new Outer<String>()
+ Outer<String>.Inner<Integer> inner = outer.new Outer.Inner<Integer>("hello", 42)
+ return inner.getT() + ":" + inner.getU()
+ }
+ }
+
+ assert Tester.test() == "hello:42"
+ '''
+ }
+
+ @Test
+ void testQualifiedParameterizedInnerType_RawInner() {
+ assertScript '''
+ class Outer<T> {
+ class Inner {
+ T t
+ Inner(T t) { this.t = t }
+ T get() { return t }
+ }
+ }
+
+ Outer<String>.Inner x = new Outer<String>().new Inner("test")
+ assert x.get() == "test"
+ '''
+ }
+
+ // =========================================================================
+ // 3. Explicit type arguments on constructors, this() and super()
+ // =========================================================================
+
+ @Test
+ void testConstructorTypeArguments_Basic() {
+ assertScript '''
+ class Box {
+ String value
+ <T> Box(T t) {
+ this.value = t.toString()
+ }
+ }
+
+ Box b = new <String>Box("hello")
+ assert b.value == "hello"
+
+ Box b2 = new <Integer>Box(12345)
+ assert b2.value == "12345"
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArguments_SameCompilationUnitTypeResolved() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+ class Token {
+ String id
+ Token(String id) { this.id = id }
+ }
+ class Box {
+ def t
+ <T> Box(T t) { this.t = t }
+ }
+ @CompileStatic
+ class Tester {
+ static String run() {
+ ((Token) new <Token>Box(new Token('ok')).t).id
+ }
+ }
+ assert Tester.run() == 'ok'
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArguments_GenericClass() {
+ assertScript '''
+ class Pair<K> {
+ K key
+ String desc
+ <V> Pair(K key, V val) {
+ this.key = key
+ this.desc = val.toString()
+ }
+ }
+
+ Pair<String> p = new <Integer>Pair<String>("k1", 999)
+ assert p.key == "k1"
+ assert p.desc == "999"
+ '''
+
+ shouldFail CompilationFailedException, '''
+ class Pair<K> {
+ <V> Pair(K key, V val) {}
+ }
+ new <Integer>Pair<>("k2", 888)
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArguments_InnerClass() {
+ assertScript '''
+ class Outer {
+ class Inner {
+ String result
+ <T> Inner(T t) {
+ this.result = "inner:" + t
+ }
+ }
+ }
+
+ Outer outer = new Outer()
+ Outer.Inner inner = outer.new <String>Outer.Inner("custom")
+ assert inner.result == "inner:custom"
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArguments_ThisCall() {
+ assertScript '''
+ class Target {
+ String text
+ <T> Target(T t) {
+ this.text = "generic:" + t
+ }
+ Target() {
+ <String>this("defaultVal")
+ }
+ }
+
+ Target t = new Target()
+ assert t.text == "generic:defaultVal"
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArguments_SuperCall() {
+ assertScript '''
+ class Base {
+ String message
+ <T> Base(T t) {
+ this.message = "base:" + t
+ }
+ }
+
+ class Derived extends Base {
+ Derived() {
+ <String>super("fromDerived")
+ }
+ }
+
+ Derived d = new Derived()
+ assert d.message == "base:fromDerived"
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArguments_CompileStatic() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+
+ class Holder {
+ String str
+ <T> Holder(T t) {
+ this.str = String.valueOf(t)
+ }
+ }
+
+ @CompileStatic
+ class Tester {
+ static String run() {
+ Holder h = new <String>Holder("static compiled")
+ return h.str
+ }
+ }
+
+ assert Tester.run() == "static compiled"
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_ExtendingClass_CompileStatic() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+
+ abstract class BaseHolder<T> {
+ T value
+ BaseHolder(T value) {
+ this.value = value
+ }
+ abstract T transform()
+ }
+
+ @CompileStatic
+ class Tester {
+ static Integer run() {
+ BaseHolder<Integer> holder = new BaseHolder<>(42) {
+ @Override
+ Integer transform() {
+ return value * 2
+ }
+ }
+ return holder.transform()
+ }
+ }
+
+ assert Tester.run() == 84
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_DefVariable() {
+ assertScript '''
+ interface Formatter<T> {
+ String format(T t)
+ }
+
+ def f = new Formatter<String>() {
+ @Override
+ String format(String s) {
+ return "fmt:" + s
+ }
+ }
+ assert f.format("hello") == "fmt:hello"
+ '''
+ }
+
+ @Test
+ void testQualifiedParameterizedInnerType_Chained() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+
+ class Level1<A> {
+ A a
+ Level1(A a) { this.a = a }
+
+ class Level2<B> {
+ B b
+ Level2(B b) { this.b = b }
+ String summarize() {
+ return "" + a + ":" + b
+ }
+ }
+ }
+
+ @CompileStatic
+ class Tester {
+ static String run() {
+ Level1<String> l1 = new Level1<String>("hello")
+ Level1<String>.Level2<Integer> l2 = l1.new Level1.Level2<Integer>(42)
+ return l2.summarize()
+ }
+ }
+
+ assert Tester.run() == "hello:42"
+ '''
+ }
+
+ @Test
+ void testQualifiedParameterizedInnerType_NestedInTypeArgument() {
+ assertScript '''
+ class Outer<T> {
+ class Inner<U> {
+ T t
+ U u
+ Inner(T t, U u) {
+ this.t = t
+ this.u = u
+ }
+ }
+ }
+
+ List<Outer<String>.Inner<Integer>> list = new ArrayList<Outer<String>.Inner<Integer>>()
+ Outer<String> o = new Outer<String>()
+ list.add(o.new Outer.Inner<Integer>("hello", 123))
+ assert list.get(0).t == "hello"
+ assert list.get(0).u == 123
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArguments_MultipleTypes() {
+ assertScript '''
+ class MultiHolder {
+ String desc
+ <K, V> MultiHolder(K k, V v) {
+ this.desc = k + " -> " + v
+ }
+ }
+
+ MultiHolder m = new <String, Integer>MultiHolder("key", 100)
+ assert m.desc == "key -> 100"
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArguments_CompileStatic_ThisAndSuper() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+
+ class BaseClass {
+ String value
+ <T> BaseClass(T t) {
+ this.value = "base:" + t
+ }
+ }
+
+ @CompileStatic
+ class ChildClass extends BaseClass {
+ ChildClass(int n) {
+ <Integer>super(n)
+ }
+ ChildClass() {
+ <String>this("default-child")
+ }
+ <T> ChildClass(T t) {
+ <T>super(t)
+ }
+ }
+
+ assert new ChildClass(123).value == "base:123"
+ assert new ChildClass().value == "base:default-child"
+ assert new ChildClass("explicit").value == "base:explicit"
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_MethodArgument() {
+ assertScript '''
+ interface Processor<T> {
+ T process(T val)
+ }
+
+ static String apply(Processor<String> p) {
+ p.process('ok')
+ }
+
+ assert apply(new Processor<>() {
+ @Override
+ String process(String val) { val.toUpperCase() }
+ }) == 'OK'
+ '''
+ }
+
+ @Test
+ void testQualifiedParameterizedInnerType_ThreeLevels() {
+ assertScript '''
+ class A<T> {
+ T a
+ A(T a) { this.a = a }
+ class B<U> {
+ U b
+ B(U b) { this.b = b }
+ class C<V> {
+ V c
+ C(V c) { this.c = c }
+ String all() { '' + a + ':' + b + ':' + c }
+ }
+ }
+ }
+
+ A<String> a = new A<String>('x')
+ A<String>.B<Integer> b = a.new A.B<Integer>(1)
+ A<String>.B<Integer>.C<Long> c = b.new A.B.C<Long>(2L)
+ assert c.all() == 'x:1:2'
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArguments_NestedThisCall() {
+ assertScript '''
+ class Box {
+ String value
+ <T> Box(T t) { this.value = String.valueOf(t) }
+ Box(int ignored) { <Double>this(1.5d) }
+ }
+
+ assert new Box(0).value == '1.5'
+ '''
+ }
+
+ @Test
+ void testInvalidDiamondUsageStillFails() {
+ shouldFail CompilationFailedException, '''
+ class MyList extends ArrayList<> {}
+ '''
+
+ shouldFail CompilationFailedException, '''
+ class Test {
+ List<> list
+ }
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArgumentsAndDiamondRejected() {
+ shouldFail CompilationFailedException, '''
+ class Box {
+ <T> Box(T t) {}
+ }
+ new <String>Box<>("x")
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArgumentsAndDiamondRejectedOnInterfaceAIC() {
+ shouldFail CompilationFailedException, '''
+ interface Processor<T> {
+ T process(T val)
+ }
+ new <String>Processor<>() {
+ String process(String val) { val }
+ }
+ '''
+ }
+
+ @Test
+ void testDiamondOnNonGenericAnonymousClassRejected() {
+ shouldFail CompilationFailedException, '''
+ new Object<>() {}
+ '''
+ }
+
+ @Test
+ void testRareTypeArityMismatchRejected() {
+ shouldFail CompilationFailedException, '''
+ class Outer<T> {
+ class Inner<U> {}
+ }
+ Outer<String, Integer>.Inner<Integer> x
+ '''
+ }
+
+ @Test
+ void testConstructorTypeArgumentsArityMismatchCompileStatic() {
+ shouldFail CompilationFailedException, '''
+ import groovy.transform.CompileStatic
+ class Box {
+ <T> Box(T t) {}
+ }
+ @CompileStatic
+ void m() {
+ new <String, Integer>Box("x")
+ }
+ m()
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_MethodArgument_CompileStatic() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+
+ interface Processor<T> {
+ T process(T val)
+ }
+
+ @CompileStatic
+ class Tester {
+ static String apply(Processor<String> p) {
+ p.process('ok')
+ }
+ static String run() {
+ apply(new Processor<>() {
+ @Override
+ String process(String val) { val.toUpperCase() }
+ })
+ }
+ }
+
+ assert Tester.run() == 'OK'
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_MethodArgument_NestedGenerics() {
+ assertScript '''
+ interface Box<T> {
+ List<T> items()
+ }
+
+ static List<String> take(Box<String> b) {
+ b.items()
+ }
+
+ assert take(new Box<>() {
+ @Override
+ List<String> items() { ['a'] }
+ }) == ['a']
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_MethodArgument_NestedGenerics_CompileStatic() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+
+ interface Box<T> {
+ List<T> items()
+ }
+
+ @CompileStatic
+ class Tester {
+ static List<String> take(Box<String> b) {
+ b.items()
+ }
+ static List<String> run() {
+ take(new Box<>() {
+ @Override
+ List<String> items() { ['a'] }
+ })
+ }
+ }
+
+ assert Tester.run() == ['a']
+ '''
+ }
+
+ @Test
+ void testQualifiedParameterizedInnerType_UnparameterizedMiddle() {
+ assertScript '''
+ class A<T> {
+ T a
+ A(T a) { this.a = a }
+ class B {
+ class C<U> {
+ U c
+ C(U c) { this.c = c }
+ String all() { '' + a + ':' + c }
+ }
+ }
+ }
+
+ A<String> a = new A<String>('x')
+ A<String>.B b = a.new A.B()
+ A<String>.B.C<Integer> c = b.new A.B.C<Integer>(7)
+ assert c.all() == 'x:7'
+ '''
+ }
+
+ @Test
+ void testRareTypeInstanceCreationInsideOuter() {
+ assertScript '''
+ class Outer<T> {
+ class Inner<U> {
+ U u
+ Inner(U u) { this.u = u }
+ }
+ Inner<Integer> make() {
+ return new Outer<T>.Inner<Integer>(7)
+ }
+ }
+
+ assert new Outer<String>().make().u == 7
+ '''
+ }
+
+ @Test
+ void testDiamondOnAnonymousInnerClass_ArrayTypeParameter() {
+ assertScript '''
+ interface ArrProc<T> {
+ T[] process(T[] val)
+ }
+ ArrProc<String> p = new ArrProc<>() {
+ @Override
+ String[] process(String[] val) { val }
+ }
+ assert p.process(['a', 'b'] as String[]).join('-') == 'a-b'
+ '''
+ }
+
+ @Test
+ void testConstructorCallExpressionTypeArgumentsText() {
+ def stringType = new GenericsType(ClassHelper.STRING_TYPE)
+ def cce = new ConstructorCallExpression(ClassHelper.make('Box'), new ArgumentListExpression(new ConstantExpression('x')))
+ assert !cce.isUsingGenerics()
+ cce.setGenericsTypes([stringType] as GenericsType[])
+ assert cce.isUsingGenerics()
+ assert cce.text.startsWith('new <java.lang.String>Box')
+
+ def thisCall = new ConstructorCallExpression(ClassNode.THIS, new ArgumentListExpression())
+ thisCall.setGenericsTypes([stringType] as GenericsType[])
+ assert thisCall.text.contains('<java.lang.String>this')
+
+ def superCall = new ConstructorCallExpression(ClassNode.SUPER, new ArgumentListExpression())
+ superCall.setGenericsTypes([stringType] as GenericsType[])
+ assert superCall.text.contains('<java.lang.String>super')
+
+ def transformed = cce.transformExpression { it }
+ assert transformed instanceof ConstructorCallExpression
+ assert transformed.isUsingGenerics()
+ assert transformed.genericsTypes.length == 1
+
+ def emptyArgs = new ConstructorCallExpression(ClassHelper.make('Box'), new ArgumentListExpression())
+ emptyArgs.setGenericsTypes(GenericsType.EMPTY_ARRAY)
+ assert !emptyArgs.isUsingGenerics()
+ assert emptyArgs.text.startsWith('new Box')
+ }
+
+ @Test
+ void testStaticFieldViaParameterizedTypeRejected() {
+ def err = shouldFail CompilationFailedException, '''
+ class Cell<T> {
+ static final String ID = 'cell'
+ }
+ Cell<String>.ID
+ '''
+ assert err.message.contains('parameterization')
+ }
+
+ @Test
+ void testMethodTypeParameterAdditionalBoundMustBeInterface() {
+ def err = shouldFail CompilationFailedException, '''
+ class C {
+ <T extends Number & String> void m() {}
+ }
+ '''
+ assert err.message.contains('interfaces')
+ }
+
+ @Test
+ void testWildcardOnEnclosingConstructorTypeRejected() {
+ def err = shouldFail CompilationFailedException, '''
+ class Outer<T> {
+ static class Inner<U> {}
+ }
+ new Outer<?>.Inner<Integer>()
+ '''
+ assert err.message.contains('wildcard type')
+ }
+
+ @Test
+ void testGenericArrayCreationOfRareTypeRejected() {
+ def err = shouldFail CompilationFailedException, '''
+ class Outer<T> {
+ class Inner {}
+ }
+ new Outer<String>.Inner[1]
+ '''
+ assert err.message.contains('generic array creation')
+ }
+
+ @Test
+ void testReifiableArrayOfUnboundedWildcardEnclosingType() {
+ // javac accepts this: Inner is a non-static member type of the reifiable
+ // enclosing type Outer<?> (JLS 4.7 / 15.10.1). The static nested
+ // counterpart Outer<?>.Nested is rejected — see the next test.
+ assertScript '''
+ class Outer<T> {
+ class Inner {}
+ }
+ def arr = new Outer<?>.Inner[0]
+ assert arr.length == 0
+ '''
+ }
+
+ @Test
+ void testReifiableArrayOfUnboundedWildcardGenericInner() {
+ // javac also accepts the generic-member form when every type argument
+ // is an unbounded wildcard (JLS 4.7).
+ assertScript '''
+ class Outer<T> {
+ class InnerG<U> {}
+ }
+ def arr = new Outer<?>.InnerG<?>[0]
+ assert arr.length == 0
+ '''
+ }
+
+ @Test
+ void testStaticNestedTypeViaParameterizedEnclosingRejected() {
+ def err = shouldFail CompilationFailedException, '''
+ class Outer<T> {
+ static class Nested {}
+ }
+ new Outer<?>.Nested[0]
+ '''
+ assert err.message.contains('parameterization')
+
+ err = shouldFail CompilationFailedException, '''
+ class Outer<T> {
+ static class Nested {}
+ }
+ Outer<?>.Nested x
+ '''
+ assert err.message.contains('parameterization')
+
+ // Nested interfaces are implicitly static (JLS 9.5); Map.Entry is the
+ // JDK example of the same rule.
+ err = shouldFail CompilationFailedException, '''
+ new java.util.Map<?,?>.Entry[0]
+ '''
+ assert err.message.contains('parameterization')
+ }
+
+ @Test
+ void testRawGenericMemberOfParameterizedEnclosingRejected() {
+ def err = shouldFail CompilationFailedException, '''
+ class Outer<T> {
+ class InnerG<U> {}
+ }
+ new Outer<?>.InnerG[0]
+ '''
+ assert err.message.contains('parameterized enclosing type')
+ }
+
+ @Test
+ void testInstanceofParameterizedRareTypeRejected() {
+ def err = shouldFail CompilationFailedException, '''
+ class Outer<T> {
+ class Inner {}
+ }
+ def x = null
+ x instanceof Outer<String>.Inner
+ '''
+ assert err.message.contains('parameterized type')
+
+ err = shouldFail CompilationFailedException, '''
+ def x = ([] instanceof java.util.ArrayList<String>)
+ '''
+ assert err.message.contains('parameterized type')
+ }
+
+ @Test
+ void testConstructorTypeArgumentsWithFunctionalArgumentCompileStatic() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+ import java.util.function.Supplier
+
+ class Box {
+ String s
+ <T> Box(Supplier<String> f) {
+ this.s = f.get()
+ }
+ }
+
+ @CompileStatic
+ class Tester {
+ static String run() {
+ new <Integer>Box({ -> 'ok' }).s
+ }
+ }
+
+ assert Tester.run() == 'ok'
+ '''
+ }
+
+ @Test
+ void testCompileStaticRareTypeVariableAndMethodCall() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+ class Outer<T> {
+ class Inner<U> {
+ U u
+ Inner(U u) { this.u = u }
+ U get() { u }
+ }
+ }
+ @CompileStatic
+ class Tester {
+ static Integer run() {
+ Outer<String>.Inner<Integer> inner = new Outer<String>().new Inner<Integer>(9)
+ def copy = inner
+ copy.get()
+ }
+ }
+ assert Tester.run() == 9
+ '''
+ }
+
+ @Test
+ void testImplementsStaticNestedViaParameterizedEnclosingRejected() {
+ // Nested interfaces are implicitly static, so Outer<String>.Inner is
+ // the same JLS 6.5.5 error as Outer<?>.Nested. javac 25 rejects this.
+ def err = shouldFail CompilationFailedException, '''
+ class Outer<T> {
+ interface Inner<U> {
+ U id(U u)
+ }
+ }
+ class Impl implements Outer<String>.Inner<Integer> {
+ Integer id(Integer u) { u }
+ }
+ '''
+ assert err.message.contains('parameterization')
+ }
+
+ @Test
+ void testImplementsNestedInterfaceOfRawEnclosing() {
+ // The legal counterpart: select the nested interface from the raw
+ // enclosing type name (JLS 4.5.2 / 6.5.5).
+ assertScript '''
+ class Outer<T> {
+ interface Inner<U> {
+ U id(U u)
+ }
+ }
+ class Impl implements Outer.Inner<Integer> {
+ Integer id(Integer u) { u }
+ }
+ assert new Impl().id(3) == 3
+ '''
+ }
+
+ @Test
+ void testInvalidDiamondOnFieldMentionsDiamond() {
+ def fieldType = ClassHelper.LIST_TYPE.getPlainNodeReference()
+ fieldType.genericsTypes = GenericsType.EMPTY_ARRAY
+ def cn = new ClassNode('C', Opcodes.ACC_PUBLIC, ClassHelper.OBJECT_TYPE)
+ cn.addField(new FieldNode('f', Opcodes.ACC_PUBLIC, fieldType, cn, null))
+ def source = SourceUnit.create('C.groovy', '')
+ new GenericsVisitor(source).visitClass(cn)
+ assert source.errorCollector.errorCount > 0
+ assert source.errorCollector.errors.any { it.cause.message.contains('invalid Diamond') }
+ }
+
+ @Test
+ void testMultiDimensionalArrayCreationIsReifiable() {
+ assertScript '''
+ def a = new String[0][0]
+ assert a.length == 0
+ def b = new String[][]{ new String[]{'a'} }
+ assert b[0][0] == 'a'
+ '''
+ }
+
+ @Test
+ void testPrimitiveArrayCreatedName() {
+ assertScript '''
+ def a = new int[2]
+ assert a.length == 2
+ '''
+ }
+
+ @Test
+ void testMethodLevelTypeCheckedAnonymousClass() {
+ assertScript '''
+ interface Processor<T> {
+ T process(T val)
+ }
+ class Host {
+ @groovy.transform.TypeChecked
+ String run() {
+ Processor<String> p = new Processor<>() {
+ @Override
+ String process(String val) { val.toUpperCase() }
+ }
+ p.process('ok')
+ }
+ }
+ assert new Host().run() == 'OK'
+ '''
+ }
+
+ @Test
+ void testNamedArgConstructorCompileStatic() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+ class Person {
+ String name
+ }
+ @CompileStatic
+ class Tester {
+ static String run() {
+ Person p = new Person(name: 'Ada')
+ p.name
+ }
+ }
+ assert Tester.run() == 'Ada'
+ '''
+ }
+
+ @Test
+ void testSpreadConstructorArgumentCompileStatic() {
+ def err = shouldFail CompilationFailedException, '''
+ import groovy.transform.CompileStatic
+ @CompileStatic
+ void m(List<String> args) {
+ new ArrayList<String>(*args)
+ }
+ '''
+ assert err.message != null
+ }
+
+ @Test
+ void testGenericClassConstructorWithSamAndConstructorTypeArguments() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+ import java.util.function.Supplier
+
+ class Box<A> {
+ String s
+ <T> Box(Supplier<String> f) {
+ this.s = f.get()
+ }
+ }
+
+ @CompileStatic
+ class Tester {
+ static String run() {
+ def a = new <Integer>Box<String>({ -> 'ok' }).s
+ def b = new Box<String>({ -> 'ok' }).s
+ a + b
+ }
+ }
+ assert Tester.run() == 'okok'
+ '''
+ }
+
+ @Test
+ void testDiamondAicConstructorArityMismatchFallsBack() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+ abstract class Base<T> {
+ T value
+ Base(int a, int b) { this.value = null }
+ abstract T get()
+ }
+ @CompileStatic
+ class Tester {
+ static String run() {
+ Base<String> b = new Base<String>(1, 2) {
+ @Override String get() { 'x' }
+ }
+ b.get()
+ }
+ }
+ assert Tester.run() == 'x'
+ '''
+ }
+
+ @Test
+ void testDiamondInterfaceAicInsideInstanceMethod() {
+ assertScript '''
+ import groovy.transform.CompileStatic
+ @CompileStatic
+ class Outer {
+ interface I<T> { T n() }
+ String run() {
+ I<String> i = new I<>() {
+ @Override String n() { 'x' }
+ }
+ i.n()
+ }
+ }
+ assert new Outer().run() == 'x'
+ '''
+ }
+
+ @Test
+ void testUnresolvedRareOuterTypeRejected() {
+ def err = shouldFail CompilationFailedException, '''
+ UnknownOuter<String>.Inner x
+ '''
+ assert err.message.toLowerCase().contains('unable to resolve') || err.message.contains('UnknownOuter')
+ }
+
+ @Test
+ void testGrabGeneratesClassLoaderPlainReference() {
+ def err = shouldFail '''
+ @Grab(group='org.apache.groovy', module='does-not-exist', version='0.0.0')
+ class GrabDummy12319 {
+ def x() { 1 }
+ }
+ new GrabDummy12319()
+ '''
+ assert err != null
+ }
+
+ @Test
+ void testStaticSuperPropertyAndMethodPlainNodeReference() {
+ assertScript '''
+ class Base<T> {
+ static String NAME = 'base'
+ static String m() { 'm' }
+ }
+ class Sub extends Base<String> {
+ static String read() { super.NAME + super.m() }
+ }
+ assert Sub.read() == 'basem'
+ '''
+ }
+}
+
diff --git a/src/test/groovy/bugs/Groovy5318.groovy b/src/test/groovy/bugs/Groovy5318.groovy
index 83ecebf088..c3249b33d3 100644
--- a/src/test/groovy/bugs/Groovy5318.groovy
+++ b/src/test/groovy/bugs/Groovy5318.groovy
@@ -26,11 +26,12 @@ import static groovy.test.GroovyAssert.shouldFail
final class Groovy5318 {
@Test
- void testTypeArgumentsOnlyOnTheLastComponent() {
+ void testTypeArgumentsOnNonClassQualifier() {
def err = shouldFail '''
def a = new java.util<Integer>.ArrayList<ArrayList<Integer>>()
'''
- assert err.message =~ 'Unexpected input: \'.\''
+ // GROOVY-12319: parsed as a rare type; java.util is a package, not a class
+ assert err.message.contains('unable to resolve class java.util')
}
}
diff --git a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
index e996753a80..6cca7ae739 100644
--- a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
+++ b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
@@ -50,13 +50,37 @@ import static groovy.test.GroovyAssert.shouldFail
* reflective Signature surface ({@link Class#getTypeParameters()},
* {@link Class#getGenericSuperclass()}, {@link Method#getGenericReturnType()},
* and so on) plus observable compile / run behaviour.
+ *
+ * Spec citations are the Java Language Specification, Java SE 25 Edition.
+ * The suite is organised around the four JCK/JLS dimensions for generics:
+ * <ol>
+ * <li>compile-time type safety and erasure, including bridge methods
+ * (JLS 4.6, 8.4.8.3);</li>
+ * <li>advanced syntax and bounds — wildcards, F-bounds, nested and
+ * multi-parameter types (JLS 4.4–4.5, 4.9);</li>
+ * <li>runtime reflection of {@code Signature} attributes
+ * ({@link Class#getGenericSuperclass()},
+ * {@link java.lang.reflect.ParameterizedType#getActualTypeArguments()},
+ * {@link java.lang.reflect.GenericArrayType});</li>
+ * <li>raw-type backward compatibility (JLS 4.8, 5.1.9).</li>
+ * </ol>
+ * Categories 1–15 below map onto those dimensions. Category 16 spells out
+ * the JCK-shaped cases that were not previously a dedicated test.
+ * Each test method names the JLS sections it exercises. Gaps where static
+ * Groovy currently diverges from javac are marked {@link org.junit.jupiter.api.Disabled}.
+ * The oracle is javac as shipped with Java SE 25.
*/
final class GenericsJavaCompatibilityTest {
// =========================================================================
- // Category 1: Generic Class & Interface Declarations and Type Bounds
+ // Category 1: Generic class and interface declarations and type bounds (JLS 4.4, 8.1.2, 9.1.2)
// =========================================================================
+ /**
+ * JLS 8.1.2, 4.4, 4.5: a generic class {@code Box<T>} declares a type parameter
+ * that each parameterization substitutes. {@code new Box<>("...")} uses diamond
+ * inference (JLS 15.9.3).
+ */
@Test
void testGenericClassSingleTypeParameter() {
final String className = 'gls.generics.test.SingleTypeParam'
@@ -79,6 +103,11 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'Apache Groovy Generics', true)
}
+ /**
+ * JLS 8.1.2, 4.5: a generic class may declare several type parameters
+ * ({@code Pair<K, V>}); the parameterization must supply exactly that many
+ * arguments.
+ */
@Test
void testGenericClassMultipleTypeParameters() {
final String className = 'gls.generics.test.MultipleTypeParams'
@@ -105,6 +134,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'score=100', true)
}
+ /**
+ * JLS 9.1.2, 8.1.5, 8.4.8.1: a class may implement a parameterization of a
+ * generic interface and override its methods with the substituted argument types.
+ */
@Test
void testGenericInterfaceImplementation() {
final String className = 'gls.generics.test.GenericInterfaceImpl'
@@ -129,6 +162,11 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 21, true)
}
+ /**
+ * JLS 4.4: a type-variable bound {@code T extends Number} restricts each
+ * type argument to a subtype of that bound; members of {@code Number} are
+ * available on values of type {@code T}.
+ */
@Test
void testSingleUpperBound() {
final String className = 'gls.generics.test.SingleUpperBound'
@@ -151,6 +189,11 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 45.14d, true)
}
+ /**
+ * JLS 4.4, 4.9: an intersection bound {@code T extends Number & Comparable<T> & Serializable}
+ * requires a class type (or type variable) first, then interfaces. The erasure of
+ * {@code T} is the leftmost bound.
+ */
@Test
void testMultipleBounds() {
final String className = 'gls.generics.test.MultipleBounds'
@@ -182,6 +225,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 29.0d, true)
}
+ /**
+ * JLS 4.4, 8.4.4: F-bounded polymorphism {@code T extends Comparable<T>} lets a
+ * generic method compare elements of {@code List<T>} using the bound's members.
+ */
@Test
void testRecursiveTypeBoundsFBounded() {
final String className = 'gls.generics.test.RecursiveBounds'
@@ -210,9 +257,13 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 2: Generic Methods & Constructors
+ // Category 2: Generic methods and constructors (JLS 8.4.4, 8.8.4, 15.12, 18.5)
// =========================================================================
+ /**
+ * JLS 8.4.4, 18.5.1, 18.5.2: invocation of a generic method {@code <T> List<T> makePairList(T, T)}
+ * infers {@code T} from the argument types and the assignment target.
+ */
@Test
void testGenericMethodInference() {
final String className = 'gls.generics.test.MethodInference'
@@ -236,6 +287,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 4)
}
+ /**
+ * JLS 15.12.2: type arguments of a generic method may be written explicitly
+ * ({@code Helper.<String>identity(...)}) instead of being inferred.
+ */
@Test
void testGenericMethodExplicitTypeArguments() {
final String className = 'gls.generics.test.ExplicitTypeArgs'
@@ -261,6 +316,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'Explicit:123')
}
+ /**
+ * JLS 8.8.4, 15.9.3: a constructor may declare its own type parameters, independent
+ * of any class type parameters, and callers may pass explicit constructor type arguments.
+ */
@Test
void testGenericConstructors() {
final String className = 'gls.generics.test.GenericConstructors'
@@ -275,8 +334,8 @@ final class GenericsJavaCompatibilityTest {
public String getTypeName() { return typeName; }
}
public static String test() {
- TypeHolder h1 = new TypeHolder("text");
- TypeHolder h2 = new TypeHolder(42);
+ TypeHolder h1 = new <String>TypeHolder("text");
+ TypeHolder h2 = new <Integer>TypeHolder(42);
return h1.getTypeName() + "-" + h2.getTypeName();
}
}
@@ -284,6 +343,11 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'String-Integer')
}
+ /**
+ * JLS 8.4.1, 9.6.4.7: a generic varargs method {@code <T> List<T> collect(T...)} is
+ * applicable by variable-arity invocation (JLS 15.12.2.4). {@code @SafeVarargs}
+ * documents that the heap-pollution risk is contained.
+ */
@Test
void testGenericVarargs() {
final String className = 'gls.generics.test.GenericVarargs'
@@ -313,9 +377,14 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 3: Wildcards & PECS
+ // Category 3: Wildcards, containment and capture conversion (JLS 4.5.1, 5.1.10)
// =========================================================================
+ /**
+ * JLS 4.5.1 (Example 4.5.1-1): {@code Collection<?>} accepts any parameterization.
+ * {@code ? extends Object} is equivalent to unbounded {@code ?}. Elements are
+ * readable as {@code Object}.
+ */
@Test
void testUnboundedWildcard() {
final String className = 'gls.generics.test.UnboundedWildcard'
@@ -340,6 +409,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 6)
}
+ /**
+ * JLS 4.5.1: {@code List<? extends Number>} is a producer (contains {@code Integer},
+ * {@code Double}, ...). Subtyping follows containment: {@code List<Integer> <= List<? extends Number>}.
+ */
@Test
void testUpperBoundedWildcardProducer() {
final String className = 'gls.generics.test.UpperBoundedWildcard'
@@ -364,6 +437,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 10.0d)
}
+ /**
+ * JLS 4.5.1: {@code List<? super Integer>} is a consumer; {@code Integer} may be
+ * added because the unknown element type is a supertype of {@code Integer}.
+ */
@Test
void testLowerBoundedWildcardConsumer() {
final String className = 'gls.generics.test.LowerBoundedWildcard'
@@ -390,6 +467,78 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 9)
}
+ /**
+ * JLS 4.5: a wildcard type argument is within bounds of {@code T extends Number}
+ * when capture conversion yields a well-formed glb with {@code Number}.
+ * Unbounded {@code ?} is always within bounds. {@code ? extends Object} is
+ * also legal: {@code glb(Object, Number)} is {@code Number}. javac 25 accepts
+ * both; it does <em>not</em> reject {@code NumberBox<? extends Object>}.
+ */
+ @Test
+ void testBoundedTypeParameterAcceptsUnboundedAndObjectWildcards() {
+ final String className = 'gls.generics.test.NumberBoxWildcards'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class NumberBoxWildcards {
+ public static class NumberBox<T extends Number> {
+ public final T value;
+ public NumberBox(T value) { this.value = value; }
+ }
+ public static int test() {
+ NumberBox<Integer> box = new NumberBox<Integer>(Integer.valueOf(7));
+ NumberBox<?> unbounded = box;
+ NumberBox<? extends Object> objectBound = box;
+ NumberBox<? extends Number> numberBound = box;
+ NumberBox<? extends Integer> integerBound = box;
+ NumberBox<? super Integer> superInteger = box;
+ NumberBox<? super Number> superNumber = new NumberBox<Number>(Integer.valueOf(1));
+ return (unbounded != null && objectBound != null && numberBound != null
+ && integerBound != null && superInteger != null && superNumber != null) ? 1 : 0;
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 1)
+ }
+
+ /**
+ * JLS 4.5: {@code ? extends String} is not within bounds of {@code T extends Number}
+ * because {@code glb(String, Number)} is an empty intersection of two classes.
+ * javac 25: "type argument ? extends String is not within bounds of type-variable T".
+ */
+ @Test
+ void testNegativeWildcardUpperBoundDisjointFromTypeParameterBound() {
+ final String className = 'gls.generics.test.NumberBoxExtendsString'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class NumberBoxExtendsString {
+ public static class NumberBox<T extends Number> {}
+ public NumberBox<? extends String> field;
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "not within bounds", "not a valid substitute for the bounded parameter")
+ }
+
+ /**
+ * JLS 4.5: {@code ? super Object} is not within bounds of {@code T extends Number}
+ * because the lower bound {@code Object} is not a subtype of {@code Number}.
+ */
+ @Test
+ void testNegativeWildcardLowerBoundOutsideTypeParameterBound() {
+ final String className = 'gls.generics.test.NumberBoxSuperObject'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class NumberBoxSuperObject {
+ public static class NumberBox<T extends Number> {}
+ public NumberBox<? super Object> field;
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "not within bounds", "not a valid substitute for the bounded parameter")
+ }
+
+ /**
+ * JLS 5.1.10: capture conversion turns {@code List<?>} into {@code List<capture#1-of ?>}
+ * so a helper {@code <T> void swapHelper(List<T>, ...)} can read and write the same {@code T}.
+ */
@Test
void testWildcardCaptureHelper() {
final String className = 'gls.generics.test.WildcardCapture'
@@ -416,9 +565,13 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 4: Diamond Operator <> & Type Inference
+ // Category 4: Diamond and target-type inference (JLS 15.9.3, 18.5.2)
// =========================================================================
+ /**
+ * JLS 15.9.1, 15.9.3, 18.5.2: diamond {@code <>} infers nested type arguments of
+ * {@code Map<String, List<Map<Integer, String>>>} from the assignment target.
+ */
@Test
void testDiamondOperatorWithNestedGenerics() {
final String className = 'gls.generics.test.NestedDiamond'
@@ -440,6 +593,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 3)
}
+ /**
+ * JLS 15.9.3, 15.9.5 (JEP 213): diamond is legal on an anonymous class
+ * {@code new Processor<>() { ... }} when the target type supplies the type arguments.
+ */
@Test
void testDiamondAnonymousInnerClass() {
final String className = 'gls.generics.test.DiamondAnonymous'
@@ -461,10 +618,14 @@ final class GenericsJavaCompatibilityTest {
}
}
'''
- // 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)
+ assertPositive(className, javaSrc, 'Processed: Data')
}
+ /**
+ * JLS 18.5.2: the assignment target of a poly invocation {@code <T> List<T> empty()}
+ * is a witness for {@code T}, so {@code List<String> s = empty()} and
+ * {@code List<Integer> i = empty()} are both well-typed.
+ */
@Test
void testTargetTypeInference() {
final String className = 'gls.generics.test.TargetTypeInference'
@@ -486,9 +647,14 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 5: Covariance, Bridge Methods & Overriding
+ // Category 5: Covariant returns and bridges (JLS 8.4.8.3, 15.12.4.5)
// =========================================================================
+ /**
+ * JLS 8.4.8.3 (covariant returns) and 15.12.4.5: {@code String get()} overrides
+ * {@code T get()} of {@code Supplier<String>}. A bridge with the erased signature
+ * is invoked through a raw {@code Supplier} and still returns the {@code String}.
+ */
@Test
void testCovariantReturnAndBridgeMethod() {
final String className = 'gls.generics.test.BridgeAndCovariance'
@@ -517,9 +683,14 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 6: Raw Types & Interoperability
+ // Category 6: Raw types and unchecked conversion (JLS 4.8, 5.1.9)
// =========================================================================
+ /**
+ * JLS 4.8, 5.1.9: assignment of a parameterized type to a raw type (and the reverse)
+ * is an unchecked conversion. The raw {@code List} may be mutated without compile-time
+ * argument checks.
+ */
@Test
void testRawTypeInterop() {
final String className = 'gls.generics.test.RawTypeInterop'
@@ -542,9 +713,14 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 7: Generic Exceptions & Parametric Throwables
+ // Category 7: Type variables in throws (JLS 8.4.6)
// =========================================================================
+ /**
+ * JLS 8.4.6 (Example 8.4.6-1): a type variable whose bound is a subtype of
+ * {@code Throwable} may appear in a {@code throws} clause, so
+ * {@code <E extends Exception> void runAction(Action<E>) throws E} is legal.
+ */
@Test
void testGenericThrowsClause() {
final String className = 'gls.generics.test.GenericThrows'
@@ -579,13 +755,15 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 8: Strict Negative Verification (Compile-time & Runtime)
+ // Category 8: Negative well-formedness (JLS 4.5, 4.10.2, 8.1.2, 8.1.4, 8.1.5)
// =========================================================================
+ /**
+ * JLS 8.1.2: it is a compile-time error if a generic class is a direct or
+ * indirect subclass of {@code Throwable}.
+ */
@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;
@@ -593,14 +771,13 @@ final class GenericsJavaCompatibilityTest {
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")
+ assertNegativeCompile(className, javaSrc, "generic class may not extend", "generic class may not extend")
}
+ /**
+ * JLS 4.5: a parameterized type is well-formed only if the number of type
+ * arguments equals the number of type parameters ({@code List} has one).
+ */
@Test
void testNegativeTypeArityMismatch() {
final String className = 'gls.generics.test.ArityMismatch'
@@ -614,6 +791,10 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "wrong number of type arguments", "supplied with 2 type parameters")
}
+ /**
+ * JLS 4.5: the same arity rule applies wherever a parameterized type is used,
+ * including a method formal ({@code Map} has two type parameters).
+ */
@Test
void testNegativeMethodParamArityMismatch() {
final String className = 'gls.generics.test.MethodArityMismatch'
@@ -627,6 +808,10 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "wrong number of type arguments", "supplied with 1 type parameter")
}
+ /**
+ * JLS 4.10.2: parameterized types are invariant in their type arguments.
+ * {@code ArrayList<Integer>} is not a subtype of {@code List<Number>}.
+ */
@Test
void testNegativeGenericInvarianceAssignment() {
final String className = 'gls.generics.test.InvarianceFail'
@@ -642,6 +827,11 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "incompatible types", "Cannot assign")
}
+ /**
+ * JLS 4.5, 4.4: a type argument must be a subtype of every type in the
+ * corresponding bound after substitution. {@code Object} is not a
+ * {@code Number & Comparable<Object>}.
+ */
@Test
void testNegativeMultipleBoundsMismatch() {
final String className = 'gls.generics.test.MultiBoundFail'
@@ -657,6 +847,10 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "not within bounds", "not a valid substitute for the bounded parameter")
}
+ /**
+ * JLS 8.1.4 (and 8.1.5): if a superclass or superinterface type has type
+ * arguments, none of those arguments may be wildcards.
+ */
@Test
void testNegativeWildcardInExtendsClause() {
final String className = 'gls.generics.test.WildcardSuper'
@@ -669,6 +863,26 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "unexpected type", "A supertype may not specify a wildcard type")
}
+ /**
+ * JLS 8.1.5: a superinterface type may not specify a wildcard
+ * ({@code implements List<?>}).
+ */
+ @Test
+ void testNegativeWildcardInImplementsClause() {
+ final String className = 'gls.generics.test.WildcardImplements'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class WildcardImplements implements List<?> {
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "unexpected type", "A supertype may not specify a wildcard type")
+ }
+
+ /**
+ * JLS 4.5: a parameterized type {@code C<T1,...,Tn>} is well-formed only if
+ * {@code C} names a generic class or interface. {@code Date} takes no type parameters.
+ */
@Test
void testNegativeParametricTypeForNonGenericClass() {
final String className = 'gls.generics.test.NonGenericParameterized'
@@ -684,6 +898,11 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "does not take parameters", "which takes no parameters")
}
+ /**
+ * JLS 8.1.5 (Example 8.1.5-3): a class may not implement two different
+ * parameterizations of the same generic interface (needed for translation by
+ * erasure, JLS 4.6).
+ */
@Test
void testNegativeDuplicateGenericInterfaceInheritance() {
final String className = 'gls.generics.test.InterfaceConflict'
@@ -697,6 +916,10 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "different arguments", "Duplicate interfaces in implements list")
}
+ /**
+ * JLS 5.3, 4.5.2: invocation of {@code List<String>#add} requires an argument
+ * compatible with the substituted element type {@code String}.
+ */
@Test
void testNegativeMethodArgumentTypeMismatch() {
final String className = 'gls.generics.test.CallMismatch'
@@ -713,6 +936,11 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "incompatible types", "Cannot call")
}
+ /**
+ * JLS 4.8, 4.12.2, 5.1.9: heap pollution through a raw {@code Sink} makes a later
+ * use of the erased {@code accept(T)} throw {@code ClassCastException} at the
+ * compiler-inserted cast (JLS 15.5).
+ */
@Test
void testNegativeHeapPollutionClassCastException() {
final String className = 'gls.generics.test.HeapPollution'
@@ -742,9 +970,13 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 9: Inheritance, enclosing types, and bound forwarding
+ // Category 9: Inheritance, enclosing types and bound forwarding (JLS 8.1.3–8.1.5, 4.5)
// =========================================================================
+ /**
+ * JLS 8.1.4, 4.5.2: a non-generic subclass may extend a parameterization
+ * ({@code Named extends Holder<String>}); inherited members have the substituted types.
+ */
@Test
void testGenericClassExtendsParameterizedType() {
final String className = 'gls.generics.test.ExtendsParameterized'
@@ -768,6 +1000,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'ok', true)
}
+ /**
+ * JLS 8.1.2, 4.5.2: a subclass may forward its type parameter to a generic
+ * superclass ({@code Box<T> extends Holder<T>}).
+ */
@Test
void testTypeParameterForwardedToSuperclass() {
final String className = 'gls.generics.test.ForwardTypeParam'
@@ -791,6 +1027,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'fwd', true)
}
+ /**
+ * JLS 9.1.2, 9.1.3: a generic interface may extend a parameterization of
+ * another generic interface ({@code SrcNum extends Src<Number>}).
+ */
@Test
void testGenericInterfaceExtendsGenericInterface() {
final String className = 'gls.generics.test.GenericIfaceExtends'
@@ -815,6 +1055,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 7, true)
}
+ /**
+ * JLS 8.1.3, 6.5.5.1: a non-static inner class is not a static context, so it
+ * may refer to a type parameter of the enclosing generic class.
+ */
@Test
void testNonStaticInnerClassUsesEnclosingTypeParameter() {
final String className = 'gls.generics.test.EnclosingTypeParam'
@@ -836,6 +1080,116 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'inner')
}
+ /**
+ * JLS 4.5: a nested parameterization such as {@code Outer<String>.Inner<Integer>}
+ * (a "rare" type) is well-formed when both the enclosing type and the member
+ * type are generic.
+ */
+ @Test
+ void testRareTypeQualifiedInnerClass() {
+ final String className = 'gls.generics.test.RareTypeInner'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RareTypeInner {
+ public static class Outer<T> {
+ public class Inner<U> {
+ private final U u;
+ Inner(U u) { this.u = u; }
+ public U getU() { return u; }
+ }
+ public Inner<Integer> make(Integer u) { return new Inner<Integer>(u); }
+ }
+ public static String test() {
+ Outer<String> outer = new Outer<String>();
+ Outer<String>.Inner<Integer> inner = outer.make(Integer.valueOf(7));
+ return String.valueOf(inner.getU());
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, '7')
+ }
+
+ /**
+ * JLS 4.5, 4.11: a rare type may appear as a field type and a method parameter
+ * or return type; the Signature attribute records the enclosing type arguments.
+ */
+ @Test
+ void testRareTypeFieldAndMethodSignatures() {
+ final String className = 'gls.generics.test.RareTypeSignatures'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RareTypeSignatures {
+ public static class Outer<T> {
+ public class Inner<U> {
+ public U u;
+ }
+ }
+ public Outer<String>.Inner<Integer> field;
+ public Outer<String>.Inner<Integer> echo(Outer<String>.Inner<Integer> p) { return p; }
+ public static String test() { return "ok"; }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok', true)
+ }
+
+ /**
+ * JLS 4.5: {@code A<String>.B.C<Integer>} is a parameterized type even though
+ * the middle member {@code B} is not generic, because type arguments appear
+ * in the enclosing type.
+ */
+ @Test
+ void testRareTypeUnparameterizedMiddleSignatures() {
+ final String className = 'gls.generics.test.RareTypeMiddle'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RareTypeMiddle {
+ public static class A<T> {
+ public class B {
+ public class C<U> {
+ public U u;
+ }
+ }
+ }
+ public A<String>.B.C<Integer> field;
+ public static String test() { return "ok"; }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok', true)
+ }
+
+ /**
+ * JLS 15.9, 8.1.3: a qualified class instance creation
+ * {@code new Outer<T>.Inner<Integer>(...)} instantiates the inner class of the
+ * enclosing parameterization.
+ */
+ @Test
+ void testRareTypeInstanceCreationInsideOuter() {
+ final String className = 'gls.generics.test.RareTypeNew'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RareTypeNew {
+ public static class Outer<T> {
+ public class Inner<U> {
+ private final U u;
+ Inner(U u) { this.u = u; }
+ public U getU() { return u; }
+ }
+ public Inner<Integer> make() {
+ return new Outer<T>.Inner<Integer>(Integer.valueOf(7));
+ }
+ }
+ public static String test() {
+ return String.valueOf(new Outer<String>().make().getU());
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, '7')
+ }
+
+ /**
+ * JLS 8.1.1.4, 8.1.2: a {@code static} nested class has no enclosing instance
+ * and must not depend on the outer class's type parameters; it may declare its own.
+ */
@Test
void testStaticNestedClassHasIndependentTypeParameters() {
final String className = 'gls.generics.test.StaticNestedIndependent'
@@ -858,6 +1212,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, '42', true)
}
+ /**
+ * JLS 4.4: a type-variable bound may itself be a type variable
+ * ({@code U extends T}). {@code T} is inferred as a supertype of the argument.
+ */
@Test
void testTypeParameterUsedAsBoundOfAnother() {
final String className = 'gls.generics.test.TypeParamAsBound'
@@ -876,6 +1234,11 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'bound')
}
+ /**
+ * JLS 8.1.2 (Example 8.1.2-1): type parameters in one section may mention each
+ * other, including a later parameter ({@code T extends List<X>, X extends Number}).
+ * A parameter may not depend on itself.
+ */
@Test
void testForwardReferencedTypeParameterBounds() {
final String className = 'gls.generics.test.ForwardRefBounds'
@@ -900,6 +1263,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 9.0d, true)
}
+ /**
+ * JLS 4.4: an F-bound {@code T extends Entity<T>} encodes a self type so
+ * {@code fluent()} can return the concrete subclass rather than the raw base.
+ */
@Test
void testFBoundedSelfType() {
final String className = 'gls.generics.test.FBoundedSelfType'
@@ -923,6 +1290,11 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'user', true)
}
+ /**
+ * JLS 4.4 (Example 4.4-1), 4.9: the members of {@code T extends Number & Comparable<T>}
+ * are the members of that intersection, so both {@code intValue()} and
+ * {@code compareTo} are applicable.
+ */
@Test
void testIntersectionBoundUsedAsBothTypes() {
final String className = 'gls.generics.test.IntersectionDispatch'
@@ -940,6 +1312,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'Integer:5:0')
}
+ /**
+ * JLS 4.4, 8.1.2: independent type parameters may each carry their own bound
+ * ({@code A extends CharSequence, B extends Number}).
+ */
@Test
void testIndependentBoundedTypeParameters() {
final String className = 'gls.generics.test.IndependentBoundedParams'
@@ -961,9 +1337,13 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 10: Generic methods on generic types, arrays as type arguments
+ // Category 10: Generic methods of generic types; arrays as type arguments (JLS 8.4.4, 4.5.1)
// =========================================================================
+ /**
+ * JLS 8.4.4: a generic method of a generic class introduces method type
+ * parameters distinct from the class's ({@code Box<T>#<U> U map(Function<T, U>)}).
+ */
@Test
void testGenericMethodOnGenericClass() {
final String className = 'gls.generics.test.GenericMethodOnGenericClass'
@@ -987,6 +1367,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 4)
}
+ /**
+ * JLS 4.5.1: a type argument is a reference type or a wildcard. An array type
+ * such as {@code String[]} is a reference type, so {@code List<String[]>} is well-formed.
+ */
@Test
void testArrayAsTypeArgument() {
final String className = 'gls.generics.test.ArrayAsTypeArg'
@@ -1005,6 +1389,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 2)
}
+ /**
+ * JLS 18.5.2: nested poly invocations {@code id(id("ok"))} constrain the inner
+ * and outer inference variables from the same target type.
+ */
@Test
void testChainedGenericMethodInference() {
final String className = 'gls.generics.test.ChainedInference'
@@ -1022,6 +1410,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'ok')
}
+ /**
+ * JLS 8.4.2, 8.4.8.1: a generic method overrides another when the signatures are
+ * override-equivalent, including after renaming type parameters with matching bounds.
+ */
@Test
void testOverrideGenericMethodWithMatchingBounds() {
final String className = 'gls.generics.test.OverrideMatchingBounds'
@@ -1047,9 +1439,13 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 11: PECS, nested wildcards, function variance
+ // Category 11: PECS, nested wildcards and function variance (JLS 4.5.1, 4.10.2)
// =========================================================================
+ /**
+ * JLS 4.5.1 containment and 4.10.2: {@code Collections.copy(List<? super T>, List<? extends T>)}
+ * is the PECS form of a copy — destination is a consumer, source is a producer.
+ */
@Test
void testPecsCollectionsCopy() {
final String className = 'gls.generics.test.PecsCopy'
@@ -1074,6 +1470,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, '1,3')
}
+ /**
+ * JLS 4.5.1: wildcards nest. {@code List<? extends List<? extends Number>>} contains
+ * {@code List<List<Integer>>} by applying containment recursively.
+ */
@Test
void testNestedWildcards() {
final String className = 'gls.generics.test.NestedWildcards'
@@ -1101,6 +1501,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 3)
}
+ /**
+ * JLS 4.5.1: {@code List<Integer>#sort} accepts {@code Comparator<? super Integer>},
+ * so a {@code Comparator<Number>} may order a list of {@code Integer}.
+ */
@Test
void testComparatorSuperBoundSort() {
final String className = 'gls.generics.test.ComparatorSuperBound'
@@ -1124,6 +1528,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, '1,2,3')
}
+ /**
+ * JLS 4.5.1, 9.9: {@code Function<? super T, ? extends R>} is the flexible function
+ * type — argument consumer, result producer — used by {@code apply}.
+ */
@Test
void testFunctionWildcardVariance() {
final String className = 'gls.generics.test.FunctionVariance'
@@ -1143,6 +1551,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 3)
}
+ /**
+ * JLS 4.5.2, 5.1.10: capture of {@code List<? super Integer>} yields an unknown
+ * supertype of {@code Integer}; reading an element therefore has type {@code Object}.
+ */
@Test
void testLowerBoundedWildcardGetReturnsObject() {
final String className = 'gls.generics.test.LowerBoundGet'
@@ -1162,6 +1574,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, '8')
}
+ /**
+ * JLS 4.5.1, 5.2: the null reference is assignment-compatible with every reference
+ * type, so {@code list.add(null)} is legal even on {@code List<?>}.
+ */
@Test
void testWildcardAllowsAddingNull() {
final String className = 'gls.generics.test.WildcardAddNull'
@@ -1181,9 +1597,13 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 12: Diamond target typing, unchecked conversion, raw assignment
+ // Category 12: Invocation-context diamond, unchecked casts, nested members (JLS 15.9.3, 5.1.9, 4.5)
// =========================================================================
+ /**
+ * JLS 15.9.3, 18.5.2, 5.3: the invocation context of {@code len(List<String>)} is a
+ * target-type witness for {@code new ArrayList<>()}.
+ */
@Test
void testDiamondInferredFromMethodArgument() {
final String className = 'gls.generics.test.DiamondMethodArg'
@@ -1203,6 +1623,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 0)
}
+ /**
+ * JLS 5.5, 5.1.9: a cast to a non-reifiable parameterized type is unchecked.
+ * The value is then used as {@code List<String>} at compile time.
+ */
@Test
void testUncheckedCastToParameterizedType() {
final String className = 'gls.generics.test.UncheckedCast'
@@ -1222,6 +1646,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'cast')
}
+ /**
+ * JLS 5.1.9, 4.8: assigning a raw type to a parameterization is an unchecked
+ * conversion; subsequent uses see the parameterized type.
+ */
@Test
void testRawTypeAssignedToParameterizedType() {
final String className = 'gls.generics.test.RawToParameterized'
@@ -1242,6 +1670,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 1)
}
+ /**
+ * JLS 4.5, 9.5: {@code Map.Entry<K, V>} is a member interface of a generic type.
+ * {@code Map.Entry<String, Integer>} is a parameterization of that member.
+ */
@Test
void testMapEntryParameterizedInnerInterface() {
final String className = 'gls.generics.test.MapEntryInner'
@@ -1264,9 +1696,13 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 13: Covariant override, conditional lub, records, instanceof
+ // Category 13: Covariant override, lub, generic records, reifiable instanceof (JLS 8.4.8.3, 4.10.4, 8.10, 4.7)
// =========================================================================
+ /**
+ * JLS 8.4.8.3: {@code String id(String)} in {@code Child extends Parent<String>}
+ * is return-type-substitutable for {@code T id(T)} and overrides it.
+ */
@Test
void testCovariantOverrideInGenericClassHierarchy() {
final String className = 'gls.generics.test.CovariantClassOverride'
@@ -1289,6 +1725,11 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'ok!', true)
}
+ /**
+ * JLS 15.25.3, 4.10.4: a reference conditional expression has the lub of its
+ * operand types ({@code Class<?>} for class literals, {@code CharSequence} for
+ * {@code String} and {@code StringBuilder}).
+ */
@Test
void testConditionalExpressionLubsClassLiterals() {
final String className = 'gls.generics.test.ConditionalLub'
@@ -1306,6 +1747,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'Integerhello')
}
+ /**
+ * JLS 8.10, 8.1.2: a record class may be generic. Canonical accessors and the
+ * compact constructor use the substituted component types.
+ */
@Test
void testGenericRecord() {
final String className = 'gls.generics.test.GenericRecordPair'
@@ -1323,6 +1768,10 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'a1', true)
}
+ /**
+ * JLS 4.7, 15.20.2: {@code List<?>} and {@code Class<?>} are reifiable (unbounded
+ * wildcards), so they may appear as the {@code instanceof} reference type.
+ */
@Test
void testReifiableInstanceofUnboundedWildcard() {
final String className = 'gls.generics.test.ReifiableInstanceof'
@@ -1341,6 +1790,11 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, true)
}
+ /**
+ * JLS 4.4, 4.5.1, 8.4.4, 13.1: the reflective Signature of type parameters,
+ * bounds, wildcards, generic methods and generic superclasses must match
+ * between javac and static Groovy.
+ */
@Test
void testDeclaredGenericSignaturesMatch() {
final String className = 'gls.generics.test.SignatureCatalog'
@@ -1381,9 +1835,13 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Category 14: Additional negative cases (JLS restrictions both sides share)
+ // Category 14: Additional negatives both compilers share (JLS 4.7, 6.5.5.1, 4.5.1)
// =========================================================================
+ /**
+ * JLS 4.7, 15.20.2: {@code List<String>} is not reifiable, so
+ * {@code o instanceof List<String>} is a compile-time error.
+ */
@Test
void testNegativeNonReifiableInstanceof() {
final String className = 'gls.generics.test.NonReifiableInstanceof'
@@ -1399,6 +1857,10 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
}
+ /**
+ * JLS 4.7, 15.20.2: {@code Class<C>} is not reifiable (the type argument is not
+ * an unbounded wildcard), so it may not be used with {@code instanceof}.
+ */
@Test
void testNegativeInstanceofParameterizedClassLiteral() {
final String className = 'gls.generics.test.InstanceofClassParam'
@@ -1413,6 +1875,10 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "cannot be converted", "Cannot perform instanceof")
}
+ /**
+ * JLS 6.5.5.1, 8.1.2: a type parameter of the enclosing class is not in scope
+ * from a static nested class.
+ */
@Test
void testNegativeTypeParameterReferencedFromStaticNestedClass() {
final String className = 'gls.generics.test.StaticNestedOuterTp'
@@ -1427,6 +1893,10 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "non-static type variable", "unable to resolve class T")
}
+ /**
+ * JLS 8.3.1.1, 6.5.5.1: a {@code static} field is a static context and may not
+ * refer to a type parameter of the enclosing class.
+ */
@Test
void testNegativeTypeParameterStaticField() {
final String className = 'gls.generics.test.StaticTypeParamField'
@@ -1439,6 +1909,11 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "non-static type variable", "unable to resolve class T")
}
+ /**
+ * JLS 4.5.1, 5.1.10: after capture, the element type of {@code List<? extends Number>}
+ * is an unknown subtype of {@code Number}; {@code Integer} is not a subtype of that
+ * capture, so {@code add} is rejected.
+ */
@Test
void testNegativeWildcardWriteThroughExtends() {
final String className = 'gls.generics.test.WildcardWriteExtends'
@@ -1455,28 +1930,954 @@ final class GenericsJavaCompatibilityTest {
}
// =========================================================================
- // Test Harness Helper Methods
+ // Category 15: Erasure, reifiable arrays, static members, remaining JLS restrictions
// =========================================================================
/**
- * 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.
+ * JLS 4.6, 8.1.2: the erasure of {@code G<T1,...,Tn>} is {@code |G|}. All
+ * parameterizations of a generic class therefore share one run-time class.
*/
- private static String toGroovy(String javaSrc) {
- javaSrc.replaceAll(/@SuppressWarnings\(\{([^}]*)\}\)/, '@SuppressWarnings([$1])')
+ @Test
+ void testErasureSharesRuntimeClass() {
+ final String className = 'gls.generics.test.ErasureRuntimeClass'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ErasureRuntimeClass {
+ public static class Box<T> {
+ public final T value;
+ public Box(T value) { this.value = value; }
+ }
+ public static boolean test() {
+ Box<String> strings = new Box<>("a");
+ Box<Integer> ints = new Box<>(Integer.valueOf(1));
+ return strings.getClass() == ints.getClass();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, true, true)
}
/**
- * Groovy class loader that always compiles with {@link CompileStatic}.
- * Dynamic Groovy is not part of this suite.
+ * JLS 4.5.1 containment and 4.10.2: {@code List<Integer>} is a subtype of
+ * {@code List<? extends Number>} even though it is not a subtype of
+ * {@code List<Number>} (invariance is covered by
+ * {@link #testNegativeGenericInvarianceAssignment()}).
*/
- private static GroovyClassLoader newStaticGroovyClassLoader() {
- CompilerConfiguration config = new CompilerConfiguration()
- config.addCompilationCustomizers(new ASTTransformationCustomizer(CompileStatic))
- new GroovyClassLoader(GenericsJavaCompatibilityTest.classLoader, config)
- }
-
+ @Test
+ void testWildcardContainmentAssignment() {
+ final String className = 'gls.generics.test.WildcardContainment'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class WildcardContainment {
+ public static int test() {
+ List<Integer> ints = new ArrayList<Integer>();
+ ints.add(Integer.valueOf(4));
+ List<? extends Number> nums = ints;
+ List<?> any = ints;
+ return nums.get(0).intValue() + any.size();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 5)
+ }
+
+ /**
+ * JLS 15.10.1, 4.7: an array creation is legal when the component type is
+ * reifiable. {@code List<?>} is reifiable, so {@code new List<?>[n]} is allowed.
+ */
+ @Test
+ void testReifiableGenericArrayCreation() {
+ final String className = 'gls.generics.test.ReifiableArrayCreation'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class ReifiableArrayCreation {
+ public static int test() {
+ List<?>[] lists = new List<?>[1];
+ lists[0] = new ArrayList<String>();
+ return lists.length;
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 1)
+ }
+
+ /**
+ * JLS 4.8: the type of a member of a raw type is the erasure of that member
+ * as declared. {@code Box.get()} therefore has return type {@code Object}.
+ */
+ @Test
+ void testRawTypeMemberErasure() {
+ final String className = 'gls.generics.test.RawMemberErasure'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RawMemberErasure {
+ public static class Box<T> {
+ private final T value;
+ public Box(T value) { this.value = value; }
+ public T get() { return value; }
+ }
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public static String test() {
+ Box raw = new Box("raw");
+ Object value = raw.get();
+ return value.getClass().getSimpleName();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'String')
+ }
+
+ /**
+ * JLS 4.5.2: a static member of a generic type must be referred to using the
+ * generic type name, not a parameterization ({@code Cell.id()}, not
+ * {@code Cell<String>.id()}).
+ */
+ @Test
+ void testStaticMemberViaGenericTypeName() {
+ final String className = 'gls.generics.test.StaticViaGenericName'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class StaticViaGenericName {
+ public static class Cell<T> {
+ public static String id() { return "cell"; }
+ }
+ public static String test() {
+ return Cell.id();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'cell', true)
+ }
+
+ /**
+ * JLS 15.8.3: in a generic class {@code C<F1,...,Fn>}, the type of {@code this}
+ * is {@code C<F1,...,Fn>}, so a method may return {@code this} at that type.
+ */
+ @Test
+ void testThisTypeInGenericClass() {
+ final String className = 'gls.generics.test.GenericThisType'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class GenericThisType {
+ public static class Box<T> {
+ public T value;
+ public Box(T value) { this.value = value; }
+ public Box<T> self() { return this; }
+ }
+ public static String test() {
+ return new Box<String>("me").self().value;
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'me', true)
+ }
+
+ /**
+ * JLS 15.9.3, 18.5.2: a diamond class instance creation in a return context
+ * takes the method result type as its target type.
+ */
+ @Test
+ void testDiamondInferredFromReturnType() {
+ final String className = 'gls.generics.test.DiamondReturn'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.List;
+ public class DiamondReturn {
+ public static List<String> empty() {
+ return new ArrayList<>();
+ }
+ public static int test() {
+ return empty().size();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 0)
+ }
+
+ /**
+ * JLS 4.5: primitive types cannot be type arguments ({@code List<int>} is
+ * not well-formed). The corresponding boxed reference type must be used.
+ */
+ @Test
+ void testNegativePrimitiveTypeArgument() {
+ final String className = 'gls.generics.test.PrimitiveTypeArg'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class PrimitiveTypeArg {
+ public List<int> list;
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "unexpected type", "primitive type")
+ }
+
+ /**
+ * JLS 15.10.1, 4.7: it is a compile-time error to create an array whose
+ * component type is not reifiable ({@code new List<String>[n]}).
+ */
+ @Test
+ void testNegativeNonReifiableArrayCreation() {
+ final String className = 'gls.generics.test.GenericArrayCreation'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class GenericArrayCreation {
+ public static Object test() {
+ return new List<String>[1];
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "generic array creation", "generic array creation")
+ }
+
+ /**
+ * JLS 15.10.1, 4.7: a parameterized type is reifiable only when every type
+ * argument is an unbounded wildcard. {@code List<? extends Number>} is not
+ * reifiable, so {@code new List<? extends Number>[n]} is a compile-time error.
+ * The legal counterpart is {@link #testReifiableGenericArrayCreation()}.
+ */
+ @Test
+ void testNegativeBoundedWildcardArrayCreation() {
+ final String className = 'gls.generics.test.BoundedWildcardArrayCreation'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class BoundedWildcardArrayCreation {
+ public static Object test() {
+ return new List<? extends Number>[1];
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "generic array creation", "generic array creation")
+ }
+
+ /**
+ * JLS 4.7, 15.10.1: {@code Outer<?>} is reifiable and a non-static member
+ * type of a reifiable type is reifiable, so {@code new Outer<?>.Inner[n]}
+ * is a legal array creation. javac 25 accepts this; Groovy must as well.
+ */
+ @Test
+ void testReifiableRareTypeArrayCreation() {
+ final String className = 'gls.generics.test.RareTypeReifiableArray'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RareTypeReifiableArray {
+ public static class Outer<T> {
+ public class Inner {}
+ }
+ public static int test() {
+ Outer<?>.Inner[] arr = new Outer<?>.Inner[0];
+ return arr.length;
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 0)
+ }
+
+ /**
+ * JLS 4.7, 15.10.1: the generic-member counterpart is also reifiable when
+ * every type argument is an unbounded wildcard
+ * ({@code new Outer<?>.InnerG<?>[n]}).
+ */
+ @Test
+ void testReifiableRareTypeGenericInnerArrayCreation() {
+ final String className = 'gls.generics.test.RareTypeReifiableGenericInnerArray'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RareTypeReifiableGenericInnerArray {
+ public static class Outer<T> {
+ public class InnerG<U> {}
+ }
+ public static int test() {
+ Outer<?>.InnerG<?>[] arr = new Outer<?>.InnerG<?>[0];
+ return arr.length;
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 0)
+ }
+
+ /**
+ * JLS 4.5.2, 6.5.5: a static member type may not be selected from a
+ * parameterized type ({@code new Outer<?>.Nested[n]}).
+ */
+ @Test
+ void testNegativeStaticNestedTypeViaParameterizedEnclosing() {
+ final String className = 'gls.generics.test.StaticNestedViaParameterized'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class StaticNestedViaParameterized {
+ public static class Outer<T> {
+ public static class Nested {}
+ }
+ public static Object test() {
+ return new Outer<?>.Nested[0];
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot select a static class from a parameterized type", "parameterization")
+ }
+
+ /**
+ * JLS 9.5, 6.5.5: a nested interface is implicitly static, so
+ * {@code implements Outer<String>.Inner} is the same error as
+ * {@code new Outer<?>.Nested[n]}.
+ */
+ @Test
+ void testNegativeImplementsStaticNestedViaParameterizedEnclosing() {
+ final String className = 'gls.generics.test.ImplementsStaticNestedViaParameterized'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ImplementsStaticNestedViaParameterized {
+ public static class Outer<T> {
+ public interface Inner<U> {
+ U id(U u);
+ }
+ }
+ public static class Impl implements Outer<String>.Inner<Integer> {
+ public Integer id(Integer u) { return u; }
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot select a static class from a parameterized type", "parameterization")
+ }
+
+ /**
+ * JLS 9.5, 6.5.5: {@code Map.Entry} is a nested interface, so
+ * {@code new Map<?,?>.Entry[n]} cannot select it from a parameterization.
+ */
+ @Test
+ void testNegativeMapEntryViaParameterizedEnclosing() {
+ final String className = 'gls.generics.test.MapEntryViaParameterized'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.Map;
+ public class MapEntryViaParameterized {
+ public static Object test() {
+ return new Map<?,?>.Entry[0];
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot select a static class from a parameterized type", "parameterization")
+ }
+
+ /**
+ * A generic member type used raw under a parameterized enclosing type is
+ * malformed ({@code new Outer<?>.InnerG[n]}).
+ */
+ @Test
+ void testNegativeRawGenericMemberOfParameterizedEnclosing() {
+ final String className = 'gls.generics.test.RawMemberOfParameterized'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RawMemberOfParameterized {
+ public static class Outer<T> {
+ public class InnerG<U> {}
+ }
+ public static Object test() {
+ return new Outer<?>.InnerG[0];
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "improperly formed type", "parameterized enclosing type")
+ }
+
+ /**
+ * JLS 15.9.1: a class instance creation may not instantiate a type variable
+ * ({@code new T()}).
+ */
+ @Test
+ void testNegativeInstantiateTypeVariable() {
+ final String className = 'gls.generics.test.NewTypeVariable'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class NewTypeVariable {
+ public static <T> T make() {
+ return new T();
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "type parameter", "type parameter")
+ }
+
+ /**
+ * JLS 15.8.2: a class literal's {@code TypeName} may not denote a type
+ * variable ({@code T.class}).
+ */
+ @Test
+ void testNegativeTypeVariableClassLiteral() {
+ final String className = 'gls.generics.test.TypeVarClassLiteral'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class TypeVarClassLiteral {
+ public static <T> Class<T> token() {
+ return T.class;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot select from a type variable", "type parameter")
+ }
+
+ /**
+ * JLS 4.5.2: it is illegal to refer to a static member of a generic type
+ * through a parameterization ({@code Cell<String>.id()}).
+ */
+ @Test
+ void testNegativeStaticMemberViaParameterizedType() {
+ final String className = 'gls.generics.test.StaticViaParameterized'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class StaticViaParameterized {
+ public static class Cell<T> {
+ public static String id() { return "cell"; }
+ }
+ public static String test() {
+ return Cell<String>.id();
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "illegal start of type", "parameterization")
+ }
+
+ /**
+ * JLS 8.4.2, 8.4.8.3 (Example 8.4.8.3-4): two methods of a class may not
+ * have override-equivalent signatures after erasure
+ * ({@code m(List<String>)} and {@code m(List<Integer>)} both erase to {@code m(List)}).
+ */
+ @Test
+ void testNegativeSameErasureOverload() {
+ final String className = 'gls.generics.test.SameErasureOverload'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class SameErasureOverload {
+ public void m(List<String> a) {}
+ public void m(List<Integer> a) {}
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "erasure", "duplicates another method")
+ }
+
+ /**
+ * JLS 15.9: a wildcard may not appear after {@code new} in a class instance
+ * creation expression ({@code new ArrayList<?>()}).
+ */
+ @Test
+ void testNegativeWildcardAfterNew() {
+ final String className = 'gls.generics.test.WildcardAfterNew'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ public class WildcardAfterNew {
+ public static Object test() {
+ return new ArrayList<?>();
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "unexpected type", "wildcard type")
+ }
+
+ /**
+ * JLS 15.9: diamond {@code <>} may not be combined with explicit constructor
+ * type arguments.
+ */
+ @Test
+ void testNegativeDiamondAndConstructorTypeArguments() {
+ final String className = 'gls.generics.test.DiamondAndCtorArgs'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class DiamondAndCtorArgs {
+ public static class Box<T> {
+ public final T value;
+ public <U> Box(T value, U extra) { this.value = value; }
+ }
+ public static Object test() {
+ return new <Integer>Box<>("x", Integer.valueOf(1));
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "explicit type parameters for constructor", "Cannot use diamond")
+ }
+
+ /**
+ * JLS 8.9: an enum class may not declare type parameters.
+ */
+ @Test
+ void testNegativeGenericEnum() {
+ final String className = 'gls.generics.test.GenericEnum'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class GenericEnum {
+ public enum Kind<T> { A }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "enum", "enum")
+ }
+
+ /**
+ * JLS 4.4: a class type or type variable may appear only as the first type
+ * of a bound; additional bounds must be interfaces
+ * ({@code T extends Comparable<T> & Number} is illegal).
+ */
+ @Test
+ void testNegativeClassTypeNotFirstInBound() {
+ final String className = 'gls.generics.test.ClassBoundNotFirst'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ClassBoundNotFirst {
+ public static <T extends Comparable<T> & Number> T id(T value) {
+ return value;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "interface", "interface")
+ }
+
+ /**
+ * JLS 15.10.1: the component type of an array creation may not be a type
+ * variable ({@code new T[n]}).
+ */
+ @Test
+ void testNegativeTypeParameterArrayCreation() {
+ final String className = 'gls.generics.test.TypeParamArray'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class TypeParamArray {
+ public static <T> T[] make(int n) {
+ return new T[n];
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "generic array creation", "generic array creation")
+ }
+
+ // =========================================================================
+ // Category 16: JCK four-dimension coverage (Java SE 25)
+ // 1. erasure and bridges 2. multi-parameter / nested syntax
+ // 3. Signature reflection 4. raw-type backward compatibility
+ // =========================================================================
+
+ /**
+ * JLS 4.6: the erasure of an unbounded type variable is {@code Object};
+ * the erasure of {@code T extends Number} is {@code Number}. The generic
+ * return type remains a {@link TypeVariable}.
+ */
+ @Test
+ void testErasureOfUnboundedAndBoundedTypeVariables() {
+ final String className = 'gls.generics.test.ErasureOfTypeVariables'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.Method;
+ import java.lang.reflect.TypeVariable;
+ public class ErasureOfTypeVariables {
+ public static class Box<T> {
+ public T id(T t) { return t; }
+ }
+ public static class NumBox<T extends Number> {
+ public T id(T t) { return t; }
+ }
+ public static String test() throws Exception {
+ Method boxId = Box.class.getMethod("id", Object.class);
+ Method numId = NumBox.class.getMethod("id", Number.class);
+ boolean boxErasure = boxId.getReturnType() == Object.class
+ && boxId.getGenericReturnType() instanceof TypeVariable;
+ boolean numErasure = numId.getReturnType() == Number.class
+ && numId.getGenericReturnType() instanceof TypeVariable;
+ return (boxErasure && numErasure) ? "ok" : "fail";
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok', true)
+ }
+
+ /**
+ * JLS 8.4.8.3, 15.12.4.5: a covariant override of {@code T get()} as
+ * {@code String get()} is compiled with a bridge {@code Object get()} so a
+ * raw or erased invoke still reaches the specialized method.
+ */
+ @Test
+ void testCovariantOverrideDeclaresBridgeMethod() {
+ final String className = 'gls.generics.test.CovariantBridgePresent'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.Method;
+ public class CovariantBridgePresent {
+ public interface Supplier<T> {
+ T get();
+ }
+ public static class StringSupplier implements Supplier<String> {
+ @Override
+ public String get() { return "bridged"; }
+ }
+ public static String test() {
+ boolean bridge = false;
+ boolean specialized = false;
+ for (Method m : StringSupplier.class.getDeclaredMethods()) {
+ if (!"get".equals(m.getName()) || m.getParameterCount() != 0) {
+ continue;
+ }
+ if (m.isBridge() && m.getReturnType() == Object.class) {
+ bridge = true;
+ }
+ if (!m.isBridge() && m.getReturnType() == String.class) {
+ specialized = true;
+ }
+ }
+ Supplier raw = new StringSupplier();
+ Object viaErased = raw.get();
+ return (bridge && specialized && "bridged".equals(viaErased)) ? "ok" : "fail";
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok')
+ }
+
+ /**
+ * JLS 8.1.2: three independent class type parameters {@code Triple<A, B, C>}
+ * are substituted together; diamond infers all three from the constructor
+ * arguments and the assignment target.
+ */
+ @Test
+ void testTripleTypeParameters() {
+ final String className = 'gls.generics.test.TripleTypeParams'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class TripleTypeParams {
+ public static class Triple<A, B, C> {
+ public final A a;
+ public final B b;
+ public final C c;
+ public Triple(A a, B b, C c) {
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ }
+ }
+ public static String test() {
+ Triple<String, Integer, Double> t = new Triple<>("k", Integer.valueOf(1), Double.valueOf(2.0d));
+ return t.a + t.b + t.c.intValue();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'k12', true)
+ }
+
+ /**
+ * JLS 4.5, 13.1: {@code Class#getGenericSuperclass()} of a class that
+ * extends {@code Box<String>} is the parameterized type {@code Box<String>},
+ * and {@code getActualTypeArguments()[0]} is {@code String.class}.
+ */
+ @Test
+ void testGenericSuperclassActualTypeArguments() {
+ final String className = 'gls.generics.test.GenericSuperclassArgs'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.ParameterizedType;
+ import java.lang.reflect.Type;
+ public class GenericSuperclassArgs {
+ public static class Box<T> {}
+ public static class NamedBox extends Box<String> {}
+ public static class StringIterable implements Iterable<String> {
+ @Override
+ public java.util.Iterator<String> iterator() {
+ return java.util.Collections.emptyIterator();
+ }
+ }
+ public static String test() {
+ Type sc = NamedBox.class.getGenericSuperclass();
+ ParameterizedType box = (ParameterizedType) sc;
+ Class<?> arg = (Class<?>) box.getActualTypeArguments()[0];
+ String ifaceArg = "missing";
+ for (Type t : StringIterable.class.getGenericInterfaces()) {
+ if (t instanceof ParameterizedType) {
+ ParameterizedType p = (ParameterizedType) t;
+ if (p.getRawType() == Iterable.class) {
+ ifaceArg = ((Class<?>) p.getActualTypeArguments()[0]).getSimpleName();
+ }
+ }
+ }
+ return arg.getName() + "|" + ifaceArg;
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'java.lang.String|String', true)
+ }
+
+ /**
+ * JLS 4.5, 10.1, 13.1: a field of type {@code List<String>[]} is a generic
+ * array type in the Signature attribute. {@link GenericArrayType} reports
+ * the component as {@code List<String>}. Creation of such an array remains
+ * illegal; see {@link #testNegativeNonReifiableArrayCreation()}.
+ */
+ @Test
+ void testGenericArrayTypeFieldSignature() {
+ final String className = 'gls.generics.test.GenericArrayField'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.GenericArrayType;
+ import java.lang.reflect.ParameterizedType;
+ import java.lang.reflect.Type;
+ import java.util.List;
+ public class GenericArrayField {
+ public List<String>[] rows;
+ public static String test() throws Exception {
+ Type t = GenericArrayField.class.getField("rows").getGenericType();
+ if (!(t instanceof GenericArrayType)) {
+ return "not-array:" + t;
+ }
+ Type component = ((GenericArrayType) t).getGenericComponentType();
+ if (!(component instanceof ParameterizedType)) {
+ return "not-param:" + component;
+ }
+ return ((Class<?>) ((ParameterizedType) component).getActualTypeArguments()[0]).getSimpleName();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'String', true)
+ }
+
+ /**
+ * JLS 8.8.4, 13.1: a generic constructor {@code <T> CtorBox(T)} records its
+ * type parameter on the constructor, independent of any class type parameters.
+ */
+ @Test
+ void testGenericConstructorTypeParameterSignature() {
+ final String className = 'gls.generics.test.GenericCtorSignature'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.Constructor;
+ import java.lang.reflect.TypeVariable;
+ public class GenericCtorSignature {
+ public static class CtorBox {
+ public final Object value;
+ public <T> CtorBox(T t) { this.value = t; }
+ }
+ public static int test() {
+ int found = 0;
+ for (Constructor<?> c : CtorBox.class.getDeclaredConstructors()) {
+ if (c.getParameterCount() == 1) {
+ TypeVariable<?>[] tps = c.getTypeParameters();
+ if (tps.length == 1 && "T".equals(tps[0].getName())) {
+ found++;
+ }
+ }
+ }
+ return found;
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 1, true)
+ }
+
+ /**
+ * JLS 4.8: a raw subclass {@code class Raw extends Box} uses the erasure of
+ * {@code Box}. {@code get()} therefore has compile-time type {@code Object}.
+ */
+ @Test
+ void testRawSubclassOfGenericClass() {
+ final String className = 'gls.generics.test.RawSubclass'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RawSubclass {
+ public static class Box<T> {
+ public final T value;
+ public Box(T value) { this.value = value; }
+ public T get() { return value; }
+ }
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public static class Raw extends Box {
+ public Raw(Object value) { super(value); }
+ }
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public static String test() {
+ Raw raw = new Raw("hi");
+ Object value = raw.get();
+ return value.toString();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'hi')
+ }
+
+ /**
+ * JLS 8.4.8.1, 4.8, 5.1.9: a raw subclass may override {@code name(T)} with
+ * {@code name(Object)} (the erasure). Invocation through {@code Super<String>}
+ * still dispatches to the subclass. javac 25 accepts this as an unchecked override.
+ */
+ @Test
+ void testUncheckedOverrideOfGenericMethod() {
+ final String className = 'gls.generics.test.UncheckedOverride'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class UncheckedOverride {
+ public static class Super<T> {
+ public String name(T t) { return "super"; }
+ }
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public static class Child extends Super {
+ public String name(Object t) { return "child"; }
+ }
+ public static String test() {
+ Super<String> s = new Child();
+ return s.name("x");
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'child')
+ }
+
+ // =========================================================================
+ // Category 17: Overload resolution against erasure; F-bounded copy; intersection
+ // erasure; Collection<Object> vs Collection<?> (JLS 4.4–4.6, 4.8, 5.1.10, 15.12.2)
+ // =========================================================================
+
+ /**
+ * JLS 4.6, 15.12.2: inside {@code <T> generic(T t)} the compile-time type of
+ * {@code t} is the erasure of {@code T} ({@code Object}), so {@code f(t)} binds
+ * to {@code f(Object)} even when the invocation is {@code generic("string")}.
+ */
+ @Test
+ void testGenericMethodOverloadResolvesAgainstErasure() {
+ final String className = 'gls.generics.test.GenericOverloadErasure'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class GenericOverloadErasure {
+ public static String f(Object s) { return "object"; }
+ public static String f(String s) { return "string"; }
+ public static <T> String generic(T t) { return f(t); }
+ public static String test() {
+ return generic("string");
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'object')
+ }
+
+ /**
+ * JLS 4.4: {@code Copyable<T extends Copyable<T>>} with {@code T copy()} makes
+ * {@code node.copy().copy()} well-typed, because the result of {@code copy()}
+ * is the same F-bounded type.
+ */
+ @Test
+ void testFBoundedCopyAllowsChainedCopy() {
+ final String className = 'gls.generics.test.FBoundedCopy'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class FBoundedCopy {
+ public interface Copyable<T extends Copyable<T>> {
+ T copy();
+ }
+ public static class Node implements Copyable<Node> {
+ @Override
+ public Node copy() { return this; }
+ public String id() { return "n"; }
+ }
+ public static String test() {
+ return new Node().copy().copy().id();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'n', true)
+ }
+
+ /**
+ * JLS 4.4, 4.6, 4.9: the {@code Collections.max} shape
+ * {@code <T extends Object & Comparable<? super T>> T max(Collection<? extends T>)}
+ * erases the return type to the leftmost bound {@code Object}.
+ */
+ @Test
+ void testIntersectionMaxErasureIsLeftmostBound() {
+ final String className = 'gls.generics.test.IntersectionMaxErasure'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.Method;
+ import java.util.Collection;
+ public class IntersectionMaxErasure {
+ public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {
+ return null;
+ }
+ public static String test() throws Exception {
+ Method m = IntersectionMaxErasure.class.getMethod("max", Collection.class);
+ return m.getReturnType() == Object.class ? "ok" : m.getReturnType().getName();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok')
+ }
+
+ /**
+ * JLS 4.5, 4.8: {@code Collection<Object>} accepts any reference.
+ * {@code Collection<?>} is a capture of some unknown element type; see
+ * {@link #testNegativeAddNonNullThroughUnboundedWildcard()}.
+ */
+ @Test
+ void testCollectionObjectIsHeterogeneous() {
+ final String className = 'gls.generics.test.HeterogeneousCollectionObject'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.ArrayList;
+ import java.util.Collection;
+ public class HeterogeneousCollectionObject {
+ public static int test() {
+ Collection<Object> hetero = new ArrayList<Object>();
+ hetero.add("a");
+ hetero.add(Integer.valueOf(1));
+ return hetero.size();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 2)
+ }
+
+ /**
+ * JLS 5.1.10: after capture, the element type of {@code Collection<?>} is a
+ * fresh type variable; {@code String} is not a subtype of that capture, so
+ * {@code add("x")} is a compile-time error. {@code add(null)} remains legal
+ * ({@link #testWildcardAllowsAddingNull()}).
+ */
+ @Test
+ void testNegativeAddNonNullThroughUnboundedWildcard() {
+ final String className = 'gls.generics.test.UnboundedWildcardAdd'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.Collection;
+ public class UnboundedWildcardAdd {
+ public static void test(Collection<?> c) {
+ c.add("x");
+ }
+ }
+ '''
+ 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}
@@ -1637,11 +3038,15 @@ final class GenericsJavaCompatibilityTest {
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(', ') + '>'
+ String args = (typeArgs == null || typeArgs.length == 0) ? '' :
+ '<' + typeArgs.collect { typeDesc(it) }.join(', ') + '>'
+ if (p.ownerType != null) {
+ String raw = rawName(p.rawType)
+ int cut = Math.max(raw.lastIndexOf('$'), raw.lastIndexOf('.'))
+ String simple = cut < 0 ? raw : raw.substring(cut + 1)
+ return typeDesc(p.ownerType) + '$' + simple + args
+ }
+ return typeDesc(p.rawType) + args
}
if (type instanceof WildcardType) {
WildcardType w = (WildcardType) type
diff --git a/src/test/groovy/gls/generics/GenericsUsageTest.groovy b/src/test/groovy/gls/generics/GenericsUsageTest.groovy
index 5957cd625c..194b97bafa 100644
--- a/src/test/groovy/gls/generics/GenericsUsageTest.groovy
+++ b/src/test/groovy/gls/generics/GenericsUsageTest.groovy
@@ -323,10 +323,9 @@ final class GenericsUsageTest {
// constructor call
assertScript '''
List<String> list = new LinkedList<>()
+ def aic = new LinkedList<>() { }
+ assert aic instanceof LinkedList
'''
- shouldFailCompilationWithMessage '''
- new LinkedList<>() { }
- ''', 'Cannot use diamond <> with anonymous inner classes'
shouldFailCompilationWithMessages '''
new LinkedList<Integer, String>()
''', '(supplied with 2 type parameters)', 'which takes 1 parameter'
diff --git a/src/test/groovy/gls/innerClass/InnerClassTest.groovy b/src/test/groovy/gls/innerClass/InnerClassTest.groovy
index 61bfde3f0c..af873da66b 100644
--- a/src/test/groovy/gls/innerClass/InnerClassTest.groovy
+++ b/src/test/groovy/gls/innerClass/InnerClassTest.groovy
@@ -2170,8 +2170,8 @@ final class InnerClassTest {
})
void bar(Runnable runner = new Runnable() {
@Override void run() {
- def comparator = new Comparator<int>() {
- int compare(int one, int two) {
+ def comparator = new Comparator<Integer>() {
+ int compare(Integer one, Integer two) {
}
}
}
diff --git a/src/test/groovy/groovy/transform/stc/ArraysAndCollectionsSTCTest.groovy b/src/test/groovy/groovy/transform/stc/ArraysAndCollectionsSTCTest.groovy
index f5e6b79980..7686eb3c4e 100644
--- a/src/test/groovy/groovy/transform/stc/ArraysAndCollectionsSTCTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/ArraysAndCollectionsSTCTest.groovy
@@ -82,13 +82,17 @@ class ArraysAndCollectionsSTCTest extends StaticTypeCheckingTestCase {
// GROOVY-10111
@Test
void testBoundedComponentTypeInArrayInitializer() {
- assertScript '''
+ // JLS 15.10.1 / 4.7: a type variable is not reifiable, so
+ // `new X[]{...}` is generic array creation even when X is bounded.
+ // Initializer conversion is checked only for well-formed creations.
+ shouldFailWithMessages '''
class C<X, Y> {
}
def <X extends C<Number, String>> X[] m() {
new X[]{new C<Number, String>()}
}
- '''
+ ''',
+ 'generic array creation'
}
@Test
diff --git a/src/test/groovy/groovy/transform/stc/DefaultGroovyMethodsSTCTest.groovy b/src/test/groovy/groovy/transform/stc/DefaultGroovyMethodsSTCTest.groovy
index bb4cd311b3..4f46949c95 100644
--- a/src/test/groovy/groovy/transform/stc/DefaultGroovyMethodsSTCTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/DefaultGroovyMethodsSTCTest.groovy
@@ -234,7 +234,7 @@ class DefaultGroovyMethodsSTCTest extends StaticTypeCheckingTestCase {
@Test
void testSortMethodsWithComparatorAcceptingSubclass() {
assertScript '''
- class SecondLetterComparator implements Comparator<? extends CharSequence> {
+ class SecondLetterComparator implements Comparator<CharSequence> {
int compare(CharSequence cs1, CharSequence cs2) {
cs1.charAt(1) <=> cs2.charAt(1)
}
diff --git a/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy b/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy
index e2045aa1e9..b10caf13c6 100644
--- a/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy
@@ -3309,12 +3309,13 @@ class GenericsSTCTest extends StaticTypeCheckingTestCase {
'''
}
+ // GROOVY-12319
@Test
void testInferDiamondUsingAIC() {
- shouldFailWithMessages '''
+ assertScript '''
Map<String,Date> map = new HashMap<>() {}
- ''',
- 'Cannot use diamond <> with anonymous inner classes'
+ assert map instanceof Map
+ '''
}
// GROOVY-5614
diff --git a/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java b/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
index 8eca306adc..e9523860cd 100644
--- a/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
+++ b/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
@@ -161,6 +161,21 @@ public final class ClassNodeTest {
assertEquals(0, classNode.getTypeAnnotations().size());
}
+ @Test // GROOVY-12319
+ public void testOuterClassTypeCopiedOnPlainReference() {
+ ClassNode outer = ClassHelper.makeWithoutCaching("Outer");
+ outer.setGenericsTypes(new GenericsType[]{new GenericsType(ClassHelper.STRING_TYPE)});
+ classNode.setOuterClassType(outer);
+
+ assertEquals(outer, classNode.getOuterClassType());
+ ClassNode plain = classNode.getPlainNodeReference();
+ assertEquals(outer, plain.getOuterClassType());
+
+ classNode.setOuterClassType(null);
+ assertNull(classNode.getOuterClassType());
+ assertNull(classNode.getPlainNodeReference().getOuterClassType());
+ }
+
@Test
public void testPermittedSubclasses() throws Exception {
assumeTrue(Runtime.version().feature() >= 17);
diff --git a/src/test/groovy/org/codehaus/groovy/ast/MethodNodeTest.groovy b/src/test/groovy/org/codehaus/groovy/ast/MethodNodeTest.groovy
index 8e84c4ae9b..f1976f79c2 100644
--- a/src/test/groovy/org/codehaus/groovy/ast/MethodNodeTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/ast/MethodNodeTest.groovy
@@ -21,6 +21,7 @@ package org.codehaus.groovy.ast
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.control.CompilePhase
import org.junit.jupiter.api.Test
+import org.objectweb.asm.Opcodes
final class MethodNodeTest {
@@ -72,6 +73,15 @@ final class MethodNodeTest {
}
}
+ @Test // GROOVY-12319
+ void testIsBridge() {
+ def ordinary = new MethodNode('foo', Opcodes.ACC_PUBLIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null)
+ assert !ordinary.isBridge()
+
+ def bridge = new MethodNode('foo', Opcodes.ACC_PUBLIC | Opcodes.ACC_BRIDGE, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null)
+ assert bridge.isBridge()
+ }
+
@Test
void testIsDynamicReturnType1() {
def methodNode = new MethodNode('foo', 0, ClassHelper.dynamicType(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null)
diff --git a/src/test/groovy/org/codehaus/groovy/ast/tools/GenericsUtilsTest.groovy b/src/test/groovy/org/codehaus/groovy/ast/tools/GenericsUtilsTest.groovy
index 350bdc75e5..0968b1cba4 100644
--- a/src/test/groovy/org/codehaus/groovy/ast/tools/GenericsUtilsTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/ast/tools/GenericsUtilsTest.groovy
@@ -22,9 +22,18 @@ package org.codehaus.groovy.ast.tools
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.GenericsType
+import org.codehaus.groovy.ast.InnerClassNode
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.Parameter
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
+import org.codehaus.groovy.ast.expr.ConstructorCallExpression
+import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor
import org.junit.jupiter.api.Test
+import org.objectweb.asm.Opcodes
+
+import static org.junit.jupiter.api.Assertions.assertThrows
final class GenericsUtilsTest {
@@ -382,4 +391,139 @@ final class GenericsUtilsTest {
assert listType.genericsTypes[0].type.unresolvedName == '#E'
assert listType.genericsTypes[0].type.name == 'java.lang.Object'
}
+
+ @Test // GROOVY-12319
+ void testApplyGenericsSpecFillsMissingFromBounds() {
+ def classNodes = compile '''
+ class BoundBox<T extends Number> {}
+ '''
+ ClassNode boundBox = findClassNode('BoundBox', classNodes).getPlainNodeReference()
+ boundBox.redirect = findClassNode('BoundBox', classNodes)
+ GenericsUtils.applyGenericsSpec(boundBox, [:])
+ assert boundBox.genericsTypes[0].type.name.contains('Number')
+
+ ClassNode list = ClassHelper.LIST_TYPE.getPlainNodeReference()
+ GenericsUtils.applyGenericsSpec(list, [:])
+ assert list.genericsTypes.length == 1
+ assert list.genericsTypes[0].type == ClassHelper.OBJECT_TYPE
+
+ GenericsUtils.applyGenericsSpec(list, [E: ClassHelper.STRING_TYPE])
+ assert list.genericsTypes[0].type == ClassHelper.STRING_TYPE
+
+ ClassNode object = ClassHelper.OBJECT_TYPE.getPlainNodeReference()
+ GenericsUtils.applyGenericsSpec(object, [E: ClassHelper.STRING_TYPE])
+ assert object.genericsTypes == null
+ }
+
+ @Test // GROOVY-12319
+ void testInferGenericsSpecFromOverridesAndDiamondTarget() {
+ assert GenericsUtils.diamondTargetOfAnonymousClass(null) == null
+
+ ClassNode noDiamond = new InnerClassNode(ClassHelper.OBJECT_TYPE, 'C$1', Opcodes.ACC_PUBLIC, ClassHelper.OBJECT_TYPE)
+ noDiamond.anonymous = true
+ assert GenericsUtils.diamondTargetOfAnonymousClass(noDiamond) == null
+
+ ClassNode diamondIface = ClassHelper.LIST_TYPE.getPlainNodeReference()
+ diamondIface.genericsTypes = GenericsType.EMPTY_ARRAY
+ ClassNode anon = new InnerClassNode(ClassHelper.OBJECT_TYPE, 'C$2', Opcodes.ACC_PUBLIC, ClassHelper.OBJECT_TYPE, [diamondIface] as ClassNode[], org.codehaus.groovy.ast.MixinNode.EMPTY_ARRAY)
+ anon.anonymous = true
+ assert GenericsUtils.diamondTargetOfAnonymousClass(anon) === diamondIface
+
+ Map empty = GenericsUtils.inferGenericsSpecFromOverrides(anon, ClassHelper.OBJECT_TYPE.getPlainNodeReference(), [:])
+ assert empty.isEmpty()
+
+ def classNodes = compile '''
+ interface Box<T> {
+ T get()
+ }
+ class Host {
+ def m() {
+ new Box<>() {
+ @Override
+ int get() { 1 }
+ }
+ }
+ }
+ '''
+ ClassNode host = findClassNode('Host', classNodes)
+ ClassNode aic = host.innerClasses.next()
+ ClassNode target = GenericsUtils.diamondTargetOfAnonymousClass(aic) ?: aic.unresolvedInterfaces[0]
+ Map spec = GenericsUtils.inferGenericsSpecFromOverrides(aic, target, [:])
+ assert spec.T == ClassHelper.Integer_TYPE || spec.T?.name == 'java.lang.Integer'
+ }
+
+ @Test // GROOVY-12319
+ void testPushEnclosingConstructorCall() {
+ def classNodes = compile '''
+ class C {}
+ '''
+ def visitor = new StaticTypeCheckingVisitor(classNodes[0].module.context, classNodes[0])
+ def ctx = visitor.typeCheckingContext
+ def ctor = new ConstructorCallExpression(ClassHelper.OBJECT_TYPE, new ArgumentListExpression())
+ ctx.pushEnclosingMethodCall(ctor)
+ assert ctx.getEnclosingMethodCall() === ctor
+ ctx.popEnclosingMethodCall()
+
+ def ex = assertThrows(IllegalArgumentException) {
+ ctx.pushEnclosingMethodCall(new ConstantExpression('x'))
+ }
+ assert ex.message.contains('constructor call')
+ }
+
+ @Test // GROOVY-12319
+ void testInferOverridesSkipsUnrelatedNameAndArityMatches() {
+ ClassNode box = ClassHelper.makeWithoutCaching('Box')
+ def t = ClassHelper.makeWithoutCaching('T')
+ t.genericsPlaceHolder = true
+ box.genericsTypes = [new GenericsType(t)] as GenericsType[]
+ def declared = new MethodNode('convert', Opcodes.ACC_PUBLIC, ClassHelper.STRING_TYPE,
+ [new Parameter(ClassHelper.STRING_TYPE, 's')] as Parameter[], ClassNode.EMPTY_ARRAY, null)
+ def unrelated = new MethodNode('convert', Opcodes.ACC_PUBLIC, ClassHelper.OBJECT_TYPE,
+ [new Parameter(ClassHelper.MAP_TYPE, 'm')] as Parameter[], ClassNode.EMPTY_ARRAY, null)
+ def superMethod = new MethodNode('convert', Opcodes.ACC_PUBLIC, t,
+ [new Parameter(t, 'v')] as Parameter[], ClassNode.EMPTY_ARRAY, null)
+ ClassNode impl = ClassHelper.makeWithoutCaching('Impl')
+ impl.addMethod(declared)
+ impl.addMethod(unrelated)
+ ClassNode superType = ClassHelper.makeWithoutCaching('Super')
+ superType.redirect = box
+ superType.addMethod(superMethod)
+ Map spec = GenericsUtils.inferGenericsSpecFromOverrides(impl, superType, [:])
+ assert spec['T'] == ClassHelper.STRING_TYPE
+ }
+
+ @Test // GROOVY-12319
+ void testInferOverridesSkipsUnrelatedParameterErasure() {
+ def t = ClassHelper.makeWithoutCaching('T')
+ t.genericsPlaceHolder = true
+ ClassNode box = ClassHelper.makeWithoutCaching('Box2')
+ box.genericsTypes = [new GenericsType(t)] as GenericsType[]
+ ClassNode listOfT = ClassHelper.LIST_TYPE.getPlainNodeReference()
+ listOfT.genericsTypes = [new GenericsType(t)] as GenericsType[]
+ ClassNode superList = ClassHelper.makeWithoutCaching('SuperList')
+ superList.redirect = box
+ superList.addMethod(new MethodNode('convert', Opcodes.ACC_PUBLIC, t,
+ [new Parameter(listOfT, 'xs')] as Parameter[], ClassNode.EMPTY_ARRAY, null))
+ ClassNode impl2 = ClassHelper.makeWithoutCaching('Impl2')
+ impl2.addMethod(new MethodNode('convert', Opcodes.ACC_PUBLIC, ClassHelper.STRING_TYPE,
+ [new Parameter(ClassHelper.Integer_TYPE, 'n')] as Parameter[], ClassNode.EMPTY_ARRAY, null))
+ Map skipped = GenericsUtils.inferGenericsSpecFromOverrides(impl2, superList, [:])
+ assert skipped['T'] == null
+ }
+
+ @Test // GROOVY-12319
+ void testInferOverridesBoxesPrimitiveActualType() {
+ def t = ClassHelper.makeWithoutCaching('T')
+ t.genericsPlaceHolder = true
+ t.redirect = ClassHelper.OBJECT_TYPE
+ ClassNode box = ClassHelper.makeWithoutCaching('Box')
+ box.genericsTypes = [new GenericsType(t)] as GenericsType[]
+ ClassNode superType = ClassHelper.makeWithoutCaching('Super')
+ superType.redirect = box
+ superType.addMethod(new MethodNode('n', Opcodes.ACC_PUBLIC, t, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null))
+ ClassNode impl = ClassHelper.makeWithoutCaching('Impl')
+ impl.addMethod(new MethodNode('n', Opcodes.ACC_PUBLIC, ClassHelper.int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null))
+ Map spec = GenericsUtils.inferGenericsSpecFromOverrides(impl, superType, [:])
+ assert spec['T'] == ClassHelper.Integer_TYPE
+ }
}
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy b/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy
index b6c579f183..d2d2794278 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy
@@ -20,6 +20,7 @@ package org.codehaus.groovy.classgen
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.GenericsType
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.ast.stmt.EmptyStatement
@@ -81,6 +82,55 @@ class BytecodeHelperTest extends AbstractBytecodeTestCase {
BytecodeHelper.getMethodDescriptor(new MethodNode('test', 0, ClassHelper.boolean_TYPE, Parameter.EMPTY_ARRAY, [] as ClassNode[], EmptyStatement.INSTANCE)))
}
+ @Test // GROOVY-12319
+ void testRareTypeAndArrayGenericsSignatures() {
+ ClassNode outer = ClassHelper.makeWithoutCaching('com.example.Outer')
+ outer.genericsTypes = [new GenericsType(ClassHelper.STRING_TYPE)] as GenericsType[]
+ ClassNode inner = ClassHelper.makeWithoutCaching('com.example.Outer$Inner')
+ inner.outerClassType = outer
+ inner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+
+ String typeSig = BytecodeHelper.getTypeGenericsSignature(inner)
+ assert typeSig.contains('Outer')
+ assert typeSig.contains('Inner')
+ assert typeSig.contains('String')
+
+ ClassNode list = ClassHelper.LIST_TYPE.getPlainNodeReference()
+ list.genericsTypes = [new GenericsType(ClassHelper.STRING_TYPE)] as GenericsType[]
+ String arrayBounds = BytecodeHelper.getGenericsBounds(list.makeArray())
+ assert arrayBounds.startsWith('[')
+ assert arrayBounds.contains('String')
+
+ assert BytecodeHelper.getGenericsBounds(ClassHelper.STRING_TYPE.makeArray()) == null
+
+ ClassNode diamond = ClassHelper.LIST_TYPE.getPlainNodeReference()
+ diamond.genericsTypes = GenericsType.EMPTY_ARRAY
+ assert BytecodeHelper.getTypeGenericsSignature(diamond) == null
+
+ ClassNode placeholder = ClassHelper.makeWithoutCaching('T')
+ placeholder.genericsPlaceHolder = true
+ placeholder.genericsTypes = [new GenericsType(placeholder)] as GenericsType[]
+ placeholder.redirect = ClassHelper.OBJECT_TYPE
+ assert BytecodeHelper.getGenericsBounds(placeholder) != null
+
+ ClassNode mismatchedInner = ClassHelper.makeWithoutCaching('totally.Different$Item')
+ mismatchedInner.outerClassType = outer
+ mismatchedInner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+ String fallback = BytecodeHelper.getTypeGenericsSignature(mismatchedInner)
+ assert fallback.contains('Item')
+
+ ClassNode superDiamond = ClassHelper.LIST_TYPE.getPlainNodeReference()
+ superDiamond.genericsTypes = GenericsType.EMPTY_ARRAY
+ ClassNode t = ClassHelper.makeWithoutCaching('T')
+ t.genericsPlaceHolder = true
+ ClassNode genericClass = new ClassNode('C', ACC_PUBLIC, superDiamond)
+ genericClass.genericsTypes = [new GenericsType(t)] as GenericsType[]
+ assert BytecodeHelper.getGenericsSignature(genericClass) != null
+
+ MethodNode mn = new MethodNode('echo', 0, inner, [new Parameter(inner, 'p')] as Parameter[], [] as ClassNode[], EmptyStatement.INSTANCE)
+ assert BytecodeHelper.getGenericsMethodSignature(mn).contains('Outer')
+ }
+
@Test
void testPushConstantUsesCompactIntegerInstructions() {
def bytecode = emitBytecode {
diff --git a/subprojects/groovy-console/src/test/groovy/groovy/console/ui/AstNodeToScriptAdapterTest.groovy b/subprojects/groovy-console/src/test/groovy/groovy/console/ui/AstNodeToScriptAdapterTest.groovy
index 370bc3d605..fbe5c88328 100644
--- a/subprojects/groovy-console/src/test/groovy/groovy/console/ui/AstNodeToScriptAdapterTest.groovy
+++ b/subprojects/groovy-console/src/test/groovy/groovy/console/ui/AstNodeToScriptAdapterTest.groovy
@@ -208,14 +208,20 @@ b $$v"""/$,
@Test
void testGenericBoundsOnClass() {
+ // JLS 8.1.5: a superinterface may not specify a wildcard, so Callable<Number>
+ // is the well-formed implements type. Wildcard bounds are still exercised on
+ // the field, which is a legal type position for ? super Number.
String script = '''import java.util.concurrent.Callable
- abstract class MyClass<T extends String & Callable<String>, U extends Integer> extends AbstractList<String> implements Callable<? super Number> { }
+ abstract class MyClass<T extends String & Callable<String>, U extends Integer> extends AbstractList<String> implements Callable<Number> {
+ Callable<? super Number> callback
+ }
'''
String result = compileToScript(script)
assert result.contains('MyClass<T extends java.lang.String & java.util.concurrent.Callable<String>, U extends java.lang.Integer> ' +
'extends java.util.AbstractList<String> ' +
- 'implements java.util.concurrent.Callable<? super java.lang.Number> {')
+ 'implements java.util.concurrent.Callable<Number> {')
+ assert result.contains('java.util.concurrent.Callable<? super java.lang.Number>')
}
@Test
(groovy) 03/05: GROOVY-12319: review follow-up for rare types and instanceof
Posted by su...@apache.org.
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 5a8cc8d1e2af6f4488859f1ba3bceb05e38d19b1
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Aug 30 12:46:28 2026 +0000
GROOVY-12319: review follow-up for rare types and instanceof
Store the JLS 4.5 enclosing type on ClassNode instead of node metadata.
Treat both parser and binary nested names in JVMS signatures, and only
emit the nested form when the enclosing type is parameterized.
Reject non-reifiable instanceof at parse; check nested-type well-formedness
on the operand without treating it as a class literal. Expand
GenericsJavaCompatibilityTest as the Java-vs-Groovy matrix.
---
.../apache/groovy/parser/antlr4/AstBuilder.java | 30 +-
.../java/org/codehaus/groovy/ast/ClassNode.java | 20 +-
.../groovy/classgen/asm/BytecodeHelper.java | 40 ++-
.../codehaus/groovy/control/GenericsVisitor.java | 29 +-
.../groovy/control/InstanceOfVerifier.java | 7 +-
.../codehaus/groovy/control/ResolveVisitor.java | 23 +-
src/test/groovy/bugs/Groovy12319.groovy | 17 --
.../generics/GenericsJavaCompatibilityTest.groovy | 340 ++++++++++++++++++++-
src/test/groovy/groovy/InstanceofTest.groovy | 4 +-
.../org/codehaus/groovy/ast/ClassNodeTest.java | 19 ++
.../groovy/classgen/BytecodeHelperTest.groovy | 61 +++-
11 files changed, 535 insertions(+), 55 deletions(-)
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 97fcb45fe3..e4b4fdef9b 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -3988,7 +3988,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
if (asBoolean(ctx.primitiveType())) {
classNode = configureAST(this.visitPrimitiveType(ctx.primitiveType()), ctx);
} else {
- classNode = this.visitParameterizedClassType(ctx);
+ classNode = this.visitParameterizedClassType(ctx, false);
if (classNode != null) {
configureAST(classNode, ctx);
}
@@ -4666,7 +4666,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
@Override
public ClassNode visitReferenceType(final ReferenceTypeContext ctx) {
- return configureAST(this.visitParameterizedClassType(ctx), ctx);
+ return configureAST(this.visitParameterizedClassType(ctx, true), ctx);
}
/**
@@ -4674,7 +4674,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
* Shared by {@code referenceType} (type arguments only) and {@code createdName}
* (type arguments or diamond).
*/
- private ClassNode visitParameterizedClassType(final GroovyParserRuleContext ctx) {
+ private ClassNode visitParameterizedClassType(final GroovyParserRuleContext ctx, final boolean attachRawEnclosing) {
ClassNode classNode = null;
for (int i = 0, n = ctx.getChildCount(); i < n; i += 1) {
ParseTree child = ctx.getChild(i);
@@ -4690,9 +4690,15 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
classNode = this.visitQualifiedStandardClassName(qscn);
} else if (child instanceof TypeArgumentsContext tac) {
GenericsType[] generics = this.visitTypeArguments(tac);
+ if (attachRawEnclosing) {
+ attachRawEnclosingIfQualified(classNode);
+ }
classNode.setGenericsTypes(generics);
rejectParameterizedInstanceof(ctx, classNode, generics);
} else if (child instanceof TypeArgumentsOrDiamondContext tad) {
+ if (attachRawEnclosing) {
+ attachRawEnclosingIfQualified(classNode);
+ }
classNode.setGenericsTypes(this.visitTypeArgumentsOrDiamond(tad));
} else if (child instanceof IdentifierContext || child instanceof ClassNameContext) {
ClassNode outer = classNode;
@@ -4707,6 +4713,24 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return classNode;
}
+ /**
+ * {@code Outer.Inner<?>} is one qualified name plus type arguments, so the
+ * nested-type loop never runs. Record a raw enclosing type from the name
+ * prefix; {@code ResolveVisitor} drops it if that prefix is only a package.
+ */
+ private static void attachRawEnclosingIfQualified(final ClassNode classNode) {
+ if (classNode == null || classNode.getOuterClassType() != null) return;
+ String name = classNode.getName();
+ int sep = Math.max(name.lastIndexOf('.'), name.lastIndexOf('$'));
+ if (sep <= 0) return;
+ classNode.setOuterClassType(ClassHelper.makeWithoutCaching(name.substring(0, sep)));
+ }
+
+ /**
+ * {@code instanceof} type arguments are legal in the grammar so {@code List<?>}
+ * can parse (JLS 15.20.2 / 4.7). Concrete arguments such as
+ * {@code Map<String,Integer>} are not reifiable and are rejected here.
+ */
private void rejectParameterizedInstanceof(final GroovyParserRuleContext ctx, final ClassNode classNode, final GenericsType[] generics) {
if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR) && Arrays.stream(generics).anyMatch(gt ->
!gt.isWildcard() || asBoolean(gt.getLowerBound()) || ArrayGroovyMethods.asBoolean(gt.getUpperBounds()))) { // GROOVY-11585
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassNode.java b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
index 7c7544190c..e0e866e868 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
@@ -118,6 +118,11 @@ import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
* they describe the type signature used at the point of declaration or the
* type signatures provided by the class. If the type signatures provided
* by the class are needed, then a call to {@link #redirect()} will help.
+ * {@link #getOuterClassType()} / {@link #setOuterClassType(ClassNode)} are
+ * likewise not proxied: they record the parameterized enclosing type of a
+ * JLS 4.5 rare type <em>use</em> ({@code Outer<T>.Inner}), distinct from
+ * {@link #getOuterClass()} which is the enclosing class of a nested class
+ * declaration.
*
* @see org.codehaus.groovy.ast.ClassHelper
*/
@@ -190,6 +195,8 @@ public class ClassNode extends AnnotatedNode {
protected Class<?> clazz;
// if not null then this instance is an array
private ClassNode componentType;
+ // parameterized enclosing type of a JLS 4.5 rare type (Outer<T>.Inner); type-use, not declaration
+ private ClassNode outerClassType;
// if not null then this instance is handled as proxy for the redirect
private ClassNode redirect;
@@ -2055,24 +2062,27 @@ faces: if (method == null && asBoolean(getInterfaces())) { // GROOVY-11323
/**
* For a JLS 4.5 rare type {@code Outer<T>.Inner}, the parameterized enclosing
- * type. Distinct from {@link #getOuterClass()}, which is the enclosing class
- * of a nested class <em>declaration</em>.
+ * type stored on this node (type-use, not declaration; not node metadata and
+ * not redirected). Distinct from {@link #getOuterClass()}, which is the
+ * enclosing class of a nested class <em>declaration</em>. Copied by
+ * {@link #getPlainNodeReference()}.
*
* @return the parameterized enclosing type, or {@code null} if this is not a rare type
* @since 6.0.0
*/
public ClassNode getOuterClassType() {
- return getNodeMetaData("outer.class");
+ return outerClassType;
}
/**
- * Records the parameterized enclosing type of a JLS 4.5 rare type.
+ * Records the parameterized enclosing type of a JLS 4.5 rare type on this
+ * node. Does not write through {@link #redirect()}.
*
* @param outer the parameterized {@code Outer<T>} node; {@code null} clears it
* @since 6.0.0
*/
public void setOuterClassType(final ClassNode outer) {
- putNodeMetaData("outer.class", outer);
+ this.outerClassType = outer;
}
/**
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java b/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
index 0b12674e16..3babbc05ec 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
@@ -478,7 +478,9 @@ public class BytecodeHelper {
*/
private static void writeParameterizedClass(StringBuilder ret, ClassNode printType) {
ClassNode owner = printType.getOuterClassType();
- if (owner != null) {
+ // JVMS 4.7.9.1 nested form is only for a parameterized enclosing type.
+ // A raw enclosing type must keep the binary name (Outer$Inner<...>).
+ if (owner != null && hasGenerics(owner)) {
writeParameterizedClass(ret, owner);
ret.append('.');
ret.append(innerClassSimpleName(printType, owner));
@@ -489,17 +491,39 @@ public class BytecodeHelper {
addSubTypes(ret, printType.getGenericsTypes(), "<", ">");
}
+ /**
+ * Simple name of {@code inner} relative to {@code owner} for a JVMS 4.7.9.1
+ * nested type signature. Accepts both {@code Outer.Inner} (parser) and
+ * {@code Outer$Inner} (binary name after resolve); {@code Foo$1} is the
+ * same prefix path. If {@code inner} is not nested under {@code owner},
+ * the result is the last identifier only — never extra qualification of
+ * {@code owner} (so owner {@code Foo} and inner {@code Bar.X} yield {@code X}).
+ */
private static String innerClassSimpleName(final ClassNode inner, final ClassNode owner) {
String innerName = inner.getName();
String ownerName = owner.getName();
- if (innerName.startsWith(ownerName) && innerName.length() > ownerName.length()) {
- char sep = innerName.charAt(ownerName.length());
- if (sep == '.' || sep == '$') {
- return innerName.substring(ownerName.length() + 1).replace('$', '.');
- }
+ String nested = nestedNameAfterOwner(innerName, ownerName);
+ if (nested != null) {
+ return nested.replace('$', '.');
+ }
+ int sep = Math.max(innerName.lastIndexOf('.'), innerName.lastIndexOf('$'));
+ return sep < 0 ? innerName : innerName.substring(sep + 1);
+ }
+
+ /**
+ * Remainder of {@code innerName} after {@code ownerName} when nested under
+ * it ({@code .} or {@code $} separator); otherwise {@code null}.
+ */
+ private static String nestedNameAfterOwner(final String innerName, final String ownerName) {
+ int ownerLen = ownerName.length();
+ if (innerName.length() <= ownerLen || !innerName.startsWith(ownerName)) {
+ return null;
+ }
+ char sep = innerName.charAt(ownerLen);
+ if (sep != '.' && sep != '$') {
+ return null;
}
- int dot = Math.max(innerName.lastIndexOf('.'), innerName.lastIndexOf('$'));
- return dot < 0 ? innerName : innerName.substring(dot + 1);
+ return innerName.substring(ownerLen + 1);
}
private static void writeGenericsBounds(StringBuilder ret, GenericsType type, boolean writeInterfaceMarker) {
diff --git a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
index 991cfdc548..341f06218f 100644
--- a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
@@ -212,14 +212,23 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
/**
* Groovy represents the type operand of {@code instanceof} as a
- * {@link ClassExpression}, but it is not a class literal. Skip it so
- * {@link InstanceOfVerifier} can diagnose JLS 15.20.2.
+ * {@link ClassExpression}, but it is not a class literal ({@code T.class}).
+ * Do not visit that operand as a class expression (that would reject
+ * {@code instanceof T} as {@code T.class}). Still run
+ * {@link #checkGenericsUsage(ClassNode)} so nested-type well-formedness
+ * applies ({@code Outer<?>.Inner} with a generic {@code Inner},
+ * {@code Outer.Inner<?>}). Parameterized {@code instanceof} is handled at
+ * parse ({@code AstBuilder.rejectParameterizedInstanceof}): the grammar
+ * allows type arguments so {@code instanceof List<?>} can parse, while
+ * {@code instanceof Map<String,Integer>} is rejected (JLS 15.20.2).
+ * {@link InstanceOfVerifier} then checks primitives and type-parameter targets.
*/
@Override
public void visitBinaryExpression(final BinaryExpression expression) {
if (expression.getOperation().isA(Types.INSTANCEOF_OPERATOR)
&& expression.getRightExpression() instanceof ClassExpression) {
expression.getLeftExpression().visit(this);
+ checkGenericsUsage(expression.getRightExpression().getType());
return;
}
super.visitBinaryExpression(expression);
@@ -288,14 +297,18 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
ClassNode oc = cn.getOuterClassType(); // GROOVY-12319: Outer<T>.Inner
if (oc != null) {
checkGenericsUsage(oc);
- // JLS 4.5 / 6.5.5: a parameterized enclosing type may only select a
- // non-static member type, and a generic member must be parameterized.
+ // JLS 4.5 / 4.8 / 6.5.5: a parameterized enclosing type may only
+ // select a non-static member type, and a generic member must be
+ // parameterized. Type arguments on a non-static member of a raw
+ // enclosing type ({@code Outer.Inner<?>}) are also illegal.
if (isParameterizedEnclosingType(oc)) {
if (isStaticMemberType(cn)) {
addError("Cannot refer to a static member of a generic type through a parameterization", cn);
} else if (cn.getGenericsTypes() == null && rn.getGenericsTypes() != null) {
addError("A raw member type may not be used with a parameterized enclosing type", cn);
}
+ } else if (cn.getGenericsTypes() != null && isRawGenericType(oc) && !isStaticMemberType(cn)) {
+ addError("Type arguments cannot be given on a raw nested type", cn);
}
}
GenericsType[] cnTypes = cn.getGenericsTypes();
@@ -433,6 +446,14 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
return gt != null && gt.length > 0 && !type.isGenericsPlaceHolder();
}
+ /**
+ * True when {@code type} is a raw use of a generic declaration ({@code Outer}
+ * for {@code class Outer<T>}).
+ */
+ private static boolean isRawGenericType(final ClassNode type) {
+ return type.getGenericsTypes() == null && type.redirect().getGenericsTypes() != null;
+ }
+
/**
* Nested interfaces and enums are implicitly static; otherwise the
* {@code ACC_STATIC} bit on the declaration is authoritative.
diff --git a/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java b/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java
index 7a303b01fa..68779b4309 100644
--- a/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java
+++ b/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java
@@ -52,9 +52,12 @@ public abstract class InstanceOfVerifier extends ClassCodeVisitorSupport {
if (referenceType.isGenericsPlaceHolder()) {
addTypeError(expression.getRightExpression(), "type parameter " + referenceType.getUnresolvedName() +
". Use its erasure " + referenceType.getNameWithoutPackage() + " instead since further generic type information will be erased at runtime");
- } else if (referenceType.getGenericsTypes() != null) {
- // TODO: Cannot perform instanceof check against parameterized type Class<Type>. Use the form Class<?> instead since further generic type information will be erased at runtime
}
+ // Parameterized uses (Map<String,Integer>) are not rejected here:
+ // resolved ClassNodes often still carry declaration placeholders
+ // (List<E>). The grammar allows type arguments so instanceof List<?>
+ // can parse; non-reifiable uses are rejected at parse by
+ // AstBuilder.rejectParameterizedInstanceof (JLS 15.20.2).
}
}
super.visitBinaryExpression(expression);
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index 0bcb6e0d69..bfe0a90fd8 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -422,8 +422,15 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
}
ClassNode oc = type.getOuterClassType(); // GROOVY-10646, GROOVY-12319: Outer<T>.Inner
if (oc != null) {
- resolveOrFail(oc, msg, node, preferImports);
- resolveGenericsTypes(oc.getGenericsTypes());
+ if (oc.getGenericsTypes() != null) {
+ resolveOrFail(oc, msg, node, preferImports);
+ resolveGenericsTypes(oc.getGenericsTypes());
+ } else if (resolve(oc, true, true, true)) {
+ resolveGenericsTypes(oc.getGenericsTypes());
+ } else {
+ // Prefix of a dotted name was a package, not a class (List<?>).
+ type.setOuterClassType(null);
+ }
}
if (preferImports && !type.isResolved() && !type.isPrimaryClassNode()) {
resolveGenericsTypes(type.getGenericsTypes());
@@ -490,10 +497,16 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
resolveGenericsTypes(genericsTypes);
ClassNode oc = type.getOuterClassType();
if (oc != null) {
- if (!resolve(oc, testModuleImports, testDefaultImports, testStaticInnerClasses)) {
- return false;
+ if (oc.getGenericsTypes() != null) {
+ if (!resolve(oc, testModuleImports, testDefaultImports, testStaticInnerClasses)) {
+ return false;
+ }
+ resolveGenericsTypes(oc.getGenericsTypes());
+ } else if (resolve(oc, testModuleImports, testDefaultImports, testStaticInnerClasses)) {
+ resolveGenericsTypes(oc.getGenericsTypes());
+ } else {
+ type.setOuterClassType(null);
}
- resolveGenericsTypes(oc.getGenericsTypes());
}
if (type.isPrimaryClassNode()) return true;
diff --git a/src/test/groovy/bugs/Groovy12319.groovy b/src/test/groovy/bugs/Groovy12319.groovy
index 2af644c6da..676c6a2cac 100644
--- a/src/test/groovy/bugs/Groovy12319.groovy
+++ b/src/test/groovy/bugs/Groovy12319.groovy
@@ -947,23 +947,6 @@ final class Groovy12319 {
assert err.message.contains('parameterized enclosing type')
}
- @Test
- void testInstanceofParameterizedRareTypeRejected() {
- def err = shouldFail CompilationFailedException, '''
- class Outer<T> {
- class Inner {}
- }
- def x = null
- x instanceof Outer<String>.Inner
- '''
- assert err.message.contains('parameterized type')
-
- err = shouldFail CompilationFailedException, '''
- def x = ([] instanceof java.util.ArrayList<String>)
- '''
- assert err.message.contains('parameterized type')
- }
-
@Test
void testConstructorTypeArgumentsWithFunctionalArgumentCompileStatic() {
assertScript '''
diff --git a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
index 6cca7ae739..9dacb83555 100644
--- a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
+++ b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
@@ -1769,8 +1769,11 @@ final class GenericsJavaCompatibilityTest {
}
/**
- * JLS 4.7, 15.20.2: {@code List<?>} and {@code Class<?>} are reifiable (unbounded
- * wildcards), so they may appear as the {@code instanceof} reference type.
+ * JLS 4.7, 15.20.2: {@code List<?>}, {@code ArrayList<?>}, {@code Class<?>}
+ * and {@code Map<?,?>} are reifiable (every type argument is an unbounded
+ * wildcard), as is the raw type {@code List}. A parameterized left operand
+ * such as {@code List<String> list} is legal; {@code null instanceof List<?>}
+ * is well-typed and yields {@code false}.
*/
@Test
void testReifiableInstanceofUnboundedWildcard() {
@@ -1778,12 +1781,88 @@ final class GenericsJavaCompatibilityTest {
final String javaSrc = '''
package gls.generics.test;
import java.util.ArrayList;
+ import java.util.HashMap;
import java.util.List;
+ import java.util.Map;
public class ReifiableInstanceof {
public static boolean test() {
- Object list = new ArrayList<String>();
- Object cls = ReifiableInstanceof.class.getClass();
- return (list instanceof List<?>) && (cls instanceof Class<?>);
+ List<String> list = new ArrayList<String>();
+ List<?> wildcard = list;
+ Map<String, Integer> map = new HashMap<String, Integer>();
+ Class<?> cls = ReifiableInstanceof.class.getClass();
+ List<String> none = null;
+ return (list instanceof List<?>)
+ && (list instanceof ArrayList<?>)
+ && (list instanceof List)
+ && (wildcard instanceof List<?>)
+ && (map instanceof Map<?, ?>)
+ && (cls instanceof Class<?>)
+ && !(none instanceof List<?>);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, true)
+ }
+
+ /**
+ * JLS 4.7, 15.20.2: an array type is reifiable when its element type is
+ * reifiable, so {@code o instanceof List<?>[]} is legal.
+ */
+ @Test
+ void testReifiableInstanceofArrayOfUnboundedWildcard() {
+ final String className = 'gls.generics.test.ReifiableInstanceofArray'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class ReifiableInstanceofArray {
+ public static boolean test() {
+ Object o = new List[0];
+ return o instanceof List<?>[];
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, true)
+ }
+
+ /**
+ * JLS 4.7, 15.20.2: a non-static member type of a reifiable type is itself
+ * reifiable, so {@code o instanceof Outer<?>.Inner} is legal when {@code Inner}
+ * is not generic.
+ */
+ @Test
+ void testReifiableInstanceofRareTypeUnboundedEnclosing() {
+ final String className = 'gls.generics.test.ReifiableRareInstanceof'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ReifiableRareInstanceof {
+ public static class Outer<T> {
+ public class Inner {}
+ }
+ public static boolean test() {
+ return !(null instanceof Outer<?>.Inner);
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, true)
+ }
+
+ /**
+ * JLS 4.7, 15.20.2 / JDK {@code T6665356}: when both the enclosing type and
+ * the member are generic, {@code Outer.Inner} (raw) and
+ * {@code Outer<?>.Inner<?>} (all unbounded wildcards) are reifiable.
+ */
+ @Test
+ void testReifiableInstanceofRawAndFullyWildcardNested() {
+ final String className = 'gls.generics.test.ReifiableNestedInstanceof'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ReifiableNestedInstanceof {
+ public static class Outer<T> {
+ public class Inner<U> {}
+ }
+ public static boolean test() {
+ return !(null instanceof Outer.Inner)
+ && !(null instanceof Outer<?>.Inner<?>);
}
}
'''
@@ -1857,6 +1936,239 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
}
+ /**
+ * JLS 4.7, 15.20.2: {@code Map<String,Integer>} is not reifiable.
+ */
+ @Test
+ void testNegativeNonReifiableInstanceofMap() {
+ final String className = 'gls.generics.test.NonReifiableInstanceofMap'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.Map;
+ public class NonReifiableInstanceofMap {
+ public static boolean test(Object o) {
+ return o instanceof Map<String, Integer>;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+ }
+
+ /**
+ * JLS 15.20.2: a non-reifiable {@code instanceof} is still illegal when the
+ * check is negated ({@code !(o instanceof Map<String,Integer>)}).
+ */
+ @Test
+ void testNegativeNegatedNonReifiableInstanceof() {
+ final String className = 'gls.generics.test.NegatedNonReifiableInstanceof'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.Map;
+ public class NegatedNonReifiableInstanceof {
+ public static boolean test(Object o) {
+ return !(o instanceof Map<String, Integer>);
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+ }
+
+ /**
+ * JLS 4.7, 15.20.2: {@code List<? extends CharSequence>} is not reifiable
+ * because the wildcard is bounded (GROOVY-11585).
+ */
+ @Test
+ void testNegativeInstanceofUpperBoundedWildcard() {
+ final String className = 'gls.generics.test.InstanceofUpperBoundedWildcard'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class InstanceofUpperBoundedWildcard {
+ public static boolean test(Object o) {
+ return o instanceof List<? extends CharSequence>;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+ }
+
+ /**
+ * JLS 4.7, 15.20.2: {@code List<? super Integer>} is not reifiable because
+ * the wildcard is bounded (GROOVY-11585).
+ */
+ @Test
+ void testNegativeInstanceofLowerBoundedWildcard() {
+ final String className = 'gls.generics.test.InstanceofLowerBoundedWildcard'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class InstanceofLowerBoundedWildcard {
+ public static boolean test(Object o) {
+ return o instanceof List<? super Integer>;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+ }
+
+ /**
+ * JLS 4.7, 15.20.2: {@code Outer<String>.Inner} is not reifiable — a nested
+ * type is reifiable only as a non-static member of a reifiable type, and
+ * {@code Outer<String>} is not. Contrast
+ * {@link #testReifiableInstanceofRareTypeUnboundedEnclosing()}.
+ */
+ @Test
+ void testNegativeInstanceofRareType() {
+ final String className = 'gls.generics.test.InstanceofRareType'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class InstanceofRareType {
+ public static class Outer<T> {
+ public class Inner {}
+ }
+ public static boolean test(Object o) {
+ return o instanceof Outer<String>.Inner;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+ }
+
+ /**
+ * JLS 4.7, 15.20.2: {@code List<String>[]} is not reifiable (the component
+ * type is not).
+ */
+ @Test
+ void testNegativeInstanceofArrayOfParameterizedType() {
+ final String className = 'gls.generics.test.InstanceofGenericArray'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.List;
+ public class InstanceofGenericArray {
+ public static boolean test(Object o) {
+ return o instanceof List<String>[];
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+ }
+
+ /**
+ * JLS 4.7, 15.20.2: {@code Map<?,String>} is not reifiable — every type
+ * argument must be an unbounded wildcard.
+ */
+ @Test
+ void testNegativeInstanceofMixedWildcard() {
+ final String className = 'gls.generics.test.InstanceofMixedWildcard'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.Map;
+ public class InstanceofMixedWildcard {
+ public static boolean test(Object o) {
+ return o instanceof Map<?, String>;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+ }
+
+ /**
+ * JLS 4.7, 15.20.2: a type variable is not reifiable, so {@code o instanceof T}
+ * is a compile-time error.
+ */
+ @Test
+ void testNegativeInstanceofTypeParameter() {
+ final String className = 'gls.generics.test.InstanceofTypeParameter'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class InstanceofTypeParameter {
+ public static <T> boolean test(Object o) {
+ return o instanceof T;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "type parameter")
+ }
+
+ /**
+ * JLS 4.7, 15.20.2: an array of a type variable is not reifiable
+ * ({@code o instanceof T[]}).
+ */
+ @Test
+ void testNegativeInstanceofTypeParameterArray() {
+ final String className = 'gls.generics.test.InstanceofTypeParameterArray'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class InstanceofTypeParameterArray {
+ public static <T> boolean test(Object o) {
+ return o instanceof T[];
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be safely cast", "type parameter")
+ }
+
+ /**
+ * JLS 4.5 / JDK {@code T6665356}: {@code Outer<?>.Inner} is malformed when
+ * {@code Inner} is generic (raw member of a parameterized enclosing type).
+ */
+ @Test
+ void testNegativeInstanceofRawGenericMemberOfParameterizedEnclosing() {
+ final String className = 'gls.generics.test.InstanceofRawMemberOfParameterized'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class InstanceofRawMemberOfParameterized {
+ public static class Outer<T> {
+ public class Inner<U> {}
+ }
+ public static boolean test(Object o) {
+ return o instanceof Outer<?>.Inner;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "improperly formed type", "parameterized enclosing type")
+ }
+
+ /**
+ * JLS 4.8 / JDK {@code T6665356}: {@code Outer.Inner<?>} gives type arguments
+ * on a member of a raw type.
+ */
+ @Test
+ void testNegativeInstanceofTypeArgsOnRawNested() {
+ final String className = 'gls.generics.test.InstanceofParamOnRawNested'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class InstanceofParamOnRawNested {
+ public static class Outer<T> {
+ public class Inner<U> {}
+ }
+ public static boolean test(Object o) {
+ return o instanceof Outer.Inner<?>;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "improperly formed type", "raw")
+ }
+
+ /**
+ * JLS 4.8 / JDK {@code T6665356}: the same malformed type is illegal as a
+ * field type, not only as an {@code instanceof} operand.
+ */
+ @Test
+ void testNegativeTypeArgsOnRawNestedField() {
+ final String className = 'gls.generics.test.RawNestedField'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class RawNestedField {
+ public static class Outer<T> {
+ public class Inner<U> {}
+ }
+ public Outer.Inner<?> field;
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "improperly formed type", "raw")
+ }
+
/**
* JLS 4.7, 15.20.2: {@code Class<C>} is not reifiable (the type argument is not
* an unbounded wildcard), so it may not be used with {@code instanceof}.
@@ -1875,6 +2187,24 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "cannot be converted", "Cannot perform instanceof")
}
+ /**
+ * JLS 4.7, 15.20.2 / JDK {@code InstanceOf3}: {@code Class<? extends C>} is
+ * not reifiable (the wildcard is bounded).
+ */
+ @Test
+ void testNegativeInstanceofClassUpperBoundedWildcard() {
+ final String className = 'gls.generics.test.InstanceofClassUpperBound'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class InstanceofClassUpperBound {
+ public static boolean test() {
+ return InstanceofClassUpperBound.class.getClass() instanceof Class<? extends InstanceofClassUpperBound>;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be converted", "Cannot perform instanceof")
+ }
+
/**
* JLS 6.5.5.1, 8.1.2: a type parameter of the enclosing class is not in scope
* from a static nested class.
diff --git a/src/test/groovy/groovy/InstanceofTest.groovy b/src/test/groovy/groovy/InstanceofTest.groovy
index 9257ad8bda..e900cd2a26 100644
--- a/src/test/groovy/groovy/InstanceofTest.groovy
+++ b/src/test/groovy/groovy/InstanceofTest.groovy
@@ -68,7 +68,9 @@ final class InstanceofTest {
assert !new Object() instanceof Boolean
}
- // GROOVY-11585
+ // GROOVY-11585 — full Java-vs-Groovy matrix is GenericsJavaCompatibilityTest
+ // (reifiable List<?>/Map<?,?>, non-reifiable List<String>/Map<String,Integer>,
+ // bounded wildcards, rare types). This is the named regression only.
@Test
void testGenerics() {
assert [] instanceof List<?>
diff --git a/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java b/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
index e9523860cd..d9b9ae6b52 100644
--- a/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
+++ b/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
@@ -174,6 +174,25 @@ public final class ClassNodeTest {
classNode.setOuterClassType(null);
assertNull(classNode.getOuterClassType());
assertNull(classNode.getPlainNodeReference().getOuterClassType());
+
+ // A leftover metadata key must not be treated as the enclosing type.
+ classNode.putNodeMetaData("outer.class", outer);
+ assertNull(classNode.getOuterClassType());
+ assertNull(classNode.getPlainNodeReference().getOuterClassType());
+ classNode.setOuterClassType(outer);
+ assertEquals(outer, classNode.getOuterClassType());
+ classNode.setOuterClassType(null);
+ assertNull(classNode.getOuterClassType());
+ assertEquals(outer, classNode.getNodeMetaData("outer.class"));
+
+ // Type-use field is not proxied through redirect, like getGenericsTypes().
+ ClassNode proxy = ClassHelper.makeWithoutCaching("Foo$Inner");
+ proxy.setRedirect(classNode);
+ classNode.setOuterClassType(outer);
+ assertNull(proxy.getOuterClassType());
+ proxy.setOuterClassType(outer);
+ assertEquals(outer, proxy.getOuterClassType());
+ assertEquals(outer, classNode.getOuterClassType());
}
@Test
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy b/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy
index d2d2794278..5dc2162672 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy
@@ -91,9 +91,7 @@ class BytecodeHelperTest extends AbstractBytecodeTestCase {
inner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
String typeSig = BytecodeHelper.getTypeGenericsSignature(inner)
- assert typeSig.contains('Outer')
- assert typeSig.contains('Inner')
- assert typeSig.contains('String')
+ assert typeSig == 'Lcom/example/Outer<Ljava/lang/String;>.Inner<Ljava/lang/Integer;>;'
ClassNode list = ClassHelper.LIST_TYPE.getPlainNodeReference()
list.genericsTypes = [new GenericsType(ClassHelper.STRING_TYPE)] as GenericsType[]
@@ -116,8 +114,61 @@ class BytecodeHelperTest extends AbstractBytecodeTestCase {
ClassNode mismatchedInner = ClassHelper.makeWithoutCaching('totally.Different$Item')
mismatchedInner.outerClassType = outer
mismatchedInner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
- String fallback = BytecodeHelper.getTypeGenericsSignature(mismatchedInner)
- assert fallback.contains('Item')
+ assert BytecodeHelper.getTypeGenericsSignature(mismatchedInner) == 'Lcom/example/Outer<Ljava/lang/String;>.Item<Ljava/lang/Integer;>;'
+
+ // Parser records Outer.Inner; after resolve the name is Outer$Inner.
+ ClassNode dottedInner = ClassHelper.makeWithoutCaching('com.example.Outer.Inner')
+ dottedInner.outerClassType = outer
+ dottedInner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+ assert BytecodeHelper.getTypeGenericsSignature(dottedInner) == 'Lcom/example/Outer<Ljava/lang/String;>.Inner<Ljava/lang/Integer;>;'
+
+ ClassNode dollarInner = ClassHelper.makeWithoutCaching('com.example.Outer$Inner')
+ dollarInner.outerClassType = outer
+ dollarInner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+ assert BytecodeHelper.getTypeGenericsSignature(dollarInner) == 'Lcom/example/Outer<Ljava/lang/String;>.Inner<Ljava/lang/Integer;>;'
+
+ // Anonymous: owner Foo, inner Foo$1 — still a prefix, not the fallback.
+ ClassNode anonOwner = ClassHelper.makeWithoutCaching('com.example.Foo')
+ anonOwner.genericsTypes = [new GenericsType(ClassHelper.STRING_TYPE)] as GenericsType[]
+ ClassNode anon = ClassHelper.makeWithoutCaching('com.example.Foo$1')
+ anon.outerClassType = anonOwner
+ assert BytecodeHelper.getTypeGenericsSignature(anon) == 'Lcom/example/Foo<Ljava/lang/String;>.1;'
+
+ // Unrelated inner name Bar.X is not extra qualification of owner Foo.
+ ClassNode unrelated = ClassHelper.makeWithoutCaching('Bar.X')
+ unrelated.outerClassType = anonOwner
+ assert BytecodeHelper.getTypeGenericsSignature(unrelated) == 'Lcom/example/Foo<Ljava/lang/String;>.X;'
+
+ // Simple identifier with no '.' / '$' (fallback sep < 0).
+ ClassNode simple = ClassHelper.makeWithoutCaching('Item')
+ simple.outerClassType = anonOwner
+ assert BytecodeHelper.getTypeGenericsSignature(simple) == 'Lcom/example/Foo<Ljava/lang/String;>.Item;'
+
+ // Equal names: not nested (length <= owner).
+ ClassNode sameName = ClassHelper.makeWithoutCaching('com.example.Foo')
+ sameName.outerClassType = anonOwner
+ sameName.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+ assert BytecodeHelper.getTypeGenericsSignature(sameName) == 'Lcom/example/Foo<Ljava/lang/String;>.Foo<Ljava/lang/Integer;>;'
+
+ // Prefix that is not a nesting separator (Foo vs FooBar).
+ ClassNode notNested = ClassHelper.makeWithoutCaching('com.example.FooBar')
+ notNested.outerClassType = anonOwner
+ assert BytecodeHelper.getTypeGenericsSignature(notNested) == 'Lcom/example/Foo<Ljava/lang/String;>.FooBar;'
+
+ // Remainder after owner may contain further $ nesting.
+ ClassNode deepRemainder = ClassHelper.makeWithoutCaching('com.example.Outer$Middle$Inner')
+ deepRemainder.outerClassType = outer
+ deepRemainder.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+ assert BytecodeHelper.getTypeGenericsSignature(deepRemainder) == 'Lcom/example/Outer<Ljava/lang/String;>.Middle.Inner<Ljava/lang/Integer;>;'
+
+ // Recursive enclosing types: Outer<T>.Middle<U>.Inner<V>.
+ ClassNode middle = ClassHelper.makeWithoutCaching('com.example.Outer$Middle')
+ middle.outerClassType = outer
+ middle.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+ ClassNode inner2 = ClassHelper.makeWithoutCaching('com.example.Outer$Middle$Inner')
+ inner2.outerClassType = middle
+ inner2.genericsTypes = [new GenericsType(ClassHelper.Long_TYPE)] as GenericsType[]
+ assert BytecodeHelper.getTypeGenericsSignature(inner2) == 'Lcom/example/Outer<Ljava/lang/String;>.Middle<Ljava/lang/Integer;>.Inner<Ljava/lang/Long;>;'
ClassNode superDiamond = ClassHelper.LIST_TYPE.getPlainNodeReference()
superDiamond.genericsTypes = GenericsType.EMPTY_ARRAY
(groovy) 04/05: GROOVY-12319: clarify generics compile error messages
Posted by su...@apache.org.
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 d5c8152c5c31df3bb119972b0dc8722b0ef30211
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Aug 30 17:31:04 2026 +0000
GROOVY-12319: clarify generics compile error messages
Align Groovy diagnostics with javac for static type-parameter references,
wildcard bounds, instanceof of rare and array types, duplicate generic
interfaces, and static nested types selected from a parameterization.
---
.../apache/groovy/parser/antlr4/AstBuilder.java | 75 ++++++++++++++++++++--
.../org/codehaus/groovy/classgen/Verifier.java | 2 +-
.../codehaus/groovy/control/GenericsVisitor.java | 42 ++++++++++--
.../groovy/control/InstanceOfVerifier.java | 2 +-
.../codehaus/groovy/control/ResolveVisitor.java | 25 ++++++++
src/test/groovy/bugs/Groovy12319.groovy | 8 +--
.../generics/GenericsJavaCompatibilityTest.groovy | 12 ++--
7 files changed, 141 insertions(+), 25 deletions(-)
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index e4b4fdef9b..32387d72d1 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -4656,6 +4656,10 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
classNode = this.createArrayType(classNode, typeAnnotations);
}
+ if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR)) {
+ rejectNonReifiableInstanceof(ctx, classNode);
+ }
+
return configureAST(classNode, ctx);
}
@@ -4694,7 +4698,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
attachRawEnclosingIfQualified(classNode);
}
classNode.setGenericsTypes(generics);
- rejectParameterizedInstanceof(ctx, classNode, generics);
+ if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR) && isNonReifiableSourceArgs(generics)) {
+ classNode.putNodeMetaData(NON_REIFIABLE_INSTANCEOF, Boolean.TRUE);
+ }
} else if (child instanceof TypeArgumentsOrDiamondContext tad) {
if (attachRawEnclosing) {
attachRawEnclosingIfQualified(classNode);
@@ -4728,14 +4734,68 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
/**
* {@code instanceof} type arguments are legal in the grammar so {@code List<?>}
- * can parse (JLS 15.20.2 / 4.7). Concrete arguments such as
- * {@code Map<String,Integer>} are not reifiable and are rejected here.
+ * can parse (JLS 15.20.2 / 4.7). Non-reifiable uses such as
+ * {@code Map<String,Integer>}, {@code Outer<String>.Inner} and
+ * {@code List<String>[]} are rejected once the full type (nesting and
+ * array dimensions) is built.
*/
- private void rejectParameterizedInstanceof(final GroovyParserRuleContext ctx, final ClassNode classNode, final GenericsType[] generics) {
- if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR) && Arrays.stream(generics).anyMatch(gt ->
- !gt.isWildcard() || asBoolean(gt.getLowerBound()) || ArrayGroovyMethods.asBoolean(gt.getUpperBounds()))) { // GROOVY-11585
- throw this.createParsingFailedException("Cannot perform instanceof check against parameterized type " + classNode, ctx);
+ private void rejectNonReifiableInstanceof(final GroovyParserRuleContext ctx, final ClassNode classNode) {
+ if (carriesNonReifiableInstanceofArg(classNode)) { // GROOVY-11585
+ throw this.createParsingFailedException(
+ "Cannot perform instanceof check against parameterized type " + describeTypeUse(classNode)
+ + " since further generic type information will be erased at runtime", ctx);
+ }
+ }
+
+ /**
+ * True when the source wrote a non-reifiable type argument (not merely a
+ * resolved ClassNode still carrying declaration placeholders such as
+ * {@code List<E>}).
+ */
+ private static boolean isNonReifiableSourceArgs(final GenericsType[] generics) {
+ return Arrays.stream(generics).anyMatch(gt ->
+ !gt.isWildcard() || asBoolean(gt.getLowerBound()) || ArrayGroovyMethods.asBoolean(gt.getUpperBounds()));
+ }
+
+ private static boolean carriesNonReifiableInstanceofArg(ClassNode type) {
+ while (type.isArray()) {
+ type = type.getComponentType();
+ }
+ if (Boolean.TRUE.equals(type.getNodeMetaData(NON_REIFIABLE_INSTANCEOF))) {
+ return true;
+ }
+ ClassNode outer = type.getOuterClassType();
+ return outer != null && carriesNonReifiableInstanceofArg(outer);
+ }
+
+ /**
+ * Source-like display of a type use, including rare enclosing arguments
+ * ({@code Outer<String>.Inner}) and array brackets.
+ */
+ private static String describeTypeUse(final ClassNode type) {
+ if (type.isArray()) {
+ return describeTypeUse(type.getComponentType()) + "[]";
+ }
+ StringBuilder sb = new StringBuilder();
+ ClassNode outer = type.getOuterClassType();
+ if (outer != null) {
+ sb.append(describeTypeUse(outer)).append('.');
+ String name = type.getName();
+ int sep = Math.max(name.lastIndexOf('.'), name.lastIndexOf('$'));
+ sb.append(sep < 0 ? name : name.substring(sep + 1));
+ } else {
+ sb.append(type.getNameWithoutPackage());
+ }
+ GenericsType[] generics = type.getGenericsTypes();
+ if (generics != null && generics.length > 0 && !type.isGenericsPlaceHolder()) {
+ sb.append('<');
+ for (int i = 0; i < generics.length; i++) {
+ if (i > 0) sb.append(", ");
+ sb.append(generics[i]);
+ }
+ sb.append('>');
}
+ return sb.toString();
}
@Override
@@ -5552,6 +5612,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
private static final String CLASS_NAME = "_CLASS_NAME";
private static final String INSIDE_PARENTHESES_LEVEL = "_INSIDE_PARENTHESES_LEVEL";
private static final String IS_INSIDE_INSTANCEOF_EXPR = "_IS_INSIDE_INSTANCEOF_EXPR";
+ private static final String NON_REIFIABLE_INSTANCEOF = "_NON_REIFIABLE_INSTANCEOF";
private static final String IS_SWITCH_DEFAULT = "_IS_SWITCH_DEFAULT";
private static final String IS_NUMERIC = "_IS_NUMERIC";
private static final String IS_STRING = "_IS_STRING";
diff --git a/src/main/java/org/codehaus/groovy/classgen/Verifier.java b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
index d948be8d36..f66da545e4 100644
--- a/src/main/java/org/codehaus/groovy/classgen/Verifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
@@ -474,7 +474,7 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
if (nInterfaces > 1) {
List<String> interfaceNames = new ArrayList<>(nInterfaces);
- for (ClassNode in : interfaces) interfaceNames.add(in.getName());
+ for (ClassNode in : interfaces) interfaceNames.add(in.toString(false));
if (interfaceNames.size() != new HashSet<>(interfaceNames).size()) {
throw new RuntimeParserException("Duplicate interfaces in implements list: " + interfaceNames, cn);
}
diff --git a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
index 341f06218f..972c72c754 100644
--- a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
@@ -191,7 +191,7 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
public void visitArrayExpression(final ArrayExpression expression) {
ClassNode elementType = expression.getElementType();
if (!isReifiable(elementType)) {
- addError("generic array creation", expression);
+ addError("generic array creation of " + describeTypeUse(elementType), expression);
}
checkGenericsUsage(expression.getType());
@@ -218,7 +218,7 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
* {@link #checkGenericsUsage(ClassNode)} so nested-type well-formedness
* applies ({@code Outer<?>.Inner} with a generic {@code Inner},
* {@code Outer.Inner<?>}). Parameterized {@code instanceof} is handled at
- * parse ({@code AstBuilder.rejectParameterizedInstanceof}): the grammar
+ * parse ({@code AstBuilder#rejectNonReifiableInstanceof}): the grammar
* allows type arguments so {@code instanceof List<?>} can parse, while
* {@code instanceof Map<String,Integer>} is rejected (JLS 15.20.2).
* {@link InstanceOfVerifier} then checks primitives and type-parameter targets.
@@ -303,12 +303,12 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
// enclosing type ({@code Outer.Inner<?>}) are also illegal.
if (isParameterizedEnclosingType(oc)) {
if (isStaticMemberType(cn)) {
- addError("Cannot refer to a static member of a generic type through a parameterization", cn);
+ addError("Cannot select a static nested type from a parameterized type", cn);
} else if (cn.getGenericsTypes() == null && rn.getGenericsTypes() != null) {
- addError("A raw member type may not be used with a parameterized enclosing type", cn);
+ addError("A raw member type may not be used with a parameterized enclosing type: " + describeTypeUse(cn), cn);
}
} else if (cn.getGenericsTypes() != null && isRawGenericType(oc) && !isStaticMemberType(cn)) {
- addError("Type arguments cannot be given on a raw nested type", cn);
+ addError("Type arguments cannot be given on a raw nested type: " + describeTypeUse(cn), cn);
}
}
GenericsType[] cnTypes = cn.getGenericsTypes();
@@ -347,7 +347,8 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
// check nested type parameters
checkGenericsUsage(cnType);
if (!isTypeArgumentWithinBounds(cnTypes[i], rnTypes[i])) {
- addError("The type " + cnTypes[i].getName() + " is not a valid substitute for the bounded parameter <" + rnTypes[i] + ">", cnTypes[i]);
+ String argument = cnTypes[i].isWildcard() ? cnTypes[i].toString() : cnTypes[i].getName();
+ addError("The type " + argument + " is not a valid substitute for the bounded parameter <" + rnTypes[i] + ">", cnTypes[i]);
}
}
}
@@ -454,6 +455,35 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
return type.getGenericsTypes() == null && type.redirect().getGenericsTypes() != null;
}
+ /**
+ * Source-like display of a type use, including rare enclosing arguments.
+ */
+ private static String describeTypeUse(final ClassNode type) {
+ if (type.isArray()) {
+ return describeTypeUse(type.getComponentType()) + "[]";
+ }
+ StringBuilder sb = new StringBuilder();
+ ClassNode outer = type.getOuterClassType();
+ if (outer != null) {
+ sb.append(describeTypeUse(outer)).append('.');
+ String name = type.getName();
+ int sep = Math.max(name.lastIndexOf('.'), name.lastIndexOf('$'));
+ sb.append(sep < 0 ? name : name.substring(sep + 1));
+ } else {
+ sb.append(type.getNameWithoutPackage());
+ }
+ GenericsType[] generics = type.getGenericsTypes();
+ if (generics != null && generics.length > 0 && !type.isGenericsPlaceHolder()) {
+ sb.append('<');
+ for (int i = 0; i < generics.length; i++) {
+ if (i > 0) sb.append(", ");
+ sb.append(generics[i]);
+ }
+ sb.append('>');
+ }
+ return sb.toString();
+ }
+
/**
* Nested interfaces and enums are implicitly static; otherwise the
* {@code ACC_STATIC} bit on the declaration is authoritative.
diff --git a/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java b/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java
index 68779b4309..1a83f2cb8e 100644
--- a/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java
+++ b/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java
@@ -57,7 +57,7 @@ public abstract class InstanceOfVerifier extends ClassCodeVisitorSupport {
// resolved ClassNodes often still carry declaration placeholders
// (List<E>). The grammar allows type arguments so instanceof List<?>
// can parse; non-reifiable uses are rejected at parse by
- // AstBuilder.rejectParameterizedInstanceof (JLS 15.20.2).
+ // AstBuilder.rejectNonReifiableInstanceof (JLS 15.20.2).
}
}
super.visitBinaryExpression(expression);
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index bfe0a90fd8..b3d77378e0 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -439,9 +439,34 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
if (resolve(type)) return;
if (resolveToInner(type)) return;
+ if (!type.hasPackageName() && isTypeParameterHiddenByStatic(type.getUnresolvedName())) {
+ addError("The non-static type parameter " + type.getUnresolvedName() + " cannot be referenced from a static context", node);
+ return;
+ }
addError("unable to resolve class " + type.toString(false) + msg, node);
}
+ /**
+ * True when {@code name} is a type parameter of {@code currentClass} or an
+ * enclosing class, but is not in {@link #genericParameterNames} because the
+ * current context is static (JLS 6.5.5.1).
+ */
+ private boolean isTypeParameterHiddenByStatic(final String name) {
+ if (genericParameterNames.containsKey(new GenericsTypeName(name))) {
+ return false;
+ }
+ for (ClassNode cn = currentClass; cn != null; cn = cn.getOuterClass()) {
+ GenericsType[] typeParameters = cn.getGenericsTypes();
+ if (typeParameters == null) continue;
+ for (GenericsType typeParameter : typeParameters) {
+ if (name.equals(typeParameter.getName())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Attempts to resolve an unqualified type against visible inner classes.
*
diff --git a/src/test/groovy/bugs/Groovy12319.groovy b/src/test/groovy/bugs/Groovy12319.groovy
index 676c6a2cac..65bf7b1523 100644
--- a/src/test/groovy/bugs/Groovy12319.groovy
+++ b/src/test/groovy/bugs/Groovy12319.groovy
@@ -918,7 +918,7 @@ final class Groovy12319 {
}
new Outer<?>.Nested[0]
'''
- assert err.message.contains('parameterization')
+ assert err.message.contains('static nested type')
err = shouldFail CompilationFailedException, '''
class Outer<T> {
@@ -926,14 +926,14 @@ final class Groovy12319 {
}
Outer<?>.Nested x
'''
- assert err.message.contains('parameterization')
+ assert err.message.contains('static nested type')
// Nested interfaces are implicitly static (JLS 9.5); Map.Entry is the
// JDK example of the same rule.
err = shouldFail CompilationFailedException, '''
new java.util.Map<?,?>.Entry[0]
'''
- assert err.message.contains('parameterization')
+ assert err.message.contains('static nested type')
}
@Test
@@ -1008,7 +1008,7 @@ final class Groovy12319 {
Integer id(Integer u) { u }
}
'''
- assert err.message.contains('parameterization')
+ assert err.message.contains('static nested type')
}
@Test
diff --git a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
index 9dacb83555..a4c954ddb9 100644
--- a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
+++ b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
@@ -913,7 +913,7 @@ final class GenericsJavaCompatibilityTest {
public int compareTo(Integer o) { return 0; }
}
'''
- assertNegativeCompile(className, javaSrc, "different arguments", "Duplicate interfaces in implements list")
+ assertNegativeCompile(className, javaSrc, "different arguments", "cannot be implemented more than once with different arguments")
}
/**
@@ -2220,7 +2220,7 @@ final class GenericsJavaCompatibilityTest {
}
}
'''
- assertNegativeCompile(className, javaSrc, "non-static type variable", "unable to resolve class T")
+ assertNegativeCompile(className, javaSrc, "non-static type variable", "cannot be referenced from a static context")
}
/**
@@ -2236,7 +2236,7 @@ final class GenericsJavaCompatibilityTest {
static T field;
}
'''
- assertNegativeCompile(className, javaSrc, "non-static type variable", "unable to resolve class T")
+ assertNegativeCompile(className, javaSrc, "non-static type variable", "cannot be referenced from a static context")
}
/**
@@ -2550,7 +2550,7 @@ final class GenericsJavaCompatibilityTest {
}
}
'''
- assertNegativeCompile(className, javaSrc, "cannot select a static class from a parameterized type", "parameterization")
+ assertNegativeCompile(className, javaSrc, "cannot select a static class from a parameterized type", "static nested type")
}
/**
@@ -2574,7 +2574,7 @@ final class GenericsJavaCompatibilityTest {
}
}
'''
- assertNegativeCompile(className, javaSrc, "cannot select a static class from a parameterized type", "parameterization")
+ assertNegativeCompile(className, javaSrc, "cannot select a static class from a parameterized type", "static nested type")
}
/**
@@ -2593,7 +2593,7 @@ final class GenericsJavaCompatibilityTest {
}
}
'''
- assertNegativeCompile(className, javaSrc, "cannot select a static class from a parameterized type", "parameterization")
+ assertNegativeCompile(className, javaSrc, "cannot select a static class from a parameterized type", "static nested type")
}
/**
(groovy) 05/05: GROOVY-12319: document JLS well-formedness breaks and fix remaining diagnostics
Posted by su...@apache.org.
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 d6ef127e4772983e53b6c7c3d4ca2420fb8b43de
Author: Daniel Sun <su...@apache.org>
AuthorDate: Mon Aug 31 13:51:44 2026 +0000
GROOVY-12319: document JLS well-formedness breaks and fix remaining diagnostics
Reject generic array creation and generic Throwable without carve-outs.
Name type variables in generic-array errors, use the class-literal
diagnostic for Foo<String>.class, and record rare enclosing types on
nested class expressions. Document the Groovy 6 breaks in
core-object-orientation §Generics.
---
.../codehaus/groovy/control/GenericsVisitor.java | 91 ++++++++++++++++++++--
.../codehaus/groovy/control/ResolveVisitor.java | 8 ++
src/spec/doc/core-object-orientation.adoc | 80 ++++++++++++++++++-
.../test/objectorientation/GenericsTest.groovy | 69 ++++++++++++++++
src/test/groovy/bugs/Groovy12319.groovy | 41 ++++++++++
.../generics/GenericsJavaCompatibilityTest.groovy | 21 +++++
6 files changed, 299 insertions(+), 11 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
index 972c72c754..a27b2d5897 100644
--- a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
@@ -199,13 +199,25 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
}
/**
- * JLS 15.8.2: a class literal may not name a type variable.
+ * Depth of {@link ClassExpression} qualifiers that are not class
+ * literals ({@code Cell<String>.ID}, {@code Cell<String>.id()}).
+ * {@link #visitClassExpression} skips the JLS 15.8.2 check while
+ * this is positive so those sites keep the JLS 4.5.2 diagnostic.
+ */
+ private int suppressClassLiteralChecks;
+
+ /**
+ * JLS 15.8.2: a class literal may not name a type variable or a
+ * parameterized type ({@code T.class}, {@code Cell<String>.class}).
+ * After resolve, {@code Foo.class} is a {@link ClassExpression};
+ * {@code Foo<String>.class} may still be a {@link PropertyExpression}
+ * whose object is that class expression — see
+ * {@link #visitPropertyExpression}.
*/
@Override
public void visitClassExpression(final ClassExpression expression) {
- ClassNode type = expression.getType();
- if (type.isGenericsPlaceHolder()) {
- addError("Cannot select from a type parameter " + type.getUnresolvedName(), expression);
+ if (suppressClassLiteralChecks == 0) {
+ checkClassLiteral(expression.getType(), expression);
}
super.visitClassExpression(expression);
}
@@ -240,17 +252,46 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
*/
@Override
public void visitMethodCallExpression(final MethodCallExpression call) {
- checkStaticMemberViaParameterizedType(call.getObjectExpression(), call);
- super.visitMethodCallExpression(call);
+ Expression object = call.getObjectExpression();
+ checkStaticMemberViaParameterizedType(object, call);
+ if (object instanceof ClassExpression) {
+ suppressClassLiteralChecks++;
+ try {
+ super.visitMethodCallExpression(call);
+ } finally {
+ suppressClassLiteralChecks--;
+ }
+ } else {
+ super.visitMethodCallExpression(call);
+ }
}
/**
* JLS 4.5.2 also applies to static fields accessed as {@code Cell<String>.ID}.
+ * JLS 15.8.2: {@code Cell<String>.class} is a class literal, not a static
+ * member selection — do not use the 4.5.2 diagnostic for {@code .class}.
*/
@Override
public void visitPropertyExpression(final PropertyExpression expression) {
- checkStaticMemberViaParameterizedType(expression.getObjectExpression(), expression);
- super.visitPropertyExpression(expression);
+ Expression object = expression.getObjectExpression();
+ // Only TypeName.class is a class literal (JLS 15.8.2).
+ // value.class / value*.class is getClass() and must stay legal.
+ if (isClassLiteralProperty(expression) && object instanceof ClassExpression) {
+ checkClassLiteral(object.getType(), expression);
+ expression.getProperty().visit(this);
+ return;
+ }
+ checkStaticMemberViaParameterizedType(object, expression);
+ if (object instanceof ClassExpression) {
+ suppressClassLiteralChecks++;
+ try {
+ super.visitPropertyExpression(expression);
+ } finally {
+ suppressClassLiteralChecks--;
+ }
+ } else {
+ super.visitPropertyExpression(expression);
+ }
}
/**
@@ -273,6 +314,37 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
}
}
+ /**
+ * JLS 15.8.2: {@code TypeName} in a class literal may not denote a type
+ * variable or a parameterized type (including a rare type whose enclosing
+ * type is parameterized). Arrays of those types are likewise illegal.
+ */
+ private void checkClassLiteral(final ClassNode type, final Expression location) {
+ ClassNode element = type;
+ while (element.isArray()) {
+ element = element.getComponentType();
+ }
+ if (element.isGenericsPlaceHolder()) {
+ addError("Cannot select from a type parameter " + element.getUnresolvedName(), location);
+ return;
+ }
+ if (isParameterizedClassLiteralType(element)) {
+ addError("Cannot select from a parameterized type", location);
+ }
+ }
+
+ private static boolean isClassLiteralProperty(final PropertyExpression expression) {
+ return "class".equals(expression.getPropertyAsString());
+ }
+
+ private static boolean isParameterizedClassLiteralType(final ClassNode type) {
+ if (isParameterizedTypeUsage(type)) {
+ return true;
+ }
+ ClassNode oc = type.getOuterClassType();
+ return oc != null && (isParameterizedEnclosingType(oc) || isParameterizedClassLiteralType(oc));
+ }
+
private boolean checkWildcard(final ClassNode sn) {
boolean wildcard = false;
if (sn.getGenericsTypes() != null) {
@@ -462,6 +534,9 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
if (type.isArray()) {
return describeTypeUse(type.getComponentType()) + "[]";
}
+ if (type.isGenericsPlaceHolder()) {
+ return type.getUnresolvedName();
+ }
StringBuilder sb = new StringBuilder();
ClassNode outer = type.getOuterClassType();
if (outer != null) {
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index b3d77378e0..820b8df030 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -1173,6 +1173,14 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
ClassNode type = new ConstructedNestedClass(propertyOwner, xe.getPropertyAsString());
if (resolve(type, false, false, false)) {
if (propertyOwner == objectExpression.getType() || isVisibleNestedClass(type, objectExpression.getType())) {
+ // Preserve Outer<T>.Inner as a rare type use so
+ // Outer<String>.Inner.class is a parameterized class
+ // literal (JLS 15.8.2), not a raw Inner.
+ ClassNode owner = objectExpression.getType();
+ if ((owner.getGenericsTypes() != null && owner.getGenericsTypes().length > 0)
+ || owner.getOuterClassType() != null) {
+ type.setOuterClassType(owner);
+ }
return new ClassExpression(type);
}
}
diff --git a/src/spec/doc/core-object-orientation.adoc b/src/spec/doc/core-object-orientation.adoc
index 1e6d45e2e7..2be2aa8cd6 100644
--- a/src/spec/doc/core-object-orientation.adoc
+++ b/src/spec/doc/core-object-orientation.adoc
@@ -120,9 +120,83 @@ List<String> roles = ['Trinity', 'Morpheus']
----
Java employs type erasure for backwards compatibility with earlier versions of Java.
-Dynamic Groovy can be thought of as more aggressively applying type erasure.
-In general, less generics type information will be checked at compile time.
-Groovy's static nature employs similar checks to Java with regard to generics information.
+From Groovy 6, every compilation unit — dynamic Groovy and scripts included —
+requires generic types to be well-formed, following the same JLS rules as Java.
+Dynamic Groovy still does not check generic invariance, inference, or
+unchecked conversions; those remain the domain of
+`@TypeChecked` / `@CompileStatic`.
+
+[[generics-well-formedness]]
+==== Well-formedness (Groovy 6)
+
+The constructs below compiled on Groovy 4 and 5 (typically by silently using
+the erasure). They are compile errors from Groovy 6, in both dynamic and
+static compilation. `new List<?>[n]` remains legal: an unbounded wildcard is
+reifiable.
+
+[cols="2,3,3"]
+.Generic well-formedness rejected in Groovy 6
+|====
+| Construct
+| Why
+| Workaround
+
+| `new T[n]` / `new T[]{ t }`
+| JLS 15.10.1, 4.7: array creation requires a reifiable component type. Groovy 4/5 emitted `Object[]` (or the bound) typed as `T[]`.
+| `(T[]) new Object[n]`, or `(T[]) new Bound[n]` when `T extends Bound`; or `Array.newInstance` with a `Class<T>` token
+
+| `class E<T> extends Exception`
+| JLS 8.1.2: a generic class may not extend `Throwable`
+| Move the type parameter onto an accessor or factory; catch the erased class
+
+| `new List<String>[n]`
+| JLS 15.10.1: a parameterized component type is not reifiable (`ArrayStoreException` cannot see the type argument)
+| `new List<?>[n]`, or an explicit `(List<String>[]) new List[n]`
+
+| `List<int>` (and nested)
+| JLS 4.5: a type argument must be a reference type (a primitive cannot appear in a `Signature` attribute)
+| `List<Integer>` (`List<int[]>` is already legal)
+
+| `new T()` / `T.class`
+| JLS 15.9.1 / 15.8.2: a type variable is not a class that can be instantiated or named by a class literal
+| Pass a `Class<T>` token
+
+| `implements X<? extends …>`
+| JLS 8.1.5: a superinterface may not specify a wildcard (the superclass form was already rejected)
+| A concrete type argument, or the raw type
+
+| `new ArrayList<?>()`
+| JLS 15.9: a class instance creation may not mention a wildcard
+| `new ArrayList<>()` or `new ArrayList<Object>()`
+
+| `<T extends A & B>` with a class type not first
+| JLS 4.4: only the first bound may be a class or type variable; additional bounds are interfaces
+| Put the class bound first: `<T extends C & I>`
+
+| `Foo<String>.class`
+| JLS 15.8.2: a class literal may not name a parameterized type (there is one `Class` per erasure)
+| `Foo.class`
+
+| `Outer.Inner<String>` with raw generic `Outer`
+| JLS 4.8: a non-static member of a raw type is raw
+| `Outer<?>.Inner<String>`
+|====
+
+Generic array creation is not a Groovy shortcut for an unchecked cast.
+Write the Java idiom:
+
+[source,groovy]
+----
+include::../test/objectorientation/GenericsTest.groovy[tags=generic_array_workaround,indent=0]
+----
+
+A generic class may not extend `Throwable`. Move the type parameter onto an
+accessor if a typed payload is needed:
+
+[source,groovy]
+----
+include::../test/objectorientation/GenericsTest.groovy[tags=generic_throwable_workaround,indent=0]
+----
== Classes
diff --git a/src/spec/test/objectorientation/GenericsTest.groovy b/src/spec/test/objectorientation/GenericsTest.groovy
new file mode 100644
index 0000000000..fba9a912ed
--- /dev/null
+++ b/src/spec/test/objectorientation/GenericsTest.groovy
@@ -0,0 +1,69 @@
+/*
+ * 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 objectorientation
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+final class GenericsTest {
+
+ @Test
+ void testGenericArrayCreationWorkaround() {
+ assertScript '''
+ // tag::generic_array_workaround[]
+ class Stack<T> {
+ private T[] items
+ private int size = 0
+ Stack(int capacity) {
+ items = (T[]) new Object[capacity] // reifiable creation + unchecked cast
+ }
+ void push(T t) { items[size++] = t }
+ T pop() { items[--size] }
+ }
+ def s = new Stack<String>(4)
+ s.push('a')
+ s.push('b')
+ assert s.pop() == 'b'
+ // end::generic_array_workaround[]
+ '''
+ }
+
+ @Test
+ void testGenericThrowableWorkaround() {
+ assertScript '''
+ // tag::generic_throwable_workaround[]
+ class ValidationException extends RuntimeException {
+ private final Object payload
+ ValidationException(String msg, Object payload) {
+ super(msg)
+ this.payload = payload
+ }
+ def <T> T payload() { (T) payload }
+ }
+ try {
+ throw new ValidationException('bad input', [field: 'name'])
+ } catch (ValidationException e) {
+ Map m = e.payload()
+ assert m.field == 'name'
+ }
+ // end::generic_throwable_workaround[]
+ '''
+ }
+}
diff --git a/src/test/groovy/bugs/Groovy12319.groovy b/src/test/groovy/bugs/Groovy12319.groovy
index 65bf7b1523..accc496bca 100644
--- a/src/test/groovy/bugs/Groovy12319.groovy
+++ b/src/test/groovy/bugs/Groovy12319.groovy
@@ -849,6 +849,47 @@ final class Groovy12319 {
Cell<String>.ID
'''
assert err.message.contains('parameterization')
+ assert !err.message.contains('Cannot select from a parameterized type')
+ }
+
+ @Test
+ void testValueClassPropertyRemainsLegal() {
+ assertScript '''
+ def values = [1, 'x']
+ assert values*.class == [Integer, String]
+ assert values[0].class == Integer
+ '''
+ }
+
+ @Test
+ void testParameterizedClassLiteralRejected() {
+ def err = shouldFail CompilationFailedException, '''
+ class Cell<T> {}
+ Cell<String>.class
+ '''
+ assert err.message.contains('Cannot select from a parameterized type')
+ assert !err.message.contains('static member')
+
+ err = shouldFail CompilationFailedException, '''
+ class Outer<T> {
+ class Inner {}
+ }
+ Outer<String>.Inner.class
+ '''
+ assert err.message.contains('Cannot select from a parameterized type')
+ assert !err.message.contains('static member')
+ }
+
+ @Test
+ void testTypeParameterArrayCreationNamesTypeVariable() {
+ def err = shouldFail CompilationFailedException, '''
+ class Stack<T> {
+ T[] items
+ Stack(int n) { items = new T[n] }
+ }
+ '''
+ assert err.message.contains('generic array creation of T')
+ assert !err.message.contains('of Object')
}
@Test
diff --git a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
index a4c954ddb9..e888999e2b 100644
--- a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
+++ b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
@@ -2653,6 +2653,27 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "cannot select from a type variable", "type parameter")
}
+ /**
+ * JLS 15.8.2: a class literal's {@code TypeName} may not include type
+ * arguments ({@code Cell<String>.class}). javac rejects this in the
+ * parser ({@code <identifier> expected}); Groovy reports the semantic
+ * 15.8.2 diagnostic.
+ */
+ @Test
+ void testNegativeParameterizedClassLiteral() {
+ final String className = 'gls.generics.test.ParameterizedClassLiteral'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ParameterizedClassLiteral {
+ public static class Cell<T> {}
+ public static Class<?> test() {
+ return Cell<String>.class;
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "<identifier> expected", "parameterized type")
+ }
+
/**
* JLS 4.5.2: it is illegal to refer to a static member of a generic type
* through a parameterization ({@code Cell<String>.id()}).