Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions repl/src/dotty/tools/repl/AbstractFileClassLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,37 @@ package repl

import scala.language.unsafeNulls

import dotty.tools.dotc.config.ScalaSettings

import io.AbstractFile

import java.net.{URL, URLConnection, URLStreamHandler}
import java.util.Collections

class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader, interruptInstrumentation: String) extends ClassLoader(parent):
private def findAbstractFile(name: String) = root.lookupPath(name.split('/').toIndexedSeq, directory = false)

// on JDK 20 the URL constructor we're using is deprecated,
// but the recommended replacement, URL.of, doesn't exist on JDK 8
@annotation.nowarn("cat=deprecation")
override protected def findResource(name: String): URL | Null =
findAbstractFile(name) match
case null => null
case file => new URL(null, s"memory:${file.path}", new URLStreamHandler {
override def openConnection(url: URL): URLConnection = new URLConnection(url) {
override def connect() = ()
override def getInputStream = file.input
}
})
override protected def findResources(name: String): java.util.Enumeration[URL] =
findResource(name) match
case null => Collections.enumeration(Collections.emptyList[URL]) //Collections.emptyEnumeration[URL]
case url => Collections.enumeration(Collections.singleton(url))
import AbstractFileClassLoader.InterruptInstrumentation


object AbstractFileClassLoader:
enum InterruptInstrumentation(val stringValue: String):
case Disabled extends InterruptInstrumentation("false")
case Enabled extends InterruptInstrumentation("true")
case Local extends InterruptInstrumentation("local")

def is(value: InterruptInstrumentation): Boolean = this == value
def isOneOf(others: InterruptInstrumentation*): Boolean = others.contains(this)

object InterruptInstrumentation:
def fromString(string: String): InterruptInstrumentation = string match {
case "false" => Disabled
case "true" => Enabled
case "local" => Local
case _ => throw new IllegalArgumentException(s"Invalid interrupt instrumentation value: $string")
}

class AbstractFileClassLoader(root: AbstractFile, parent: ClassLoader, interruptInstrumentation: InterruptInstrumentation)
extends io.AbstractFileClassLoader(root, parent):

def this(root: AbstractFile, parent: ClassLoader) = this(root, parent, InterruptInstrumentation.fromString(ScalaSettings.XreplInterruptInstrumentation.default))

override def findClass(name: String): Class[?] = {
var file: AbstractFile | Null = root
Expand All @@ -52,23 +59,23 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader, inter

val bytes = file.toByteArray

if interruptInstrumentation != "false" then defineClassInstrumented(name, bytes)
if !interruptInstrumentation.is(InterruptInstrumentation.Enabled) then defineClassInstrumented(name, bytes)
else defineClass(name, bytes, 0, bytes.length)
}

def defineClassInstrumented(name: String, originalBytes: Array[Byte]) = {
private def defineClassInstrumented(name: String, originalBytes: Array[Byte]) = {
val instrumentedBytes = ReplBytecodeInstrumentation.instrument(originalBytes)
defineClass(name, instrumentedBytes, 0, instrumentedBytes.length)
}

override def loadClass(name: String): Class[?] =
if interruptInstrumentation == "false" || interruptInstrumentation == "local"
then return super.loadClass(name)
if interruptInstrumentation.isOneOf(InterruptInstrumentation.Disabled, InterruptInstrumentation.Local) then
return super.loadClass(name)

val loaded = findLoadedClass(name) // Check if already loaded
if loaded != null then return loaded

name match {
name match {
// Don't instrument JDK classes or StopRepl. These are often restricted to load from a single classloader
// due to the JDK module system, and so instrumenting them and loading the modified copy of the class
// results in runtime exceptions
Expand Down
8 changes: 5 additions & 3 deletions repl/src/dotty/tools/repl/DependencyResolver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import java.net.{URL, URLClassLoader}
import scala.jdk.CollectionConverters.*
import scala.util.control.NonFatal

import dotty.tools.repl.AbstractFileClassLoader

import coursierapi.{Repository, Dependency, MavenRepository}
import com.virtuslab.using_directives.UsingDirectivesProcessor
import com.virtuslab.using_directives.custom.model.{Path, StringValue, Value}
Expand Down Expand Up @@ -90,7 +92,7 @@ object DependencyResolver:
import dotty.tools.dotc.classpath.ClassPathFactory
import dotty.tools.dotc.core.SymbolLoaders
import dotty.tools.dotc.core.Symbols.defn
import dotty.tools.io.*
import dotty.tools.io.{AbstractFile, ClassPath}
import dotty.tools.repl.ScalaClassLoader.fromURLsParallelCapable

// Create a classloader with all the resolved JAR files
Expand All @@ -106,10 +108,10 @@ object DependencyResolver:
SymbolLoaders.mergeNewEntries(defn.RootClass, ClassPath.RootPackage, jarClassPath, ctx.platform.classPath)

// Create new classloader with previous output dir and resolved dependencies
new dotty.tools.repl.AbstractFileClassLoader(
new AbstractFileClassLoader(
prevOutputDir,
depsClassLoader,
ctx.settings.XreplInterruptInstrumentation.value
AbstractFileClassLoader.InterruptInstrumentation.fromString(ctx.settings.XreplInterruptInstrumentation.value)
)

end DependencyResolver
2 changes: 1 addition & 1 deletion repl/src/dotty/tools/repl/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
myClassLoader = new AbstractFileClassLoader(
ctx.settings.outputDir.value,
parent,
ctx.settings.XreplInterruptInstrumentation.value
AbstractFileClassLoader.InterruptInstrumentation.fromString(ctx.settings.XreplInterruptInstrumentation.value)
)
myClassLoader
}
Expand Down
4 changes: 2 additions & 2 deletions repl/src/dotty/tools/repl/ReplBytecodeInstrumentation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import scala.language.unsafeNulls
import scala.tools.asm.*
import scala.tools.asm.Opcodes.*
import scala.tools.asm.tree.*
import scala.collection.JavaConverters.*
import scala.jdk.CollectionConverters.*
import java.util.concurrent.atomic.AtomicBoolean

object ReplBytecodeInstrumentation:
/** Instrument bytecode to add checks to throw an exception if the REPL command is cancelled
*/
Expand Down
2 changes: 1 addition & 1 deletion repl/src/dotty/tools/repl/ReplDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ class ReplDriver(settings: Array[String],
rendering.myClassLoader = new AbstractFileClassLoader(
prevOutputDir,
jarClassLoader,
ctx.settings.XreplInterruptInstrumentation.value
AbstractFileClassLoader.InterruptInstrumentation.fromString(ctx.settings.XreplInterruptInstrumentation.value)
)

out.println(s"Added '$path' to classpath.")
Expand Down
24 changes: 12 additions & 12 deletions repl/test/dotty/tools/repl/AbstractFileClassLoaderTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ class AbstractFileClassLoaderTest:
@Test def afclGetsParent(): Unit =
val p = new URLClassLoader(Array.empty[URL])
val d = new VirtualDirectory("vd", None)
val x = new AbstractFileClassLoader(d, p, "false")
val x = new AbstractFileClassLoader(d, p)
assertSame(p, x.getParent)

@Test def afclGetsResource(): Unit =
val (fuzz, booz) = fuzzBuzzBooz
booz.writeContent("hello, world")
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader)
val res = sut.getResource("buzz/booz.class")
assertNotNull("Find buzz/booz.class", res)
assertEquals("hello, world", slurp(res))
Expand All @@ -67,8 +67,8 @@ class AbstractFileClassLoaderTest:
val (fuzz_, booz_) = fuzzBuzzBooz
booz.writeContent("hello, world")
booz_.writeContent("hello, world_")
val p = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
val sut = new AbstractFileClassLoader(fuzz_, p, "false")
val p = new AbstractFileClassLoader(fuzz, NoClassLoader)
val sut = new AbstractFileClassLoader(fuzz_, p)
val res = sut.getResource("buzz/booz.class")
assertNotNull("Find buzz/booz.class", res)
assertEquals("hello, world", slurp(res))
Expand All @@ -79,7 +79,7 @@ class AbstractFileClassLoaderTest:
val bass = fuzz.fileNamed("bass")
booz.writeContent("hello, world")
bass.writeContent("lo tone")
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader)
val res = sut.getResource("booz.class")
assertNotNull(res)
assertEquals("hello, world", slurp(res))
Expand All @@ -89,7 +89,7 @@ class AbstractFileClassLoaderTest:
@Test def afclGetsResources(): Unit =
val (fuzz, booz) = fuzzBuzzBooz
booz.writeContent("hello, world")
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader)
val e = sut.getResources("buzz/booz.class")
assertTrue("At least one buzz/booz.class", e.hasMoreElements)
assertEquals("hello, world", slurp(e.nextElement))
Expand All @@ -100,8 +100,8 @@ class AbstractFileClassLoaderTest:
val (fuzz_, booz_) = fuzzBuzzBooz
booz.writeContent("hello, world")
booz_.writeContent("hello, world_")
val p = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
val x = new AbstractFileClassLoader(fuzz_, p, "false")
val p = new AbstractFileClassLoader(fuzz, NoClassLoader)
val x = new AbstractFileClassLoader(fuzz_, p)
val e = x.getResources("buzz/booz.class")
assertTrue(e.hasMoreElements)
assertEquals("hello, world", slurp(e.nextElement))
Expand All @@ -112,15 +112,15 @@ class AbstractFileClassLoaderTest:
@Test def afclGetsResourceAsStream(): Unit =
val (fuzz, booz) = fuzzBuzzBooz
booz.writeContent("hello, world")
val x = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
val x = new AbstractFileClassLoader(fuzz, NoClassLoader)
val r = x.getResourceAsStream("buzz/booz.class")
assertNotNull(r)
assertEquals("hello, world", closing(r)(is => Source.fromInputStream(is).mkString))

@Test def afclGetsClassBytes(): Unit =
val (fuzz, booz) = fuzzBuzzBooz
booz.writeContent("hello, world")
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader)
val b = sut.classBytes("buzz/booz.class")
assertEquals("hello, world", new String(b, UTF8.charSet))

Expand All @@ -130,8 +130,8 @@ class AbstractFileClassLoaderTest:
booz.writeContent("hello, world")
booz_.writeContent("hello, world_")

val p = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
val sut = new AbstractFileClassLoader(fuzz_, p, "false")
val p = new AbstractFileClassLoader(fuzz, NoClassLoader)
val sut = new AbstractFileClassLoader(fuzz_, p)
val b = sut.classBytes("buzz/booz.class")
assertEquals("hello, world", new String(b, UTF8.charSet))
end AbstractFileClassLoaderTest
Loading