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 18:06:51 UTC

(groovy) branch generics-tck updated: GROOVY-12319: centralize generic type-use helpers and rare-type copies

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 76344606e1 GROOVY-12319: centralize generic type-use helpers and rare-type copies
76344606e1 is described below

commit 76344606e1b2159a337286fd49793481a12fc315
Author: Daniel Sun <su...@apache.org>
AuthorDate: Mon Aug 31 18:05:45 2026 +0000

    GROOVY-12319: centralize generic type-use helpers and rare-type copies
    
    Share parameterized/raw/reifiable classification and describeTypeUse in
    TypeUseUtils. Write outerClassType onto INFERRED_TYPE at storeType, resolve
    type-parameter headers only at declaration sites, extract nested Signature
    writing from BytecodeHelper, and copy genericDeclaration through asGenericsType.
---
 .../org/apache/groovy/ast/tools/TypeUseUtils.java  | 182 +++++++++++++++++++++
 .../apache/groovy/parser/antlr4/AstBuilder.java    |  33 +---
 .../java/org/codehaus/groovy/ast/ClassNode.java    |   6 +-
 .../codehaus/groovy/ast/tools/GenericsUtils.java   |   1 +
 .../groovy/classgen/asm/BytecodeHelper.java        |  64 +-------
 .../groovy/classgen/asm/TypeSignatureWriter.java   |  86 ++++++++++
 .../codehaus/groovy/control/GenericsVisitor.java   | 182 ++++-----------------
 .../codehaus/groovy/control/ResolveVisitor.java    |  64 +++-----
 .../transform/stc/StaticTypeCheckingVisitor.java   |  45 ++---
 .../generics/GenericsJavaCompatibilityTest.groovy  |   2 +-
 .../groovy/ast/tools/TypeUseUtilsTest.groovy       | 109 ++++++++++++
 .../org/codehaus/groovy/ast/ClassNodeTest.java     |  18 ++
 12 files changed, 490 insertions(+), 302 deletions(-)

diff --git a/src/main/java/org/apache/groovy/ast/tools/TypeUseUtils.java b/src/main/java/org/apache/groovy/ast/tools/TypeUseUtils.java
new file mode 100644
index 0000000000..e84e9b4dad
--- /dev/null
+++ b/src/main/java/org/apache/groovy/ast/tools/TypeUseUtils.java
@@ -0,0 +1,182 @@
+/*
+ *  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 org.apache.groovy.ast.tools;
+
+import groovy.transform.Internal;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.GenericsType;
+
+import static org.codehaus.groovy.ast.ClassHelper.isObjectType;
+import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType;
+
+/**
+ * Classification of a {@link ClassNode} as a type <em>use</em> (JLS 4.5, 4.7,
+ * 4.8). Distinct from a generic class <em>declaration</em>, whose
+ * {@link ClassNode#getGenericsTypes()} are the formals.
+ *
+ * @since 6.0.0
+ */
+@Internal
+public final class TypeUseUtils {
+
+    private TypeUseUtils() {
+    }
+
+    /**
+     * 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).
+     */
+    public static boolean isParameterizedTypeUsage(final ClassNode type) {
+        if (type == null || type.isGenericsPlaceHolder()) return false;
+        GenericsType[] gt = type.getGenericsTypes();
+        return gt != null && gt.length > 0 && type.isRedirectNode();
+    }
+
+    /**
+     * True when {@code type} or any enclosing rare type is a parameterized
+     * usage. Used for class literals (JLS 15.8.2): {@code Outer<String>.Inner.class}.
+     * Declaration formals on {@code Map} must not make {@code Map.Entry} a
+     * class-literal error.
+     */
+    public static boolean isParameterizedTypeUsageIncludingEnclosing(final ClassNode type) {
+        if (type == null) return false;
+        if (isParameterizedTypeUsage(type)) return true;
+        ClassNode oc = type.getOuterClassType();
+        return oc != null && (isParameterizedTypeUsage(oc) || isParameterizedTypeUsageIncludingEnclosing(oc));
+    }
+
+    /**
+     * True when {@code type} carries type arguments, including wildcards
+     * ({@code Outer<?>}). Raw names ({@code Outer}) are not parameterized.
+     * Unlike {@link #isParameterizedTypeUsage(ClassNode)}, this does not
+     * require a redirect node: an enclosing type on a nested use may still
+     * be the parser's unresolved node.
+     */
+    public static boolean isParameterizedEnclosingType(final ClassNode type) {
+        if (type == null || type.isGenericsPlaceHolder()) return false;
+        GenericsType[] gt = type.getGenericsTypes();
+        return gt != null && gt.length > 0;
+    }
+
+    /**
+     * True when {@code type} is a raw use of a generic declaration ({@code Outer}
+     * for {@code class Outer<T>}).
+     */
+    public static boolean isRawGenericType(final ClassNode type) {
+        return type != null
+                && type.getGenericsTypes() == null
+                && type.redirect().getGenericsTypes() != null;
+    }
+
+    /**
+     * JLS 4.7: types available in full at run time. Used for array creation
+     * (JLS 15.10.1) and {@code instanceof} (JLS 15.20.2).
+     */
+    public static boolean isReifiable(ClassNode type) {
+        if (type == null) return false;
+        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);
+    }
+
+    /**
+     * Nested interfaces and enums are implicitly static; otherwise the
+     * {@code ACC_STATIC} bit on the declaration is authoritative.
+     */
+    public static boolean isStaticMemberType(final ClassNode type) {
+        ClassNode declared = type.redirect();
+        return declared.isStatic()
+                || declared.isInterface()
+                || declared.isEnum();
+    }
+
+    /**
+     * True when a type argument of {@code type} or an enclosing rare type is a
+     * wildcard.
+     */
+    public static boolean hasWildcardTypeArgument(final ClassNode type) {
+        if (type == null) return false;
+        GenericsType[] gt = type.getGenericsTypes();
+        if (gt != null) {
+            for (GenericsType t : gt) {
+                if (t.isWildcard()) return true;
+            }
+        }
+        ClassNode oc = type.getOuterClassType();
+        return oc != null && hasWildcardTypeArgument(oc);
+    }
+
+    /**
+     * Source-like display of a type use, including rare enclosing arguments
+     * ({@code Outer<String>.Inner}) and array brackets.
+     */
+    public static String describeTypeUse(final ClassNode type) {
+        if (type == null) return "null";
+        if (type.isArray()) {
+            return describeTypeUse(type.getComponentType()) + "[]";
+        }
+        if (type.isGenericsPlaceHolder()) {
+            return type.getUnresolvedName();
+        }
+        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();
+    }
+
+    /**
+     * Indicates whether the wildcard has no explicit bounds.
+     */
+    private static boolean isUnboundedWildcard(final GenericsType gt) {
+        if (gt.isWildcard() && gt.getLowerBound() == null) {
+            ClassNode[] upperBounds = gt.getUpperBounds();
+            return (upperBounds == null || upperBounds.length == 0 || (upperBounds.length == 1
+                    && isObjectType(upperBounds[0]) && !upperBounds[0].isGenericsPlaceHolder()));
+        }
+        return false;
+    }
+}
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 32387d72d1..4c9db12f1e 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -38,6 +38,7 @@ import org.antlr.v4.runtime.misc.Interval;
 import org.antlr.v4.runtime.misc.ParseCancellationException;
 import org.antlr.v4.runtime.tree.ParseTree;
 import org.antlr.v4.runtime.tree.TerminalNode;
+import org.apache.groovy.ast.tools.TypeUseUtils;
 import org.apache.groovy.parser.antlr4.internal.DescriptiveErrorStrategy;
 import org.apache.groovy.parser.antlr4.internal.atnmanager.AtnManager;
 import org.apache.groovy.parser.antlr4.util.StringUtils;
@@ -4742,7 +4743,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     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)
+                    "Cannot perform instanceof check against parameterized type " + TypeUseUtils.describeTypeUse(classNode)
                             + " since further generic type information will be erased at runtime", ctx);
         }
     }
@@ -4768,36 +4769,6 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         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
     public GenericsType[] visitTypeArgumentsOrDiamond(final TypeArgumentsOrDiamondContext ctx) {
         if (asBoolean(ctx.typeArguments())) {
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassNode.java b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
index e0e866e868..9158c938be 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
@@ -2168,7 +2168,11 @@ faces:  if (method == null && asBoolean(getInterfaces())) { // GROOVY-11323
             return genericsTypes[0];
         } else {
             ClassNode upper = (redirect != null ? redirect : this);
-            return new GenericsType(this, new ClassNode[]{upper}, null);
+            GenericsType gt = new GenericsType(this, new ClassNode[]{upper}, null);
+            if (genericsTypes != null && genericsTypes[0] != null) {
+                gt.setGenericDeclaration(genericsTypes[0].getGenericDeclaration());
+            }
+            return gt;
         }
     }
 
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 4385234072..88bf80cc7f 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GenericsUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GenericsUtils.java
@@ -963,6 +963,7 @@ public class GenericsUtils {
                     ClassNode newPlaceHolder = ClassHelper.make(old.getName());
                     GenericsType gt = new GenericsType(newPlaceHolder, newUpper, newLower);
                     gt.setPlaceholder(true);
+                    gt.setGenericDeclaration(old.getGenericDeclaration());
                     newTypes[i] = gt;
                 }
             }
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 3babbc05ec..0c813c85c5 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
@@ -218,7 +218,7 @@ public class BytecodeHelper {
      *
      * @return the ASM type description
      */
-    private static String getTypeDescription(ClassNode c, boolean end) {
+    static String getTypeDescription(ClassNode c, boolean end) {
         ClassNode d = c;
         if (isPrimitiveType(d.redirect())) {
             d = d.redirect();
@@ -336,7 +336,7 @@ public class BytecodeHelper {
         return false;
     }
 
-    private static boolean hasGenerics(ClassNode type) {
+    static boolean hasGenerics(ClassNode type) {
         if (type.isArray()) return hasGenerics(type.getComponentType());
         if (type.getGenericsTypes() != null && type.getGenericsTypes().length > 0) return true;
         ClassNode outer = type.getOuterClassType();
@@ -396,7 +396,7 @@ public class BytecodeHelper {
     public static String getTypeGenericsSignature(ClassNode node) {
         if (!usesGenericsInTypeSignature(node)) return null;
         StringBuilder ret = new StringBuilder(100);
-        writeParameterizedClass(ret, node);
+        TypeSignatureWriter.writeParameterizedClass(ret, node);
         ret.append(";");
         return ret.toString();
     }
@@ -467,65 +467,11 @@ public class BytecodeHelper {
             ret.append(printType.getGenericsTypes()[0].getName());
             ret.append(";");
         } else {
-            writeParameterizedClass(ret, printType);
+            TypeSignatureWriter.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&lt;...&gt;.Inner&lt;...&gt;} when an enclosing rare type is present.
-     */
-    private static void writeParameterizedClass(StringBuilder ret, ClassNode printType) {
-        ClassNode owner = printType.getOuterClassType();
-        // 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));
-            addSubTypes(ret, printType.getGenericsTypes(), "<", ">");
-            return;
-        }
-        ret.append(getTypeDescription(printType, false));
-        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();
-        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;
-        }
-        return innerName.substring(ownerLen + 1);
-    }
-
     private static void writeGenericsBounds(StringBuilder ret, GenericsType type, boolean writeInterfaceMarker) {
         if (type.getUpperBounds() != null) {
             ClassNode[] bounds = type.getUpperBounds();
@@ -539,7 +485,7 @@ public class BytecodeHelper {
         }
     }
 
-    private static void addSubTypes(StringBuilder ret, GenericsType[] types, String start, String end) {
+    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);
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/TypeSignatureWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/TypeSignatureWriter.java
new file mode 100644
index 0000000000..f9ba1d8617
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/TypeSignatureWriter.java
@@ -0,0 +1,86 @@
+/*
+ *  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 org.codehaus.groovy.classgen.asm;
+
+import org.codehaus.groovy.ast.ClassNode;
+
+/**
+ * Writes the JVMS 4.7.9.1 nested form of a parameterized type for a Signature
+ * attribute ({@code LOuter<...>.Inner<...>}). Package-private helper of
+ * {@link BytecodeHelper}.
+ */
+final class TypeSignatureWriter {
+
+    private TypeSignatureWriter() {
+    }
+
+    /**
+     * Writes a class type and its type arguments, using the JLS 4.5 nested form
+     * {@code LOuter&lt;...&gt;.Inner&lt;...&gt;} when an enclosing rare type is present.
+     */
+    static void writeParameterizedClass(final StringBuilder ret, final ClassNode printType) {
+        ClassNode owner = printType.getOuterClassType();
+        // 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 && BytecodeHelper.hasGenerics(owner)) {
+            writeParameterizedClass(ret, owner);
+            ret.append('.');
+            ret.append(innerClassSimpleName(printType, owner));
+            BytecodeHelper.addSubTypes(ret, printType.getGenericsTypes(), "<", ">");
+            return;
+        }
+        ret.append(BytecodeHelper.getTypeDescription(printType, false));
+        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}).
+     */
+    static String innerClassSimpleName(final ClassNode inner, final ClassNode owner) {
+        String innerName = inner.getName();
+        String ownerName = owner.getName();
+        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}.
+     */
+    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;
+        }
+        return innerName.substring(ownerLen + 1);
+    }
+}
diff --git a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
index de5e51b47a..bdf8d47d5e 100644
--- a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
@@ -45,6 +45,14 @@ import org.codehaus.groovy.transform.trait.Traits;
 import java.util.HashSet;
 import java.util.Set;
 
+import static org.apache.groovy.ast.tools.TypeUseUtils.describeTypeUse;
+import static org.apache.groovy.ast.tools.TypeUseUtils.hasWildcardTypeArgument;
+import static org.apache.groovy.ast.tools.TypeUseUtils.isParameterizedEnclosingType;
+import static org.apache.groovy.ast.tools.TypeUseUtils.isParameterizedTypeUsage;
+import static org.apache.groovy.ast.tools.TypeUseUtils.isParameterizedTypeUsageIncludingEnclosing;
+import static org.apache.groovy.ast.tools.TypeUseUtils.isRawGenericType;
+import static org.apache.groovy.ast.tools.TypeUseUtils.isReifiable;
+import static org.apache.groovy.ast.tools.TypeUseUtils.isStaticMemberType;
 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;
@@ -102,10 +110,10 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
         if (checkWildcard(sc)) return;
 
         boolean isAIC = node instanceof InnerClassNode && ((InnerClassNode) node).isAnonymous();
-        checkGenericsUsage(sc, sc.redirect(), isAIC ? Boolean.TRUE : null);
+        checkGenericsUsage(sc, sc.redirect(), isAIC);
         for (ClassNode face : node.getInterfaces()) {
             checkWildcard(face); // JLS 8.1.5: superinterfaces may not specify a wildcard
-            checkGenericsUsage(face, face.redirect(), isAIC ? Boolean.TRUE : null);
+            checkGenericsUsage(face, face.redirect(), isAIC);
         }
 
         visitObjectInitializerStatements(node);
@@ -178,7 +186,7 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
         }
         boolean isAIC = type instanceof InnerClassNode
                 && ((InnerClassNode) type).isAnonymous();
-        checkGenericsUsage(type, type.redirect(), isAIC);
+        checkGenericsUsage(type, type.redirect(), true);
         if (expression.isUsingGenerics()) {
             ClassNode created = isAIC ? diamondTargetOfAnonymousClass(type) : type;
             if (created != null && created.getGenericsTypes() != null && created.getGenericsTypes().length == 0) {
@@ -223,27 +231,21 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
         super.visitArrayExpression(expression);
     }
 
-    /**
-     * 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}.
+     * {@link #visitPropertyExpression}. {@link ClassExpression} is also
+     * the TypeName of a static qualifier, {@code instanceof} operand, or
+     * method reference; those parents visit the type without entering
+     * this method, so a class-literal check here is only for an actual
+     * class literal.
      */
     @Override
     public void visitClassExpression(final ClassExpression expression) {
-        if (suppressClassLiteralChecks == 0) {
-            checkClassLiteral(expression.getType(), expression);
-        }
+        checkClassLiteral(expression.getType(), expression);
         super.visitClassExpression(expression);
     }
 
@@ -280,12 +282,9 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
         Expression object = call.getObjectExpression();
         checkStaticMemberViaParameterizedType(object, call);
         if (object instanceof ClassExpression) {
-            suppressClassLiteralChecks++;
-            try {
-                super.visitMethodCallExpression(call);
-            } finally {
-                suppressClassLiteralChecks--;
-            }
+            // TypeName of a static qualifier, not a class literal.
+            call.getMethod().visit(this);
+            call.getArguments().visit(this);
         } else {
             super.visitMethodCallExpression(call);
         }
@@ -306,17 +305,13 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
             expression.getProperty().visit(this);
             return;
         }
-        checkStaticMemberViaParameterizedType(object, expression);
         if (object instanceof ClassExpression) {
-            suppressClassLiteralChecks++;
-            try {
-                super.visitPropertyExpression(expression);
-            } finally {
-                suppressClassLiteralChecks--;
-            }
-        } else {
-            super.visitPropertyExpression(expression);
+            // TypeName of a static qualifier, not a class literal.
+            checkStaticMemberViaParameterizedType(object, expression);
+            expression.getProperty().visit(this);
+            return;
         }
+        super.visitPropertyExpression(expression);
     }
 
     /**
@@ -330,13 +325,9 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
     public void visitMethodPointerExpression(final MethodPointerExpression expression) {
         Expression object = expression.getExpression();
         if (object instanceof ClassExpression) {
+            // TypeName of a method or constructor reference, not a class literal.
             checkGenericsUsage(object.getType());
-            suppressClassLiteralChecks++;
-            try {
-                super.visitMethodPointerExpression(expression);
-            } finally {
-                suppressClassLiteralChecks--;
-            }
+            expression.getMethodName().visit(this);
         } else {
             super.visitMethodPointerExpression(expression);
         }
@@ -376,7 +367,7 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
             addError("Cannot select from a type parameter " + element.getUnresolvedName(), location);
             return;
         }
-        if (isParameterizedClassLiteralType(element)) {
+        if (isParameterizedTypeUsageIncludingEnclosing(element)) {
             addError("Cannot select from a parameterized type", location);
         }
     }
@@ -385,16 +376,6 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
         return "class".equals(expression.getPropertyAsString());
     }
 
-    private static boolean isParameterizedClassLiteralType(final ClassNode type) {
-        if (isParameterizedTypeUsage(type)) {
-            return true;
-        }
-        ClassNode oc = type.getOuterClassType();
-        // Use-site parameterization only (Outer<String>.Inner.class).
-        // Declaration formals on Map must not make Map.Entry a class-literal error.
-        return oc != null && (isParameterizedTypeUsage(oc) || isParameterizedClassLiteralType(oc));
-    }
-
     private boolean checkWildcard(final ClassNode sn) {
         boolean wildcard = false;
         if (sn.getGenericsTypes() != null) {
@@ -411,10 +392,10 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
     private void checkGenericsUsage(ClassNode cn) {
         while (cn.isArray())
             cn = cn.getComponentType();
-        checkGenericsUsage(cn, cn.redirect(), null);
+        checkGenericsUsage(cn, cn.redirect(), false);
     }
 
-    private void checkGenericsUsage(final ClassNode cn, final ClassNode rn, final Boolean isAIC) {
+    private void checkGenericsUsage(final ClassNode cn, final ClassNode rn, final boolean allowDiamond) {
         if (cn.isGenericsPlaceHolder()) return;
         ClassNode oc = cn.getOuterClassType(); // GROOVY-12319: Outer<T>.Inner
         if (oc != null) {
@@ -449,7 +430,7 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
         }
         // parameterize a type by using all the parameters only
         if (cnTypes.length != rnTypes.length) {
-            if (isAIC != null && cnTypes.length == 0) {
+            if (allowDiamond && cnTypes.length == 0) {
                 return; // diamond on constructor calls, including anonymous classes (GROOVY-12319 / JEP 213)
             }
             String message = "The class " + cn.toString(false) + " (supplied with " + plural("type parameter", cnTypes.length) +
@@ -576,107 +557,6 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
         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
-     * 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();
-    }
-
-    /**
-     * 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;
-    }
-
-    /**
-     * 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()) + "[]";
-        }
-        if (type.isGenericsPlaceHolder()) {
-            return type.getUnresolvedName();
-        }
-        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.
-     */
-    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 e7ac06bf24..8aa5ba0644 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -86,6 +86,7 @@ import java.util.function.Predicate;
 
 import static groovy.lang.Tuple.tuple;
 import static org.apache.groovy.ast.tools.ExpressionUtils.transformInlineConstants;
+import static org.apache.groovy.ast.tools.TypeUseUtils.isParameterizedTypeUsage;
 import static org.codehaus.groovy.ast.tools.ClosureUtils.getParametersSafe;
 
 /**
@@ -1199,15 +1200,7 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
                 || (!Modifier.isPrivate(modifiers) && Objects.equals(innerType.getPackageName(), outerType.getPackageName()));
     }
 
-    /**
-     * True for a use-site parameterization such as {@code Outer<String>}, not
-     * the generic type name {@code Map} whose {@code getGenericsTypes()} are
-     * declaration formals.
-     */
-    private static boolean isParameterizedTypeUsage(final ClassNode type) {
-        GenericsType[] gt = type.getGenericsTypes();
-        return gt != null && gt.length > 0 && type.isRedirectNode() && !type.isGenericsPlaceHolder();
-    }
+
 
     private boolean directlyImplementsTrait(final ClassNode trait) {
         ClassNode[] interfaces = currentClass.getInterfaces();
@@ -1746,23 +1739,22 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
         return resolved;
     }
 
+    /**
+     * Resolves a type-parameter section (class, method or constructor header).
+     * Bound type arguments such as {@code T} in {@code T extends Comparable<T>}
+     * are uses, not declarations, and go through {@link #resolveGenericsType}.
+     */
     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, final AnnotatedNode declaration) {
+        if (types == null) return;
         currentClass.setUsingGenerics(true);
         List<Tuple2<ClassNode, ClassNode>> upperBoundsToResolve = new LinkedList<>();
-        List<Tuple2<ClassNode, GenericsType>> upperBoundsWithGenerics = new LinkedList<>();
+        List<ClassNode> upperBoundsWithGenerics = new LinkedList<>();
         for (GenericsType type : types) {
-            if (level > 0 && type.getName().equals(rootType.getName())) continue; // cycle!
-
             String name = type.getName();
             ClassNode typeType = type.getType();
             visitTypeAnnotations(typeType); // JSR 308 support
             GenericsTypeName gtn = new GenericsTypeName(name);
             boolean isWildcardGT = QUESTION_MARK.equals(name);
-            boolean dealWithGenerics = (level == 0 || (level > 0 && genericParameterNames.get(gtn) != null));
 
             if (type.getUpperBounds() != null) {
                 boolean nameAdded = false;
@@ -1771,33 +1763,26 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
 
                     if (!isWildcardGT) {
                         if (!nameAdded || !resolve(typeType)) {
-                            if (dealWithGenerics) {
-                                type.setPlaceholder(true);
-                                typeType.setRedirect(upperBound);
-                                genericParameterNames.put(gtn, type);
-                                if (level == 0) type.setGenericDeclaration(declaration);
-                                nameAdded = true;
-                            }
+                            type.setPlaceholder(true);
+                            typeType.setRedirect(upperBound);
+                            genericParameterNames.put(gtn, type);
+                            type.setGenericDeclaration(declaration);
+                            nameAdded = true;
                         }
                         upperBoundsToResolve.add(tuple(upperBound, typeType));
                     }
                     if (upperBound.getGenericsTypes() != null) {
-                        upperBoundsWithGenerics.add(tuple(upperBound, type));
+                        upperBoundsWithGenerics.add(upperBound);
                     }
                 }
-            } else if (dealWithGenerics && !isWildcardGT) {
+            } else if (!isWildcardGT) {
                 type.setPlaceholder(true);
-                GenericsType last = genericParameterNames.put(gtn, 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());
-                }
+                genericParameterNames.put(gtn, type);
+                // 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);
             }
         }
 
@@ -1810,9 +1795,8 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
             }
         }
 
-        for (var tuple : upperBoundsWithGenerics) {
-            GenericsType[] bounds = tuple.getV1().getGenericsTypes();
-            resolveGenericsHeader(bounds, level == 0 ? tuple.getV2() : rootType, level + 1, declaration);
+        for (ClassNode upperBound : upperBoundsWithGenerics) {
+            resolveGenericsTypes(upperBound.getGenericsTypes());
         }
     }
 
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 e31fb71edc..6b048f15b3 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -5720,6 +5720,8 @@ trying: for (ClassNode[] signature : signatures) {
             }
         }
 
+        cn = withRareTypeFromOrigin(exp, cn);
+
         ClassNode oldValue = (ClassNode) exp.putNodeMetaData(INFERRED_TYPE, cn);
         if (oldValue != null) {
             // this may happen when a variable declaration type is wider than the subsequent assignment values
@@ -5756,6 +5758,29 @@ trying: for (ClassNode[] signature : signatures) {
         }
     }
 
+    /**
+     * Rare types ({@code Outer<T>.Inner}) store the enclosing parameterization
+     * on the origin type. Inference often produces a redirect of {@code Inner}
+     * without that field; copy it at the {@link #storeType} write site so
+     * {@link #getType} does not have to reconstruct it.
+     */
+    private static ClassNode withRareTypeFromOrigin(final Expression exp, final ClassNode cn) {
+        if (cn == null || cn.getOuterClassType() != null) {
+            return cn;
+        }
+        ClassNode origin = null;
+        if (exp instanceof VariableExpression vexp) {
+            origin = vexp.getOriginType();
+        }
+        if (origin == null || origin.getOuterClassType() == null) {
+            return cn;
+        }
+        ClassNode copy = cn.getPlainNodeReference();
+        copy.setGenericsTypes(origin.getGenericsTypes());
+        copy.setOuterClassType(origin.getOuterClassType());
+        return copy;
+    }
+
     /**
      * Computes the result type of a binary operation.
      */
@@ -6367,13 +6392,6 @@ 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;
         }
 
@@ -6398,15 +6416,7 @@ trying: for (ClassNode[] signature : signatures) {
                 return fieldType;
             }
             if (variable != vexp && variable instanceof VariableExpression) {
-                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;
+                return getType((VariableExpression) variable);
             }
             if (variable instanceof Parameter parameter) {
                 if (getTemporaryTypesForExpression(vexp).isEmpty()) { // not instanceof
@@ -7142,9 +7152,6 @@ 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();
             }
diff --git a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
index 47dda1c5dc..7f95e2d3fc 100644
--- a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
+++ b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
@@ -3428,7 +3428,7 @@ final class GenericsJavaCompatibilityTest {
     }
 
     // =========================================================================
-    // Category 17: Parameter-type bridges, capture conversion, varargs heap
+    // Category 17 (continued): Parameter-type bridges, capture conversion, varargs heap
     //   pollution, catch of a type variable, nested type arguments, extra
     //   interface type parameters, and diamond on a generic class with a
     //   generic constructor (JLS 4.5, 4.12.2, 5.1.10, 8.4.8.3, 8.8.4, 14.20)
diff --git a/src/test/groovy/org/apache/groovy/ast/tools/TypeUseUtilsTest.groovy b/src/test/groovy/org/apache/groovy/ast/tools/TypeUseUtilsTest.groovy
new file mode 100644
index 0000000000..1edd679d8d
--- /dev/null
+++ b/src/test/groovy/org/apache/groovy/ast/tools/TypeUseUtilsTest.groovy
@@ -0,0 +1,109 @@
+/*
+ *  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 org.apache.groovy.ast.tools
+
+import org.codehaus.groovy.ast.ClassHelper
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.GenericsType
+import org.junit.jupiter.api.Test
+
+import static org.apache.groovy.ast.tools.TypeUseUtils.describeTypeUse
+import static org.apache.groovy.ast.tools.TypeUseUtils.isParameterizedEnclosingType
+import static org.apache.groovy.ast.tools.TypeUseUtils.isParameterizedTypeUsage
+import static org.apache.groovy.ast.tools.TypeUseUtils.isParameterizedTypeUsageIncludingEnclosing
+import static org.apache.groovy.ast.tools.TypeUseUtils.isRawGenericType
+import static org.apache.groovy.ast.tools.TypeUseUtils.isReifiable
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertFalse
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+/**
+ * Tests for {@link TypeUseUtils}.
+ */
+final class TypeUseUtilsTest {
+
+    @Test
+    void 'declaration formals are not a parameterized usage'() {
+        ClassNode list = ClassHelper.LIST_TYPE
+        assertFalse(isParameterizedTypeUsage(list))
+        assertFalse(list.redirectNode)
+    }
+
+    @Test
+    void 'use-site arguments are a parameterized usage'() {
+        ClassNode usage = ClassHelper.LIST_TYPE.plainNodeReference
+        usage.genericsTypes = [new GenericsType(ClassHelper.STRING_TYPE)] as GenericsType[]
+        assertTrue(isParameterizedTypeUsage(usage))
+        assertTrue(isParameterizedEnclosingType(usage))
+        assertFalse(isReifiable(usage))
+        assertEquals('List<java.lang.String>', describeTypeUse(usage))
+    }
+
+    @Test
+    void 'raw generic type is reifiable'() {
+        ClassNode raw = ClassHelper.LIST_TYPE.plainNodeReference
+        raw.genericsTypes = null
+        assertTrue(isRawGenericType(raw))
+        assertTrue(isReifiable(raw))
+        assertFalse(isParameterizedTypeUsage(raw))
+    }
+
+    @Test
+    void 'unbounded wildcard is reifiable'() {
+        ClassNode star = ClassHelper.LIST_TYPE.plainNodeReference
+        GenericsType wildcard = new GenericsType(ClassHelper.OBJECT_TYPE, null, null)
+        wildcard.wildcard = true
+        star.genericsTypes = [wildcard] as GenericsType[]
+        assertTrue(isReifiable(star))
+        assertTrue(isReifiable(star.makeArray()))
+    }
+
+    @Test
+    void 'type parameter and diamond are not reifiable'() {
+        ClassNode placeholder = ClassHelper.makeWithoutCaching('T')
+        placeholder.genericsPlaceHolder = true
+        placeholder.redirect = ClassHelper.OBJECT_TYPE
+        assertFalse(isReifiable(placeholder))
+        assertEquals('T', describeTypeUse(placeholder))
+
+        ClassNode diamond = ClassHelper.LIST_TYPE.plainNodeReference
+        diamond.genericsTypes = GenericsType.EMPTY_ARRAY
+        assertFalse(isReifiable(diamond))
+    }
+
+    @Test
+    void 'generic type name of Map Entry is not a parameterized class literal'() {
+        ClassNode map = ClassHelper.MAP_TYPE
+        ClassNode entry = ClassHelper.makeWithoutCaching('java.util.Map$Entry')
+        entry.redirect = ClassHelper.make(Map.Entry)
+        // Declaration formals on Map must not make Map.Entry a class-literal error.
+        assertFalse(isParameterizedTypeUsage(map))
+        assertFalse(isParameterizedTypeUsageIncludingEnclosing(entry))
+    }
+
+    @Test
+    void 'rare type with parameterized enclosing is a parameterized class literal'() {
+        ClassNode outer = ClassHelper.LIST_TYPE.plainNodeReference
+        outer.genericsTypes = [new GenericsType(ClassHelper.STRING_TYPE)] as GenericsType[]
+        ClassNode inner = ClassHelper.makeWithoutCaching('java.util.List$Inner')
+        inner.outerClassType = outer
+        assertTrue(isParameterizedTypeUsageIncludingEnclosing(inner))
+        assertEquals('List<java.lang.String>.Inner', describeTypeUse(inner))
+    }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java b/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
index d9b9ae6b52..247fe1a701 100644
--- a/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
+++ b/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
@@ -19,6 +19,7 @@
 package org.codehaus.groovy.ast;
 
 import org.codehaus.groovy.GroovyBugError;
+import org.codehaus.groovy.ast.stmt.EmptyStatement;
 import org.codehaus.groovy.ast.tools.GenericsUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -195,6 +196,23 @@ public final class ClassNodeTest {
         assertEquals(outer, classNode.getOuterClassType());
     }
 
+    @Test // GROOVY-12319
+    public void testAsGenericsTypeCopiesGenericDeclaration() {
+        MethodNode method = new MethodNode("m", ACC_PUBLIC, ClassHelper.OBJECT_TYPE,
+                Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE);
+        ClassNode placeholder = ClassHelper.makeWithoutCaching("T");
+        placeholder.setGenericsPlaceHolder(true);
+        placeholder.setRedirect(ClassHelper.OBJECT_TYPE);
+        GenericsType header = new GenericsType(placeholder, null, null);
+        header.setPlaceholder(true);
+        header.setGenericDeclaration(method);
+        placeholder.setGenericsTypes(new GenericsType[]{header});
+
+        GenericsType copied = placeholder.asGenericsType();
+        assertEquals(method, copied.getGenericDeclaration());
+        assertTrue(copied.isPlaceholder());
+    }
+
     @Test
     public void testPermittedSubclasses() throws Exception {
         assumeTrue(Runtime.version().feature() >= 17);