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/30 03:39:26 UTC

(groovy) branch GROOVY-12303 updated: GROOVY-12303: keep ClassFiles as resource lookup; read names from AsmDecompiler

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

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


The following commit(s) were added to refs/heads/GROOVY-12303 by this push:
     new ab53e451df GROOVY-12303: keep ClassFiles as resource lookup; read names from AsmDecompiler
ab53e451df is described below

commit ab53e451df49b9f9a2e2ced106702be3ff44ab6b
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Aug 30 03:38:22 2026 +0000

    GROOVY-12303: keep ClassFiles as resource lookup; read names from AsmDecompiler
    
    ClassFiles only maps binaryName to a .class resource. Bytecode identity is
    AsmDecompiler.readClassName, which uses parseClass and the stub cache.
    ClassNodeResolver compares DecompiledClassNode.getName() again. GCL asks
    isConfirmedBytecodeNameMismatch so missing/unreadable files keep NCDFE.
---
 src/main/java/groovy/lang/GroovyClassLoader.java   | 33 ++++++++++---------
 .../apache/groovy/internal/util/ClassFiles.java    | 33 +++----------------
 .../groovy/ast/decompiled/AsmDecompiler.java       | 11 +++++++
 .../codehaus/groovy/ast/decompiled/ClassStub.java  |  7 ----
 .../codehaus/groovy/control/ClassNodeResolver.java | 20 ++++++------
 .../groovy/internal/util/ClassFilesTest.groovy     | 37 +++-------------------
 6 files changed, 45 insertions(+), 96 deletions(-)

diff --git a/src/main/java/groovy/lang/GroovyClassLoader.java b/src/main/java/groovy/lang/GroovyClassLoader.java
index 41c3672140..de8a01513c 100644
--- a/src/main/java/groovy/lang/GroovyClassLoader.java
+++ b/src/main/java/groovy/lang/GroovyClassLoader.java
@@ -23,6 +23,7 @@ import org.codehaus.groovy.ast.ClassHelper;
 import org.codehaus.groovy.ast.ClassNode;
 import org.codehaus.groovy.ast.FieldNode;
 import org.codehaus.groovy.ast.ModuleNode;
+import org.codehaus.groovy.ast.decompiled.AsmDecompiler;
 import org.codehaus.groovy.ast.expr.ConstantExpression;
 import org.codehaus.groovy.classgen.GeneratorContext;
 import org.codehaus.groovy.classgen.Verifier;
@@ -705,17 +706,16 @@ public class GroovyClassLoader extends URLClassLoader {
         } catch (ClassNotFoundException cnfe) {
             last = cnfe;
         } catch (NoClassDefFoundError ncdfe) {
-            // A .class whose bytecode name is not the requested name (JVMS 5.3.5 /
-            // case-insensitive getResource). Do not inspect ncdfe.getMessage():
-            // it can be null or localized. A confirmed mismatch is
-            // ClassNotFoundException so a groovy source of the requested name
-            // can still be compiled. A matching name, or a missing/unreadable
-            // class file, keeps the linking error.
-            if (isBytecodeNameMatch(name)) {
+            // JVMS 5.3.5 / case-insensitive getResource. Do not inspect
+            // ncdfe.getMessage(): it can be null or localized. Only a confirmed
+            // bytecode-name mismatch becomes ClassNotFoundException so a groovy
+            // source of the requested name can still be compiled.
+            if (isConfirmedBytecodeNameMismatch(name)) {
+                last = new ClassNotFoundException(name);
+                last.addSuppressed(ncdfe);
+            } else {
                 throw ncdfe;
             }
-            last = new ClassNotFoundException(name);
-            last.addSuppressed(ncdfe);
         }
 
         // check security manager
@@ -767,20 +767,19 @@ public class GroovyClassLoader extends URLClassLoader {
     }
 
     /**
-     * True when {@code name}'s {@code .class} decompiles to {@code name}.
-     * A missing or unreadable resource is treated as a match so a linking
-     * {@link NoClassDefFoundError} is not turned into {@link ClassNotFoundException}.
-     * False only for a confirmed bytecode-name mismatch.
+     * True only when a {@code .class} resource exists and decompiles to a
+     * different binary name. Missing or unreadable resources are not a
+     * confirmed mismatch: the linking {@link NoClassDefFoundError} is kept.
      */
-    private boolean isBytecodeNameMatch(final String name) {
+    private boolean isConfirmedBytecodeNameMismatch(final String name) {
         URL resource = ClassFiles.findResource(this, name);
         if (resource == null) {
-            return true;
+            return false;
         }
         try {
-            return ClassFiles.isBytecodeNameMatch(resource, name);
+            return !name.equals(AsmDecompiler.readClassName(resource));
         } catch (IOException | IllegalArgumentException | IndexOutOfBoundsException | GroovyRuntimeException e) {
-            return true;
+            return false;
         }
     }
 
diff --git a/src/main/java/org/apache/groovy/internal/util/ClassFiles.java b/src/main/java/org/apache/groovy/internal/util/ClassFiles.java
index 1f5b708da6..5a46e69f17 100644
--- a/src/main/java/org/apache/groovy/internal/util/ClassFiles.java
+++ b/src/main/java/org/apache/groovy/internal/util/ClassFiles.java
@@ -18,22 +18,15 @@
  */
 package org.apache.groovy.internal.util;
 
-import org.codehaus.groovy.ast.decompiled.AsmDecompiler;
-import org.codehaus.groovy.ast.decompiled.ClassStub;
-
-import java.io.IOException;
 import java.net.URL;
 
 /**
- * Locates a {@code .class} resource and tests whether it decompiles to the
- * requested binary name, using {@link AsmDecompiler#parseClass(URL)}.
+ * Locates a {@code .class} resource on a class loader.
  * <p>
- * {@link groovy.lang.GroovyClassLoader} and
- * {@link org.codehaus.groovy.control.ClassNodeResolver} both use
- * {@link #isBytecodeNameMatch} so the bytecode-name check is one comparison
- * ({@link ClassStub#getClassName()}). {@link AsmDecompiler} caches stubs by
- * URI, so an ASM-on resolve that later calls {@code loadClass} does not parse
- * the same file twice.
+ * Shared by {@link groovy.lang.GroovyClassLoader} and
+ * {@link org.codehaus.groovy.control.ClassNodeResolver} so the
+ * {@code binaryName → path} mapping lives in one place. Bytecode identity
+ * (the class file's {@code this_class}) is {@link org.codehaus.groovy.ast.decompiled.AsmDecompiler}'s job.
  *
  * @since 6.0.0
  */
@@ -55,20 +48,4 @@ public final class ClassFiles {
     public static URL findResource(final ClassLoader loader, final String binaryName) {
         return loader.getResource(resourcePath(binaryName));
     }
-
-    /**
-     * True when {@code stub}'s binary name is {@code expectedName}
-     * (JVMS 5.3.5 / case-insensitive {@link ClassLoader#getResource}).
-     */
-    public static boolean isBytecodeNameMatch(final ClassStub stub, final String expectedName) {
-        return expectedName.equals(stub.getClassName());
-    }
-
-    /**
-     * {@link AsmDecompiler#parseClass(URL)} then
-     * {@link #isBytecodeNameMatch(ClassStub, String)}. Hits the stub cache.
-     */
-    public static boolean isBytecodeNameMatch(final URL url, final String expectedName) throws IOException {
-        return isBytecodeNameMatch(AsmDecompiler.parseClass(url), expectedName);
-    }
 }
diff --git a/src/main/java/org/codehaus/groovy/ast/decompiled/AsmDecompiler.java b/src/main/java/org/codehaus/groovy/ast/decompiled/AsmDecompiler.java
index 5f3837f80a..58f77c5fc7 100644
--- a/src/main/java/org/codehaus/groovy/ast/decompiled/AsmDecompiler.java
+++ b/src/main/java/org/codehaus/groovy/ast/decompiled/AsmDecompiler.java
@@ -19,6 +19,7 @@
 package org.codehaus.groovy.ast.decompiled;
 
 import groovy.lang.GroovyRuntimeException;
+import groovy.transform.Internal;
 import org.codehaus.groovy.util.URLStreams;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.ClassReader;
@@ -102,6 +103,16 @@ public abstract class AsmDecompiler {
         return stub;
     }
 
+    /**
+     * Reads the binary name of the class at {@code url} after {@link #parseClass(URL)}.
+     * Hits the stub cache, so a resolver that already decompiled {@code url}
+     * does not parse again when a class loader later asks the same question.
+     */
+    @Internal
+    public static String readClassName(final URL url) throws IOException {
+        return parseClass(url).className;
+    }
+
     private static AnnotationReader readAnnotationMembers(final AnnotationStub stub) {
         /**
          * Creates an {@link AnnotationReader} that populates an annotation stub's member map with attribute values.
diff --git a/src/main/java/org/codehaus/groovy/ast/decompiled/ClassStub.java b/src/main/java/org/codehaus/groovy/ast/decompiled/ClassStub.java
index 5e9601cc7b..89a4430959 100644
--- a/src/main/java/org/codehaus/groovy/ast/decompiled/ClassStub.java
+++ b/src/main/java/org/codehaus/groovy/ast/decompiled/ClassStub.java
@@ -133,13 +133,6 @@ public class ClassStub extends MemberStub {
         this.superName = superName;
         this.interfaceNames = interfaceNames;
     }
-
-    /**
-     * Binary name of this class (dots, including {@code $} for nested types).
-     */
-    public String getClassName() {
-        return className;
-    }
 }
 
 /**
diff --git a/src/main/java/org/codehaus/groovy/control/ClassNodeResolver.java b/src/main/java/org/codehaus/groovy/control/ClassNodeResolver.java
index a28646bdcd..ea366239e6 100644
--- a/src/main/java/org/codehaus/groovy/control/ClassNodeResolver.java
+++ b/src/main/java/org/codehaus/groovy/control/ClassNodeResolver.java
@@ -27,7 +27,6 @@ import org.codehaus.groovy.ast.ClassNode;
 import org.codehaus.groovy.ast.PackageNode;
 import org.codehaus.groovy.ast.decompiled.AsmDecompiler;
 import org.codehaus.groovy.ast.decompiled.AsmReferenceResolver;
-import org.codehaus.groovy.ast.decompiled.ClassStub;
 import org.codehaus.groovy.ast.decompiled.DecompiledClassNode;
 import org.codehaus.groovy.classgen.Verifier;
 import org.codehaus.groovy.util.URLStreams;
@@ -245,13 +244,12 @@ public class ClassNodeResolver {
             return null;
         }
         try {
-            ClassStub stub = AsmDecompiler.parseClass(resource);
-            if (!ClassFiles.isBytecodeNameMatch(stub, packageName + ".package-info")) {
+            DecompiledClassNode packageInfo = new DecompiledClassNode(
+                    AsmDecompiler.parseClass(resource), new AsmReferenceResolver(this, compilationUnit));
+            if (!packageInfo.getName().equals(packageName + ".package-info")) {
                 // this may happen under Windows/macOS because getResource is case-insensitive there!
                 return null;
             }
-            DecompiledClassNode packageInfo = new DecompiledClassNode(
-                    stub, new AsmReferenceResolver(this, compilationUnit));
             List<AnnotationNode> annotations = packageInfo.getAnnotations();
             if (annotations == null || annotations.isEmpty()) {
                 return null;
@@ -386,9 +384,8 @@ public class ClassNodeResolver {
      * {@link ClassFiles#findResource(ClassLoader, String)}. Distinguishes a
      * matching node from a bytecode-name mismatch (JVMS 5.3.5 / case-insensitive
      * {@link ClassLoader#getResource}) from a missing or unreadable resource.
-     * Does not decide script replacement. The name check is
-     * {@link ClassFiles#isBytecodeNameMatch(ClassStub, String)} on the stub
-     * from {@link AsmDecompiler#parseClass(URL)}.
+     * Does not decide script replacement. The bytecode name is
+     * {@link DecompiledClassNode#getName()} after {@link AsmDecompiler#parseClass(URL)}.
      *
      * @param failOnParse if true, class-format errors are thrown rather than
      *                    returned as {@link ClassFile#ABSENT}
@@ -400,12 +397,13 @@ public class ClassNodeResolver {
             return ClassFile.ABSENT;
         }
         try {
-            ClassStub stub = AsmDecompiler.parseClass(resource);
-            if (!ClassFiles.isBytecodeNameMatch(stub, name)) {
+            DecompiledClassNode asmClass = new DecompiledClassNode(
+                    AsmDecompiler.parseClass(resource), new AsmReferenceResolver(this, compilationUnit));
+            if (!name.equals(asmClass.getName())) {
                 // this may happen under Windows because getResource is case-insensitive under that OS!
                 return ClassFile.MISMATCH;
             }
-            return ClassFile.of(new DecompiledClassNode(stub, new AsmReferenceResolver(this, compilationUnit)));
+            return ClassFile.of(asmClass);
         } catch (IOException e) {
             return ClassFile.ABSENT;
         } catch (IllegalArgumentException | IndexOutOfBoundsException e) {
diff --git a/src/test/groovy/org/apache/groovy/internal/util/ClassFilesTest.groovy b/src/test/groovy/org/apache/groovy/internal/util/ClassFilesTest.groovy
index 4a4cc81235..0d82a0c9d2 100644
--- a/src/test/groovy/org/apache/groovy/internal/util/ClassFilesTest.groovy
+++ b/src/test/groovy/org/apache/groovy/internal/util/ClassFilesTest.groovy
@@ -25,7 +25,6 @@ import org.objectweb.asm.ClassWriter
 import java.nio.file.Files
 import java.nio.file.Path
 
-import static groovy.test.GroovyAssert.shouldFail
 import static org.objectweb.asm.Opcodes.ACC_PUBLIC
 import static org.objectweb.asm.Opcodes.V17
 
@@ -42,43 +41,15 @@ final class ClassFilesTest {
     }
 
     @Test
-    void isBytecodeNameMatchWhenResourceThisClassMatches() {
-        String name = 'cf.Linked'
-        writeBytes(name.replace('.', '/'), classBytes(name.replace('.', '/'), 'cf/MissingSuper'))
+    void findResourceLocatesAClassFileAndMissesAnUnknownName() {
+        String name = 'cf.Present'
+        writeBytes(name.replace('.', '/'), classBytes(name.replace('.', '/')))
         parentLoader().withCloseable { parent ->
-            URL resource = ClassFiles.findResource(parent, name)
-            assert resource != null
-            assert ClassFiles.isBytecodeNameMatch(resource, name)
-        }
-    }
-
-    @Test
-    void isBytecodeNameMatchIsFalseWhenResourceThisClassDiffers() {
-        String requested = 'cf.Requested'
-        writeBytes(requested.replace('.', '/'), classBytes('cf/Actual'))
-        parentLoader().withCloseable { parent ->
-            URL resource = ClassFiles.findResource(parent, requested)
-            assert resource != null
-            assert !ClassFiles.isBytecodeNameMatch(resource, requested)
+            assert ClassFiles.findResource(parent, name) != null
             assert ClassFiles.findResource(parent, 'cf.missing.NoSuch') == null
         }
     }
 
-    @Test
-    void isBytecodeNameMatchThrowsForUnreadableBytes() {
-        String name = 'cf.Garbage'
-        Path file = tempDir.resolve(name.replace('.', '/') + '.class')
-        Files.createDirectories(file.parent)
-        Files.write(file, [0, 1, 2, 3, 4] as byte[])
-        parentLoader().withCloseable { parent ->
-            URL resource = ClassFiles.findResource(parent, name)
-            assert resource != null
-            shouldFail {
-                ClassFiles.isBytecodeNameMatch(resource, name)
-            }
-        }
-    }
-
     private URLClassLoader parentLoader() {
         new URLClassLoader(tempDir.toUri().toURL() as URL[], (ClassLoader) null)
     }