You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/08/31 14:31:46 UTC

(groovy) 03/05: GROOVY-12319: review follow-up for rare types and instanceof

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch generics-tck
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 5a8cc8d1e2af6f4488859f1ba3bceb05e38d19b1
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Aug 30 12:46:28 2026 +0000

    GROOVY-12319: review follow-up for rare types and instanceof
    
    Store the JLS 4.5 enclosing type on ClassNode instead of node metadata.
    Treat both parser and binary nested names in JVMS signatures, and only
    emit the nested form when the enclosing type is parameterized.
    
    Reject non-reifiable instanceof at parse; check nested-type well-formedness
    on the operand without treating it as a class literal. Expand
    GenericsJavaCompatibilityTest as the Java-vs-Groovy matrix.
---
 .../apache/groovy/parser/antlr4/AstBuilder.java    |  30 +-
 .../java/org/codehaus/groovy/ast/ClassNode.java    |  20 +-
 .../groovy/classgen/asm/BytecodeHelper.java        |  40 ++-
 .../codehaus/groovy/control/GenericsVisitor.java   |  29 +-
 .../groovy/control/InstanceOfVerifier.java         |   7 +-
 .../codehaus/groovy/control/ResolveVisitor.java    |  23 +-
 src/test/groovy/bugs/Groovy12319.groovy            |  17 --
 .../generics/GenericsJavaCompatibilityTest.groovy  | 340 ++++++++++++++++++++-
 src/test/groovy/groovy/InstanceofTest.groovy       |   4 +-
 .../org/codehaus/groovy/ast/ClassNodeTest.java     |  19 ++
 .../groovy/classgen/BytecodeHelperTest.groovy      |  61 +++-
 11 files changed, 535 insertions(+), 55 deletions(-)

diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 97fcb45fe3..e4b4fdef9b 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -3988,7 +3988,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         if (asBoolean(ctx.primitiveType())) {
             classNode = configureAST(this.visitPrimitiveType(ctx.primitiveType()), ctx);
         } else {
-            classNode = this.visitParameterizedClassType(ctx);
+            classNode = this.visitParameterizedClassType(ctx, false);
             if (classNode != null) {
                 configureAST(classNode, ctx);
             }
@@ -4666,7 +4666,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
     @Override
     public ClassNode visitReferenceType(final ReferenceTypeContext ctx) {
-        return configureAST(this.visitParameterizedClassType(ctx), ctx);
+        return configureAST(this.visitParameterizedClassType(ctx, true), ctx);
     }
 
     /**
@@ -4674,7 +4674,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
      * Shared by {@code referenceType} (type arguments only) and {@code createdName}
      * (type arguments or diamond).
      */
-    private ClassNode visitParameterizedClassType(final GroovyParserRuleContext ctx) {
+    private ClassNode visitParameterizedClassType(final GroovyParserRuleContext ctx, final boolean attachRawEnclosing) {
         ClassNode classNode = null;
         for (int i = 0, n = ctx.getChildCount(); i < n; i += 1) {
             ParseTree child = ctx.getChild(i);
@@ -4690,9 +4690,15 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
                 classNode = this.visitQualifiedStandardClassName(qscn);
             } else if (child instanceof TypeArgumentsContext tac) {
                 GenericsType[] generics = this.visitTypeArguments(tac);
+                if (attachRawEnclosing) {
+                    attachRawEnclosingIfQualified(classNode);
+                }
                 classNode.setGenericsTypes(generics);
                 rejectParameterizedInstanceof(ctx, classNode, generics);
             } else if (child instanceof TypeArgumentsOrDiamondContext tad) {
+                if (attachRawEnclosing) {
+                    attachRawEnclosingIfQualified(classNode);
+                }
                 classNode.setGenericsTypes(this.visitTypeArgumentsOrDiamond(tad));
             } else if (child instanceof IdentifierContext || child instanceof ClassNameContext) {
                 ClassNode outer = classNode;
@@ -4707,6 +4713,24 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         return classNode;
     }
 
+    /**
+     * {@code Outer.Inner<?>} is one qualified name plus type arguments, so the
+     * nested-type loop never runs. Record a raw enclosing type from the name
+     * prefix; {@code ResolveVisitor} drops it if that prefix is only a package.
+     */
+    private static void attachRawEnclosingIfQualified(final ClassNode classNode) {
+        if (classNode == null || classNode.getOuterClassType() != null) return;
+        String name = classNode.getName();
+        int sep = Math.max(name.lastIndexOf('.'), name.lastIndexOf('$'));
+        if (sep <= 0) return;
+        classNode.setOuterClassType(ClassHelper.makeWithoutCaching(name.substring(0, sep)));
+    }
+
+    /**
+     * {@code instanceof} type arguments are legal in the grammar so {@code List<?>}
+     * can parse (JLS 15.20.2 / 4.7). Concrete arguments such as
+     * {@code Map<String,Integer>} are not reifiable and are rejected here.
+     */
     private void rejectParameterizedInstanceof(final GroovyParserRuleContext ctx, final ClassNode classNode, final GenericsType[] generics) {
         if (isTrue(ctx, IS_INSIDE_INSTANCEOF_EXPR) && Arrays.stream(generics).anyMatch(gt ->
                 !gt.isWildcard() || asBoolean(gt.getLowerBound()) || ArrayGroovyMethods.asBoolean(gt.getUpperBounds()))) { // GROOVY-11585
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassNode.java b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
index 7c7544190c..e0e866e868 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
@@ -118,6 +118,11 @@ import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
  * they describe the type signature used at the point of declaration or the
  * type signatures provided by the class. If the type signatures provided
  * by the class are needed, then a call to {@link #redirect()} will help.
+ * {@link #getOuterClassType()} / {@link #setOuterClassType(ClassNode)} are
+ * likewise not proxied: they record the parameterized enclosing type of a
+ * JLS 4.5 rare type <em>use</em> ({@code Outer<T>.Inner}), distinct from
+ * {@link #getOuterClass()} which is the enclosing class of a nested class
+ * declaration.
  *
  * @see org.codehaus.groovy.ast.ClassHelper
  */
@@ -190,6 +195,8 @@ public class ClassNode extends AnnotatedNode {
     protected Class<?> clazz;
     // if not null then this instance is an array
     private ClassNode componentType;
+    // parameterized enclosing type of a JLS 4.5 rare type (Outer<T>.Inner); type-use, not declaration
+    private ClassNode outerClassType;
     // if not null then this instance is handled as proxy for the redirect
     private ClassNode redirect;
 
@@ -2055,24 +2062,27 @@ faces:  if (method == null && asBoolean(getInterfaces())) { // GROOVY-11323
 
     /**
      * For a JLS 4.5 rare type {@code Outer<T>.Inner}, the parameterized enclosing
-     * type. Distinct from {@link #getOuterClass()}, which is the enclosing class
-     * of a nested class <em>declaration</em>.
+     * type stored on this node (type-use, not declaration; not node metadata and
+     * not redirected). Distinct from {@link #getOuterClass()}, which is the
+     * enclosing class of a nested class <em>declaration</em>. Copied by
+     * {@link #getPlainNodeReference()}.
      *
      * @return the parameterized enclosing type, or {@code null} if this is not a rare type
      * @since 6.0.0
      */
     public ClassNode getOuterClassType() {
-        return getNodeMetaData("outer.class");
+        return outerClassType;
     }
 
     /**
-     * Records the parameterized enclosing type of a JLS 4.5 rare type.
+     * Records the parameterized enclosing type of a JLS 4.5 rare type on this
+     * node. Does not write through {@link #redirect()}.
      *
      * @param outer the parameterized {@code Outer<T>} node; {@code null} clears it
      * @since 6.0.0
      */
     public void setOuterClassType(final ClassNode outer) {
-        putNodeMetaData("outer.class", outer);
+        this.outerClassType = outer;
     }
 
     /**
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java b/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
index 0b12674e16..3babbc05ec 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/BytecodeHelper.java
@@ -478,7 +478,9 @@ public class BytecodeHelper {
      */
     private static void writeParameterizedClass(StringBuilder ret, ClassNode printType) {
         ClassNode owner = printType.getOuterClassType();
-        if (owner != null) {
+        // JVMS 4.7.9.1 nested form is only for a parameterized enclosing type.
+        // A raw enclosing type must keep the binary name (Outer$Inner<...>).
+        if (owner != null && hasGenerics(owner)) {
             writeParameterizedClass(ret, owner);
             ret.append('.');
             ret.append(innerClassSimpleName(printType, owner));
@@ -489,17 +491,39 @@ public class BytecodeHelper {
         addSubTypes(ret, printType.getGenericsTypes(), "<", ">");
     }
 
+    /**
+     * Simple name of {@code inner} relative to {@code owner} for a JVMS 4.7.9.1
+     * nested type signature. Accepts both {@code Outer.Inner} (parser) and
+     * {@code Outer$Inner} (binary name after resolve); {@code Foo$1} is the
+     * same prefix path. If {@code inner} is not nested under {@code owner},
+     * the result is the last identifier only — never extra qualification of
+     * {@code owner} (so owner {@code Foo} and inner {@code Bar.X} yield {@code X}).
+     */
     private static String innerClassSimpleName(final ClassNode inner, final ClassNode owner) {
         String innerName = inner.getName();
         String ownerName = owner.getName();
-        if (innerName.startsWith(ownerName) && innerName.length() > ownerName.length()) {
-            char sep = innerName.charAt(ownerName.length());
-            if (sep == '.' || sep == '$') {
-                return innerName.substring(ownerName.length() + 1).replace('$', '.');
-            }
+        String nested = nestedNameAfterOwner(innerName, ownerName);
+        if (nested != null) {
+            return nested.replace('$', '.');
+        }
+        int sep = Math.max(innerName.lastIndexOf('.'), innerName.lastIndexOf('$'));
+        return sep < 0 ? innerName : innerName.substring(sep + 1);
+    }
+
+    /**
+     * Remainder of {@code innerName} after {@code ownerName} when nested under
+     * it ({@code .} or {@code $} separator); otherwise {@code null}.
+     */
+    private static String nestedNameAfterOwner(final String innerName, final String ownerName) {
+        int ownerLen = ownerName.length();
+        if (innerName.length() <= ownerLen || !innerName.startsWith(ownerName)) {
+            return null;
+        }
+        char sep = innerName.charAt(ownerLen);
+        if (sep != '.' && sep != '$') {
+            return null;
         }
-        int dot = Math.max(innerName.lastIndexOf('.'), innerName.lastIndexOf('$'));
-        return dot < 0 ? innerName : innerName.substring(dot + 1);
+        return innerName.substring(ownerLen + 1);
     }
 
     private static void writeGenericsBounds(StringBuilder ret, GenericsType type, boolean writeInterfaceMarker) {
diff --git a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
index 991cfdc548..341f06218f 100644
--- a/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/GenericsVisitor.java
@@ -212,14 +212,23 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
 
     /**
      * Groovy represents the type operand of {@code instanceof} as a
-     * {@link ClassExpression}, but it is not a class literal. Skip it so
-     * {@link InstanceOfVerifier} can diagnose JLS 15.20.2.
+     * {@link ClassExpression}, but it is not a class literal ({@code T.class}).
+     * Do not visit that operand as a class expression (that would reject
+     * {@code instanceof T} as {@code T.class}). Still run
+     * {@link #checkGenericsUsage(ClassNode)} so nested-type well-formedness
+     * applies ({@code Outer<?>.Inner} with a generic {@code Inner},
+     * {@code Outer.Inner<?>}). Parameterized {@code instanceof} is handled at
+     * parse ({@code AstBuilder.rejectParameterizedInstanceof}): the grammar
+     * allows type arguments so {@code instanceof List<?>} can parse, while
+     * {@code instanceof Map<String,Integer>} is rejected (JLS 15.20.2).
+     * {@link InstanceOfVerifier} then checks primitives and type-parameter targets.
      */
     @Override
     public void visitBinaryExpression(final BinaryExpression expression) {
         if (expression.getOperation().isA(Types.INSTANCEOF_OPERATOR)
                 && expression.getRightExpression() instanceof ClassExpression) {
             expression.getLeftExpression().visit(this);
+            checkGenericsUsage(expression.getRightExpression().getType());
             return;
         }
         super.visitBinaryExpression(expression);
@@ -288,14 +297,18 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
         ClassNode oc = cn.getOuterClassType(); // GROOVY-12319: Outer<T>.Inner
         if (oc != null) {
             checkGenericsUsage(oc);
-            // JLS 4.5 / 6.5.5: a parameterized enclosing type may only select a
-            // non-static member type, and a generic member must be parameterized.
+            // JLS 4.5 / 4.8 / 6.5.5: a parameterized enclosing type may only
+            // select a non-static member type, and a generic member must be
+            // parameterized. Type arguments on a non-static member of a raw
+            // enclosing type ({@code Outer.Inner<?>}) are also illegal.
             if (isParameterizedEnclosingType(oc)) {
                 if (isStaticMemberType(cn)) {
                     addError("Cannot refer to a static member of a generic type through a parameterization", cn);
                 } else if (cn.getGenericsTypes() == null && rn.getGenericsTypes() != null) {
                     addError("A raw member type may not be used with a parameterized enclosing type", cn);
                 }
+            } else if (cn.getGenericsTypes() != null && isRawGenericType(oc) && !isStaticMemberType(cn)) {
+                addError("Type arguments cannot be given on a raw nested type", cn);
             }
         }
         GenericsType[] cnTypes = cn.getGenericsTypes();
@@ -433,6 +446,14 @@ public class GenericsVisitor extends ClassCodeVisitorSupport {
         return gt != null && gt.length > 0 && !type.isGenericsPlaceHolder();
     }
 
+    /**
+     * True when {@code type} is a raw use of a generic declaration ({@code Outer}
+     * for {@code class Outer<T>}).
+     */
+    private static boolean isRawGenericType(final ClassNode type) {
+        return type.getGenericsTypes() == null && type.redirect().getGenericsTypes() != null;
+    }
+
     /**
      * Nested interfaces and enums are implicitly static; otherwise the
      * {@code ACC_STATIC} bit on the declaration is authoritative.
diff --git a/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java b/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java
index 7a303b01fa..68779b4309 100644
--- a/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java
+++ b/src/main/java/org/codehaus/groovy/control/InstanceOfVerifier.java
@@ -52,9 +52,12 @@ public abstract class InstanceOfVerifier extends ClassCodeVisitorSupport {
                 if (referenceType.isGenericsPlaceHolder()) {
                     addTypeError(expression.getRightExpression(), "type parameter " + referenceType.getUnresolvedName() +
                         ". Use its erasure " + referenceType.getNameWithoutPackage() + " instead since further generic type information will be erased at runtime");
-                } else if (referenceType.getGenericsTypes() != null) {
-                    // TODO: Cannot perform instanceof check against parameterized type Class<Type>. Use the form Class<?> instead since further generic type information will be erased at runtime
                 }
+                // Parameterized uses (Map<String,Integer>) are not rejected here:
+                // resolved ClassNodes often still carry declaration placeholders
+                // (List<E>). The grammar allows type arguments so instanceof List<?>
+                // can parse; non-reifiable uses are rejected at parse by
+                // AstBuilder.rejectParameterizedInstanceof (JLS 15.20.2).
             }
         }
         super.visitBinaryExpression(expression);
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index 0bcb6e0d69..bfe0a90fd8 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -422,8 +422,15 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
         }
         ClassNode oc = type.getOuterClassType(); // GROOVY-10646, GROOVY-12319: Outer<T>.Inner
         if (oc != null) {
-            resolveOrFail(oc, msg, node, preferImports);
-            resolveGenericsTypes(oc.getGenericsTypes());
+            if (oc.getGenericsTypes() != null) {
+                resolveOrFail(oc, msg, node, preferImports);
+                resolveGenericsTypes(oc.getGenericsTypes());
+            } else if (resolve(oc, true, true, true)) {
+                resolveGenericsTypes(oc.getGenericsTypes());
+            } else {
+                // Prefix of a dotted name was a package, not a class (List<?>).
+                type.setOuterClassType(null);
+            }
         }
         if (preferImports && !type.isResolved() && !type.isPrimaryClassNode()) {
             resolveGenericsTypes(type.getGenericsTypes());
@@ -490,10 +497,16 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
         resolveGenericsTypes(genericsTypes);
         ClassNode oc = type.getOuterClassType();
         if (oc != null) {
-            if (!resolve(oc, testModuleImports, testDefaultImports, testStaticInnerClasses)) {
-                return false;
+            if (oc.getGenericsTypes() != null) {
+                if (!resolve(oc, testModuleImports, testDefaultImports, testStaticInnerClasses)) {
+                    return false;
+                }
+                resolveGenericsTypes(oc.getGenericsTypes());
+            } else if (resolve(oc, testModuleImports, testDefaultImports, testStaticInnerClasses)) {
+                resolveGenericsTypes(oc.getGenericsTypes());
+            } else {
+                type.setOuterClassType(null);
             }
-            resolveGenericsTypes(oc.getGenericsTypes());
         }
 
         if (type.isPrimaryClassNode()) return true;
diff --git a/src/test/groovy/bugs/Groovy12319.groovy b/src/test/groovy/bugs/Groovy12319.groovy
index 2af644c6da..676c6a2cac 100644
--- a/src/test/groovy/bugs/Groovy12319.groovy
+++ b/src/test/groovy/bugs/Groovy12319.groovy
@@ -947,23 +947,6 @@ final class Groovy12319 {
         assert err.message.contains('parameterized enclosing type')
     }
 
-    @Test
-    void testInstanceofParameterizedRareTypeRejected() {
-        def err = shouldFail CompilationFailedException, '''
-            class Outer<T> {
-                class Inner {}
-            }
-            def x = null
-            x instanceof Outer<String>.Inner
-        '''
-        assert err.message.contains('parameterized type')
-
-        err = shouldFail CompilationFailedException, '''
-            def x = ([] instanceof java.util.ArrayList<String>)
-        '''
-        assert err.message.contains('parameterized type')
-    }
-
     @Test
     void testConstructorTypeArgumentsWithFunctionalArgumentCompileStatic() {
         assertScript '''
diff --git a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
index 6cca7ae739..9dacb83555 100644
--- a/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
+++ b/src/test/groovy/gls/generics/GenericsJavaCompatibilityTest.groovy
@@ -1769,8 +1769,11 @@ final class GenericsJavaCompatibilityTest {
     }
 
     /**
-     * JLS 4.7, 15.20.2: {@code List<?>} and {@code Class<?>} are reifiable (unbounded
-     * wildcards), so they may appear as the {@code instanceof} reference type.
+     * JLS 4.7, 15.20.2: {@code List<?>}, {@code ArrayList<?>}, {@code Class<?>}
+     * and {@code Map<?,?>} are reifiable (every type argument is an unbounded
+     * wildcard), as is the raw type {@code List}. A parameterized left operand
+     * such as {@code List<String> list} is legal; {@code null instanceof List<?>}
+     * is well-typed and yields {@code false}.
      */
     @Test
     void testReifiableInstanceofUnboundedWildcard() {
@@ -1778,12 +1781,88 @@ final class GenericsJavaCompatibilityTest {
         final String javaSrc = '''
             package gls.generics.test;
             import java.util.ArrayList;
+            import java.util.HashMap;
             import java.util.List;
+            import java.util.Map;
             public class ReifiableInstanceof {
                 public static boolean test() {
-                    Object list = new ArrayList<String>();
-                    Object cls = ReifiableInstanceof.class.getClass();
-                    return (list instanceof List<?>) && (cls instanceof Class<?>);
+                    List<String> list = new ArrayList<String>();
+                    List<?> wildcard = list;
+                    Map<String, Integer> map = new HashMap<String, Integer>();
+                    Class<?> cls = ReifiableInstanceof.class.getClass();
+                    List<String> none = null;
+                    return (list instanceof List<?>)
+                            && (list instanceof ArrayList<?>)
+                            && (list instanceof List)
+                            && (wildcard instanceof List<?>)
+                            && (map instanceof Map<?, ?>)
+                            && (cls instanceof Class<?>)
+                            && !(none instanceof List<?>);
+                }
+            }
+        '''
+        assertPositive(className, javaSrc, true)
+    }
+
+    /**
+     * JLS 4.7, 15.20.2: an array type is reifiable when its element type is
+     * reifiable, so {@code o instanceof List<?>[]} is legal.
+     */
+    @Test
+    void testReifiableInstanceofArrayOfUnboundedWildcard() {
+        final String className = 'gls.generics.test.ReifiableInstanceofArray'
+        final String javaSrc = '''
+            package gls.generics.test;
+            import java.util.List;
+            public class ReifiableInstanceofArray {
+                public static boolean test() {
+                    Object o = new List[0];
+                    return o instanceof List<?>[];
+                }
+            }
+        '''
+        assertPositive(className, javaSrc, true)
+    }
+
+    /**
+     * JLS 4.7, 15.20.2: a non-static member type of a reifiable type is itself
+     * reifiable, so {@code o instanceof Outer<?>.Inner} is legal when {@code Inner}
+     * is not generic.
+     */
+    @Test
+    void testReifiableInstanceofRareTypeUnboundedEnclosing() {
+        final String className = 'gls.generics.test.ReifiableRareInstanceof'
+        final String javaSrc = '''
+            package gls.generics.test;
+            public class ReifiableRareInstanceof {
+                public static class Outer<T> {
+                    public class Inner {}
+                }
+                public static boolean test() {
+                    return !(null instanceof Outer<?>.Inner);
+                }
+            }
+        '''
+        assertPositive(className, javaSrc, true)
+    }
+
+    /**
+     * JLS 4.7, 15.20.2 / JDK {@code T6665356}: when both the enclosing type and
+     * the member are generic, {@code Outer.Inner} (raw) and
+     * {@code Outer<?>.Inner<?>} (all unbounded wildcards) are reifiable.
+     */
+    @Test
+    void testReifiableInstanceofRawAndFullyWildcardNested() {
+        final String className = 'gls.generics.test.ReifiableNestedInstanceof'
+        final String javaSrc = '''
+            package gls.generics.test;
+            public class ReifiableNestedInstanceof {
+                public static class Outer<T> {
+                    public class Inner<U> {}
+                }
+                public static boolean test() {
+                    return !(null instanceof Outer.Inner)
+                            && !(null instanceof Outer<?>.Inner<?>);
                 }
             }
         '''
@@ -1857,6 +1936,239 @@ final class GenericsJavaCompatibilityTest {
         assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
     }
 
+    /**
+     * JLS 4.7, 15.20.2: {@code Map<String,Integer>} is not reifiable.
+     */
+    @Test
+    void testNegativeNonReifiableInstanceofMap() {
+        final String className = 'gls.generics.test.NonReifiableInstanceofMap'
+        final String javaSrc = '''
+            package gls.generics.test;
+            import java.util.Map;
+            public class NonReifiableInstanceofMap {
+                public static boolean test(Object o) {
+                    return o instanceof Map<String, Integer>;
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+    }
+
+    /**
+     * JLS 15.20.2: a non-reifiable {@code instanceof} is still illegal when the
+     * check is negated ({@code !(o instanceof Map<String,Integer>)}).
+     */
+    @Test
+    void testNegativeNegatedNonReifiableInstanceof() {
+        final String className = 'gls.generics.test.NegatedNonReifiableInstanceof'
+        final String javaSrc = '''
+            package gls.generics.test;
+            import java.util.Map;
+            public class NegatedNonReifiableInstanceof {
+                public static boolean test(Object o) {
+                    return !(o instanceof Map<String, Integer>);
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+    }
+
+    /**
+     * JLS 4.7, 15.20.2: {@code List<? extends CharSequence>} is not reifiable
+     * because the wildcard is bounded (GROOVY-11585).
+     */
+    @Test
+    void testNegativeInstanceofUpperBoundedWildcard() {
+        final String className = 'gls.generics.test.InstanceofUpperBoundedWildcard'
+        final String javaSrc = '''
+            package gls.generics.test;
+            import java.util.List;
+            public class InstanceofUpperBoundedWildcard {
+                public static boolean test(Object o) {
+                    return o instanceof List<? extends CharSequence>;
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+    }
+
+    /**
+     * JLS 4.7, 15.20.2: {@code List<? super Integer>} is not reifiable because
+     * the wildcard is bounded (GROOVY-11585).
+     */
+    @Test
+    void testNegativeInstanceofLowerBoundedWildcard() {
+        final String className = 'gls.generics.test.InstanceofLowerBoundedWildcard'
+        final String javaSrc = '''
+            package gls.generics.test;
+            import java.util.List;
+            public class InstanceofLowerBoundedWildcard {
+                public static boolean test(Object o) {
+                    return o instanceof List<? super Integer>;
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+    }
+
+    /**
+     * JLS 4.7, 15.20.2: {@code Outer<String>.Inner} is not reifiable — a nested
+     * type is reifiable only as a non-static member of a reifiable type, and
+     * {@code Outer<String>} is not. Contrast
+     * {@link #testReifiableInstanceofRareTypeUnboundedEnclosing()}.
+     */
+    @Test
+    void testNegativeInstanceofRareType() {
+        final String className = 'gls.generics.test.InstanceofRareType'
+        final String javaSrc = '''
+            package gls.generics.test;
+            public class InstanceofRareType {
+                public static class Outer<T> {
+                    public class Inner {}
+                }
+                public static boolean test(Object o) {
+                    return o instanceof Outer<String>.Inner;
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+    }
+
+    /**
+     * JLS 4.7, 15.20.2: {@code List<String>[]} is not reifiable (the component
+     * type is not).
+     */
+    @Test
+    void testNegativeInstanceofArrayOfParameterizedType() {
+        final String className = 'gls.generics.test.InstanceofGenericArray'
+        final String javaSrc = '''
+            package gls.generics.test;
+            import java.util.List;
+            public class InstanceofGenericArray {
+                public static boolean test(Object o) {
+                    return o instanceof List<String>[];
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+    }
+
+    /**
+     * JLS 4.7, 15.20.2: {@code Map<?,String>} is not reifiable — every type
+     * argument must be an unbounded wildcard.
+     */
+    @Test
+    void testNegativeInstanceofMixedWildcard() {
+        final String className = 'gls.generics.test.InstanceofMixedWildcard'
+        final String javaSrc = '''
+            package gls.generics.test;
+            import java.util.Map;
+            public class InstanceofMixedWildcard {
+                public static boolean test(Object o) {
+                    return o instanceof Map<?, String>;
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be safely cast", "Cannot perform instanceof")
+    }
+
+    /**
+     * JLS 4.7, 15.20.2: a type variable is not reifiable, so {@code o instanceof T}
+     * is a compile-time error.
+     */
+    @Test
+    void testNegativeInstanceofTypeParameter() {
+        final String className = 'gls.generics.test.InstanceofTypeParameter'
+        final String javaSrc = '''
+            package gls.generics.test;
+            public class InstanceofTypeParameter {
+                public static <T> boolean test(Object o) {
+                    return o instanceof T;
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be safely cast", "type parameter")
+    }
+
+    /**
+     * JLS 4.7, 15.20.2: an array of a type variable is not reifiable
+     * ({@code o instanceof T[]}).
+     */
+    @Test
+    void testNegativeInstanceofTypeParameterArray() {
+        final String className = 'gls.generics.test.InstanceofTypeParameterArray'
+        final String javaSrc = '''
+            package gls.generics.test;
+            public class InstanceofTypeParameterArray {
+                public static <T> boolean test(Object o) {
+                    return o instanceof T[];
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be safely cast", "type parameter")
+    }
+
+    /**
+     * JLS 4.5 / JDK {@code T6665356}: {@code Outer<?>.Inner} is malformed when
+     * {@code Inner} is generic (raw member of a parameterized enclosing type).
+     */
+    @Test
+    void testNegativeInstanceofRawGenericMemberOfParameterizedEnclosing() {
+        final String className = 'gls.generics.test.InstanceofRawMemberOfParameterized'
+        final String javaSrc = '''
+            package gls.generics.test;
+            public class InstanceofRawMemberOfParameterized {
+                public static class Outer<T> {
+                    public class Inner<U> {}
+                }
+                public static boolean test(Object o) {
+                    return o instanceof Outer<?>.Inner;
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "improperly formed type", "parameterized enclosing type")
+    }
+
+    /**
+     * JLS 4.8 / JDK {@code T6665356}: {@code Outer.Inner<?>} gives type arguments
+     * on a member of a raw type.
+     */
+    @Test
+    void testNegativeInstanceofTypeArgsOnRawNested() {
+        final String className = 'gls.generics.test.InstanceofParamOnRawNested'
+        final String javaSrc = '''
+            package gls.generics.test;
+            public class InstanceofParamOnRawNested {
+                public static class Outer<T> {
+                    public class Inner<U> {}
+                }
+                public static boolean test(Object o) {
+                    return o instanceof Outer.Inner<?>;
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "improperly formed type", "raw")
+    }
+
+    /**
+     * JLS 4.8 / JDK {@code T6665356}: the same malformed type is illegal as a
+     * field type, not only as an {@code instanceof} operand.
+     */
+    @Test
+    void testNegativeTypeArgsOnRawNestedField() {
+        final String className = 'gls.generics.test.RawNestedField'
+        final String javaSrc = '''
+            package gls.generics.test;
+            public class RawNestedField {
+                public static class Outer<T> {
+                    public class Inner<U> {}
+                }
+                public Outer.Inner<?> field;
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "improperly formed type", "raw")
+    }
+
     /**
      * JLS 4.7, 15.20.2: {@code Class<C>} is not reifiable (the type argument is not
      * an unbounded wildcard), so it may not be used with {@code instanceof}.
@@ -1875,6 +2187,24 @@ final class GenericsJavaCompatibilityTest {
         assertNegativeCompile(className, javaSrc, "cannot be converted", "Cannot perform instanceof")
     }
 
+    /**
+     * JLS 4.7, 15.20.2 / JDK {@code InstanceOf3}: {@code Class<? extends C>} is
+     * not reifiable (the wildcard is bounded).
+     */
+    @Test
+    void testNegativeInstanceofClassUpperBoundedWildcard() {
+        final String className = 'gls.generics.test.InstanceofClassUpperBound'
+        final String javaSrc = '''
+            package gls.generics.test;
+            public class InstanceofClassUpperBound {
+                public static boolean test() {
+                    return InstanceofClassUpperBound.class.getClass() instanceof Class<? extends InstanceofClassUpperBound>;
+                }
+            }
+        '''
+        assertNegativeCompile(className, javaSrc, "cannot be converted", "Cannot perform instanceof")
+    }
+
     /**
      * JLS 6.5.5.1, 8.1.2: a type parameter of the enclosing class is not in scope
      * from a static nested class.
diff --git a/src/test/groovy/groovy/InstanceofTest.groovy b/src/test/groovy/groovy/InstanceofTest.groovy
index 9257ad8bda..e900cd2a26 100644
--- a/src/test/groovy/groovy/InstanceofTest.groovy
+++ b/src/test/groovy/groovy/InstanceofTest.groovy
@@ -68,7 +68,9 @@ final class InstanceofTest {
        assert !new Object() instanceof Boolean
     }
 
-    // GROOVY-11585
+    // GROOVY-11585 — full Java-vs-Groovy matrix is GenericsJavaCompatibilityTest
+    // (reifiable List<?>/Map<?,?>, non-reifiable List<String>/Map<String,Integer>,
+    // bounded wildcards, rare types). This is the named regression only.
     @Test
     void testGenerics() {
         assert [] instanceof List<?>
diff --git a/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java b/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
index e9523860cd..d9b9ae6b52 100644
--- a/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
+++ b/src/test/groovy/org/codehaus/groovy/ast/ClassNodeTest.java
@@ -174,6 +174,25 @@ public final class ClassNodeTest {
         classNode.setOuterClassType(null);
         assertNull(classNode.getOuterClassType());
         assertNull(classNode.getPlainNodeReference().getOuterClassType());
+
+        // A leftover metadata key must not be treated as the enclosing type.
+        classNode.putNodeMetaData("outer.class", outer);
+        assertNull(classNode.getOuterClassType());
+        assertNull(classNode.getPlainNodeReference().getOuterClassType());
+        classNode.setOuterClassType(outer);
+        assertEquals(outer, classNode.getOuterClassType());
+        classNode.setOuterClassType(null);
+        assertNull(classNode.getOuterClassType());
+        assertEquals(outer, classNode.getNodeMetaData("outer.class"));
+
+        // Type-use field is not proxied through redirect, like getGenericsTypes().
+        ClassNode proxy = ClassHelper.makeWithoutCaching("Foo$Inner");
+        proxy.setRedirect(classNode);
+        classNode.setOuterClassType(outer);
+        assertNull(proxy.getOuterClassType());
+        proxy.setOuterClassType(outer);
+        assertEquals(outer, proxy.getOuterClassType());
+        assertEquals(outer, classNode.getOuterClassType());
     }
 
     @Test
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy b/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy
index d2d2794278..5dc2162672 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/BytecodeHelperTest.groovy
@@ -91,9 +91,7 @@ class BytecodeHelperTest extends AbstractBytecodeTestCase {
         inner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
 
         String typeSig = BytecodeHelper.getTypeGenericsSignature(inner)
-        assert typeSig.contains('Outer')
-        assert typeSig.contains('Inner')
-        assert typeSig.contains('String')
+        assert typeSig == 'Lcom/example/Outer<Ljava/lang/String;>.Inner<Ljava/lang/Integer;>;'
 
         ClassNode list = ClassHelper.LIST_TYPE.getPlainNodeReference()
         list.genericsTypes = [new GenericsType(ClassHelper.STRING_TYPE)] as GenericsType[]
@@ -116,8 +114,61 @@ class BytecodeHelperTest extends AbstractBytecodeTestCase {
         ClassNode mismatchedInner = ClassHelper.makeWithoutCaching('totally.Different$Item')
         mismatchedInner.outerClassType = outer
         mismatchedInner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
-        String fallback = BytecodeHelper.getTypeGenericsSignature(mismatchedInner)
-        assert fallback.contains('Item')
+        assert BytecodeHelper.getTypeGenericsSignature(mismatchedInner) == 'Lcom/example/Outer<Ljava/lang/String;>.Item<Ljava/lang/Integer;>;'
+
+        // Parser records Outer.Inner; after resolve the name is Outer$Inner.
+        ClassNode dottedInner = ClassHelper.makeWithoutCaching('com.example.Outer.Inner')
+        dottedInner.outerClassType = outer
+        dottedInner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+        assert BytecodeHelper.getTypeGenericsSignature(dottedInner) == 'Lcom/example/Outer<Ljava/lang/String;>.Inner<Ljava/lang/Integer;>;'
+
+        ClassNode dollarInner = ClassHelper.makeWithoutCaching('com.example.Outer$Inner')
+        dollarInner.outerClassType = outer
+        dollarInner.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+        assert BytecodeHelper.getTypeGenericsSignature(dollarInner) == 'Lcom/example/Outer<Ljava/lang/String;>.Inner<Ljava/lang/Integer;>;'
+
+        // Anonymous: owner Foo, inner Foo$1 — still a prefix, not the fallback.
+        ClassNode anonOwner = ClassHelper.makeWithoutCaching('com.example.Foo')
+        anonOwner.genericsTypes = [new GenericsType(ClassHelper.STRING_TYPE)] as GenericsType[]
+        ClassNode anon = ClassHelper.makeWithoutCaching('com.example.Foo$1')
+        anon.outerClassType = anonOwner
+        assert BytecodeHelper.getTypeGenericsSignature(anon) == 'Lcom/example/Foo<Ljava/lang/String;>.1;'
+
+        // Unrelated inner name Bar.X is not extra qualification of owner Foo.
+        ClassNode unrelated = ClassHelper.makeWithoutCaching('Bar.X')
+        unrelated.outerClassType = anonOwner
+        assert BytecodeHelper.getTypeGenericsSignature(unrelated) == 'Lcom/example/Foo<Ljava/lang/String;>.X;'
+
+        // Simple identifier with no '.' / '$' (fallback sep < 0).
+        ClassNode simple = ClassHelper.makeWithoutCaching('Item')
+        simple.outerClassType = anonOwner
+        assert BytecodeHelper.getTypeGenericsSignature(simple) == 'Lcom/example/Foo<Ljava/lang/String;>.Item;'
+
+        // Equal names: not nested (length <= owner).
+        ClassNode sameName = ClassHelper.makeWithoutCaching('com.example.Foo')
+        sameName.outerClassType = anonOwner
+        sameName.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+        assert BytecodeHelper.getTypeGenericsSignature(sameName) == 'Lcom/example/Foo<Ljava/lang/String;>.Foo<Ljava/lang/Integer;>;'
+
+        // Prefix that is not a nesting separator (Foo vs FooBar).
+        ClassNode notNested = ClassHelper.makeWithoutCaching('com.example.FooBar')
+        notNested.outerClassType = anonOwner
+        assert BytecodeHelper.getTypeGenericsSignature(notNested) == 'Lcom/example/Foo<Ljava/lang/String;>.FooBar;'
+
+        // Remainder after owner may contain further $ nesting.
+        ClassNode deepRemainder = ClassHelper.makeWithoutCaching('com.example.Outer$Middle$Inner')
+        deepRemainder.outerClassType = outer
+        deepRemainder.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+        assert BytecodeHelper.getTypeGenericsSignature(deepRemainder) == 'Lcom/example/Outer<Ljava/lang/String;>.Middle.Inner<Ljava/lang/Integer;>;'
+
+        // Recursive enclosing types: Outer<T>.Middle<U>.Inner<V>.
+        ClassNode middle = ClassHelper.makeWithoutCaching('com.example.Outer$Middle')
+        middle.outerClassType = outer
+        middle.genericsTypes = [new GenericsType(ClassHelper.Integer_TYPE)] as GenericsType[]
+        ClassNode inner2 = ClassHelper.makeWithoutCaching('com.example.Outer$Middle$Inner')
+        inner2.outerClassType = middle
+        inner2.genericsTypes = [new GenericsType(ClassHelper.Long_TYPE)] as GenericsType[]
+        assert BytecodeHelper.getTypeGenericsSignature(inner2) == 'Lcom/example/Outer<Ljava/lang/String;>.Middle<Ljava/lang/Integer;>.Inner<Ljava/lang/Long;>;'
 
         ClassNode superDiamond = ClassHelper.LIST_TYPE.getPlainNodeReference()
         superDiamond.genericsTypes = GenericsType.EMPTY_ARRAY