diff --git a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala index f3c2f295ce82..a6dbf696a575 100644 --- a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala +++ b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala @@ -27,6 +27,13 @@ class JavaPlatform extends Platform { case _ => false }) + def addToClassPath(cPath: ClassPath)(using Context): Unit = classPath match { + case AggregateClassPath(entries) => + currentClassPath = Some(AggregateClassPath(entries :+ cPath)) + case cp: ClassPath => + currentClassPath = Some(AggregateClassPath(cp :: cPath :: Nil)) + } + /** Update classpath with a substituted subentry */ def updateClassPath(subst: Map[ClassPath, ClassPath]): Unit = currentClassPath.get match { case AggregateClassPath(entries) => diff --git a/compiler/src/dotty/tools/dotc/config/Platform.scala b/compiler/src/dotty/tools/dotc/config/Platform.scala index 2a0b207e68c1..c7d319e736b4 100644 --- a/compiler/src/dotty/tools/dotc/config/Platform.scala +++ b/compiler/src/dotty/tools/dotc/config/Platform.scala @@ -21,6 +21,9 @@ abstract class Platform { /** Update classpath with a substitution that maps entries to entries */ def updateClassPath(subst: Map[ClassPath, ClassPath]): Unit + /** Add new entry to classpath */ + def addToClassPath(cPath: ClassPath)(using Context): Unit + /** Any platform-specific phases. */ //def platformPhases: List[SubComponent] diff --git a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala index 68f2e350c3e4..39297697f29a 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -7,6 +7,7 @@ import java.nio.channels.ClosedByInterruptException import scala.util.control.NonFatal +import dotty.tools.dotc.classpath.{ ClassPathFactory, PackageNameUtils } import dotty.tools.dotc.classpath.FileUtils.{hasTastyExtension, hasBetastyExtension} import dotty.tools.io.{ ClassPath, ClassRepresentation, AbstractFile, NoAbstractFile } import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions @@ -272,7 +273,7 @@ object SymbolLoaders { def maybeModuleClass(classRep: ClassRepresentation): Boolean = classRep.name.nonEmpty && classRep.name.last == '$' - private def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = { + def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = { def isAbsent(classRep: ClassRepresentation) = !root.unforcedDecls.lookup(classRep.name.toTypeName).exists @@ -316,6 +317,32 @@ object SymbolLoaders { } } } + + def mergeNewEntries( + packageClass: ClassSymbol, fullPackageName: String, + jarClasspath: ClassPath, fullClasspath: ClassPath, + )(using Context): Unit = + if jarClasspath.classes(fullPackageName).nonEmpty then + // if the package contains classes in jarClasspath, the package is invalidated (or removed if there are no more classes in it) + val packageVal = packageClass.sourceModule.asInstanceOf[TermSymbol] + if packageClass.isRoot then + val loader = new PackageLoader(packageVal, fullClasspath) + loader.enterClasses(defn.EmptyPackageClass, fullPackageName, flat = false) + loader.enterClasses(defn.EmptyPackageClass, fullPackageName, flat = true) + else if packageClass.ownersIterator.contains(defn.ScalaPackageClass) then + () // skip + else if fullClasspath.hasPackage(fullPackageName) then + packageClass.info = new PackageLoader(packageVal, fullClasspath) + else + packageClass.owner.info.decls.openForMutations.unlink(packageVal) + else + for p <- jarClasspath.packages(fullPackageName) do + val subPackageName = PackageNameUtils.separatePkgAndClassNames(p.name)._2.toTermName + val subPackage = packageClass.info.decl(subPackageName).orElse: + // package does not exist in symbol table, create a new symbol + enterPackage(packageClass, subPackageName, (module, modcls) => new PackageLoader(module, fullClasspath)) + mergeNewEntries(subPackage.asSymDenotation.moduleClass.asClass, p.name, jarClasspath, fullClasspath) + end mergeNewEntries } /** A lazy type that completes itself by calling parameter doComplete. diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index 2674a385a10c..ae21f735edae 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -52,6 +52,20 @@ object Load { val command: String = ":load" } +/** `:require` is a deprecated alias for :jar` + */ +case class Require(path: String) extends Command +object Require { + val command: String = ":require" +} + +/** `:jar ` adds a jar to the classpath + */ +case class JarCmd(path: String) extends Command +object JarCmd { + val command: String = ":jar" +} + /** `:kind ` display the kind of a type. see also :help kind */ case class KindOf(expr: String) extends Command @@ -145,8 +159,10 @@ object ParseResult { Help.command -> (_ => Help), Reset.command -> (arg => Reset(arg)), Imports.command -> (_ => Imports), + JarCmd.command -> (arg => JarCmd(arg)), KindOf.command -> (arg => KindOf(arg)), Load.command -> (arg => Load(arg)), + Require.command -> (arg => Require(arg)), TypeOf.command -> (arg => TypeOf(arg)), DocOf.command -> (arg => DocOf(arg)), Settings.command -> (arg => Settings(arg)), diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 0f2921fd736c..3c8618cf3198 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets import dotty.tools.dotc.ast.Trees.* import dotty.tools.dotc.ast.{tpd, untpd} +import dotty.tools.dotc.classpath.ClassPathFactory import dotty.tools.dotc.config.CommandLineParser.tokenize import dotty.tools.dotc.config.Properties.{javaVersion, javaVmName, simpleVersionString} import dotty.tools.dotc.core.Contexts.* @@ -21,6 +22,7 @@ import dotty.tools.dotc.core.NameOps.* import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.core.Symbols.{Symbol, defn} +import dotty.tools.dotc.core.SymbolLoaders import dotty.tools.dotc.interfaces import dotty.tools.dotc.interactive.Completion import dotty.tools.dotc.printing.SyntaxHighlighting @@ -39,6 +41,7 @@ import scala.annotation.tailrec import scala.collection.mutable import scala.compiletime.uninitialized import scala.jdk.CollectionConverters.* +import scala.tools.asm.ClassReader import scala.util.control.NonFatal import scala.util.Using @@ -510,10 +513,65 @@ class ReplDriver(settings: Array[String], state } + case Require(path) => + out.println(":require is no longer supported, but has been replaced with :jar. Please use :jar") + state + + case JarCmd(path) => + val jarFile = AbstractFile.getDirectory(path) + if (jarFile == null) + out.println(s"""Cannot add "$path" to classpath.""") + state + else + def flatten(f: AbstractFile): Iterator[AbstractFile] = + if (f.isClassContainer) f.iterator.flatMap(flatten) + else Iterator(f) + + def tryClassLoad(classFile: AbstractFile): Option[String] = { + val input = classFile.input + try { + val reader = new ClassReader(input) + val clsName = reader.getClassName.replace('/', '.') + rendering.myClassLoader.loadClass(clsName) + Some(clsName) + } catch + case _: ClassNotFoundException => None + finally { + input.close() + } + } + + try { + val entries = flatten(jarFile) + + val existingClass = entries.filter(_.ext.isClass).find(tryClassLoad(_).isDefined) + if (existingClass.nonEmpty) + out.println(s"The path '$path' cannot be loaded, it contains a classfile that already exists on the classpath: ${existingClass.get}") + else inContext(state.context): + val jarClassPath = ClassPathFactory.newClassPath(jarFile) + val prevOutputDir = ctx.settings.outputDir.value + + // add to compiler class path + ctx.platform.addToClassPath(jarClassPath) + SymbolLoaders.mergeNewEntries(defn.RootClass, ClassPath.RootPackage, jarClassPath, ctx.platform.classPath) + + // new class loader with previous output dir and specified jar + val prevClassLoader = rendering.classLoader() + val jarClassLoader = fromURLsParallelCapable( + jarClassPath.asURLs, prevClassLoader) + rendering.myClassLoader = new AbstractFileClassLoader( + prevOutputDir, jarClassLoader) + + out.println(s"Added '$path' to classpath.") + } catch { + case e: Throwable => + out.println(s"Failed to load '$path' to classpath: ${e.getMessage}") + } + state + case KindOf(expr) => out.println(s"""The :kind command is not currently supported.""") state - case TypeOf(expr) => expr match { case "" => out.println(s":type ") diff --git a/compiler/test-resources/jars/MyLibrary.scala b/compiler/test-resources/jars/MyLibrary.scala new file mode 100644 index 000000000000..bd752d2df864 --- /dev/null +++ b/compiler/test-resources/jars/MyLibrary.scala @@ -0,0 +1,8 @@ +/** + * JAR used for testing repl :jar + * Generated using: mkdir out; scalac -d out MyLibrary.scala; jar cf mylibrary.jar -C out . + */ +package mylibrary + +object Utils: + def greet(name: String): String = s"Hello, $name!" diff --git a/compiler/test-resources/jars/MyLibrary2.scala b/compiler/test-resources/jars/MyLibrary2.scala new file mode 100644 index 000000000000..df5a944c0bcf --- /dev/null +++ b/compiler/test-resources/jars/MyLibrary2.scala @@ -0,0 +1,9 @@ +/** + * JAR used for testing repl :jar + * Generated using: mkdir out2; scalac -d out MyLibrary2.scala; jar cf mylibrary2.jar -C out2 . + */ +package mylibrary2 + +object Utils2: + def greet(name: String): String = s"Greetings, $name!" + diff --git a/compiler/test-resources/jars/mylibrary.jar b/compiler/test-resources/jars/mylibrary.jar new file mode 100644 index 000000000000..4537e10ee491 Binary files /dev/null and b/compiler/test-resources/jars/mylibrary.jar differ diff --git a/compiler/test-resources/jars/mylibrary2.jar b/compiler/test-resources/jars/mylibrary2.jar new file mode 100644 index 000000000000..1d6584bdf545 Binary files /dev/null and b/compiler/test-resources/jars/mylibrary2.jar differ diff --git a/compiler/test-resources/repl/jar-command b/compiler/test-resources/repl/jar-command new file mode 100644 index 000000000000..b0ded22d3654 --- /dev/null +++ b/compiler/test-resources/repl/jar-command @@ -0,0 +1,13 @@ +scala> val z = 1 +val z: Int = 1 + +scala>:jar sbt-test/source-dependencies/canon/actual/a.jar +Added 'sbt-test/source-dependencies/canon/actual/a.jar' to classpath. + +scala> import A.x + +scala> x +val res0: Int = 3 + +scala> z +val res1: Int = 1 diff --git a/compiler/test-resources/repl/jar-errors b/compiler/test-resources/repl/jar-errors new file mode 100644 index 000000000000..7d5720fcb233 --- /dev/null +++ b/compiler/test-resources/repl/jar-errors @@ -0,0 +1,11 @@ +scala>:jar path/does/not/exist +Cannot add "path/does/not/exist" to classpath. + +scala>:jar sbt-test/source-dependencies/canon/actual/a.jar +Added 'sbt-test/source-dependencies/canon/actual/a.jar' to classpath. + +scala>:jar sbt-test/source-dependencies/canon/actual/a.jar +The path 'sbt-test/source-dependencies/canon/actual/a.jar' cannot be loaded, it contains a classfile that already exists on the classpath: sbt-test/source-dependencies/canon/actual/a.jar(A.class) + +scala>:require sbt-test/source-dependencies/canon/actual/a.jar +:require is no longer supported, but has been replaced with :jar. Please use :jar \ No newline at end of file diff --git a/compiler/test-resources/repl/jar-multiple b/compiler/test-resources/repl/jar-multiple new file mode 100644 index 000000000000..453ccc40dbf6 --- /dev/null +++ b/compiler/test-resources/repl/jar-multiple @@ -0,0 +1,32 @@ +scala> val z = 1 +val z: Int = 1 + +scala>:jar compiler/test-resources/jars/mylibrary.jar +Added 'compiler/test-resources/jars/mylibrary.jar' to classpath. + +scala> import mylibrary.Utils + +scala> Utils.greet("Alice") +val res0: String = Hello, Alice! + +scala>:jar compiler/test-resources/jars/mylibrary2.jar +Added 'compiler/test-resources/jars/mylibrary2.jar' to classpath. + +scala> import mylibrary2.Utils2 + +scala> Utils2.greet("Alice") +val res1: String = Greetings, Alice! + +scala> Utils.greet("Alice") +val res2: String = Hello, Alice! + +scala> import mylibrary.Utils.greet + +scala> greet("Tom") +val res3: String = Hello, Tom! + +scala> Utils.greet("Alice") +val res4: String = Hello, Alice! + +scala> z +val res5: Int = 1 diff --git a/compiler/test/dotty/tools/repl/ReplTest.scala b/compiler/test/dotty/tools/repl/ReplTest.scala index 3e827a0f1e36..a0975d603ae1 100644 --- a/compiler/test/dotty/tools/repl/ReplTest.scala +++ b/compiler/test/dotty/tools/repl/ReplTest.scala @@ -98,10 +98,7 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na FileDiff.dump(checkFile.toPath.toString, actualOutput) println(s"Wrote updated script file to $checkFile") else - println("expected =========>") - println(expectedOutput.mkString(EOL)) - println("actual ===========>") - println(actualOutput.mkString(EOL)) + println(dotc.util.DiffUtil.mkColoredHorizontalLineDiff(actualOutput.mkString(EOL), expectedOutput.mkString(EOL))) fail(s"Error in script $name, expected output did not match actual") end if diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 66c0fd8a9ce7..3e537d195bdf 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -213,9 +213,11 @@ class TabcompleteTests extends ReplTest { ":exit", ":help", ":imports", + ":jar", ":kind", ":load", ":quit", + ":require", ":reset", ":settings", ":silent",