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 17:13:43 UTC
(groovy) branch generics-tck updated: GROOVY-12319: distinguish type variables by declaration, not name alone
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
The following commit(s) were added to refs/heads/generics-tck by this push:
new 6ef40fac00 GROOVY-12319: distinguish type variables by declaration, not name alone
6ef40fac00 is described below
commit 6ef40fac00da270756eb7487d011a74d83bdafec
Author: Daniel Sun <su...@apache.org>
AuthorDate: Mon Aug 31 17:12:55 2026 +0000
GROOVY-12319: distinguish type variables by declaration, not name alone
Stamp GenericsType.genericDeclaration at resolve for class, method, and
constructor type-parameter sections. Assignment of two placeholders
named T is rejected when they come from different declarations, so a
method T cannot be stored in a field of the class T.
An unbounded type-parameter declaration no longer inherits a shadowed
variable's erasure. Duplicate names in one type-parameter section and
throws of a type variable that does not erase to Throwable are rejected.
Method and constructor references such as Iterable<String>::iterator
are not treated as class literals.
Cover the cases in GenericsJavaCompatibilityTest.
---
.../java/org/codehaus/groovy/ast/GenericsType.java | 100 +++++-
.../ast/decompiled/ClassSignatureParser.java | 1 +
.../ast/decompiled/MemberSignatureParser.java | 1 +
.../codehaus/groovy/control/GenericsVisitor.java | 66 +++-
.../codehaus/groovy/control/ResolveVisitor.java | 28 +-
.../transform/stc/StaticTypeCheckingSupport.java | 7 +
.../org/codehaus/groovy/vmplugin/v8/Java8.java | 11 +-
.../generics/GenericsJavaCompatibilityTest.groovy | 390 ++++++++++++++++++++-
.../groovy/transform/stc/GenericsSTCTest.groovy | 2 +-
9 files changed, 580 insertions(+), 26 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/ast/GenericsType.java b/src/main/java/org/codehaus/groovy/ast/GenericsType.java
index ef573f5dc8..a0ece5b2a0 100644
--- a/src/main/java/org/codehaus/groovy/ast/GenericsType.java
+++ b/src/main/java/org/codehaus/groovy/ast/GenericsType.java
@@ -47,6 +47,15 @@ public class GenericsType extends ASTNode {
private final ClassNode lowerBound;
private final ClassNode[] upperBounds;
private boolean placeholder, resolved, wildcard;
+ /**
+ * The generic declaration that introduced this type variable
+ * ({@link ClassNode} or {@link MethodNode}), or {@code null} when
+ * unknown (capture, inference, or a use that has not yet been bound).
+ * Analogous to {@code java.lang.reflect.TypeVariable#getGenericDeclaration()}.
+ * Not used as a {@link GenericsTypeName} map key: those maps are
+ * in-scope name lookup and stay name-only (JLS 6.4.1).
+ */
+ private AnnotatedNode genericDeclaration;
/**
* Creates a generics type with optional upper and lower bounds.
@@ -218,6 +227,79 @@ public class GenericsType extends ASTNode {
return placeholder;
}
+ /**
+ * Returns the generic declaration that introduced this type variable, or
+ * {@code null} if this is not a type-variable placeholder or the
+ * declaration is unknown.
+ *
+ * @return the declaring {@link ClassNode} or {@link MethodNode}, or {@code null}
+ * @since 6.0.0
+ */
+ public AnnotatedNode getGenericDeclaration() {
+ return genericDeclaration;
+ }
+
+ /**
+ * Records the generic declaration that introduced this type variable.
+ * Callers must pass the class, method or constructor that owns the
+ * type-parameter section, not a use-site {@link ClassNode}.
+ *
+ * @param genericDeclaration the declaring {@link ClassNode} or {@link MethodNode}
+ * @since 6.0.0
+ */
+ public void setGenericDeclaration(final AnnotatedNode genericDeclaration) {
+ this.genericDeclaration = genericDeclaration;
+ }
+
+ /**
+ * Stamps every placeholder in {@code types} with {@code declaration}.
+ *
+ * @since 6.0.0
+ */
+ public static void setGenericDeclaration(final GenericsType[] types, final AnnotatedNode declaration) {
+ if (types == null || declaration == null) return;
+ for (GenericsType type : types) {
+ if (type != null && type.isPlaceholder()) {
+ type.setGenericDeclaration(declaration);
+ }
+ }
+ }
+
+ /**
+ * The generic declaration recorded on a placeholder {@link ClassNode}, or
+ * {@code null} if {@code type} is not a type variable or has no stamp.
+ *
+ * @since 6.0.0
+ */
+ public static AnnotatedNode genericDeclarationOf(final ClassNode type) {
+ if (type == null || !type.isGenericsPlaceHolder()) return null;
+ GenericsType[] genericsTypes = type.getGenericsTypes();
+ if (genericsTypes != null && genericsTypes.length > 0 && genericsTypes[0] != null) {
+ return genericsTypes[0].getGenericDeclaration();
+ }
+ return null;
+ }
+
+ /**
+ * True when both nodes are type-variable placeholders with the same
+ * spelling but known, different generic declarations (class {@code T}
+ * vs method {@code T}). Different names are left to the existing bound
+ * and redirect checks. Unstamped placeholders keep name-only behaviour.
+ *
+ * @since 6.0.0
+ */
+ public static boolean areDistinctTypeVariables(final ClassNode left, final ClassNode right) {
+ if (left == null || right == null || !left.isGenericsPlaceHolder() || !right.isGenericsPlaceHolder()) {
+ return false;
+ }
+ if (!left.getUnresolvedName().equals(right.getUnresolvedName())) {
+ return false;
+ }
+ AnnotatedNode leftDeclaration = genericDeclarationOf(left);
+ AnnotatedNode rightDeclaration = genericDeclarationOf(right);
+ return leftDeclaration != null && rightDeclaration != null && leftDeclaration != rightDeclaration;
+ }
+
/**
* Marks this generic type as a placeholder type variable. Setting to true also sets resolved=true
* and clears the wildcard flag, since placeholders and wildcards are mutually exclusive.
@@ -530,17 +612,15 @@ public class GenericsType extends ASTNode {
}
/**
- * Represents the name of a {@link GenericsType} for use as a map key or in generic type comparisons.
- * This inner class provides value-based equality and hashing for generic type name matching.
+ * Represents the name of a {@link GenericsType} for use as a map key.
+ * Equality and hashing are name-only: that is the JLS 6.4.1 in-scope
+ * lookup rule and the JVMS 4.7.9.1 {@code TypeVariableSignature} spelling.
*
- * <p>TODO: In order to distinguish GenericsType with same name, we should add a property to keep the declaring class.
- * <ol>
- * <li> change the signature of constructor GenericsTypeName to `GenericsTypeName(String name, ClassNode declaringClass)`
- * <li> try to fix all compilation errors(if `GenericsType` has declaringClass property, the step would be a bit easy to fix...)
- * <li> run all tests to see whether the change breaks anything
- * <li> if all tests pass, congratulations! but if some tests are broken, try to debug and find why...
- * </ol>
- * We should find a way to set declaring class for `GenericsType` first, it can be completed at the resolving phase.
+ * <p>Type-variable <em>identity</em> (class {@code T} vs method {@code T})
+ * lives on {@link GenericsType#getGenericDeclaration()}, not on this key.
+ * Do not add a declaring class to {@code equals}/{@code hashCode}: mixed
+ * null-owner keys are not an equivalence, and substitution maps that mean
+ * "in this scope, name T means String" would stop matching uses.
*/
public static class GenericsTypeName {
private final String name;
diff --git a/src/main/java/org/codehaus/groovy/ast/decompiled/ClassSignatureParser.java b/src/main/java/org/codehaus/groovy/ast/decompiled/ClassSignatureParser.java
index 3a45674272..e9a047cc01 100644
--- a/src/main/java/org/codehaus/groovy/ast/decompiled/ClassSignatureParser.java
+++ b/src/main/java/org/codehaus/groovy/ast/decompiled/ClassSignatureParser.java
@@ -196,6 +196,7 @@ class ClassSignatureParser {
}
}
classNode.setGenericsTypes(typeParameters);
+ GenericsType.setGenericDeclaration(typeParameters, classNode);
}
}
}
diff --git a/src/main/java/org/codehaus/groovy/ast/decompiled/MemberSignatureParser.java b/src/main/java/org/codehaus/groovy/ast/decompiled/MemberSignatureParser.java
index b8a21bf17e..466b097b07 100644
--- a/src/main/java/org/codehaus/groovy/ast/decompiled/MemberSignatureParser.java
+++ b/src/main/java/org/codehaus/groovy/ast/decompiled/MemberSignatureParser.java
@@ -250,6 +250,7 @@ class MemberSignatureParser {
}
if (typeParameters != null && typeParameters.length > 0) {
result.setGenericsTypes(typeParameters);
+ GenericsType.setGenericDeclaration(typeParameters, result);
}
return result;
}
diff --git a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
index 1c998eab84..de5e51b47a 100644
--- a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
@@ -18,6 +18,7 @@
*/
package org.codehaus.groovy.control;
+import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
@@ -34,12 +35,16 @@ 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.MethodPointerExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.CatchStatement;
import org.codehaus.groovy.syntax.Types;
import org.codehaus.groovy.transform.trait.Traits;
+import java.util.HashSet;
+import java.util.Set;
+
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;
@@ -147,6 +152,12 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
if (!isConstructor) {
checkGenericsUsage(node.getReturnType());
}
+ ClassNode[] exceptions = node.getExceptions();
+ if (exceptions != null) {
+ for (ClassNode exception : exceptions) {
+ checkThrowsType(exception, node);
+ }
+ }
super.visitConstructorOrMethod(node, isConstructor);
}
@@ -308,6 +319,29 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
}
}
+ /**
+ * JLS 15.13: {@code TypeName::m} and {@code TypeName::new} are method or
+ * constructor references, not class literals. A parameterization of
+ * {@code TypeName} is legal for an instance method or constructor
+ * reference ({@code Iterable<String>::asCollection},
+ * {@code HashMap<String,Integer>::new}).
+ */
+ @Override
+ public void visitMethodPointerExpression(final MethodPointerExpression expression) {
+ Expression object = expression.getExpression();
+ if (object instanceof ClassExpression) {
+ checkGenericsUsage(object.getType());
+ suppressClassLiteralChecks++;
+ try {
+ super.visitMethodPointerExpression(expression);
+ } finally {
+ suppressClassLiteralChecks--;
+ }
+ } else {
+ super.visitMethodPointerExpression(expression);
+ }
+ }
+
/**
* Validates generic usage for cast target types.
*
@@ -500,22 +534,48 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
/**
* JLS 4.4: a class type or type variable may appear only as the first type
- * of a bound; additional bounds must be interfaces.
+ * of a bound; additional bounds must be interfaces. Type-parameter names
+ * in one section must be distinct. A primitive type is not a legal bound.
*/
private void checkTypeParameterBounds(final GenericsType[] typeParameters) {
if (typeParameters == null) return;
+ Set<String> names = new HashSet<>();
for (GenericsType tp : typeParameters) {
+ if (!names.add(tp.getName())) {
+ addError("Duplicate type parameter " + tp.getName(), tp);
+ }
ClassNode[] bounds = tp.getUpperBounds();
if (bounds == null) continue;
- for (int i = 1; i < bounds.length; i++) {
+ for (int i = 0; i < bounds.length; i++) {
ClassNode bound = bounds[i];
- if (bound != null && !bound.isInterface() && !Traits.isTrait(bound)) {
+ if (bound == null) continue;
+ if (isPrimitiveType(bound)) {
+ addError("The bound of type parameter " + tp.getName() + " must be a class type, not the primitive type " + bound.getName(), tp);
+ continue;
+ }
+ if (i > 0 && !bound.isInterface() && !Traits.isTrait(bound)) {
addError("Additional bounds of a type parameter must be interfaces", tp);
}
}
}
}
+ /**
+ * JLS 8.4.6: a type variable may appear in {@code throws} only when its
+ * erasure is a subtype of {@code Throwable}. An unbounded {@code T}
+ * erases to {@code Object}.
+ */
+ private void checkThrowsType(final ClassNode type, final ASTNode location) {
+ if (type == null || !type.isGenericsPlaceHolder()) return;
+ ClassNode erasure = type.redirect();
+ if (erasure != null && !isObjectType(erasure)
+ && (erasure.isDerivedFrom(ClassHelper.THROWABLE_TYPE)
+ || erasure.implementsInterface(ClassHelper.THROWABLE_TYPE))) {
+ return;
+ }
+ addError("The type parameter " + type.getUnresolvedName() + " is not a valid type for a throws clause", location);
+ }
+
/**
* True when {@code type} is a parameterized usage ({@code Cell<String>}), as
* opposed to the generic type name ({@code Cell}) or the class declaration
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index 2e2770b313..e7ac06bf24 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -22,6 +22,7 @@ import groovy.lang.Tuple2;
import groovy.transform.Internal;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.ClassHelper;
@@ -374,7 +375,7 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
canSeeTypeVars(node.getModifiers(), node.getDeclaringClass())
? new HashMap<>(genericParameterNames) : new HashMap<>();
try {
- resolveGenericsHeader(node.getGenericsTypes());
+ resolveGenericsHeader(node.getGenericsTypes(), node);
{
ClassNode t = node.getReturnType();
@@ -1599,7 +1600,7 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
} else {
genericParameterNames.clear(); // outer class: new generic namespace
}
- resolveGenericsHeader(node.getGenericsTypes());
+ resolveGenericsHeader(node.getGenericsTypes(), node);
switch (phase) { // GROOVY-9866, GROOVY-10466
case 0:
case 1:
@@ -1622,7 +1623,7 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
if (cn.isAnonymous()) {
MethodNode enclosingMethod = cn.getEnclosingMethod();
if (enclosingMethod != null) {
- resolveGenericsHeader(enclosingMethod.getGenericsTypes()); // GROOVY-6977
+ resolveGenericsHeader(enclosingMethod.getGenericsTypes(), enclosingMethod); // GROOVY-6977
}
resolveOrFail(cn.getUnresolvedSuperClass(false), cn); // GROOVY-9642
}
@@ -1745,11 +1746,11 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
return resolved;
}
- private void resolveGenericsHeader(final GenericsType[] types) {
- if (types != null) resolveGenericsHeader(types, null, 0);
+ private void resolveGenericsHeader(final GenericsType[] types, final AnnotatedNode declaration) {
+ if (types != null) resolveGenericsHeader(types, null, 0, declaration);
}
- private void resolveGenericsHeader(final GenericsType[] types, final GenericsType rootType, final int level) {
+ private void resolveGenericsHeader(final GenericsType[] types, final GenericsType rootType, final int level, final AnnotatedNode declaration) {
currentClass.setUsingGenerics(true);
List<Tuple2<ClassNode, ClassNode>> upperBoundsToResolve = new LinkedList<>();
List<Tuple2<ClassNode, GenericsType>> upperBoundsWithGenerics = new LinkedList<>();
@@ -1774,6 +1775,7 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
type.setPlaceholder(true);
typeType.setRedirect(upperBound);
genericParameterNames.put(gtn, type);
+ if (level == 0) type.setGenericDeclaration(declaration);
nameAdded = true;
}
}
@@ -1786,7 +1788,16 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
} else if (dealWithGenerics && !isWildcardGT) {
type.setPlaceholder(true);
GenericsType last = genericParameterNames.put(gtn, type);
- typeType.setRedirect(last != null ? last.getType().redirect() : ClassHelper.OBJECT_TYPE);
+ if (level == 0) {
+ // A new type-parameter declaration must not inherit a
+ // shadowed variable's erasure. Unbounded T erases to
+ // Object even when an enclosing T extends Number.
+ typeType.setRedirect(ClassHelper.OBJECT_TYPE);
+ type.setGenericDeclaration(declaration);
+ } else {
+ typeType.setRedirect(last != null ? last.getType().redirect() : ClassHelper.OBJECT_TYPE);
+ if (last != null) type.setGenericDeclaration(last.getGenericDeclaration());
+ }
}
}
@@ -1801,7 +1812,7 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
for (var tuple : upperBoundsWithGenerics) {
GenericsType[] bounds = tuple.getV1().getGenericsTypes();
- resolveGenericsHeader(bounds, level == 0 ? tuple.getV2() : rootType, level + 1);
+ resolveGenericsHeader(bounds, level == 0 ? tuple.getV2() : rootType, level + 1, declaration);
}
}
@@ -1823,6 +1834,7 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
type.setRedirect(tp.getType());
}
genericsType.setPlaceholder(true);
+ genericsType.setGenericDeclaration(tp.getGenericDeclaration());
} else {
ClassNode[] upperBounds = genericsType.getUpperBounds();
if (upperBounds != null) {
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 1be2df4e32..255697af04 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
@@ -691,6 +691,12 @@ public abstract class StaticTypeCheckingSupport {
ClassNode leftRedirect = left.redirect();
ClassNode rightRedirect = right.redirect();
+ // JLS 4.4: two type variables are the same type only when they come
+ // from the same declaration. Unbounded variables share the Object
+ // erasure, so redirect identity is not type-variable identity.
+ if (GenericsType.areDistinctTypeVariables(left, right)) {
+ return false;
+ }
if (leftRedirect == rightRedirect) return true;
if (leftRedirect == VOID_TYPE) return rightRedirect == void_WRAPPER_TYPE;
@@ -1989,6 +1995,7 @@ public abstract class StaticTypeCheckingSupport {
try {
GenericsType newGT = new GenericsType(type, applyGenericsContext(ctx, spec, gt.getUpperBounds()), applyGenericsContext(ctx, spec, gt.getLowerBound()));
newGT.setPlaceholder(true);
+ newGT.setGenericDeclaration(gt.getGenericDeclaration());
return newGT;
} finally {
ctx.exit();
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java
index 524a78afde..c3eafbc93d 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java
@@ -158,7 +158,7 @@ public class Java8 implements VMPlugin {
/** {@inheritDoc} */
@Override
public void setAdditionalClassInformation(final ClassNode cn) {
- cn.setGenericsTypes(configureTypeParameters(cn.getTypeClass().getTypeParameters()));
+ cn.setGenericsTypes(configureTypeParameters(cn.getTypeClass().getTypeParameters(), cn));
}
private ClassNode[] configureTypes(final Type[] types) {
@@ -246,7 +246,7 @@ public class Java8 implements VMPlugin {
return gts;
}
- private GenericsType[] configureTypeParameters(final TypeVariable<?>[] tp) {
+ private GenericsType[] configureTypeParameters(final TypeVariable<?>[] tp, final AnnotatedNode declaration) {
final int n = tp.length;
if (n == 0) return null;
GenericsType[] gt = new GenericsType[n];
@@ -254,6 +254,7 @@ public class Java8 implements VMPlugin {
ClassNode t = configureTypeVariableReference(tp[i].getName());
ClassNode[] bounds = configureTypes(tp[i].getBounds());
gt[i] = configureTypeVariableDefinition(t, bounds);
+ gt[i].setGenericDeclaration(declaration);
for (Annotation annotation : tp[i].getAnnotations()) {
gt[i].setType(addTypeAnnotation(gt[i].getType(), annotation));
}
@@ -373,7 +374,7 @@ public class Java8 implements VMPlugin {
mn.setAnnotationDefault(true); // GROOVY-10862
mn.setCode(new ReturnStatement(new ConstantExpression(m.getDefaultValue(), true)));
}
- mn.setGenericsTypes(configureTypeParameters(m.getTypeParameters()));
+ mn.setGenericsTypes(configureTypeParameters(m.getTypeParameters(), mn));
mn.setSynthetic(m.isSynthetic());
classNode.addMethod(mn);
}
@@ -384,6 +385,10 @@ public class Java8 implements VMPlugin {
applyExceptionTypeAnnotations(c, exceptions);
ConstructorNode cn = classNode.addConstructor(c.getModifiers(), params, exceptions, null);
setAnnotationMetaData(c.getAnnotations(), cn);
+ GenericsType[] constructorTypeParameters = configureTypeParameters(c.getTypeParameters(), cn);
+ if (constructorTypeParameters != null) {
+ cn.setGenericsTypes(constructorTypeParameters);
+ }
}
Class<?> sc = clazz.getSuperclass();
diff --git a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
index 41276bb47e..47dda1c5dc 100644
--- a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
+++ b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
@@ -26,7 +26,9 @@ import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.junit.jupiter.api.Test
+import java.lang.reflect.Constructor
import java.lang.reflect.GenericArrayType
+import java.lang.reflect.GenericDeclaration
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
@@ -66,6 +68,9 @@ import static groovy.test.GroovyAssert.shouldFail
* </ol>
* Categories 1–15 below map onto those dimensions. Category 16 spells out
* the JCK-shaped cases that were not previously a dedicated test.
+ * Category 18 is type-variable identity (JLS 4.4, 6.3, 6.4.1) plus remaining
+ * cheap well-formedness of type-parameter sections (duplicate names, cyclic
+ * bounds, {@code throws} of a type variable).
* 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.
@@ -2783,6 +2788,36 @@ final class GenericsJavaCompatibilityTest {
assertPositive(className, javaSrc, 'java.util.Map$Entry', true)
}
+ /**
+ * JLS 15.13: a method or constructor reference may parameterize
+ * {@code TypeName}. That is not a class literal, so
+ * {@code HashMap<String,Integer>::new} and
+ * {@code Iterable<String>::iterator} are well-formed.
+ */
+ @Test
+ void testParameterizedTypeMethodAndConstructorReferences() {
+ final String className = 'gls.generics.test.ParameterizedTypeRefs'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.util.HashMap;
+ import java.util.Iterator;
+ import java.util.List;
+ import java.util.function.Function;
+ import java.util.function.Supplier;
+ public class ParameterizedTypeRefs {
+ public static String test() {
+ Supplier<HashMap<String, Integer>> supplier = HashMap<String, Integer>::new;
+ Function<Iterable<String>, Iterator<String>> iterate = Iterable<String>::iterator;
+ HashMap<String, Integer> map = supplier.get();
+ map.put("k", Integer.valueOf(1));
+ String first = iterate.apply(List.of("ok")).next();
+ return first + map.get("k").intValue();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok1')
+ }
+
/**
* JLS 15.8.2: {@code Outer<String>.Inner.class} names a parameterized
* (rare) type. javac rejects this in the parser; Groovy reports 15.8.2.
@@ -3692,6 +3727,338 @@ final class GenericsJavaCompatibilityTest {
assertNegativeCompile(className, javaSrc, "type parameter", "Cannot catch type parameter")
}
+ // =========================================================================
+ // Category 18: Type-variable identity and type-parameter section
+ // well-formedness (JLS 4.4, 6.3, 6.4.1, 8.1.2, 8.4.4, 8.4.6)
+ // =========================================================================
+
+ /**
+ * JLS 6.4.1, 8.4.4: a method type parameter {@code T} shadows the class
+ * type parameter {@code T}. They are distinct types; a value of the method
+ * {@code T} cannot be assigned to a field of the class {@code T}.
+ */
+ @Test
+ void testNegativeMethodTAssignedToClassT() {
+ final String className = 'gls.generics.test.MethodTAssignedToClassT'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class MethodTAssignedToClassT {
+ public static class C<T> {
+ public T field;
+ public <T> void m(T t) { field = t; }
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "incompatible types", "Cannot assign")
+ }
+
+ /**
+ * JLS 4.4, 6.4.1: even when bounds differ, the method {@code T} and the
+ * class {@code T} remain distinct. {@code CharSequence} is not a
+ * {@code Number}.
+ */
+ @Test
+ void testNegativeBoundedMethodTAssignedToBoundedClassT() {
+ final String className = 'gls.generics.test.BoundedMethodTVsClassT'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class BoundedMethodTVsClassT {
+ public static class C<T extends Number> {
+ public T n;
+ public <T extends CharSequence> T m(T x) { n = x; return x; }
+ }
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "incompatible types", "Cannot assign")
+ }
+
+ /**
+ * JLS 6.4.1, 8.4.4: the method type parameter is independent of the class
+ * type parameter. {@code new C<String>().m(1)} infers the method {@code T}
+ * as {@code Integer}, not {@code String}.
+ */
+ @Test
+ void testMethodTIndependentOfClassT() {
+ final String className = 'gls.generics.test.MethodTIndependentOfClassT'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class MethodTIndependentOfClassT {
+ public static class C<T> {
+ public <T> T m(T t) { return t; }
+ }
+ public static int test() {
+ return new C<String>().m(Integer.valueOf(1)).intValue();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 1, true)
+ }
+
+ /**
+ * JLS 18.5.2, 8.4.4: a helper whose method type parameter is inferred as
+ * the class {@code T} may legally return the field. This is inference of a
+ * distinct variable, not identity of the two {@code T}s.
+ */
+ @Test
+ void testInferMethodUAsClassT() {
+ final String className = 'gls.generics.test.InferMethodUAsClassT'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class InferMethodUAsClassT<T> {
+ public T field;
+ public static <U> U id(U u) { return u; }
+ public T m() { return id(field); }
+ public static String test() {
+ InferMethodUAsClassT<String> c = new InferMethodUAsClassT<String>();
+ c.field = "ok";
+ return c.m();
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok')
+ }
+
+ /**
+ * JLS 4.4, 4.6, 8.4.4: an unbounded method type parameter that shadows a
+ * bounded class type parameter still erases to {@code Object}, not to the
+ * class bound. Callers may therefore pass a {@code String} where the class
+ * {@code T} is {@code Number}.
+ */
+ @Test
+ void testShadowedMethodTErasesToObject() {
+ final String className = 'gls.generics.test.ShadowedMethodTErasure'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.Method;
+ import java.lang.reflect.TypeVariable;
+ public class ShadowedMethodTErasure {
+ public static class C<T extends Number> {
+ public <T> T m(T x) { return x; }
+ }
+ public static String test() throws Exception {
+ Method m = C.class.getDeclaredMethod("m", Object.class);
+ TypeVariable<?> tv = (TypeVariable<?>) m.getGenericParameterTypes()[0];
+ boolean declIsMethod = tv.getGenericDeclaration().equals(m);
+ boolean boundIsObject = tv.getBounds().length == 1 && tv.getBounds()[0] == Object.class;
+ String value = new C<Integer>().m("ok");
+ return (declIsMethod && boundIsObject && "ok".equals(value)) ? "ok" : "fail";
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok', true)
+ }
+
+ /**
+ * JLS 4.4, 4.6, 8.1.2: an inner class that redeclares {@code T} does not
+ * inherit the outer bound. {@code Outer<Integer>.Inner<String>} is legal
+ * when the inner {@code T} is unbounded.
+ */
+ @Test
+ void testShadowedInnerTErasesToObject() {
+ final String className = 'gls.generics.test.ShadowedInnerTErasure'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.Field;
+ import java.lang.reflect.TypeVariable;
+ public class ShadowedInnerTErasure<T extends Number> {
+ public class Inner<T> {
+ public T x;
+ }
+ public Inner makeInner() { return new Inner(); }
+ public static String test() throws Exception {
+ ShadowedInnerTErasure<Integer> outer = new ShadowedInnerTErasure<Integer>();
+ Object inner = outer.makeInner();
+ Field f = inner.getClass().getDeclaredField("x");
+ TypeVariable<?> tv = (TypeVariable<?>) f.getGenericType();
+ boolean declIsInner = tv.getGenericDeclaration() == inner.getClass();
+ boolean boundIsObject = tv.getBounds().length == 1 && tv.getBounds()[0] == Object.class;
+ f.setAccessible(true);
+ f.set(inner, "ok");
+ return (declIsInner && boundIsObject && "ok".equals(f.get(inner))) ? "ok" : "fail";
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok')
+ }
+
+ /**
+ * JLS 6.3, 8.1.2: an inner class that does not redeclare {@code T} still
+ * sees the enclosing class type parameter.
+ */
+ @Test
+ void testInnerClassUsesEnclosingTypeParameter() {
+ final String className = 'gls.generics.test.InnerUsesEnclosingT'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.Field;
+ import java.lang.reflect.TypeVariable;
+ public class InnerUsesEnclosingT<T> {
+ public class Inner {
+ public T x;
+ }
+ public Inner make() { return new Inner(); }
+ public static String test() throws Exception {
+ InnerUsesEnclosingT<String> outer = new InnerUsesEnclosingT<String>();
+ InnerUsesEnclosingT<String>.Inner inner = outer.make();
+ Field f = inner.getClass().getDeclaredField("x");
+ TypeVariable<?> tv = (TypeVariable<?>) f.getGenericType();
+ boolean declIsOuter = tv.getGenericDeclaration() == InnerUsesEnclosingT.class;
+ inner.x = "ok";
+ return (declIsOuter && "ok".equals(inner.x)) ? "ok" : "fail";
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok')
+ }
+
+ /**
+ * JLS 8.4.4: a method type parameter with an interface bound uses that
+ * bound as its erasure. Shadowing a class {@code T extends Number} does
+ * not steal the class bound.
+ */
+ @Test
+ void testShadowedMethodTWithOwnInterfaceBound() {
+ final String className = 'gls.generics.test.ShadowedMethodTInterfaceBound'
+ final String javaSrc = '''
+ package gls.generics.test;
+ import java.lang.reflect.Method;
+ import java.lang.reflect.TypeVariable;
+ public class ShadowedMethodTInterfaceBound {
+ public static class C<T extends Number> {
+ public <T extends CharSequence> T m(T x) { return x; }
+ }
+ public static String test() throws Exception {
+ Method m = C.class.getDeclaredMethod("m", CharSequence.class);
+ TypeVariable<?> tv = (TypeVariable<?>) m.getGenericParameterTypes()[0];
+ boolean declIsMethod = tv.getGenericDeclaration().equals(m);
+ boolean boundIsCharSeq = tv.getBounds().length == 1 && tv.getBounds()[0] == CharSequence.class;
+ String value = new C<Integer>().m("ok");
+ return (declIsMethod && boundIsCharSeq && "ok".equals(value)) ? "ok" : "fail";
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok', true)
+ }
+
+ /**
+ * JLS 4.4: two type parameters in one section must have distinct names.
+ */
+ @Test
+ void testNegativeDuplicateClassTypeParameters() {
+ final String className = 'gls.generics.test.DupClassTypeParams'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class DupClassTypeParams<T, T> {
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "already defined", "Duplicate type parameter")
+ }
+
+ /**
+ * JLS 8.4.4: the same name may not appear twice in a method type-parameter
+ * section.
+ */
+ @Test
+ void testNegativeDuplicateMethodTypeParameters() {
+ final String className = 'gls.generics.test.DupMethodTypeParams'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class DupMethodTypeParams {
+ public <T, T> void m() {}
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "already defined", "Duplicate type parameter")
+ }
+
+ /**
+ * JLS 4.4: a type variable may not be a bound of itself.
+ * {@code T extends Comparable<T>} remains legal
+ * ({@link #testRecursiveTypeBoundsFBounded()}).
+ */
+ @Test
+ void testNegativeSelfBoundTypeParameter() {
+ final String className = 'gls.generics.test.SelfBoundTypeParam'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class SelfBoundTypeParam<T extends T> {
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cyclic inheritance", "Cycle detected")
+ }
+
+ /**
+ * JLS 4.4: mutually recursive type-variable bounds without an intervening
+ * class type are cyclic.
+ */
+ @Test
+ void testNegativeMutualTypeParameterBounds() {
+ final String className = 'gls.generics.test.MutualTypeParamBounds'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class MutualTypeParamBounds<T extends U, U extends T> {
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cyclic inheritance", "Cycle detected")
+ }
+
+ /**
+ * JLS 8.4.6: a type variable may appear in {@code throws} only when its
+ * bound is a subtype of {@code Throwable}. An unbounded {@code T}
+ * erases to {@code Object}.
+ */
+ @Test
+ void testNegativeThrowsUnboundedTypeParameter() {
+ final String className = 'gls.generics.test.ThrowsUnboundedT'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ThrowsUnboundedT {
+ public <T> void m() throws T {}
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be converted to Throwable", "not a valid type for a throws clause")
+ }
+
+ /**
+ * JLS 8.4.6: {@code T extends Object} is still not a {@code Throwable}.
+ */
+ @Test
+ void testNegativeThrowsObjectBoundedTypeParameter() {
+ final String className = 'gls.generics.test.ThrowsObjectBoundedT'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ThrowsObjectBoundedT {
+ public <T extends Object> void m() throws T {}
+ }
+ '''
+ assertNegativeCompile(className, javaSrc, "cannot be converted to Throwable", "not a valid type for a throws clause")
+ }
+
+ /**
+ * JLS 8.4.6: a type variable whose bound is itself a type variable bounded
+ * by {@code Exception} may appear in {@code throws}.
+ */
+ @Test
+ void testThrowsTypeParameterBoundedByTypeParameter() {
+ final String className = 'gls.generics.test.ThrowsTBoundedByE'
+ final String javaSrc = '''
+ package gls.generics.test;
+ public class ThrowsTBoundedByE {
+ public static <E extends Exception, T extends E> void m(T t) throws T {
+ throw t;
+ }
+ public static String test() {
+ try {
+ m(new Exception("ok"));
+ return "fail";
+ } catch (Exception e) {
+ return e.getMessage();
+ }
+ }
+ }
+ '''
+ assertPositive(className, javaSrc, 'ok')
+ }
+
// =========================================================================
// Test Harness Helper Methods
// =========================================================================
@@ -3853,6 +4220,26 @@ final class GenericsJavaCompatibilityTest {
return typeDesc(type)
}
+ /**
+ * Reflective identity of a type variable's generic declaration, recovered
+ * from the Signature attribute (JVMS 4.7.9.1) rather than from a qualifier
+ * in the class file. Distinguishes class {@code T} from method {@code T}.
+ */
+ private static String genericDeclarationDesc(GenericDeclaration declaration) {
+ if (declaration instanceof Class) {
+ return 'declared in class ' + ((Class<?>) declaration).name
+ }
+ if (declaration instanceof Method) {
+ Method method = (Method) declaration
+ return 'declared in method ' + method.declaringClass.name + '.' + method.name
+ }
+ if (declaration instanceof Constructor) {
+ Constructor<?> constructor = (Constructor<?>) declaration
+ return 'declared in constructor ' + constructor.declaringClass.name
+ }
+ return 'declared in ' + String.valueOf(declaration)
+ }
+
/**
* Canonical, JDK-stable description of a reflective {@link Type}. Uses binary
* class names and type-variable names rather than {@code Type.toString()}, which
@@ -3870,7 +4257,8 @@ final class GenericsJavaCompatibilityTest {
return c.name
}
if (type instanceof TypeVariable) {
- return ((TypeVariable<?>) type).name
+ TypeVariable<?> tv = (TypeVariable<?>) type
+ return tv.name + ' ' + genericDeclarationDesc(tv.genericDeclaration)
}
if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType) type
diff --git a/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy b/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy
index b10caf13c6..7d447a9be5 100644
--- a/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy
@@ -1565,7 +1565,7 @@ class GenericsSTCTest extends StaticTypeCheckingTestCase {
assertScript '''
class C<T> {
}
- def <T,T> T m(C<T> c) {
+ def <T> T m(C<T> c) {
}
Number n = m(new C<>())
'''