Skip to content

Commit 964a197

Browse files
committed
SI-8843 AbsFileCL acts like a CL
Let the AbstractFileClassLoader override just the usual suspects. Normal delegation behavior should ensue. That's instead of overriding `getResourceAsStream`, which was intended that "The repl classloader now works more like you'd expect a classloader to." (Workaround for "Don't know how to construct an URL for something which exists only in memory.") Also override `findResources` so that `getResources` does the obvious thing, namely, return one iff `getResource` does. The translating class loader for REPL only special-cases `foo.class`: as a fallback, take `foo` as `$line42.$read$something$foo` and try that class file. That's the use case for "works like you'd expect it to." There was a previous fix to ensure `getResource` doesn't take a class name. The convenience behavior, that `classBytes` takes either a class name or a resource path ending in ".class", has been promoted to `ScalaClassLoader`.
1 parent 7b2c3cb commit 964a197

File tree

6 files changed

+230
-53
lines changed

6 files changed

+230
-53
lines changed

src/reflect/scala/reflect/internal/util/AbstractFileClassLoader.scala

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,24 @@
55
package scala
66
package reflect.internal.util
77

8-
import scala.reflect.io.AbstractFile
8+
import scala.collection.{ mutable, immutable }
9+
import scala.reflect.io.{ AbstractFile, Streamable }
10+
import java.net.{ URL, URLConnection, URLStreamHandler }
911
import java.security.cert.Certificate
1012
import java.security.{ ProtectionDomain, CodeSource }
11-
import java.net.{ URL, URLConnection, URLStreamHandler }
12-
import scala.collection.{ mutable, immutable }
13+
import java.util.{ Collections => JCollections, Enumeration => JEnumeration }
1314

14-
/**
15-
* A class loader that loads files from a {@link scala.tools.nsc.io.AbstractFile}.
15+
/** A class loader that loads files from a {@link scala.tools.nsc.io.AbstractFile}.
1616
*
17-
* @author Lex Spoon
17+
* @author Lex Spoon
1818
*/
1919
class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader)
2020
extends ClassLoader(parent)
2121
with ScalaClassLoader
2222
{
2323
protected def classNameToPath(name: String): String =
2424
if (name endsWith ".class") name
25-
else name.replace('.', '/') + ".class"
25+
else s"${name.replace('.', '/')}.class"
2626

2727
protected def findAbstractFile(name: String): AbstractFile = {
2828
var file: AbstractFile = root
@@ -56,35 +56,25 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader)
5656
file
5757
}
5858

59-
// parent delegation in JCL uses getResource; so either add parent.getResAsStream
60-
// or implement findResource, which we do here as a study in scarlet (my complexion
61-
// after looking at CLs and URLs)
62-
override def findResource(name: String): URL = findAbstractFile(name) match {
59+
override protected def findClass(name: String): Class[_] = {
60+
val bytes = classBytes(name)
61+
if (bytes.length == 0)
62+
throw new ClassNotFoundException(name)
63+
else
64+
defineClass(name, bytes, 0, bytes.length, protectionDomain)
65+
}
66+
override protected def findResource(name: String): URL = findAbstractFile(name) match {
6367
case null => null
64-
case file => new URL(null, "repldir:" + file.path, new URLStreamHandler {
68+
case file => new URL(null, s"memory:${file.path}", new URLStreamHandler {
6569
override def openConnection(url: URL): URLConnection = new URLConnection(url) {
66-
override def connect() { }
70+
override def connect() = ()
6771
override def getInputStream = file.input
6872
}
6973
})
7074
}
71-
72-
// this inverts delegation order: super.getResAsStr calls parent.getRes if we fail
73-
override def getResourceAsStream(name: String) = findAbstractFile(name) match {
74-
case null => super.getResourceAsStream(name)
75-
case file => file.input
76-
}
77-
// ScalaClassLoader.classBytes uses getResAsStream, so we'll try again before delegating
78-
override def classBytes(name: String): Array[Byte] = findAbstractFile(classNameToPath(name)) match {
79-
case null => super.classBytes(name)
80-
case file => file.toByteArray
81-
}
82-
override def findClass(name: String): Class[_] = {
83-
val bytes = classBytes(name)
84-
if (bytes.length == 0)
85-
throw new ClassNotFoundException(name)
86-
else
87-
defineClass(name, bytes, 0, bytes.length, protectionDomain)
75+
override protected def findResources(name: String): JEnumeration[URL] = findResource(name) match {
76+
case null => JCollections.enumeration(JCollections.emptyList[URL]) //JCollections.emptyEnumeration[URL]
77+
case url => JCollections.enumeration(JCollections.singleton(url))
8878
}
8979

9080
lazy val protectionDomain = {
@@ -106,15 +96,13 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader)
10696
throw new UnsupportedOperationException()
10797
}
10898

109-
override def getPackage(name: String): Package = {
110-
findAbstractDir(name) match {
111-
case null => super.getPackage(name)
112-
case file => packages.getOrElseUpdate(name, {
113-
val ctor = classOf[Package].getDeclaredConstructor(classOf[String], classOf[String], classOf[String], classOf[String], classOf[String], classOf[String], classOf[String], classOf[URL], classOf[ClassLoader])
114-
ctor.setAccessible(true)
115-
ctor.newInstance(name, null, null, null, null, null, null, null, this)
116-
})
117-
}
99+
override def getPackage(name: String): Package = findAbstractDir(name) match {
100+
case null => super.getPackage(name)
101+
case file => packages.getOrElseUpdate(name, {
102+
val ctor = classOf[Package].getDeclaredConstructor(classOf[String], classOf[String], classOf[String], classOf[String], classOf[String], classOf[String], classOf[String], classOf[URL], classOf[ClassLoader])
103+
ctor.setAccessible(true)
104+
ctor.newInstance(name, null, null, null, null, null, null, null, this)
105+
})
118106
}
119107

120108
override def getPackages(): Array[Package] =

src/reflect/scala/reflect/internal/util/ScalaClassLoader.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ trait ScalaClassLoader extends JClassLoader {
5353
}
5454

5555
/** An InputStream representing the given class name, or null if not found. */
56-
def classAsStream(className: String) =
57-
getResourceAsStream(className.replaceAll("""\.""", "/") + ".class")
56+
def classAsStream(className: String) = getResourceAsStream {
57+
if (className endsWith ".class") className
58+
else s"${className.replace('.', '/')}.class" // classNameToPath
59+
}
5860

5961
/** Run the main method of a class to be loaded by this classloader */
6062
def run(objectName: String, arguments: Seq[String]) {

src/repl/scala/tools/nsc/interpreter/IMain.scala

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -295,22 +295,38 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
295295
def originalPath(name: Name): String = typerOp path name
296296
def originalPath(sym: Symbol): String = typerOp path sym
297297
def flatPath(sym: Symbol): String = flatOp shift sym.javaClassName
298+
298299
def translatePath(path: String) = {
299300
val sym = if (path endsWith "$") symbolOfTerm(path.init) else symbolOfIdent(path)
300301
sym.toOption map flatPath
301302
}
303+
304+
/** If path represents a class resource in the default package,
305+
* see if the corresponding symbol has a class file that is a REPL artifact
306+
* residing at a different resource path. Translate X.class to $line3/$read$$iw$$iw$X.class.
307+
*/
308+
def translateSimpleResource(path: String): Option[String] = {
309+
if (!(path contains '/') && (path endsWith ".class")) {
310+
val name = path stripSuffix ".class"
311+
val sym = if (name endsWith "$") symbolOfTerm(name.init) else symbolOfIdent(name)
312+
def pathOf(s: String) = s"${s.replace('.', '/')}.class"
313+
sym.toOption map (s => pathOf(flatPath(s)))
314+
} else {
315+
None
316+
}
317+
}
302318
def translateEnclosingClass(n: String) = symbolOfTerm(n).enclClass.toOption map flatPath
303319

320+
/** If unable to find a resource foo.class, try taking foo as a symbol in scope
321+
* and use its java class name as a resource to load.
322+
*
323+
* $intp.classLoader classBytes "Bippy" or $intp.classLoader getResource "Bippy.class" just work.
324+
*/
304325
private class TranslatingClassLoader(parent: ClassLoader) extends util.AbstractFileClassLoader(replOutput.dir, parent) {
305-
/** Overridden here to try translating a simple name to the generated
306-
* class name if the original attempt fails. This method is used by
307-
* getResourceAsStream as well as findClass.
308-
*/
309-
override protected def findAbstractFile(name: String): AbstractFile =
310-
super.findAbstractFile(name) match {
311-
case null if _initializeComplete => translatePath(name) map (super.findAbstractFile(_)) orNull
312-
case file => file
313-
}
326+
override protected def findAbstractFile(name: String): AbstractFile = super.findAbstractFile(name) match {
327+
case null if _initializeComplete => translateSimpleResource(name) map super.findAbstractFile orNull
328+
case file => file
329+
}
314330
}
315331
private def makeClassLoader(): util.AbstractFileClassLoader =
316332
new TranslatingClassLoader(parentClassLoader match {

test/files/run/t8843-repl-xlat.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
import scala.tools.partest.SessionTest
3+
4+
// Handy hamburger helper for repl resources
5+
object Test extends SessionTest {
6+
def session =
7+
"""Type in expressions to have them evaluated.
8+
Type :help for more information.
9+
10+
scala> $intp.isettings.unwrapStrings = false
11+
$intp.isettings.unwrapStrings: Boolean = false
12+
13+
scala> class Bippy
14+
defined class Bippy
15+
16+
scala> $intp.classLoader getResource "Bippy.class"
17+
res0: java.net.URL = memory:(memory)/$line4/$read$$iw$$iw$Bippy.class
18+
19+
scala> ($intp.classLoader getResources "Bippy.class").nextElement
20+
res1: java.net.URL = memory:(memory)/$line4/$read$$iw$$iw$Bippy.class
21+
22+
scala> ($intp.classLoader classBytes "Bippy").nonEmpty
23+
res2: Boolean = true
24+
25+
scala> ($intp.classLoader classAsStream "Bippy") != null
26+
res3: Boolean = true
27+
28+
scala> $intp.classLoader getResource "Bippy"
29+
res4: java.net.URL = null
30+
31+
scala> :quit"""
32+
}
33+

test/junit/scala/reflect/internal/PrintersTest.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ object PrinterHelper {
2424
resultCode.lines mkString s"$LF"
2525

2626
def assertResultCode(code: String)(parsedCode: String = "", typedCode: String = "", wrap: Boolean = false, printRoot: Boolean = false) = {
27-
def toolboxTree(tree: => Tree) = try{
27+
def toolboxTree(tree: => Tree) = try {
2828
tree
2929
} catch {
30-
case e:scala.tools.reflect.ToolBoxError => throw new Exception(e.getMessage + ": " + code)
30+
case e:scala.tools.reflect.ToolBoxError => throw new Exception(e.getMessage + ": " + code, e)
3131
}
3232

3333
def wrapCode(source: String) = {
@@ -1186,4 +1186,4 @@ trait QuasiTreesPrintTests {
11861186
| };
11871187
| ()
11881188
|}""")
1189-
}
1189+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package scala.reflect.internal.util
2+
3+
import org.junit.Assert._
4+
import org.junit.Test
5+
import org.junit.runner.RunWith
6+
import org.junit.runners.JUnit4
7+
8+
@RunWith(classOf[JUnit4])
9+
class AbstractFileClassLoaderTest {
10+
11+
import scala.reflect.io._
12+
import scala.io.Source
13+
import scala.io.Codec.UTF8
14+
import scala.reflect.io.Streamable
15+
import java.net.{ URLClassLoader, URL }
16+
17+
implicit def `we love utf8` = UTF8
18+
implicit class `abs file ops`(f: AbstractFile) {
19+
def writeContent(s: String): Unit = Streamable.closing(f.bufferedOutput)(os => os write s.getBytes(UTF8.charSet))
20+
}
21+
implicit class `url slurp`(url: URL) {
22+
def slurp(): String = Streamable.slurp(url)
23+
}
24+
25+
val NoClassLoader: ClassLoader = null
26+
27+
def fuzzBuzzBooz: (AbstractFile, AbstractFile) = {
28+
val fuzz = new VirtualDirectory("fuzz", None)
29+
val buzz = fuzz subdirectoryNamed "buzz"
30+
val booz = buzz fileNamed "booz.class"
31+
(fuzz, booz)
32+
}
33+
34+
@Test
35+
def afclGetsParent(): Unit = {
36+
val p = new URLClassLoader(Array.empty[URL])
37+
val d = new VirtualDirectory("vd", None)
38+
val x = new AbstractFileClassLoader(d, p)
39+
assertSame(p, x.getParent)
40+
}
41+
42+
@Test
43+
def afclGetsResource(): Unit = {
44+
val (fuzz, booz) = fuzzBuzzBooz
45+
booz writeContent "hello, world"
46+
val x = new AbstractFileClassLoader(fuzz, NoClassLoader)
47+
val r = x.getResource("buzz/booz.class")
48+
assertNotNull(r)
49+
assertEquals("hello, world", r.slurp())
50+
}
51+
52+
@Test
53+
def afclGetsResourceFromParent(): Unit = {
54+
val (fuzz, booz) = fuzzBuzzBooz
55+
val (fuzz_, booz_) = fuzzBuzzBooz
56+
booz writeContent "hello, world"
57+
booz_ writeContent "hello, world_"
58+
val p = new AbstractFileClassLoader(fuzz, NoClassLoader)
59+
val x = new AbstractFileClassLoader(fuzz_, p)
60+
val r = x.getResource("buzz/booz.class")
61+
assertNotNull(r)
62+
assertEquals("hello, world", r.slurp())
63+
}
64+
65+
@Test
66+
def afclGetsResourceInDefaultPackage(): Unit = {
67+
val fuzz = new VirtualDirectory("fuzz", None)
68+
val booz = fuzz fileNamed "booz.class"
69+
val bass = fuzz fileNamed "bass"
70+
booz writeContent "hello, world"
71+
bass writeContent "lo tone"
72+
val x = new AbstractFileClassLoader(fuzz, NoClassLoader)
73+
val r = x.getResource("booz.class")
74+
assertNotNull(r)
75+
assertEquals("hello, world", r.slurp())
76+
assertEquals("lo tone", (x getResource "bass").slurp())
77+
}
78+
79+
// SI-8843
80+
@Test
81+
def afclGetsResources(): Unit = {
82+
val (fuzz, booz) = fuzzBuzzBooz
83+
booz writeContent "hello, world"
84+
val x = new AbstractFileClassLoader(fuzz, NoClassLoader)
85+
val e = x.getResources("buzz/booz.class")
86+
assertTrue(e.hasMoreElements)
87+
assertEquals("hello, world", e.nextElement.slurp())
88+
assertFalse(e.hasMoreElements)
89+
}
90+
91+
@Test
92+
def afclGetsResourcesFromParent(): Unit = {
93+
val (fuzz, booz) = fuzzBuzzBooz
94+
val (fuzz_, booz_) = fuzzBuzzBooz
95+
booz writeContent "hello, world"
96+
booz_ writeContent "hello, world_"
97+
val p = new AbstractFileClassLoader(fuzz, NoClassLoader)
98+
val x = new AbstractFileClassLoader(fuzz_, p)
99+
val e = x.getResources("buzz/booz.class")
100+
assertTrue(e.hasMoreElements)
101+
assertEquals("hello, world", e.nextElement.slurp())
102+
assertTrue(e.hasMoreElements)
103+
assertEquals("hello, world_", e.nextElement.slurp())
104+
assertFalse(e.hasMoreElements)
105+
}
106+
107+
@Test
108+
def afclGetsResourceAsStream(): Unit = {
109+
val (fuzz, booz) = fuzzBuzzBooz
110+
booz writeContent "hello, world"
111+
val x = new AbstractFileClassLoader(fuzz, NoClassLoader)
112+
val r = x.getResourceAsStream("buzz/booz.class")
113+
assertNotNull(r)
114+
assertEquals("hello, world", Streamable.closing(r)(is => Source.fromInputStream(is).mkString))
115+
}
116+
117+
@Test
118+
def afclGetsClassBytes(): Unit = {
119+
val (fuzz, booz) = fuzzBuzzBooz
120+
booz writeContent "hello, world"
121+
val x = new AbstractFileClassLoader(fuzz, NoClassLoader)
122+
val b = x.classBytes("buzz/booz.class")
123+
assertEquals("hello, world", new String(b, UTF8.charSet))
124+
}
125+
126+
@Test
127+
def afclGetsClassBytesFromParent(): Unit = {
128+
val (fuzz, booz) = fuzzBuzzBooz
129+
val (fuzz_, booz_) = fuzzBuzzBooz
130+
booz writeContent "hello, world"
131+
booz_ writeContent "hello, world_"
132+
133+
val p = new AbstractFileClassLoader(fuzz, NoClassLoader)
134+
val x = new AbstractFileClassLoader(fuzz_, p)
135+
val b = x.classBytes("buzz/booz.class")
136+
assertEquals("hello, world", new String(b, UTF8.charSet))
137+
}
138+
}

0 commit comments

Comments
 (0)