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