diff --git a/bench/src/main/scala/Benchmarks.scala b/bench/src/main/scala/Benchmarks.scala index 677a95aa66cf..6e0bae6e72de 100644 --- a/bench/src/main/scala/Benchmarks.scala +++ b/bench/src/main/scala/Benchmarks.scala @@ -17,6 +17,8 @@ import scala.collection.JavaConverters._ import scala.io.Source import scala.util.Using +import dotty.tools.io.AbstractFile + object Bench { val COMPILE_OPTS_FILE = "compile.txt" @@ -93,11 +95,11 @@ class CompilerOptions { class Worker extends Driver { // override to avoid printing summary information - override def doCompile(compiler: Compiler, fileNames: List[String])(implicit ctx: Context): Reporter = - if (fileNames.nonEmpty) + override def doCompile(compiler: Compiler, files: List[AbstractFile])(implicit ctx: Context): Reporter = + if (files.nonEmpty) try { val run = compiler.newRun - run.compile(fileNames) + run.compile(files) ctx.reporter } catch { diff --git a/compiler/src/dotty/tools/dotc/Bench.scala b/compiler/src/dotty/tools/dotc/Bench.scala index 3d264141eb71..d555ad14a493 100644 --- a/compiler/src/dotty/tools/dotc/Bench.scala +++ b/compiler/src/dotty/tools/dotc/Bench.scala @@ -3,6 +3,7 @@ package dotc import core.Contexts._ import reporting.Reporter +import io.AbstractFile import scala.annotation.internal.sharable @@ -19,12 +20,12 @@ object Bench extends Driver: @sharable private var times: Array[Int] = _ - override def doCompile(compiler: Compiler, fileNames: List[String])(using Context): Reporter = + override def doCompile(compiler: Compiler, files: List[AbstractFile])(using Context): Reporter = times = new Array[Int](numRuns) var reporter: Reporter = emptyReporter for i <- 0 until numRuns do val start = System.nanoTime() - reporter = super.doCompile(compiler, fileNames) + reporter = super.doCompile(compiler, files) times(i) = ((System.nanoTime - start) / 1000000).toInt println(s"time elapsed: ${times(i)}ms") if ctx.settings.Xprompt.value then diff --git a/compiler/src/dotty/tools/dotc/Driver.scala b/compiler/src/dotty/tools/dotc/Driver.scala index f24da34cc6ec..cb672da1e70b 100644 --- a/compiler/src/dotty/tools/dotc/Driver.scala +++ b/compiler/src/dotty/tools/dotc/Driver.scala @@ -9,10 +9,11 @@ import core.Contexts._ import core.{MacroClassLoader, Mode, TypeError} import core.StdNames.nme import dotty.tools.dotc.ast.Positioned -import dotty.tools.io.File +import dotty.tools.io.AbstractFile import reporting._ import core.Decorators._ import config.Feature +import util.SourceFile import scala.util.control.NonFatal import fromtasty.{TASTYCompiler, TastyFileUtil} @@ -31,41 +32,39 @@ class Driver { protected def emptyReporter: Reporter = new StoreReporter(null) - protected def doCompile(compiler: Compiler, fileNames: List[String])(using Context): Reporter = - if (fileNames.nonEmpty) + protected def doCompile(compiler: Compiler, files: List[AbstractFile])(using Context): Reporter = + if files.nonEmpty then try val run = compiler.newRun - run.compile(fileNames) - - def finish(run: Run)(using Context): Unit = - run.printSummary() - if !ctx.reporter.errorsReported && run.suspendedUnits.nonEmpty then - val suspendedUnits = run.suspendedUnits.toList - if (ctx.settings.XprintSuspension.value) - report.echo(i"compiling suspended $suspendedUnits%, %") - val run1 = compiler.newRun - for unit <- suspendedUnits do unit.suspended = false - run1.compileUnits(suspendedUnits) - finish(run1)(using MacroClassLoader.init(ctx.fresh)) - - finish(run) + run.compile(files) + finish(compiler, run) catch - case ex: FatalError => + case ex: FatalError => report.error(ex.getMessage) // signals that we should fail compilation. case ex: TypeError => - println(s"${ex.toMessage} while compiling ${fileNames.mkString(", ")}") + println(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}") throw ex case ex: Throwable => - println(s"$ex while compiling ${fileNames.mkString(", ")}") + println(s"$ex while compiling ${files.map(_.path).mkString(", ")}") throw ex ctx.reporter - end doCompile + + protected def finish(compiler: Compiler, run: Run)(using Context): Unit = + run.printSummary() + if !ctx.reporter.errorsReported && run.suspendedUnits.nonEmpty then + val suspendedUnits = run.suspendedUnits.toList + if (ctx.settings.XprintSuspension.value) + report.echo(i"compiling suspended $suspendedUnits%, %") + val run1 = compiler.newRun + for unit <- suspendedUnits do unit.suspended = false + run1.compileUnits(suspendedUnits) + finish(compiler, run1)(using MacroClassLoader.init(ctx.fresh)) protected def initCtx: Context = (new ContextBase).initialCtx protected def sourcesRequired: Boolean = true - def setup(args: Array[String], rootCtx: Context): (List[String], Context) = { + def setup(args: Array[String], rootCtx: Context): (List[AbstractFile], Context) = { val ictx = rootCtx.fresh val summary = CompilerCommand.distill(args)(using ictx) ictx.setSettings(summary.sstate) @@ -76,43 +75,37 @@ class Driver { if !ctx.settings.YdropComments.value || ctx.mode.is(Mode.ReadComments) then ictx.setProperty(ContextDoc, new ContextDocstrings) val fileNames = CompilerCommand.checkUsage(summary, sourcesRequired) - fromTastySetup(fileNames, ctx) + val files = fileNames.map(ctx.getFile) + (files, fromTastySetup(files)) } } - /** Setup extra classpath and figure out class names for tasty file inputs */ - protected def fromTastySetup(fileNames0: List[String], ctx0: Context): (List[String], Context) = - given Context = ctx0 - if (ctx0.settings.fromTasty.value) { - val fromTastyIgnoreList = ctx0.settings.YfromTastyIgnoreList.value.toSet - // Resolve classpath and class names of tasty files - val (classPaths, classNames) = fileNames0.flatMap { name => - val path = Paths.get(name) - if !Files.exists(path) then - report.error(s"File does not exist: $name") - Nil - else if name.endsWith(".jar") then - new dotty.tools.io.Jar(File(name)).toList.collect { - case e if e.getName.endsWith(".tasty") && !fromTastyIgnoreList(e.getName) => - (name, e.getName.stripSuffix(".tasty").replace("/", ".")) - } - else if name.endsWith(".tasty") then - TastyFileUtil.getClassName(path) match - case Some(res) => res :: Nil + /** Setup extra classpath of tasty and jar files */ + protected def fromTastySetup(files: List[AbstractFile])(using Context): Context = + if ctx.settings.fromTasty.value then + val newEntries: List[String] = files + .flatMap { file => + if !file.exists then + report.error(s"File does not exist: ${file.path}") + None + else file.extension match + case "jar" => Some(file.path) + case "tasty" => + TastyFileUtil.getClassPath(file) match + case Some(classpath) => Some(classpath) + case _ => + report.error(s"Could not load classname from: ${file.path}") + None case _ => - report.error(s"Could not load classname from: $name") - Nil - else - report.error(s"File extension is not `tasty` or `jar`: $name") - Nil - }.unzip - val ctx1 = ctx0.fresh - val classPaths1 = classPaths.distinct.filter(_ != "") - val fullClassPath = (classPaths1 :+ ctx1.settings.classpath.value(using ctx1)).mkString(java.io.File.pathSeparator) + report.error(s"File extension is not `tasty` or `jar`: ${file.path}") + None + } + .distinct + val ctx1 = ctx.fresh + val fullClassPath = + (newEntries :+ ctx.settings.classpath.value).mkString(java.io.File.pathSeparator) ctx1.setSetting(ctx1.settings.classpath, fullClassPath) - (classNames, ctx1) - } - else (fileNames0, ctx0) + else ctx /** Entry point to the compiler that can be conveniently used with Java reflection. * @@ -189,8 +182,8 @@ class Driver { * if compilation succeeded. */ def process(args: Array[String], rootCtx: Context): Reporter = { - val (fileNames, compileCtx) = setup(args, rootCtx) - doCompile(newCompiler(using compileCtx), fileNames)(using compileCtx) + val (files, compileCtx) = setup(args, rootCtx) + doCompile(newCompiler(using compileCtx), files)(using compileCtx) } def main(args: Array[String]): Unit = { diff --git a/compiler/src/dotty/tools/dotc/Resident.scala b/compiler/src/dotty/tools/dotc/Resident.scala index 889b31ba4e76..d186bf5ec01e 100644 --- a/compiler/src/dotty/tools/dotc/Resident.scala +++ b/compiler/src/dotty/tools/dotc/Resident.scala @@ -40,8 +40,8 @@ class Resident extends Driver { final override def process(args: Array[String], rootCtx: Context): Reporter = { @tailrec def loop(args: Array[String], prevCtx: Context): Reporter = { - var (fileNames, ctx) = setup(args, prevCtx) - inContext(ctx){doCompile(residentCompiler, fileNames)} + var (files, ctx) = setup(args, prevCtx) + inContext(ctx) { doCompile(residentCompiler, files) } var nextCtx = ctx var line = getLine() while (line == reset) { diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 5e000754436b..0edc668cd614 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -124,16 +124,15 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint /** Actions that need to be performed at the end of the current compilation run */ private var finalizeActions = mutable.ListBuffer[() => Unit]() - def compile(fileNames: List[String]): Unit = try { - val sources = fileNames.map(runContext.getSource(_)) - compileSources(sources) - } - catch { - case NonFatal(ex) => - if units != null then report.echo(i"exception occurred while compiling $units%, %") - else report.echo(s"exception occurred while compiling ${fileNames.mkString(", ")}") - throw ex - } + def compile(files: List[AbstractFile]): Unit = + try + val sources = files.map(runContext.getSource(_)) + compileSources(sources) + catch + case NonFatal(ex) => + if units != null then report.echo(i"exception occurred while compiling $units%, %") + else report.echo(s"exception occurred while compiling ${files.map(_.name).mkString(", ")}") + throw ex /** TODO: There's a fundamental design problem here: We assemble phases using `fusePhases` * when we first build the compiler. But we modify them with -Yskip, -Ystop @@ -147,6 +146,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint compileUnits() } + def compileUnits(us: List[CompilationUnit]): Unit = { units = us compileUnits() @@ -218,7 +218,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint if (!files.contains(file) && !lateFiles.contains(file)) { lateFiles += file - val unit = CompilationUnit(ctx.getSource(file.path)) + val unit = CompilationUnit(ctx.getSource(file)) val unitCtx = runContext.fresh .setCompilationUnit(unit) .withRootImports diff --git a/compiler/src/dotty/tools/dotc/config/CompilerCommand.scala b/compiler/src/dotty/tools/dotc/config/CompilerCommand.scala index 71b36709fb67..25f41c6b0955 100644 --- a/compiler/src/dotty/tools/dotc/config/CompilerCommand.scala +++ b/compiler/src/dotty/tools/dotc/config/CompilerCommand.scala @@ -33,6 +33,12 @@ object CompilerCommand { def versionMsg: String = s"Scala compiler $versionString -- $copyrightString" + def shouldStopWithInfo(using Context): Boolean = { + val settings = ctx.settings + import settings._ + Set(help, Xhelp, Yhelp, showPlugins, XshowPhases) exists (_.value) + } + /** Distill arguments into summary detailing settings, errors and files to compiler */ def distill(args: Array[String])(using Context): ArgsSummary = { /** @@ -110,11 +116,6 @@ object CompilerCommand { def xusageMessage = createUsageMsg("Possible advanced", shouldExplain = true, isAdvanced) def yusageMessage = createUsageMsg("Possible private", shouldExplain = true, isPrivate) - def shouldStopWithInfo = { - import settings._ - Set(help, Xhelp, Yhelp, showPlugins, XshowPhases) exists (_.value) - } - def phasesMessage: String = { (new Compiler()).phases.map { case List(single) => single.phaseName diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 3385ddffba75..83e08ae0cff0 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -266,25 +266,32 @@ object Contexts { base.sources.getOrElseUpdate(file, new SourceFile(file, codec)) } - /** Sourcefile with given path name, memoized */ - def getSource(path: TermName): SourceFile = base.sourceNamed.get(path) match { - case Some(source) => - source - case None => try { - val f = new PlainFile(Path(path.toString)) - val src = getSource(f) - base.sourceNamed(path) = src - src - } catch { - case ex: InvalidPathException => - report.error(s"invalid file path: ${ex.getMessage}") - NoSource - } - } + /** SourceFile with given path name, memoized */ + def getSource(path: TermName): SourceFile = getFile(path) match + case NoAbstractFile => NoSource + case file => getSource(file) - /** Sourcefile with given path, memoized */ + /** SourceFile with given path, memoized */ def getSource(path: String): SourceFile = getSource(path.toTermName) + /** AbstraFile with given path name, memoized */ + def getFile(name: TermName): AbstractFile = base.files.get(name) match + case Some(file) => + file + case None => + try + val file = new PlainFile(Path(name.toString)) + base.files(name) = file + file + catch + case ex: InvalidPathException => + report.error(s"invalid file path: ${ex.getMessage}") + NoAbstractFile + + /** AbstractFile with given path, memoized */ + def getFile(name: String): AbstractFile = getFile(name.toTermName) + + private var related: SimpleIdentityMap[Phase | SourceFile, Context] = null private def lookup(key: Phase | SourceFile): Context = @@ -841,9 +848,9 @@ object Contexts { private var _nextSymId: Int = 0 def nextSymId: Int = { _nextSymId += 1; _nextSymId } - /** Sources that were loaded */ + /** Sources and Files that were loaded */ val sources: util.HashMap[AbstractFile, SourceFile] = util.HashMap[AbstractFile, SourceFile]() - val sourceNamed: util.HashMap[TermName, SourceFile] = util.HashMap[TermName, SourceFile]() + val files: util.HashMap[TermName, AbstractFile] = util.HashMap() // Types state /** A table for hash consing unique types */ @@ -927,7 +934,7 @@ object Contexts { emptyWildcardBounds = null errorTypeMsg.clear() sources.clear() - sourceNamed.clear() + files.clear() comparers.clear() // forces re-evaluation of top and bottom classes in TypeComparer // Test that access is single threaded diff --git a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala index 7ac37a0a2fca..785d0d1e7e70 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -173,7 +173,7 @@ object SymbolLoaders { Nil) } - val unit = CompilationUnit(ctx.getSource(src.path)) + val unit = CompilationUnit(ctx.getSource(src)) enterScanned(unit)(using ctx.fresh.setCompilationUnit(unit)) /** The package objects of scala and scala.reflect should always diff --git a/compiler/src/dotty/tools/dotc/decompiler/IDEDecompilerDriver.scala b/compiler/src/dotty/tools/dotc/decompiler/IDEDecompilerDriver.scala index b738a294d1f0..630884656c10 100644 --- a/compiler/src/dotty/tools/dotc/decompiler/IDEDecompilerDriver.scala +++ b/compiler/src/dotty/tools/dotc/decompiler/IDEDecompilerDriver.scala @@ -6,6 +6,7 @@ import dotty.tools.dotc.core.Contexts._ import dotty.tools.dotc.core._ import dotty.tools.dotc.core.tasty.TastyHTMLPrinter import dotty.tools.dotc.reporting._ +import dotty.tools.io.AbstractFile import scala.quoted.runtime.impl.QuotesImpl @@ -25,13 +26,13 @@ class IDEDecompilerDriver(val settings: List[String]) extends dotc.Driver { private val decompiler = new PartialTASTYDecompiler - def run(className: String): (String, String) = { + def run(tastyFile: AbstractFile): (String, String) = { val reporter = new StoreReporter(null) with HideNonSensicalMessages val run = decompiler.newRun(using myInitCtx.fresh.setReporter(reporter)) inContext(run.runContext) { - run.compile(List(className)) + run.compile(List(tastyFile)) run.printSummary() val unit = ctx.run.units.head diff --git a/compiler/src/dotty/tools/dotc/decompiler/Main.scala b/compiler/src/dotty/tools/dotc/decompiler/Main.scala index e78dcb9d6def..e386abfa7bf8 100644 --- a/compiler/src/dotty/tools/dotc/decompiler/Main.scala +++ b/compiler/src/dotty/tools/dotc/decompiler/Main.scala @@ -4,6 +4,7 @@ import java.nio.file.Files import dotty.tools.dotc import dotty.tools.dotc.core.Contexts._ +import dotty.tools.io.AbstractFile /** Main class of the `dotc -decompiler` decompiler. * @@ -17,7 +18,7 @@ object Main extends dotc.Driver { new TASTYDecompiler } - override def setup(args0: Array[String], rootCtx: Context): (List[String], Context) = { + override def setup(args0: Array[String], rootCtx: Context): (List[AbstractFile], Context) = { var args = args0.filter(a => a != "-decompile") if (!args.contains("-from-tasty")) args = "-from-tasty" +: args if (args.contains("-d")) args = "-color:never" +: args diff --git a/compiler/src/dotty/tools/dotc/fromtasty/TASTYRun.scala b/compiler/src/dotty/tools/dotc/fromtasty/TASTYRun.scala index 09d4d03c4e5c..2d2d41ae50ed 100644 --- a/compiler/src/dotty/tools/dotc/fromtasty/TASTYRun.scala +++ b/compiler/src/dotty/tools/dotc/fromtasty/TASTYRun.scala @@ -2,11 +2,29 @@ package dotty.tools package dotc package fromtasty +import io.{JarArchive, AbstractFile, Path} import core.Contexts._ class TASTYRun(comp: Compiler, ictx: Context) extends Run(comp, ictx) { - override def compile(classNames: List[String]): Unit = { - val units = classNames.map(new TASTYCompilationUnit(_)) + override def compile(files: List[AbstractFile]): Unit = { + val units = tastyUnits(files) compileUnits(units) } + + private def tastyUnits(files: List[AbstractFile]): List[TASTYCompilationUnit] = + val fromTastyIgnoreList = ctx.settings.YfromTastyIgnoreList.value.toSet + // Resolve class names of tasty and jar files + val classNames = files.flatMap { file => + file.extension match + case "jar" => + JarArchive.open(Path(file.path), create = false).iterator() + .filter(e => e.extension == "tasty" && !fromTastyIgnoreList(e.name)) + .map(e => e.name.stripSuffix(".tasty").replace("/", ".")) + .toList + case "tasty" => TastyFileUtil.getClassName(file) + case _ => + report.error(s"File extension is not `tasty` or `jar`: ${file.path}") + Nil + } + classNames.map(new TASTYCompilationUnit(_)) } diff --git a/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala b/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala index e92ebc4a6eed..fa926371f562 100644 --- a/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala +++ b/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala @@ -1,40 +1,46 @@ package dotty.tools.dotc package fromtasty -import java.nio.file.{Files, Path, Paths} -import java.io - -import dotty.tools.dotc.core.Contexts._ -import dotty.tools.dotc.core.NameKinds -import dotty.tools.dotc.core.Names.SimpleName -import dotty.tools.dotc.core.StdNames.nme -import dotty.tools.dotc.core.tasty.{TastyUnpickler, TreePickler} +import dotty.tools.dotc.core.tasty.TastyClassName import dotty.tools.dotc.core.StdNames.nme.EMPTY_PACKAGE +import dotty.tools.io.AbstractFile object TastyFileUtil { + /** Get the class path of a tasty file + * + * If + * ```scala + * package foo + * class Foo + * ``` + * then `getClassName("./out/foo/Foo.tasty") returns `Some("./out")` + */ + def getClassPath(file: AbstractFile): Option[String] = + getClassName(file).map { className => + val classInPath = className.replace(".", java.io.File.separator) + ".tasty" + file.path.replace(classInPath, "") + } - /** Get the class path and the class name including packages + /** Get the class path of a tasty file * * If * ```scala * package foo * class Foo * ``` - * then `getClassName("./out/foo/Foo.tasty") returns `Some(("./out", "foo.Foo"))` + * then `getClassName("./out/foo/Foo.tasty") returns `Some("foo.Foo")` */ - def getClassName(path: Path): Option[(String, String)] = { - assert(path.toString.endsWith(".tasty")) - assert(Files.exists(path)) - val bytes = Files.readAllBytes(path) - val names = new core.tasty.TastyClassName(bytes).readName() + def getClassName(file: AbstractFile): Option[String] = { + assert(file.exists) + assert(file.extension == "tasty") + val bytes = file.toByteArray + val names = new TastyClassName(bytes).readName() names.map { case (packageName, className) => val fullName = packageName match { case EMPTY_PACKAGE => s"${className.lastPart}" case _ => s"$packageName.${className.lastPart}" } - val classInPath = fullName.replace(".", io.File.separator) + ".tasty" - val classpath = path.toString.replace(classInPath, "") - (classpath, fullName) + fullName } } } diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 0ac4cc0dc64b..2b64069c6314 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -26,6 +26,7 @@ import dotc.interfaces.Diagnostic.ERROR import dotc.reporting.{Reporter, TestReporter} import dotc.reporting.Diagnostic import dotc.util.DiffUtil +import io.AbstractFile import dotty.tools.vulpix.TestConfiguration.defaultOptions /** A parallel testing suite whose goal is to integrate nicely with JUnit @@ -472,7 +473,7 @@ trait ParallelTesting extends RunnerOrchestration { self => private def ntimes(n: Int)(op: Int => Reporter): Reporter = (1 to n).foldLeft(emptyReporter) ((_, i) => op(i)) - override def doCompile(comp: Compiler, files: List[String])(using Context) = + override def doCompile(comp: Compiler, files: List[AbstractFile])(using Context) = ntimes(times) { run => val start = System.nanoTime() val rep = super.doCompile(comp, files) diff --git a/doc-tool/src/dotty/tools/dottydoc/DocDriver.scala b/doc-tool/src/dotty/tools/dottydoc/DocDriver.scala index 36960250d89b..3f095826b707 100644 --- a/doc-tool/src/dotty/tools/dottydoc/DocDriver.scala +++ b/doc-tool/src/dotty/tools/dottydoc/DocDriver.scala @@ -12,6 +12,7 @@ import dotc.config._ import dotc.core.Comments.ContextDoc import dotc.report import staticsite.Site +import io.AbstractFile /** `DocDriver` implements the main entry point to the Dotty documentation * tool. It's methods are used by the external scala and java APIs. @@ -20,7 +21,7 @@ class DocDriver extends Driver { import java.util.{ Map => JMap } import model.JavaConverters._ - override def setup(args: Array[String], rootCtx: Context): (List[String], Context) = { + override def setup(args: Array[String], rootCtx: Context): (List[AbstractFile], Context) = { val ctx = rootCtx.fresh val summary = CompilerCommand.distill(args)(using ctx) @@ -28,8 +29,11 @@ class DocDriver extends Driver { ctx.setSetting(ctx.settings.YcookComments, true) ctx.setProperty(ContextDoc, new ContextDottydoc) - val fileNames = CompilerCommand.checkUsage(summary, sourcesRequired)(using ctx) - fromTastySetup(fileNames, ctx) + inContext(ctx) { + val fileNames = CompilerCommand.checkUsage(summary, sourcesRequired) + val files = fileNames.map(ctx.getFile) + (files, fromTastySetup(files)) + } } override def newCompiler(using Context): Compiler = new DocCompiler diff --git a/doc-tool/test/dotty/tools/dottydoc/ConstructorTest.scala b/doc-tool/test/dotty/tools/dottydoc/ConstructorTest.scala index ce90fd020212..e3ff3652b8ef 100644 --- a/doc-tool/test/dotty/tools/dottydoc/ConstructorTest.scala +++ b/doc-tool/test/dotty/tools/dottydoc/ConstructorTest.scala @@ -22,9 +22,9 @@ abstract class ConstructorsBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Class" + val tastyFile = "scala/Class.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(cls: Class), _, _, _, _) => cls.constructors.headOption match { @@ -45,9 +45,9 @@ abstract class ConstructorsBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Class" + val tastyFile = "scala/Class.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(cls: Class), _, _, _, _) => cls.constructors match { @@ -71,9 +71,9 @@ abstract class ConstructorsBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Class" + val tastyFile = "scala/Class.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(cls: Class), _, _, _, _) => cls.constructors match { @@ -104,9 +104,9 @@ abstract class ConstructorsBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Class" + val tastyFile = "scala/Class.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(cls: Class), _, _, _, _) => cls.constructors match { @@ -143,9 +143,9 @@ abstract class ConstructorsBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Class" + val tastyFile = "scala/Class.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(cls: CaseClass, obj: Object), _, _, _, _) => cls.constructors match { @@ -177,9 +177,9 @@ abstract class ConstructorsBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Trait" + val tastyFile = "scala/Trait.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(trt: Trait), _, _, _, _) => trt.traitParams match { diff --git a/doc-tool/test/dotty/tools/dottydoc/DottyDocTest.scala b/doc-tool/test/dotty/tools/dottydoc/DottyDocTest.scala index 62d1be892ddf..26ae91070581 100644 --- a/doc-tool/test/dotty/tools/dottydoc/DottyDocTest.scala +++ b/doc-tool/test/dotty/tools/dottydoc/DottyDocTest.scala @@ -16,7 +16,7 @@ import dotty.tools.dottydoc.util.syntax._ import dotty.tools.io.AbstractFile import dotc.reporting.{ StoreReporter, MessageRendering } import dotc.interfaces.Diagnostic.ERROR -import io.Directory +import io.{Directory, PlainFile, Path, PlainDirectory} import org.junit.Assert.fail import java.io.{ BufferedWriter, OutputStreamWriter } @@ -95,7 +95,8 @@ trait DottyDocTest extends MessageRendering { def checkFiles(sources: List[String])(assertion: (Context, Map[String, Package]) => Unit): Unit = { val c = compilerWithChecker(assertion) val run = c.newRun - run.compile(sources) + val files = sources.map(path => new PlainFile(Path(path))) + run.compile(files) } def checkFromSource(sourceFiles: List[SourceFile])(assertion: (Context, Map[String, Package]) => Unit): Unit = { @@ -104,7 +105,7 @@ trait DottyDocTest extends MessageRendering { run.compileSources(sourceFiles) } - def checkFromTasty(classNames: List[String], sources: List[SourceFile])(assertion: (Context, Map[String, Package]) => Unit): Unit = { + def checkFromTasty(tastyFiles: List[String], sources: List[SourceFile])(assertion: (Context, Map[String, Package]) => Unit): Unit = { Directory.inTempDirectory { tmp => val ctx = "shadow ctx" val out = tmp./(Directory("out")) @@ -125,23 +126,26 @@ trait DottyDocTest extends MessageRendering { } val fromTastyCompiler = compilerWithChecker(assertion) val fromTastyRun = fromTastyCompiler.newRun(using fromTastyCtx) - fromTastyRun.compile(classNames) + val outDir = new PlainDirectory(out) + val files = tastyFiles.map(outDir.fileNamed) + fromTastyRun.compile(files) + fromTastyCtx.reporter.allErrors.foreach(println) assert(!fromTastyCtx.reporter.hasErrors) } } - def check(classNames: List[String], sources: List[SourceFile])(assertion: (Context, Map[String, Package]) => Unit): Unit + def check(tastyFiles: List[String], sources: List[SourceFile])(assertion: (Context, Map[String, Package]) => Unit): Unit } trait CheckFromSource extends DottyDocTest { - override def check(classNames: List[String], sources: List[SourceFile])(assertion: (Context, Map[String, Package]) => Unit): Unit = { + override def check(tastyFiles: List[String], sources: List[SourceFile])(assertion: (Context, Map[String, Package]) => Unit): Unit = { checkFromSource(sources)(assertion) } } trait CheckFromTasty extends DottyDocTest { - override def check(classNames: List[String], sources: List[SourceFile])(assertion: (Context, Map[String, Package]) => Unit): Unit = { - checkFromTasty(classNames, sources)(assertion) + override def check(tastyFiles: List[String], sources: List[SourceFile])(assertion: (Context, Map[String, Package]) => Unit): Unit = { + checkFromTasty(tastyFiles, sources)(assertion) } } diff --git a/doc-tool/test/dotty/tools/dottydoc/PackageStructure.scala b/doc-tool/test/dotty/tools/dottydoc/PackageStructure.scala index 5d235a0a6821..152ef13ca29e 100644 --- a/doc-tool/test/dotty/tools/dottydoc/PackageStructure.scala +++ b/doc-tool/test/dotty/tools/dottydoc/PackageStructure.scala @@ -21,9 +21,9 @@ abstract class PackageStructureBase extends DottyDocTest { """.stripMargin ) - val className = "scala.A" + val tastyFile = "scala/A.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(trt: Trait), _, _, _, _) => assert(trt.annotations.isEmpty) @@ -48,9 +48,9 @@ abstract class PackageStructureBase extends DottyDocTest { """.stripMargin ) - val classNames = "scala.A" :: "scala.B" :: Nil + val tastyFiles = "scala/A.tasty" :: "scala/B.tasty" :: Nil - check(classNames, source1 :: source2 :: Nil) { (ctx, packages) => + check(tastyFiles, source1 :: source2 :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(tA, tB), _, _, _, _) => assert( @@ -80,9 +80,9 @@ abstract class PackageStructureBase extends DottyDocTest { |trait B """.stripMargin) - val classNames = "scala.collection.A" :: "scala.collection.B" :: Nil + val tastyFiles = "scala/collection/A.tasty" :: "scala/collection/B.tasty" :: Nil - check(classNames, source1 :: source2 :: Nil) { (ctx, packages) => + check(tastyFiles, source1 :: source2 :: Nil) { (ctx, packages) => packages("scala.collection") match { case PackageImpl(_, _, "scala.collection", List(tA, tB), _, _, _, _) => assert( diff --git a/doc-tool/test/dotty/tools/dottydoc/SimpleComments.scala b/doc-tool/test/dotty/tools/dottydoc/SimpleComments.scala index cbfd57fb568e..56c35b0738a1 100644 --- a/doc-tool/test/dotty/tools/dottydoc/SimpleComments.scala +++ b/doc-tool/test/dotty/tools/dottydoc/SimpleComments.scala @@ -46,9 +46,9 @@ abstract class SimpleCommentsBase extends DottyDocTest { ) - val className = "scala.HelloWorld" + val tastyFile = "scala/HelloWorld.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => val traitCmt = packages("scala") .children.find(_.path.mkString(".") == "scala.HelloWorld") diff --git a/doc-tool/test/dotty/tools/dottydoc/UsecaseTest.scala b/doc-tool/test/dotty/tools/dottydoc/UsecaseTest.scala index da6814aba9a5..681c0773e6f0 100644 --- a/doc-tool/test/dotty/tools/dottydoc/UsecaseTest.scala +++ b/doc-tool/test/dotty/tools/dottydoc/UsecaseTest.scala @@ -29,9 +29,9 @@ abstract class UsecaseBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Test" + val tastyFile = "scala/Test.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(trt: Trait), _, _, _, _) => val List(foo: Def) = trt.members @@ -74,9 +74,9 @@ abstract class UsecaseBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Test" + val tastyFile = "scala/Test.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(trt: Trait), _, _, _, _) => val List(foo: Def) = trt.members @@ -120,9 +120,9 @@ abstract class UsecaseBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Test" + val tastyFile = "scala/Test.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(trt: Trait), _, _, _, _) => val List(foo: Def) = trt.members @@ -169,9 +169,9 @@ abstract class UsecaseBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Iterable" + val tastyFile = "scala/Iterable.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(trt: Trait), _, _, _, _) => val List(map: Def) = trt.members @@ -213,9 +213,9 @@ abstract class UsecaseBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Iterable" + val tastyFile = "scala/Iterable.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(trt: Trait), _, _, _, _) => val List(map: Def) = trt.members @@ -245,9 +245,9 @@ abstract class UsecaseBase extends DottyDocTest { """.stripMargin ) - val className = "scala.Test" + val tastyFile = "scala/Test.tasty" - check(className :: Nil, source :: Nil) { (ctx, packages) => + check(tastyFile :: Nil, source :: Nil) { (ctx, packages) => packages("scala") match { case PackageImpl(_, _, _, List(trt: Trait), _, _, _, _) => val List(foo0: Def, foo1: Def) = trt.members diff --git a/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerService.scala b/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerService.scala index cf2faf679ac8..708eb9fb4b86 100644 --- a/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerService.scala +++ b/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerService.scala @@ -7,6 +7,7 @@ import java.nio.file._ import java.util.concurrent.CompletableFuture import dotty.tools.tasty.UnpickleException +import dotty.tools.io.{PlainFile, Path} import dotc.fromtasty.TastyFileUtil @@ -22,11 +23,13 @@ trait TastyDecompilerService { computeAsync(synchronize = false, fun = { cancelChecker => val uri = new URI(params.textDocument.getUri) try { - TastyFileUtil.getClassName(Paths.get(uri)) match { - case Some((classPath, className)) => + val jpath = Paths.get(uri) + val tastyFile = new PlainFile(Path(jpath)) + TastyFileUtil.getClassPath(tastyFile) match { + case Some(classPath) => val driver = thisServer.decompilerDriverFor(uri, classPath) - val (tree, source) = driver.run(className) + val (tree, source) = driver.run(tastyFile) TastyDecompileResult(tree, source) case _ => diff --git a/project/Build.scala b/project/Build.scala index 8aee10f4fc20..51f12999de02 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -337,7 +337,8 @@ object Build { scalaLibrary, dottyLibrary, dottyCompiler, - allJars + allJars, + appConfiguration.value ) }, // sbt-dotty defines `scalaInstance in doc` so we need to override it manually @@ -489,7 +490,7 @@ object Build { // get libraries onboard libraryDependencies ++= Seq( "org.scala-lang.modules" % "scala-asm" % "7.3.1-scala-1", // used by the backend - Dependencies.`compiler-interface`, + Dependencies.oldCompilerInterface, // we stick to the old version to avoid deprecation warnings "org.jline" % "jline-reader" % "3.15.0", // used by the REPL "org.jline" % "jline-terminal" % "3.15.0", "org.jline" % "jline-terminal-jna" % "3.15.0" // needed for Windows @@ -952,12 +953,14 @@ object Build { sources in Test := Seq(), scalaSource in Compile := baseDirectory.value, javaSource in Compile := baseDirectory.value, + resourceDirectory in Compile := baseDirectory.value.getParentFile / "resources", // Referring to the other project using a string avoids an infinite loop // when sbt reads the settings. test in Test := (test in (LocalProject("scala3-sbt-bridge-tests"), Test)).value, - libraryDependencies += Dependencies.`compiler-interface` % Provided + // The `newCompilerInterface` is backward compatible with the `oldCompilerInterface` + libraryDependencies += Dependencies.newCompilerInterface % Provided ) // We use a separate project for the bridge tests since they can only be run @@ -972,8 +975,7 @@ object Build { // Tests disabled until zinc-api-info cross-compiles with 2.13, // alternatively we could just copy in sources the part of zinc-api-info we need. - sources in Test := Seq(), - // libraryDependencies += (Dependencies.`zinc-api-info` % Test).withDottyCompat(scalaVersion.value) + sources in Test := Seq() ) lazy val `scala3-language-server` = project.in(file("language-server")). @@ -1238,7 +1240,7 @@ object Build { // Keep in sync with inject-sbt-dotty.sbt libraryDependencies ++= Seq( Dependencies.`jackson-databind`, - Dependencies.`compiler-interface` + Dependencies.newCompilerInterface ), unmanagedSourceDirectories in Compile += baseDirectory.value / "../language-server/src/dotty/tools/languageserver/config", diff --git a/project/Dependencies.scala b/project/Dependencies.scala index de8ab2f96e32..b15d5abaa355 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -10,7 +10,6 @@ object Dependencies { val `jackson-dataformat-yaml` = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % jacksonVersion - private val zincVersion = "1.2.5" - val `compiler-interface` = "org.scala-sbt" % "compiler-interface" % zincVersion - val `zinc-api-info` = "org.scala-sbt" %% "zinc-apiinfo" % zincVersion + val newCompilerInterface = "org.scala-sbt" % "compiler-interface" % "1.4.3" + val oldCompilerInterface = "org.scala-sbt" % "compiler-interface" % "1.3.5" } diff --git a/project/build.sbt b/project/build.sbt index 3d61db3c3623..7b9213644935 100644 --- a/project/build.sbt +++ b/project/build.sbt @@ -10,7 +10,7 @@ unmanagedSourceDirectories in Compile += baseDirectory.value / "../sbt-dotty/src // Keep in sync with `sbt-dotty` config in Build.scala libraryDependencies ++= Seq( Dependencies.`jackson-databind`, - Dependencies.`compiler-interface` + Dependencies.newCompilerInterface ) unmanagedSourceDirectories in Compile += baseDirectory.value / "../language-server/src/dotty/tools/languageserver/config" diff --git a/sbt-bridge/resources/META-INF/services/xsbti.compile.CompilerInterface2 b/sbt-bridge/resources/META-INF/services/xsbti.compile.CompilerInterface2 new file mode 100644 index 000000000000..e5a708dae604 --- /dev/null +++ b/sbt-bridge/resources/META-INF/services/xsbti.compile.CompilerInterface2 @@ -0,0 +1 @@ +dotty.tools.xsbt.CompilerBridge \ No newline at end of file diff --git a/sbt-bridge/src/dotty/tools/xsbt/CompilerBridge.java b/sbt-bridge/src/dotty/tools/xsbt/CompilerBridge.java new file mode 100644 index 000000000000..92b8062700c4 --- /dev/null +++ b/sbt-bridge/src/dotty/tools/xsbt/CompilerBridge.java @@ -0,0 +1,24 @@ +/* + * Zinc - The incremental compiler for Scala. + * Copyright Lightbend, Inc. and Mark Harrah + */ + +package dotty.tools.xsbt; + +import xsbti.AnalysisCallback; +import xsbti.Logger; +import xsbti.Reporter; +import xsbti.VirtualFile; +import xsbti.compile.CompileProgress; +import xsbti.compile.CompilerInterface2; +import xsbti.compile.DependencyChanges; +import xsbti.compile.Output; + +public final class CompilerBridge implements CompilerInterface2 { + @Override + public void run(VirtualFile[] sources, DependencyChanges changes, String[] options, Output output, + AnalysisCallback callback, Reporter delegate, CompileProgress progress, Logger log) { + CompilerBridgeDriver driver = new CompilerBridgeDriver(options, output); + driver.run(sources, callback, log, delegate); + } +} diff --git a/sbt-bridge/src/dotty/tools/xsbt/CompilerBridgeDriver.java b/sbt-bridge/src/dotty/tools/xsbt/CompilerBridgeDriver.java new file mode 100644 index 000000000000..f73256a7ddde --- /dev/null +++ b/sbt-bridge/src/dotty/tools/xsbt/CompilerBridgeDriver.java @@ -0,0 +1,134 @@ +/* + * Zinc - The incremental compiler for Scala. + * Copyright Lightbend, Inc. and Mark Harrah + */ + +package dotty.tools.xsbt; + +import dotty.tools.dotc.Compiler; +import dotty.tools.dotc.Driver; +import dotty.tools.dotc.config.CompilerCommand; +import dotty.tools.dotc.config.Properties; +import dotty.tools.dotc.core.Contexts; +import dotty.tools.io.AbstractFile; +import scala.collection.mutable.ListBuffer; +import scala.io.Codec; +import xsbti.Problem; +import xsbti.*; +import xsbti.compile.Output; + +import java.io.IOException; +import java.util.Comparator; +import java.util.Arrays; + +public class CompilerBridgeDriver extends Driver { + private final String[] scalacOptions; + private final String[] args; + + public CompilerBridgeDriver(String[] scalacOptions, Output output) { + super(); + this.scalacOptions = scalacOptions; + + if (!output.getSingleOutput().isPresent()) + throw new IllegalArgumentException("output should be a SingleOutput, was a " + output.getClass().getName()); + + this.args = new String[scalacOptions.length + 2]; + System.arraycopy(scalacOptions, 0, args, 0, scalacOptions.length); + args[scalacOptions.length] = "-d"; + args[scalacOptions.length + 1] = output.getSingleOutput().get().getAbsolutePath(); + } + + private static final String StopInfoError = + "Compiler option supplied that disabled Zinc compilation."; + + /** + * `sourcesRequired` is set to false because the context is set up with no sources + * The sources are passed programmatically to the compiler in the form of AbstractFiles + */ + @Override + public boolean sourcesRequired() { + return false; + } + + synchronized public void run(VirtualFile[] sources, AnalysisCallback callback, Logger log, Reporter delegate) { + DelegatingReporter reporter = new DelegatingReporter(delegate); + try { + log.debug(this::infoOnCachedCompiler); + + Contexts.Context initialCtx = initCtx() + .fresh() + .setReporter(reporter) + .setSbtCallback(callback); + + Contexts.Context context = setup(args, initialCtx)._2; + + if (CompilerCommand.shouldStopWithInfo(context)) { + throw new InterfaceCompileFailed(args, new Problem[0], StopInfoError); + } + + if (!delegate.hasErrors()) { + log.debug(this::prettyPrintCompilationArguments); + Compiler compiler = newCompiler(context); + + VirtualFile[] sortedSources = new VirtualFile[sources.length]; + System.arraycopy(sources, 0, sortedSources, 0, sources.length); + Arrays.sort( + sortedSources, + new Comparator() { + @Override + public int compare(VirtualFile x0, VirtualFile x1) { + return x0.id().compareTo(x1.id()); + } + } + ); + + ListBuffer sourcesBuffer = new ListBuffer<>(); + for (VirtualFile file: sortedSources) + sourcesBuffer.append(asDottyFile(file)); + doCompile(compiler, sourcesBuffer.toList(), context); + + for (xsbti.Problem problem: delegate.problems()) { + callback.problem(problem.category(), problem.position(), problem.message(), problem.severity(), + true); + } + } + + delegate.printSummary(); + + if (delegate.hasErrors()) { + log.debug(() -> "Compilation failed"); + throw new InterfaceCompileFailed(args, delegate.problems(), "Compilation failed"); + } + } finally { + reporter.dropDelegate(); + } + } + + private static AbstractFile asDottyFile(VirtualFile virtualFile) { + if (virtualFile instanceof PathBasedFile) + return new ZincPlainFile((PathBasedFile) virtualFile); + + try { + return new ZincVirtualFile(virtualFile); + } catch (IOException e) { + throw new IllegalArgumentException("invalid file " + virtualFile.name(), e); + } + } + + private String infoOnCachedCompiler() { + String compilerId = Integer.toHexString(hashCode()); + String compilerVersion = Properties.versionString(); + return String.format("[zinc] Running cached compiler %s for Scala Compiler %s", compilerId, compilerVersion); + } + + private String prettyPrintCompilationArguments() { + StringBuilder builder = new StringBuilder(); + builder.append("[zinc] The Scala compiler is invoked with:"); + for (String opt: scalacOptions) { + builder.append("\n\t"); + builder.append(opt); + } + return builder.toString(); + } + +} diff --git a/sbt-bridge/src/dotty/tools/xsbt/DelegatingReporter.java b/sbt-bridge/src/dotty/tools/xsbt/DelegatingReporter.java new file mode 100644 index 000000000000..da436aa75b5d --- /dev/null +++ b/sbt-bridge/src/dotty/tools/xsbt/DelegatingReporter.java @@ -0,0 +1,65 @@ +/* sbt -- Simple Build Tool + * Copyright 2008, 2009 Mark Harrah + */ +package dotty.tools.xsbt; + +import dotty.tools.dotc.core.Contexts.Context; +import dotty.tools.dotc.reporting.AbstractReporter; +import dotty.tools.dotc.reporting.Diagnostic; +import dotty.tools.dotc.reporting.Message; +import dotty.tools.dotc.util.SourcePosition; +import xsbti.Position; +import xsbti.Severity; + +final public class DelegatingReporter extends AbstractReporter { + private xsbti.Reporter delegate; + + public DelegatingReporter(xsbti.Reporter delegate) { + super(); + this.delegate = delegate; + } + + public void dropDelegate() { + delegate = null; + } + + @Override + public void printSummary(Context ctx) { + delegate.printSummary(); + } + + public void doReport(Diagnostic dia, Context ctx) { + Severity severity = severityOf(dia.level()); + Position position = positionOf(dia.pos()); + + Message message = dia.msg(); + StringBuilder rendered = new StringBuilder(); + rendered.append(messageAndPos(message, dia.pos(), diagnosticLevel(dia), ctx)); + boolean shouldExplain = Diagnostic.shouldExplain(dia, ctx); + if (shouldExplain && !message.explanation().isEmpty()) { + rendered.append(explanation(message, ctx)); + } + + delegate.log(new Problem(position, message.msg(), severity, rendered.toString())); + } + + private static Severity severityOf(int level) { + Severity severity; + switch (level) { + case dotty.tools.dotc.interfaces.Diagnostic.ERROR: severity = Severity.Error; break; + case dotty.tools.dotc.interfaces.Diagnostic.WARNING: severity = Severity.Warn; break; + case dotty.tools.dotc.interfaces.Diagnostic.INFO: severity = Severity.Info; break; + default: + throw new IllegalArgumentException(String.format("Bad diagnostic level: %s", level)); + } + return severity; + } + + private static Position positionOf(SourcePosition pos) { + if (pos.exists()){ + return new PositionBridge(pos, pos.source()); + } else { + return PositionBridge.noPosition; + } + } +} diff --git a/sbt-bridge/src/xsbt/InterfaceCompileFailed.java b/sbt-bridge/src/dotty/tools/xsbt/InterfaceCompileFailed.java similarity index 79% rename from sbt-bridge/src/xsbt/InterfaceCompileFailed.java rename to sbt-bridge/src/dotty/tools/xsbt/InterfaceCompileFailed.java index 1bc8056b053d..80a2f7e33e07 100644 --- a/sbt-bridge/src/xsbt/InterfaceCompileFailed.java +++ b/sbt-bridge/src/dotty/tools/xsbt/InterfaceCompileFailed.java @@ -1,18 +1,20 @@ /* sbt -- Simple Build Tool * Copyright 2008, 2009 Mark Harrah */ -package xsbt; +package dotty.tools.xsbt; import xsbti.Problem; public class InterfaceCompileFailed extends xsbti.CompileFailed { private final String[] _arguments; private final Problem[] _problems; + private final String _toString; - public InterfaceCompileFailed(String[] arguments, Problem[] problems) { + public InterfaceCompileFailed(String[] arguments, Problem[] problems, String toString) { super(); this._arguments = arguments; this._problems = problems; + this._toString = toString; } public String[] arguments() { @@ -25,6 +27,6 @@ public Problem[] problems() { @Override public String toString() { - return "Compilation failed"; + return _toString; } } diff --git a/sbt-bridge/src/dotty/tools/xsbt/PositionBridge.java b/sbt-bridge/src/dotty/tools/xsbt/PositionBridge.java new file mode 100644 index 000000000000..2f20609578db --- /dev/null +++ b/sbt-bridge/src/dotty/tools/xsbt/PositionBridge.java @@ -0,0 +1,119 @@ +/* + * Zinc - The incremental compiler for Scala. + * Copyright Lightbend, Inc. and Mark Harrah + */ + +package dotty.tools.xsbt; + +import dotty.tools.dotc.util.SourceFile; +import dotty.tools.dotc.util.SourcePosition; +import dotty.tools.io.AbstractFile; +import xsbti.Position; + +import java.io.File; +import java.util.Optional; + +public class PositionBridge implements Position { + private final SourcePosition pos; + private final SourceFile src; + + public static final Position noPosition = new Position() { + public Optional sourceFile() { + return Optional.empty(); + } + public Optional sourcePath() { + return Optional.empty(); + } + public Optional line() { + return Optional.empty(); + } + public String lineContent() { + return ""; + } + public Optional offset() { + return Optional.empty(); + } + public Optional pointer() { + return Optional.empty(); + } + public Optional pointerSpace() { + return Optional.empty(); + } + }; + + public PositionBridge(SourcePosition pos, SourceFile src) { + this.pos = pos; + this.src = src; + } + + @Override + public Optional line() { + if (src.content().length == 0) + return Optional.empty(); + + int line = pos.line(); + if (line == -1) return Optional.empty(); + else return Optional.of(line + 1); + } + + @Override + public String lineContent() { + if (src.content().length == 0) + return ""; + + String lineContent = pos.lineContent(); + if (lineContent.endsWith("\r\n")) { + return lineContent.substring(0, lineContent.length() - 2); + } else if (lineContent.endsWith("\n") || lineContent.endsWith("\u000c")) { + return lineContent.substring(0, lineContent.length() - 1); + } else { + return lineContent; + } + } + + @Override + public Optional offset() { + return Optional.of(pos.point()); + } + + @Override + public Optional sourcePath() { + if (!src.exists()) + return Optional.empty(); + + AbstractFile sourceFile = pos.source().file(); + if (sourceFile instanceof ZincPlainFile) { + return Optional.of(((ZincPlainFile) sourceFile).underlying().id()); + } else if (sourceFile instanceof ZincVirtualFile) { + return Optional.of(((ZincVirtualFile) sourceFile).underlying().id()); + } else { + return Optional.of(sourceFile.path()); + } + } + + @Override + public Optional sourceFile() { + if (!src.exists()) return Optional.empty(); + return Optional.ofNullable(src.file().file()); + } + + @Override + public Optional pointer() { + if (src.content().length == 0) return Optional.empty(); + return Optional.of(pos.point() - src.startOfLine(pos.point())); + } + + @Override + public Optional pointerSpace() { + if (!pointer().isPresent()) + return Optional.empty(); + + // Don't crash if pointer is out-of-bounds (happens with some macros) + int fixedPointer = Math.min(pointer().get(), lineContent().length()); + String lineContent = this.lineContent(); + StringBuilder result = new StringBuilder(); + for (int i = 0; i < fixedPointer; i++) + result.append(lineContent.charAt(i) == '\t' ? '\t' : ' '); + return Optional.of(result.toString()); + } +} diff --git a/sbt-bridge/src/xsbt/Problem.java b/sbt-bridge/src/dotty/tools/xsbt/Problem.java similarity index 97% rename from sbt-bridge/src/xsbt/Problem.java rename to sbt-bridge/src/dotty/tools/xsbt/Problem.java index 7594476a29cf..2f1a7acec00b 100644 --- a/sbt-bridge/src/xsbt/Problem.java +++ b/sbt-bridge/src/dotty/tools/xsbt/Problem.java @@ -1,4 +1,4 @@ -package xsbt; +package dotty.tools.xsbt; import java.util.Optional; import xsbti.Position; diff --git a/sbt-bridge/src/dotty/tools/xsbt/ZincPlainFile.java b/sbt-bridge/src/dotty/tools/xsbt/ZincPlainFile.java new file mode 100644 index 000000000000..68b3494cb84b --- /dev/null +++ b/sbt-bridge/src/dotty/tools/xsbt/ZincPlainFile.java @@ -0,0 +1,21 @@ +/* + * Zinc - The incremental compiler for Scala. + * Copyright Lightbend, Inc. and Mark Harrah + */ + +package dotty.tools.xsbt; + +import xsbti.PathBasedFile; + +public class ZincPlainFile extends dotty.tools.io.PlainFile { + private final PathBasedFile _underlying; + + public ZincPlainFile(PathBasedFile underlying) { + super(new dotty.tools.io.Path(underlying.toPath())); + this._underlying = underlying; + } + + public PathBasedFile underlying() { + return _underlying; + } +} \ No newline at end of file diff --git a/sbt-bridge/src/dotty/tools/xsbt/ZincVirtualFile.java b/sbt-bridge/src/dotty/tools/xsbt/ZincVirtualFile.java new file mode 100644 index 000000000000..a79686270f34 --- /dev/null +++ b/sbt-bridge/src/dotty/tools/xsbt/ZincVirtualFile.java @@ -0,0 +1,40 @@ +/* + * Zinc - The incremental compiler for Scala. + * Copyright Lightbend, Inc. and Mark Harrah + */ + +package dotty.tools.xsbt; + +import dotty.tools.io.Streamable; +import xsbti.VirtualFile; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ZincVirtualFile extends dotty.tools.io.VirtualFile { + private final VirtualFile _underlying; + + public ZincVirtualFile(VirtualFile underlying) throws IOException { + super(underlying.name(), underlying.id()); + this._underlying = underlying; + + // fill in the content + OutputStream output = output(); + try { + Streamable.Bytes bytes = new Streamable.Bytes() { + @Override + public InputStream inputStream() { + return underlying.input(); + } + }; + output.write(bytes.toByteArray()); + } finally { + output.close(); + } + } + + public VirtualFile underlying() { + return _underlying; + } +} diff --git a/sbt-bridge/src/xsbt/CachedCompilerImpl.java b/sbt-bridge/src/xsbt/CachedCompilerImpl.java index caaecf8ca732..8ade3bf4b994 100644 --- a/sbt-bridge/src/xsbt/CachedCompilerImpl.java +++ b/sbt-bridge/src/xsbt/CachedCompilerImpl.java @@ -3,10 +3,7 @@ */ package xsbt; -import xsbti.AnalysisCallback; -import xsbti.Logger; -import xsbti.Reporter; -import xsbti.Severity; +import xsbti.*; import xsbti.compile.*; import java.io.File; @@ -14,25 +11,22 @@ import dotty.tools.dotc.core.Contexts.Context; import dotty.tools.dotc.core.Contexts.ContextBase; import dotty.tools.dotc.Main; -import dotty.tools.dotc.interfaces.*; - -import java.net.URLClassLoader; +import dotty.tools.xsbt.InterfaceCompileFailed; +import dotty.tools.xsbt.DelegatingReporter; public class CachedCompilerImpl implements CachedCompiler { private final String[] args; - private final Output output; private final String[] outputArgs; public CachedCompilerImpl(String[] args, Output output) { super(); this.args = args; - this.output = output; if (!(output instanceof SingleOutput)) throw new IllegalArgumentException("output should be a SingleOutput, was a " + output.getClass().getName()); this.outputArgs = - new String[] { "-d", ((SingleOutput) output).getOutputDirectory().getAbsolutePath().toString() }; + new String[] { "-d", ((SingleOutput) output).getOutputDirectory().getAbsolutePath() }; } public String[] commandArguments(File[] sources) { @@ -54,7 +48,8 @@ public String[] commandArguments(File[] sources) { return result; } - synchronized public void run(File[] sources, DependencyChanges changes, AnalysisCallback callback, Logger log, Reporter delegate, CompileProgress progress) { + synchronized public void run(File[] sources, DependencyChanges changes, AnalysisCallback callback, Logger log, + Reporter delegate, CompileProgress progress) { log.debug(() -> { String msg = "Calling Dotty compiler with arguments (CompilerInterface):"; for (String arg : args) @@ -68,7 +63,7 @@ synchronized public void run(File[] sources, DependencyChanges changes, Analysis dotty.tools.dotc.reporting.Reporter reporter = Main.process(commandArguments(sources), ctx); if (reporter.hasErrors()) { - throw new InterfaceCompileFailed(args, new Problem[0]); + throw new InterfaceCompileFailed(args, new Problem[0], "Compilation failed"); } } } diff --git a/sbt-bridge/src/xsbt/CompilerInterface.java b/sbt-bridge/src/xsbt/CompilerInterface.java index 10b90adbb0e3..1af338fe93de 100644 --- a/sbt-bridge/src/xsbt/CompilerInterface.java +++ b/sbt-bridge/src/xsbt/CompilerInterface.java @@ -18,6 +18,10 @@ import java.lang.reflect.InvocationTargetException; import java.net.URLClassLoader; +/** + * The new compiler interface is [[dotty.tools.xsbt.CompilerBridge]] that extends the new `xsbti.CompilerInterface2`. + * This interface is kept for compatibility with Mill and the sbt 1.3.x series. + */ public final class CompilerInterface { public CachedCompiler newCompiler(String[] options, Output output, Logger initialLog, Reporter initialDelegate) { // The classloader that sbt uses to load the compiler bridge is broken diff --git a/sbt-bridge/src/xsbt/DelegatingReporter.java b/sbt-bridge/src/xsbt/DelegatingReporter.java deleted file mode 100644 index b37ccddbdc71..000000000000 --- a/sbt-bridge/src/xsbt/DelegatingReporter.java +++ /dev/null @@ -1,151 +0,0 @@ -/* sbt -- Simple Build Tool - * Copyright 2008, 2009 Mark Harrah - */ -package xsbt; - -import java.util.Optional; - -import xsbti.Position; -import xsbti.Severity; - -import dotty.tools.*; -import dotty.tools.dotc.*; -import dotty.tools.dotc.interfaces.Diagnostic; -import dotty.tools.dotc.util.SourceFile; -import dotty.tools.dotc.util.SourcePosition; -import dotty.tools.dotc.reporting.*; -import dotty.tools.dotc.reporting.Message; -import dotty.tools.dotc.core.Contexts.*; - -import static dotty.tools.dotc.reporting.Diagnostic.*; - -final public class DelegatingReporter extends AbstractReporter { - private final xsbti.Reporter delegate; - - private static final Position noPosition = new Position() { - public Optional sourceFile() { - return Optional.empty(); - } - public Optional sourcePath() { - return Optional.empty(); - } - public Optional line() { - return Optional.empty(); - } - public String lineContent() { - return ""; - } - public Optional offset() { - return Optional.empty(); - } - public Optional pointer() { - return Optional.empty(); - } - public Optional pointerSpace() { - return Optional.empty(); - } - }; - - public DelegatingReporter(xsbti.Reporter delegate) { - super(); - this.delegate = delegate; - } - - @Override - public void printSummary(Context ctx) { - delegate.printSummary(); - } - - public void doReport(dotty.tools.dotc.reporting.Diagnostic dia, Context ctx) { - Severity severity; - switch (dia.level()) { - case Diagnostic.ERROR: - severity = Severity.Error; - break; - case Diagnostic.WARNING: - severity = Severity.Warn; - break; - case Diagnostic.INFO: - severity = Severity.Info; - break; - default: - throw new IllegalArgumentException("Bad diagnostic level: " + dia.level()); - } - - Position position; - if (dia.pos().exists()) { - SourcePosition pos = dia.pos(); - SourceFile src = pos.source(); - position = new Position() { - public Optional sourcePath() { - if (!src.exists()) - return Optional.empty(); - - return Optional.ofNullable(src.file().path()); - } - public Optional sourceFile() { - if (!src.exists()) - return Optional.empty(); - - return Optional.ofNullable(src.file().file()); - } - public Optional line() { - if (src.content().length == 0) - return Optional.empty(); - - int line = pos.line() + 1; - if (line == -1) - return Optional.empty(); - - return Optional.of(line); - } - public String lineContent() { - if (src.content().length == 0) - return ""; - - String line = pos.lineContent(); - if (line.endsWith("\r\n")) - return line.substring(0, line.length() - 2); - if (line.endsWith("\n") || line.endsWith("\u000c")) - return line.substring(0, line.length() - 1); - - return line; - } - public Optional offset() { - return Optional.of(pos.point()); - } - public Optional pointer() { - if (src.content().length == 0) - return Optional.empty(); - - return Optional.of(pos.point() - src.startOfLine(pos.point())); - } - public Optional pointerSpace() { - if (src.content().length == 0) - return Optional.empty(); - - String lineContent = this.lineContent(); - int pointer = this.pointer().get(); - StringBuilder result = new StringBuilder(); - // Don't crash if pointer is out-of-bounds (happens with some macros) - int fixedPointer = Math.min(pointer, lineContent.length()); - for (int i = 0; i < fixedPointer; i++) - result.append(lineContent.charAt(i) == '\t' ? '\t' : ' '); - return Optional.of(result.toString()); - } - }; - } else { - position = noPosition; - } - - Message message = dia.msg(); - StringBuilder rendered = new StringBuilder(); - rendered.append(messageAndPos(message, dia.pos(), diagnosticLevel(dia), ctx)); - boolean shouldExplain = dotty.tools.dotc.reporting.Diagnostic.shouldExplain(dia, ctx); - if (shouldExplain && !message.explanation().isEmpty()) { - rendered.append(explanation(message, ctx)); - } - - delegate.log(new Problem(position, message.msg(), severity, rendered.toString())); - } -} diff --git a/sbt-bridge/src/xsbt/DottydocRunner.java b/sbt-bridge/src/xsbt/DottydocRunner.java index 31a9cc97a43b..5ec3faa0ce40 100644 --- a/sbt-bridge/src/xsbt/DottydocRunner.java +++ b/sbt-bridge/src/xsbt/DottydocRunner.java @@ -6,6 +6,7 @@ import xsbti.Logger; import xsbti.Severity; + import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; @@ -16,6 +17,8 @@ import dotty.tools.dotc.core.Contexts.Context; import dotty.tools.dotc.core.Contexts.ContextBase; import dotty.tools.dotc.reporting.Reporter; +import dotty.tools.xsbt.InterfaceCompileFailed; +import dotty.tools.xsbt.DelegatingReporter; public class DottydocRunner { private final String[] args0; @@ -80,7 +83,7 @@ public void run() { Method processMethod = dottydocMainClass.getMethod("process", args.getClass(), Context.class); // args.getClass() is String[] Reporter reporter = (Reporter) processMethod.invoke(null, args, ctx); if (reporter.hasErrors()) - throw new InterfaceCompileFailed(args, new xsbti.Problem[0]); + throw new InterfaceCompileFailed(args, new xsbti.Problem[0], "DottyDoc Compilation Failed"); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } diff --git a/sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala b/sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala index 654cd32e87ab..de18d85158f1 100644 --- a/sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala +++ b/sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala @@ -8,11 +8,16 @@ import sbt.librarymanagement.{ VersionNumber } import sbt.internal.inc.ScalaInstance +import sbt.internal.inc.classpath.ClassLoaderCache import xsbti.compile._ +import xsbti.AppConfiguration import java.net.URLClassLoader import java.util.Optional +import java.util.{Enumeration, Collections} +import java.net.URL import scala.util.Properties.isJavaAtLeast + object DottyPlugin extends AutoPlugin { object autoImport { val isDotty = settingKey[Boolean]("Is this project compiled with Dotty?") @@ -524,15 +529,34 @@ object DottyPlugin extends AutoPlugin { scalaLibraryJar, dottyLibraryJar, compilerJar, - allJars + allJars, + appConfiguration.value ) } // Adapted from private mkScalaInstance in sbt def makeScalaInstance( - state: State, dottyVersion: String, scalaLibrary: File, dottyLibrary: File, compiler: File, all: Seq[File] + state: State, dottyVersion: String, scalaLibrary: File, dottyLibrary: File, compiler: File, all: Seq[File], appConfiguration: AppConfiguration ): ScalaInstance = { - val libraryLoader = state.classLoaderCache(List(dottyLibrary, scalaLibrary)) + /** + * The compiler bridge must load the xsbti classes from the sbt + * classloader, and similarly the Scala repl must load the sbt provided + * jline terminal. To do so we add the `appConfiguration` loader in + * the parent hierarchy of the scala 3 instance loader. + * + * The [[TopClassLoader]] ensures that the xsbti and jline classes + * only are loaded from the sbt loader. That is necessary because + * the sbt class loader contains the Scala 2.12 library and compiler + * bridge. + */ + val topLoader = new TopClassLoader(appConfiguration.provider.loader) + + val libraryJars = Array(dottyLibrary, scalaLibrary) + val libraryLoader = state.classLoaderCache.cachedCustomClassloader( + libraryJars.toList, + () => new URLClassLoader(libraryJars.map(_.toURI.toURL), topLoader) + ) + class DottyLoader extends URLClassLoader(all.map(_.toURI.toURL).toArray, libraryLoader) val fullLoader = state.classLoaderCache.cachedCustomClassloader( @@ -543,10 +567,53 @@ object DottyPlugin extends AutoPlugin { dottyVersion, fullLoader, libraryLoader, - Array(dottyLibrary, scalaLibrary), + libraryJars, compiler, all.toArray, None) + } +} +/** + * The parent classloader of the Scala compiler. + * + * A TopClassLoader is constructed from the sbt classloader. + * + * To understand why a custom parent classloader is needed for the compiler, + * let us describe some alternatives that wouldn't work. + * + * - `new URLClassLoader(urls)`: + * The compiler contains sbt phases that callback to sbt using the `xsbti.*` + * interfaces. If `urls` does not contain the sbt interfaces we'll get a + * `ClassNotFoundException` in the compiler when we try to use them, if + * `urls` does contain the interfaces we'll get a `ClassCastException` or a + * `LinkageError` because if the same class is loaded by two different + * classloaders, they are considered distinct by the JVM. + * + * - `new URLClassLoader(urls, sbtLoader)`: + * Because of the JVM delegation model, this means that we will only load + * a class from `urls` if it's not present in the parent `sbtLoader`, but + * sbt uses its own version of the scala compiler and scala library which + * is not the one we need to run the compiler. + * + * Our solution is to implement an URLClassLoader whose parent is + * `new TopClassLoader(sbtLoader)`. We override `loadClass` to load the + * `xsbti.*` interfaces from `sbtLoader`. + * + * The parent loader of the TopClassLoader is set to `null` so that the JDK + * classes and only the JDK classes are loade from it. + */ +private class TopClassLoader(sbtLoader: ClassLoader) extends ClassLoader(null) { + // We can't use the loadClass overload with two arguments because it's + // protected, but we can do the same by hand (the classloader instance + // from which we call resolveClass does not matter). + // The one argument overload of loadClass delegates to this one. + override protected def loadClass(name: String, resolve: Boolean): Class[_] = { + if (name.startsWith("xsbti.") || name.startsWith("org.jline.")) { + val c = sbtLoader.loadClass(name) + if (resolve) resolveClass(c) + c + } + else super.loadClass(name, resolve) } }