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/10/11 14:27:46 UTC

(groovy) branch GROOVY-12446 updated (ba8b53422d -> 05497fccae)

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

asf-gitbox-commits pushed a change to branch GROOVY-12446
in repository https://gitbox.apache.org/repos/asf/groovy.git


    omit ba8b53422d GROOVY-12446: STC: stop writing UnionTypeClassNode into inferred-return signatures
     new 05497fccae GROOVY-12446: STC: stop writing UnionTypeClassNode into inferred-return signatures

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (ba8b53422d)
            \
             N -- N -- N   refs/heads/GROOVY-12446 (05497fccae)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../groovy/transform/stc/SignatureCodecTest.groovy | 120 +++++++++++++++++++++
 1 file changed, 120 insertions(+)


(groovy) 01/01: GROOVY-12446: STC: stop writing UnionTypeClassNode into inferred-return signatures

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 05497fccae95f544690f5e31a2972f29d78023de
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Oct 11 17:44:43 2026 +0900

    GROOVY-12446: STC: stop writing UnionTypeClassNode into inferred-return signatures
---
 COMPATIBILITY.md                                   |  12 ++
 .../transform/stc/SignatureCodecVersion1.java      |  50 ++++-
 .../groovy/transform/stc/SignatureCodecTest.groovy | 212 ++++++++++++++++++++-
 3 files changed, 260 insertions(+), 14 deletions(-)

diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 9b54168beb..f9397eed9f 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -548,6 +548,18 @@ so `japicmp` sees nothing — this is a behavioural change only.
 Groovy 6 is the major version for the change. On 3.0.x/4.0.x/5.0.x/5.1.x the
 coercion still initializes the class.
 
+### Groovy 7 — `SignatureCodec` version 1 no longer round-trips a union node
+
+`SignatureCodecVersion1` used to encode a checker-only `UnionTypeClassNode` by
+name and rebuild one on decode. Newly encoded signatures store the lowest upper
+bound that has a stable binary name, or `Object`. Historical payloads that still
+carry the `UnionTypeClassNode` tag are consumed and then widened the same way.
+
+The method descriptors are unchanged, so `japicmp` does not report this.
+Callers of `SignatureCodecVersion1.decode` that expected a union node now
+receive the shared supertype. The compiler has not written
+`@TypeCheckingInfo` inferred-return annotations since GROOVY-6401 (2013).
+
 ## The binary-compatibility check
 
 The [`subprojects/binary-compatibility/`](subprojects/binary-compatibility)
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/SignatureCodecVersion1.java b/src/main/java/org/codehaus/groovy/transform/stc/SignatureCodecVersion1.java
index 39b8854203..dbfd9c42e5 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/SignatureCodecVersion1.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/SignatureCodecVersion1.java
@@ -33,8 +33,11 @@ import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.io.Writer;
+import java.util.Arrays;
 
 import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.isObjectType;
+import static org.codehaus.groovy.ast.tools.WideningCategories.lowestUpperBound;
 import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE;
 import static org.codehaus.groovy.ast.ClassHelper.boolean_TYPE;
 import static org.codehaus.groovy.ast.ClassHelper.byte_TYPE;
@@ -59,16 +62,44 @@ public class SignatureCodecVersion1 implements SignatureCodec {
         this.classLoader = classLoader;
     }
 
+    /** Checker-only unions must not appear in class-file signatures. */
+    private static ClassNode toClassFileType(final UnionTypeClassNode union) {
+        ClassNode[] arms = union.getDelegates();
+        if (arms == null || arms.length == 0) {
+            return OBJECT_TYPE;
+        }
+        return stableBound(lowestUpperBound(Arrays.asList(arms)));
+    }
+
+    /** A lowest-upper-bound node has no single binary name. */
+    private static ClassNode stableBound(final ClassNode lub) {
+        if (lub == null) {
+            return OBJECT_TYPE;
+        }
+        if (lub instanceof UnionTypeClassNode nested) {
+            return toClassFileType(nested);
+        }
+        if (lub instanceof WideningCategories.LowestUpperBoundClassNode) {
+            ClassNode upper = lub.getUnresolvedSuperClass(false);
+            if (upper == null || isObjectType(upper)) {
+                return OBJECT_TYPE;
+            }
+            return stableBound(upper);
+        }
+        String name = lub.getName();
+        if (name == null || name.isEmpty() || name.charAt(0) == '<') {
+            return OBJECT_TYPE;
+        }
+        return lub;
+    }
+
     private void doEncode(final ClassNode node, DataOutputStream dos) throws IOException {
-        dos.writeUTF(node.getClass().getSimpleName());
         if (node instanceof UnionTypeClassNode union) {
-            ClassNode[] delegates = union.getDelegates();
-            dos.writeInt(delegates.length);
-            for (ClassNode delegate : delegates) {
-                doEncode(delegate, dos);
-            }
+            doEncode(toClassFileType(union), dos);
             return;
-        } else if (node instanceof WideningCategories.LowestUpperBoundClassNode lub) {
+        }
+        dos.writeUTF(node.getClass().getSimpleName());
+        if (node instanceof WideningCategories.LowestUpperBoundClassNode lub) {
             dos.writeUTF(lub.getLubName());
             doEncode(lub.getUnresolvedSuperClass(), dos);
             ClassNode[] interfaces = lub.getInterfaces();
@@ -140,11 +171,14 @@ public class SignatureCodecVersion1 implements SignatureCodec {
         String classNodeType = dis.readUTF();
         if (UnionTypeClassNode.class.getSimpleName().equals(classNodeType)) {
             int len = dis.readInt();
+            if (len <= 0) {
+                return OBJECT_TYPE;
+            }
             ClassNode[] delegates = new ClassNode[len];
             for (int i = 0; i < len; i++) {
                 delegates[i] = doDecode(dis);
             }
-            return new UnionTypeClassNode(delegates);
+            return toClassFileType(new UnionTypeClassNode(delegates));
         } else if (WideningCategories.LowestUpperBoundClassNode.class.getSimpleName().equals(classNodeType)) {
             String name = dis.readUTF();
             ClassNode upper = doDecode(dis);
diff --git a/src/test/groovy/org/codehaus/groovy/transform/stc/SignatureCodecTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/stc/SignatureCodecTest.groovy
index 6e40a97a39..ed6552f9bb 100644
--- a/src/test/groovy/org/codehaus/groovy/transform/stc/SignatureCodecTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/transform/stc/SignatureCodecTest.groovy
@@ -20,6 +20,8 @@ package org.codehaus.groovy.transform.stc
 
 import org.codehaus.groovy.ast.ClassNode
 import org.codehaus.groovy.ast.GenericsType
+import org.codehaus.groovy.ast.tools.WideningCategories
+import org.codehaus.groovy.runtime.EncodingGroovyMethods
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.FieldSource
@@ -27,6 +29,11 @@ import org.junit.jupiter.params.provider.FieldSource
 import static org.codehaus.groovy.ast.ClassHelper.*
 import static org.codehaus.groovy.ast.tools.WideningCategories.lowestUpperBound
 
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.DataInputStream
+import java.io.DataOutputStream
+
 /**
  * Unit tests for signature codecs.
  */
@@ -65,22 +72,215 @@ final class SignatureCodecTest {
     }
 
     @Test
-    void testCodecWithUnionType() {
+    void testCodecWithUnionTypeWritesStableBound() {
         ClassNode cn = new UnionTypeClassNode(STRING_TYPE, LIST_TYPE)
         String signature = codec.encode(cn)
+        assertFirstTagIsNotUnion(signature)
         ClassNode decoded = codec.decode(signature)
-        assert decoded == cn
+        assert decoded == OBJECT_TYPE
+        assert !(decoded instanceof UnionTypeClassNode)
+    }
+
+    @Test
+    void testCodecWithUnionTypeWritesCommonSuperclass() {
+        ClassNode cn = new UnionTypeClassNode(make(RuntimeException), make(Exception))
+        String signature = codec.encode(cn)
+        assertFirstTagIsNotUnion(signature)
+        ClassNode decoded = codec.decode(signature)
+        assert decoded == make(Exception)
+        assert !(decoded instanceof UnionTypeClassNode)
     }
 
     @Test
-    void testCodecWithUnionTypeAndGenerics() {
+    void testCodecWithUnionTypeAndGenericsDoesNotLeakUnion() {
         ClassNode list = LIST_TYPE.getPlainNodeReference()
         list.setGenericsTypes(new GenericsType(STRING_TYPE))
         ClassNode cn = new UnionTypeClassNode(STRING_TYPE, list)
         String signature = codec.encode(cn)
         ClassNode decoded = codec.decode(signature)
-        assert decoded == cn
-        assert decoded.delegates[1] == LIST_TYPE
-        assert decoded.delegates[1].genericsTypes[0].type == STRING_TYPE
+        assert !(decoded instanceof UnionTypeClassNode)
+        assert !decoded.name.startsWith('<UnionType:')
+    }
+
+    @Test
+    void testCodecWithUnionTypeWritesSingleSharedInterface() {
+        ClassNode marker = make(SharedMarker)
+        ClassNode left = make('stc.MarkerLeft')
+        left.interfaces = [marker] as ClassNode[]
+        ClassNode right = make('stc.MarkerRight')
+        right.interfaces = [marker] as ClassNode[]
+        ClassNode cn = new UnionTypeClassNode(left, right)
+        String signature = codec.encode(cn)
+        assertFirstTagIsNotUnion(signature)
+        ClassNode decoded = codec.decode(signature)
+        assert decoded == marker
+        assert decoded != OBJECT_TYPE
+    }
+
+    @Test
+    void testHistoricalUnionPayloadWidensToObjectForDisjointTypes() {
+        String signature = historicalUnionPayload(STRING_TYPE, LIST_TYPE)
+        ClassNode decoded = codec.decode(signature)
+        assert decoded == OBJECT_TYPE
+        assert !(decoded instanceof UnionTypeClassNode)
+    }
+
+    @Test
+    void testHistoricalUnionPayloadWidensToCommonSuperclass() {
+        String signature = historicalUnionPayload(make(RuntimeException), make(Exception))
+        ClassNode decoded = codec.decode(signature)
+        assert decoded == make(Exception)
+        assert !(decoded instanceof UnionTypeClassNode)
+    }
+
+    @Test
+    void testHistoricalUnionPayloadWithEmptyDelegateCountIsObject() {
+        def baos = new ByteArrayOutputStream()
+        def dos = new DataOutputStream(baos)
+        dos.writeUTF('UnionTypeClassNode')
+        dos.writeInt(0)
+        dos.flush()
+        String signature = EncodingGroovyMethods.encodeBase64(baos.toByteArray()).toString()
+        assert codec.decode(signature) == OBJECT_TYPE
+    }
+
+    @Test
+    void testHistoricalUnionPayloadWithNegativeDelegateCountIsObject() {
+        def baos = new ByteArrayOutputStream()
+        def dos = new DataOutputStream(baos)
+        dos.writeUTF('UnionTypeClassNode')
+        dos.writeInt(-1)
+        dos.flush()
+        String signature = EncodingGroovyMethods.encodeBase64(baos.toByteArray()).toString()
+        assert codec.decode(signature) == OBJECT_TYPE
+    }
+
+    @Test
+    void testCodecWithEmptyUnionDelegatesWritesObject() {
+        assert codec.decode(codec.encode(withDelegates(ClassNode.EMPTY_ARRAY))) == OBJECT_TYPE
+    }
+
+    @Test
+    void testCodecWithNullUnionDelegatesWritesObject() {
+        assert codec.decode(codec.encode(withDelegates(null))) == OBJECT_TYPE
+    }
+
+    @Test
+    void testCodecWithNestedUnionWritesStableBound() {
+        ClassNode inner = new UnionTypeClassNode(STRING_TYPE, LIST_TYPE)
+        ClassNode outer = new UnionTypeClassNode(inner)
+        String signature = codec.encode(outer)
+        assertFirstTagIsNotUnion(signature)
+        ClassNode decoded = codec.decode(signature)
+        assert decoded == OBJECT_TYPE
+        assert !(decoded instanceof UnionTypeClassNode)
     }
+
+    @Test
+    void testCodecWithUnionWhoseLubIsNullWritesObject() {
+        assert codec.decode(codec.encode(withDelegates([STRING_TYPE, null] as ClassNode[]))) == OBJECT_TYPE
+    }
+
+    @Test
+    void testCodecWithUnionOfLubWithObjectSuperWritesObject() {
+        ClassNode lub = new WideningCategories.LowestUpperBoundClassNode(
+                'Virtual$Object', OBJECT_TYPE, make(SharedMarker), make(SecondMarker))
+        assert codec.decode(codec.encode(new UnionTypeClassNode(lub))) == OBJECT_TYPE
+    }
+
+    @Test
+    void testCodecWithUnionOfLubWithNamedSuperWritesSuper() {
+        ClassNode extra = make(SharedMarker)
+        ClassNode base = make(Exception)
+        ClassNode lub = new WideningCategories.LowestUpperBoundClassNode('Virtual$Ex', base, extra)
+        assert codec.decode(codec.encode(new UnionTypeClassNode(lub))) == base
+    }
+
+    @Test
+    void testCodecWithUnionOfLubWithNullSuperWritesObject() {
+        ClassNode lub = new WideningCategories.LowestUpperBoundClassNode('Virtual$Null', OBJECT_TYPE) {
+            @Override
+            ClassNode getUnresolvedSuperClass(boolean deref) {
+                null
+            }
+        }
+        assert codec.decode(codec.encode(new UnionTypeClassNode(lub))) == OBJECT_TYPE
+    }
+
+    @Test
+    void testCodecWithUnionOfTypesSharingSuperclassAndInterfaceWritesSuper() {
+        ClassNode extra = make(SharedMarker)
+        ClassNode base = make(Exception)
+        ClassNode left = makeWithoutCaching('stc.LubLeft')
+        left.superClass = base
+        left.interfaces = [extra] as ClassNode[]
+        ClassNode right = makeWithoutCaching('stc.LubRight')
+        right.superClass = base
+        right.interfaces = [extra] as ClassNode[]
+        assert lowestUpperBound([left, right]) instanceof WideningCategories.LowestUpperBoundClassNode
+        ClassNode decoded = codec.decode(codec.encode(new UnionTypeClassNode(left, right)))
+        assert decoded == base
+        assert !(decoded instanceof UnionTypeClassNode)
+        assert !(decoded instanceof WideningCategories.LowestUpperBoundClassNode)
+    }
+
+    @Test
+    void testCodecWithSingleArmUnionWritesThatType() {
+        ClassNode cn = new UnionTypeClassNode(STRING_TYPE)
+        String signature = codec.encode(cn)
+        assertFirstTagIsNotUnion(signature)
+        assert codec.decode(signature) == STRING_TYPE
+    }
+
+    @Test
+    void testCodecWithUnionOfPlaceholderNamedTypeWritesObject() {
+        ClassNode placeholder = makeWithoutCaching('<T>')
+        assert codec.decode(codec.encode(new UnionTypeClassNode(placeholder))) == OBJECT_TYPE
+    }
+
+    @Test
+    void testCodecWithUnionOfEmptyNamedTypeWritesObject() {
+        ClassNode empty = makeWithoutCaching('')
+        assert codec.decode(codec.encode(new UnionTypeClassNode(empty))) == OBJECT_TYPE
+    }
+
+    @Test
+    void testCodecWithUnionOfNullNamedTypeWritesObject() {
+        ClassNode unnamed = makeWithoutCaching('stc.Unnamed')
+        ClassNode cn = new UnionTypeClassNode(unnamed)
+        unnamed.setName(null)
+        assert codec.decode(codec.encode(cn)) == OBJECT_TYPE
+    }
+
+    private static void assertFirstTagIsNotUnion(String signature) {
+        byte[] bytes = EncodingGroovyMethods.decodeBase64(signature)
+        def dis = new DataInputStream(new ByteArrayInputStream(bytes))
+        assert dis.readUTF() != 'UnionTypeClassNode'
+        assert !new String(bytes, 'ISO-8859-1').contains('UnionTypeClassNode')
+    }
+
+    private String historicalUnionPayload(ClassNode... arms) {
+        def baos = new ByteArrayOutputStream()
+        def dos = new DataOutputStream(baos)
+        dos.writeUTF('UnionTypeClassNode')
+        dos.writeInt(arms.length)
+        for (ClassNode arm : arms) {
+            dos.write(EncodingGroovyMethods.decodeBase64(codec.encode(arm)))
+        }
+        dos.flush()
+        EncodingGroovyMethods.encodeBase64(baos.toByteArray()).toString()
+    }
+
+    private static UnionTypeClassNode withDelegates(ClassNode[] arms) {
+        new UnionTypeClassNode(STRING_TYPE) {
+            @Override
+            ClassNode[] getDelegates() {
+                arms
+            }
+        }
+    }
+
+    interface SharedMarker {}
+
+    interface SecondMarker {}
 }