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)
}