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.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to