[
https://issues.apache.org/jira/browse/GROOVY-12303?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18108902#comment-18108902
]
ASF GitHub Bot commented on GROOVY-12303:
-----------------------------------------
daniellansun commented on code in PR #2834:
URL: https://github.com/apache/groovy/pull/2834#discussion_r3873675506
##########
src/main/java/org/codehaus/groovy/control/ClassNodeResolver.java:
##########
@@ -304,28 +324,91 @@ private static LookupResult findByClassLoading(final
String name, final Compilat
return tryAsScript(name, compilationUnit, null);
} catch (CompilationFailedException cfe) {
throw new GroovyBugError("The lookup for " + name + " caused a
failed compilation. There should not have been any compilation from this
call.", cfe);
+ } catch (NoClassDefFoundError ncdfe) {
+ return recoverFromNoClassDefFoundError(name, compilationUnit,
loader, ncdfe);
}
- //TODO: The case of a NoClassDefFoundError needs a bit more research;
- // a simple recompilation is not possible it seems. The current class
- // we are searching for is there, so we should mark that somehow.
- // Basically the missing class needs to be completely compiled before
- // we can again search for the current name.
- /*catch (NoClassDefFoundError ncdfe) {
- cachedClasses.put(name,SCRIPT);
- return false;
- }*/
if (cls == null) return null;
- //NOTE: we might return false here even if we found a class,
- // because we want to give a possible script a chance to
- // recompile. This can only be done if the loader was not
- // the instance defining the class.
+ // NOTE: even if we found a class we still give a possible script a
chance
+ // to recompile, but only when this loader was not the instance that
defined it.
ClassNode cn = ClassHelper.make(cls);
if (cls.getClassLoader() != loader) {
return tryAsScript(name, compilationUnit, cn);
}
return new LookupResult(null,cn);
}
+ /**
+ * Recovers from {@link NoClassDefFoundError} thrown while defining or
linking
+ * {@code name}. The class being looked up is present — a referenced type
is not —
+ * so treating the failure as a miss would poison {@link #resolveName} with
+ * {@link #NO_CLASS} and hide bytecode or a groovy source that can still
be used.
+ * <p>
+ * Recovery order:
+ * <ol>
+ * <li>ASM decompilation of matching bytecode, which does not link the
class,
+ * so a missing superclass or interface does not prevent producing a
+ * {@link ClassNode}.</li>
+ * <li>If a {@code .class} resource exists for this path but declares a
+ * different binary name (the JVM {@code defineClass} name check,
also
+ * seen on case-insensitive filesystems), treat the lookup as a miss
and
+ * fall through to script lookup. Detection uses the bytecode name,
not
+ * {@link NoClassDefFoundError} text.</li>
+ * <li>A groovy source of the same name, added to the compilation
queue.</li>
+ * </ol>
+ * If none of those succeed the error is rethrown with {@code name} in the
+ * message so callers see both the class under lookup and the missing type.
+ */
+ private LookupResult recoverFromNoClassDefFoundError(final String name,
final CompilationUnit compilationUnit,
+ final GroovyClassLoader loader, final NoClassDefFoundError ncdfe) {
+ LookupResult decompiled = findDecompiled(name, compilationUnit,
loader, false);
Review Comment:
Thank you — that reading of the recovery path was right, and the patch has
been restructured around it.
### 1. The extra `findDecompiled` in recovery
When `asmResolving` is on we have already parsed the resource. Calling
`findDecompiled` again on the same name was redundant; a hit would have
returned before `loadClass`.
The guard you wondered about is in the caller (`tryAsLoaderClassOrScript`),
not in `findDecompiled`. So when `asmResolving` is off, that first parse never
ran — and that is the GROOVY-12303 case (unlinkable bytecode, class-loader
lookup only). There we still decompile **once**, as a last resort, because
`loadClass` cannot produce a `ClassNode` for a type that failed to link. That
hatch is no longer an unmarked second copy of the happy-path decompile.
`recoverFromNoClassDefFoundError` is gone. The first pass stores a
`ClassFile` (`MATCH` / `MISMATCH` / `ABSENT`, or `null` if ASM was not
attempted). The `NoClassDefFoundError` arm uses that result; it parses only
when the value is `null`.
### 2. `classFileDeclaresDifferentName` belongs on the first parse
Agreed. The bytecode-name check is now part of that single ASM read
(`readClassFile`). A mismatch is no longer collapsed to “not found” and
rediscovered with a second `DecompiledClassNode`.
The check still requires bytecode, so it does not run as a free-standing
decompile when ASM was already skipped — except in the last-resort hatch above,
which is the first parse of that lookup. We do not inspect
`NoClassDefFoundError` text (HotSpot’s `"wrong name"` phrase is not a portable
API).
A mismatch means this resource is not `name`. `loadClass` still runs
afterwards: `getResource` and `defineClass` / `findLoadedClass` are independent
(in-memory class of the right name, case-insensitive `getResource`). If
`loadClass` then throws `NoClassDefFoundError` and we already know the resource
was a mismatch, that is treated as “the requested name never existed”
(`tryAsScript` with no `oldClass`), not as a linkage failure of `name`.
### 3. `tryAsScript(name, cu, null)` is not legal for `NoClassDefFoundError`
Agreed. That was the CNFE contract (`oldClass == null` ⇒ any groovy source
wins, no `isSourceNewer`, no origin check). `NoClassDefFoundError` means some
loader found a class (or bytes it tried to derive). Replacing it with a script
is only legal when that class is from a parent — the same rule as the success
path (`cls.getClassLoader() != loader`) and as the decompile path
(`isFromAnotherClassLoader` then `tryAsScript(name, cu, decompiled)` so
`isSourceNewer` applies).
That is now the only class→script path. Same-loader unlinkable bytecode
keeps the `DecompiledClassNode`. If there is no matching class-file resource,
we cannot get a timestamp or a defining loader, so we rethrow the wrapped
`NoClassDefFoundError` and do not cache `NO_CLASS` — including when a groovy
source of the same name happens to exist.
Thank you again for catching this.
> ClassNodeResolver: NoClassDefFoundError during class-loader lookup aborts
> resolution
> ------------------------------------------------------------------------------------
>
> Key: GROOVY-12303
> URL: https://issues.apache.org/jira/browse/GROOVY-12303
> Project: Groovy
> Issue Type: Bug
> Reporter: Daniel Sun
> Priority: Major
>
> When class-loader lookup is used ({{{}asmResolving{}}} off, or the type
> exists only in memory), {{ClassNodeResolver}} calls
> {{{}GroovyClassLoader.loadClass{}}}. If the requested class *exists* but
> cannot be linked (missing superclass or interface), the JVM throws
> {{{}NoClassDefFoundError{}}}.
> That error used to escape resolution. Compilation aborted with an {{Error}}
> that named the {*}missing dependency{*}, not the type being resolved.
> {{resolveName}} could also cache the name as a miss ({{{}NO_CLASS{}}}), so a
> later successful compile of the dependency would not be retried.
> A TODO in {{findByClassLoading}} has noted this since the 2012 split out of
> {{{}ResolveVisitor{}}}.
> h3. Expected
> * If bytecode for the requested name is still on the class path, decompile
> it (ASM does not link) and continue.
> * Else if a groovy source of the same name is available, add it to the
> compilation queue.
> * Else if a {{.class}} resource exists for that path but declares a
> different binary name (JVM {{defineClass}} name check; also case-insensitive
> filesystems), treat the lookup as a miss. Detect this from the bytecode name,
> not from {{NoClassDefFoundError}} text (HotSpot's {{wrong name}} phrase is
> English-only; OpenJ9 does not use it).
> * Otherwise rethrow {{NoClassDefFoundError}} with the looked-up name in the
> message, and do not cache {{{}NO_CLASS{}}}.
> h3. Actual
> {{NoClassDefFoundError}} propagated out of
> {{{}ClassNodeResolver.findByClassLoading{}}}.
> h3. Reproducer
> Put {{HasDep.class}} (extends a type that is not loadable) on the class path,
> disable ASM resolving, and compile:
> {code:groovy}
> HasDep x = null
> {code}
> This fails with {{NoClassDefFoundError}} for the missing super-type instead
> of resolving {{{}HasDep{}}}.
>
--
This message was sent by Atlassian Jira
(v8.20.10#820010)