Skip to content

Parse classfile to extract inner class info#516

Closed
eed3si9n wants to merge 1 commit intosbt:developfrom
eed3si9n:wip/innerclass
Closed

Parse classfile to extract inner class info#516
eed3si9n wants to merge 1 commit intosbt:developfrom
eed3si9n:wip/innerclass

Conversation

@eed3si9n
Copy link
Copy Markdown
Member

Fixes sbt/sbt#117

I was browsing around old sbt issue, and still was able to reproduce sbt/sbt#117 using https://github.com/duboisf/sbt_issue_117_example, so I fixed it.

Inside ClassToAPI, Java reflection is used to calculate the inner classes. Under certain circumstances this fails to load some of the classes and result to a NoClassDefFoundError.
I've switched to parse the classfile manually, and wrapped cl.loadClass with a try-catch. I was able to confirm the fix using sbt_issue_117_example.

@eed3si9n eed3si9n requested review from dwijnand and jvican March 26, 2018 03:00
Fixes sbt/sbt#117
Inside `ClassToAPI`, Java reflection is used to calculate the inner classes. Under certain circumstances this fails to load some of the classes and result to a `NoClassDefFoundError`.
I've switched to parse the classfile manually, and wrapped `cl.loadClass` with a try-catch. I was able to confirm the fix using https://github.com/duboisf/sbt_issue_117_example
@typesafe-tools
Copy link
Copy Markdown

The validator has checked the following projects against Scala 2.12,
tested using dbuild, projects built on top of each other.

Project Reference Commit
sbt 1.1.x sbt/sbt@131c2a7
zinc pull/516/head e15c88b
io 1.1.x sbt/io@13eb241
librarymanagement 1.1.x sbt/librarymanagement@0e3a9e6
util 1.1.x sbt/util@226a543
website 1.1.x

✅ The result is: SUCCESS
(restart)

@dwijnand
Copy link
Copy Markdown
Member

noting for the record before I smash the restart button..

failure in 2.11.11:

[info] CachedHashingSpec:
[info] zinc
[info] - should cache jar generation *** FAILED ***
[info] 38 was not less than 19.6 Cache jar didn't work: 38 is >= than 20% of 98. (CachedHashingSpec.scala:61)

failures in 2.12.3:

[info] ZincComponentCompilerSpec:
[info] - should compile the bridge for Scala 2.10.5 and 2.10.6
[info] - should compile the bridge for Scala 2.11.8 and 2.11.11 *** FAILED ***
[info] sbt.internal.inc.InvalidComponent: The Scala compiler and library could not be retrieved.
[info] at sbt.internal.inc.ZincComponentCompiler$ZincCompilerBridgeProvider.getScalaArtifacts(ZincComponentCompiler.scala:122)
[info] at sbt.internal.inc.ZincComponentCompiler$ZincCompilerBridgeProvider.fetchScalaInstance(ZincComponentCompiler.scala:138)
[info] at sbt.internal.inc.BridgeProviderSpecification.getCompilerBridge(BridgeProviderSpecification.scala:42)
[info] at sbt.internal.inc.ZincComponentCompilerSpec.$anonfun$new$5(ZincComponentCompilerSpec.scala:23)
[info] at sbt.io.IO$.withTemporaryDirectory(IO.scala:376)
[info] at sbt.io.IO$.withTemporaryDirectory(IO.scala:383)
[info] at sbt.internal.inc.ZincComponentCompilerSpec.$anonfun$new$4(ZincComponentCompilerSpec.scala:23)
[info] at org.scalatest.OutcomeOf.outcomeOf(OutcomeOf.scala:85)
[info] at org.scalatest.OutcomeOf.outcomeOf$(OutcomeOf.scala:83)
[info] at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
[info] ...

[info] MultiProjectIncrementalSpec:
[info] incremental compiler
[info] sbt.inc.MultiProjectIncrementalSpec *** ABORTED ***
[info] java.lang.NoClassDefFoundError: scala/tools/nsc/plugins/Plugins
[info] at java.lang.ClassLoader.defineClass1(Native Method)
[info] at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
[info] at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
[info] at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
[info] at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
[info] at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
[info] at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
[info] at java.security.AccessController.doPrivileged(Native Method)
[info] at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
[info] at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
[info] ...
[info] Cause: java.lang.ClassNotFoundException: scala.tools.nsc.plugins.Plugins
[info] at java.net.URLClassLoader$1.run(URLClassLoader.java:370)
[info] at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
[info] at java.security.AccessController.doPrivileged(Native Method)
[info] at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
[info] at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
[info] at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
[info] at java.lang.ClassLoader.defineClass1(Native Method)
[info] at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
[info] at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
[info] at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
[info] ...
[info] Cause: java.util.zip.ZipException: invalid LOC header (bad signature)
[info] at java.util.zip.ZipFile.read(Native Method)
[info] at java.util.zip.ZipFile.access$1400(ZipFile.java:60)
[info] at java.util.zip.ZipFile$ZipFileInputStream.read(ZipFile.java:717)
[info] at java.util.zip.ZipFile$ZipFileInflaterInputStream.fill(ZipFile.java:419)
[info] at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:158)
[info] at sun.misc.Resource.getBytes(Resource.java:124)
[info] at java.net.URLClassLoader.defineClass(URLClassLoader.java:462)
[info] at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
[info] at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
[info] at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
[info] ...
[warn] [FAILED ] org.scala-lang#scala-compiler;2.11.8!scala-compiler.jar: Downloaded file size doesn't match expected Content Length for https://repo1.maven.org/maven2/org/scala-lang/scala-compiler/2.11.8/scala-compiler-2.11.8.jar. Please retry. (495ms)
[warn] Detected merged artifact: [FAILED ] org.scala-lang#scala-compiler;2.11.8!scala-compiler.jar: (0ms).
[warn] ==== local: tried
[warn] /root/.ivy2/local/org.scala-lang/scala-compiler/2.11.8/jars/scala-compiler.jar
[warn] ==== public: tried
[warn] https://repo1.maven.org/maven2/org/scala-lang/scala-compiler/2.11.8/scala-compiler-2.11.8.jar
[warn] ::::::::::::::::::::::::::::::::::::::::::::::
[warn] :: FAILED DOWNLOADS ::
[warn] :: ^ see resolution messages for details ^ ::
[warn] ::::::::::::::::::::::::::::::::::::::::::::::
[warn] :: org.scala-lang#scala-compiler;2.11.8!scala-compiler.jar
[warn] ::::::::::::::::::::::::::::::::::::::::::::::

@jvican
Copy link
Copy Markdown
Member

jvican commented Mar 28, 2018

Need some time to have a look at this. Will try to review tomorrow.

@jvican
Copy link
Copy Markdown
Member

jvican commented Apr 3, 2018

I have a few questions:

  1. In all the examples I've seen, this weird bug seems to happen only in getClasses. Why are we re-implementing getDeclaredClasses too? From @duboisf's comment in the ticket you mention: "From the stacktrace you can see that it explodes in ClassToAPI:50, because of c.getClasses:".
  2. Do we have any idea what we're after here? Why is this exception being thrown? Are we now not throwing this exception because we're potentially doing less work, which could affect the incremental compiler for java sources?
  3. Have we tried this in different JVM versions?
  4. Can we add sbt_issue_117_example as a scripted test?

val classes = merge[Class[_]](c,
c.getDeclaredClasses,
c.getClasses,
innerClasses.toArray[Class[_]],
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jvican

In all the examples I've seen, this weird bug seems to happen only in getClasses. Why are we re-implementing getDeclaredClasses too?

https://docs.oracle.com/javase/10/docs/api/java/lang/Class.html

getDeclaredClasses() | Returns an array of Class objects reflecting all the classes and interfaces declared as members of the class represented by this Class object.

getClasses() | Returns an array containing Class objects representing all the public classes and interfaces that are members of the class represented by this Class object.

These are more or less the same thing except one is public. I figured I would do a double duty here to save a reflection call.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your implementation also reflecting all interface classes or only inner classes? e.g. maybe you can add a test proving that interfaces defined inside classes are detected by the new implementation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's specified in the JVM spec, so I think we are good here. https://docs.oracle.com/javase/specs/jvms/se10/html/jvms-4.html#jvms-4.7.6

Every CONSTANT_Class_info entry in the constant_pool table which represents a class or interface C that is not a package member must have exactly one corresponding entry in the classes array.

Bold added by me.

}
}
} catch {
case _: ClassNotFoundException => // do nothing
Copy link
Copy Markdown
Member Author

@eed3si9n eed3si9n Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jvican

  1. Do we have any idea what we're after here? Why is this exception being thrown? Are we now not throwing this exception because we're potentially doing less work, which could affect the incremental compiler for java sources?

The case that we have repro project (https://github.com/duboisf/sbt_issue_117_example) is a simple case of java.lang.NoClassDefFoundError: org/jboss/virtual/VirtualFileVisitor while loading some class. Why is this happening? Look no further than spring-core 3.0.0.RELEASE's POM file:

http://central.maven.org/maven2/org/springframework/spring-core/3.0.0.RELEASE/spring-core-3.0.0.RELEASE.pom

<dependency>
  <groupId>org.jboss.vfs</groupId>
  <artifactId>com.springsource.org.jboss.virtual</artifactId>
  <version>2.1.0.GA</version>
  <scope>compile</scope>
  <optional>true</optional>
</dependency>

Maven allows optional dependency, and Spring optionally depends on JBoss VFS. We can find that some of the inner classes in Spring Core exposes org.jboss.virtual.VirtualFileVisitor as parent class. Based on the release date, it's likely this code here: https://github.com/spring-projects/spring-framework/blob/784068346df62225661d9ce1bbd1a32a1fddde66/org.springframework.core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java#L662:

import org.jboss.virtual.VirtualFileVisitor;
...
	private static class PatternVirtualFileVisitor implements VirtualFileVisitor {

Zinc is written to keep tracking parent classes, and doing so requires loading the class. I am doing the same, but I am just ignoring the ClassNotFoundException.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Have we tried this in different JVM versions?

No, but optional JAR missing from classpath is a problem that's independent from JVM versions. Since it's reported in 2011 and 2014, likely it covers Java 6, 7, and 8.

@eed3si9n
Copy link
Copy Markdown
Member Author

eed3si9n commented Apr 3, 2018

  1. Can we add sbt_issue_117_example as a scripted test?

Not at this point since Zinc's scripted build.json doesn't have the feature to resolve library dependencies.

Copy link
Copy Markdown
Member

@jvican jvican left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have two more comments, thanks for the extensive reply describing the problem.

val constructors =
mergeMap(c, c.getDeclaredConstructors, c.getConstructors, constructorToDef(enclPkg))
val cl = c.getClassLoader
val name = c.getCanonicalName
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the original implementation of getDeclaredClasses and getClasses method using getCanonicalName?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't read the original implementation to avoid license violation. Here the name has to match what's stored in the classfile, and then later converted to dot-notation via getClassConstantName. In any case, I am now thinking c.getName might actually be the correct answer since at the classfile level they'll likely store foo/bar/A$B.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a battery of tests to check that both implementations are behaviourally the same? I want to be as certain as possible that this doesn't cause regressions, as Java incremental compilation is not battle-tested in our scripted tests.

Copy link
Copy Markdown
Contributor

@dragos dragos Aug 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't read the original implementation to avoid license violation.

My understanding was that this only applies if you're doing a a clean-room implementation of the Java specification, and even then it's not clear what license you'd be violating.

At any rate, OpenJDK is GPL (at least the parts related to java.lang.Class), for instance here or here.

val classes = merge[Class[_]](c,
c.getDeclaredClasses,
c.getClasses,
innerClasses.toArray[Class[_]],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your implementation also reflecting all interface classes or only inner classes? e.g. maybe you can add a test proving that interfaces defined inside classes are detected by the new implementation.

@dwijnand dwijnand removed their request for review May 3, 2018 09:07
@jvican
Copy link
Copy Markdown
Member

jvican commented Aug 9, 2018

@eed3si9n Are you interested in picking this up and adding the test battery to make sure that they are the same?

@eed3si9n
Copy link
Copy Markdown
Member Author

eed3si9n commented Aug 9, 2018

I guess eventually, but it's not high on my work or hobby hour list.

@jvican
Copy link
Copy Markdown
Member

jvican commented Aug 9, 2018

No rush @eed3si9n 😄

@jvican jvican added the blocked label Aug 10, 2018
@jvican jvican changed the title parse classfile to extract inner class info Parse classfile to extract inner class info Aug 10, 2018
@eed3si9n eed3si9n changed the base branch from 1.1.x to develop August 30, 2018 15:28
@SethTisue SethTisue marked this pull request as draft January 24, 2022 04:30
@SethTisue
Copy link
Copy Markdown
Member

closing for inactivity; I left a breadcrumb on the original issue, in case anyone wants to pick this up again.

@SethTisue SethTisue closed this Feb 13, 2022
BrianHotopp added a commit to BrianHotopp/zinc that referenced this pull request Mar 20, 2026
Fixes sbt/sbt#117

ClassToAPI used Java reflection (c.getClasses, c.getDeclaredClasses) to
discover inner classes during post-compile analysis. When an inner class
references types not on the classpath (e.g. optional dependencies like
JBoss VFS in Spring), these reflection calls throw NoClassDefFoundError,
crashing compilation.

Instead, parse the classfile's InnerClasses attribute directly to
discover inner classes, and gracefully skip any that can't be loaded
via the classloader.

This is a revival of sbt#516 by @eed3si9n, rebased onto the current
codebase with tests added.

Co-Authored-By: Eugene Yokota <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
BrianHotopp added a commit to BrianHotopp/zinc that referenced this pull request Mar 21, 2026
Fixes sbt/sbt#117

ClassToAPI used Java reflection (c.getClasses, c.getDeclaredClasses) to
discover inner classes during post-compile analysis. When an inner class
references types not on the classpath (e.g. optional dependencies like
JBoss VFS in Spring), these reflection calls throw NoClassDefFoundError,
crashing compilation.

Instead, parse the classfile's InnerClasses attribute directly to
discover inner classes, and gracefully skip any that can't be loaded
via the classloader.

This is a revival of sbt#516 by @eed3si9n, rebased onto the current
codebase with tests added.

Co-Authored-By: Eugene Yokota <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
BrianHotopp added a commit to BrianHotopp/zinc that referenced this pull request Mar 21, 2026
Fixes sbt/sbt#117

ClassToAPI used Java reflection (c.getClasses, c.getDeclaredClasses) to
discover inner classes during post-compile analysis. When an inner class
references types not on the classpath (e.g. optional dependencies like
JBoss VFS in Spring), these reflection calls throw NoClassDefFoundError,
crashing compilation.

Instead, parse the classfile's InnerClasses attribute directly to
discover inner classes, and log a warning for any that can't be loaded
via the classloader.

This is a revival of sbt#516 by @eed3si9n, rebased onto the current
codebase with tests added.

Co-Authored-By: Eugene Yokota <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
BrianHotopp added a commit to BrianHotopp/zinc that referenced this pull request Mar 21, 2026
Fixes sbt/sbt#117

ClassToAPI used Java reflection (c.getClasses, c.getDeclaredClasses) to
discover inner classes during post-compile analysis. When an inner class
references types not on the classpath (e.g. optional dependencies like
JBoss VFS in Spring), these reflection calls throw NoClassDefFoundError,
crashing compilation.

The fix tries reflection first (preserving exact existing semantics),
and falls back to parsing the classfile's InnerClasses attribute when
reflection fails. The fallback logs a warning and gracefully skips any
inner classes that can't be loaded.

This is a revival of sbt#516 by @eed3si9n, rebased onto the current
codebase with tests added.

Co-Authored-By: Eugene Yokota <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NoClassDefFound during analysis after java with exportJars = true

6 participants