From 0de71b92ae1dd6f7b424e8aee1f3f0fd8f6556d0 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sun, 20 Oct 2019 19:30:00 -0700 Subject: [PATCH] Forward port javap support --- .../tools/nsc/interpreter/shell/ILoop.scala | 56 +- .../nsc/interpreter/shell/JavapClass.scala | 661 ++++++++++-------- .../scala/tools/nsc/interpreter/IMain.scala | 2 + .../tools/nsc/interpreter/Interface.scala | 5 + 4 files changed, 381 insertions(+), 343 deletions(-) diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala index 8e9ba1a2a76f..231761d80b85 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala @@ -18,14 +18,14 @@ import java.io.{BufferedReader, PrintWriter} import java.nio.file.Files import java.util.concurrent.TimeUnit -import scala.PartialFunction.{cond => when} +import scala.PartialFunction.cond import scala.Predef.{println => _, _} import scala.annotation.tailrec import scala.language.implicitConversions import scala.util.Properties.jdkHome import scala.reflect.classTag import scala.reflect.internal.util.ScalaClassLoader._ -import scala.reflect.internal.util.{BatchSourceFile, NoPosition, ScalaClassLoader} +import scala.reflect.internal.util.{BatchSourceFile, NoPosition} import scala.reflect.io.{AbstractFile, Directory, File, Path} import scala.tools.asm.ClassReader import scala.tools.util.PathResolver @@ -36,7 +36,7 @@ import scala.tools.nsc.interpreter.Results.{Error, Incomplete, Success} import scala.tools.nsc.interpreter.StdReplTags._ import scala.tools.nsc.util.Exceptional.rootCause import scala.util.control.ControlThrowable -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ @@ -252,7 +252,7 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, buffer.substring(0, cursor) match { case trailingWord(s) => val maybes = intp.visibleSettings.filter(_.name.startsWith(s)).map(_.name) - .filterNot(when(_) { case "-"|"-X"|"-Y" => true }).sorted + .filterNot(cond(_) { case "-"|"-X"|"-Y" => true }).sorted if (maybes.isEmpty) NoCompletions else CompletionResult(cursor - s.length, maybes) case _ => NoCompletions } @@ -263,38 +263,6 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, private def importsCommand(line: String): Result = intp.importsCommandInternal(words(line)) mkString ("\n") - - private def findToolsJar() = PathResolver.SupplementalLocations.platformTools - - private def addToolsJarToLoader() = { - val cl = findToolsJar() match { - case Some(tools) => ScalaClassLoader.fromURLs(Seq(tools.toURL), intp.classLoader) - case _ => intp.classLoader - } - if (Javap.isAvailable(cl)) { - repldbg(":javap available.") - cl - } - else { - repldbg(":javap unavailable: no tools.jar at " + jdkHome) - intp.classLoader - } - } - - protected def newJavap() = JavapClass(addToolsJarToLoader(), intp.reporter.out, intp) - - private lazy val javap = - try newJavap() - catch { - case t: ControlThrowable => throw t - case t: Throwable => - repldbg("javap: " + rootCause(t)) - repltrace(stackTraceString(rootCause(t))) - NoJavap - } - - - private def implicitsCommand(line: String): Result = { val (implicits, res) = intp.implicitsCommandInternal(line) implicits foreach echoCommandMessage @@ -369,17 +337,11 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, intp.lastWarnings foreach { case (pos, msg) => intp.reporter.warning(pos, msg) } } - private def javapCommand(line: String): Result = { - if (javap == null) - s":javap unavailable, no tools.jar at $jdkHome. Set JDK_HOME." - else if (line == "") - Javap.helpText - else - javap(words(line)) foreach { res => - if (res.isError) return s"Failed: ${res.value}" - else res.show() - } - } + private def javapCommand(line: String): Result = + Javap(intp)(words(line): _*) foreach { res => + if (res.isError) return s"${res.value}" + else res.show() + } private def pathToPhaseWrapper = intp.originalPath("$r") + ".phased.atCurrent" diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/JavapClass.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/JavapClass.scala index 3bb677186405..5fd99ad62495 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/JavapClass.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/JavapClass.scala @@ -10,55 +10,52 @@ * additional information regarding copyright ownership. */ -package scala.tools.nsc.interpreter.shell - -import java.io._ -import java.lang.{Iterable => JIterable} -import java.net.URL -import java.util.Locale -import java.util.concurrent.ConcurrentLinkedQueue -import javax.tools._ - -import scala.collection.JavaConverters._ -import scala.collection.mutable.Clearable -import scala.io.Source +package scala.tools.nsc.interpreter +package shell + import scala.language.reflectiveCalls + +import java.io.{InputStream, PrintWriter} import scala.reflect.internal.util.ScalaClassLoader -import scala.reflect.io.File -import scala.util.Properties.{lineSeparator => EOL} +import scala.tools.nsc.util.stringFromWriter import scala.util.{Failure, Success, Try} -import Javap._ -import scala.tools.nsc.interpreter.Repl +import scala.util.{Either, Left, Right} + +import Javap.JpResult /** Javap command implementation. */ class JavapClass( val loader: ScalaClassLoader, - val printWriter: PrintWriter, - intp: Repl -) extends Javap { + intp: Repl, + tool: JavapTool +) { import JavapClass._ + import Javap.{DefaultOptions, HashSplit, helper, toolArgs} + import JavapTool.Input + import java.io.FileNotFoundException + import scala.reflect.io.File - lazy val tool = JavapTool() + private val printWriter: PrintWriter = intp.reporter.out def apply(args: Seq[String]): List[JpResult] = { - val (options0, targets) = args partition (s => (s startsWith "-") && s.length > 1) + val (options0, targets) = args.partition(s => s.startsWith("-") && s.length > 1) val (options, filter) = { val (opts, flag) = toolArgs(options0) (if (opts.isEmpty) DefaultOptions else opts, flag) } - if ((options contains "-help") || targets.isEmpty) + if (options.contains("-help") || targets.isEmpty) List(JpResult(helper(printWriter))) else - tool(options, filter)(targets map targeted) + tool(options, filter)(targets.map(targeted)) } /** Associate the requested path with a possibly failed or empty array of bytes. */ - private def targeted(path: String): (String, Try[Array[Byte]]) = + private def targeted(path: String): Input = bytesFor(path) match { - case Success((target, bytes)) => (target, Try(bytes)) - case f: Failure[_] => (path, Failure(f.exception)) + case Success((actual, bytes)) => Input(path, actual, Try(bytes)) + case f: Failure[_] => Input(path, path, Failure(f.exception)) } /** Find bytes. Handle "-", "Foo#bar" (by ignoring member), "#bar" (by taking "bar"). @@ -71,18 +68,18 @@ class JavapClass( case HashSplit(_, member) if member != null => member case s => s } - (path, findBytes(req)) match { - case (_, bytes) if bytes.isEmpty => throw new FileNotFoundException(s"Could not find class bytes for '$path'") - case ok => ok + findBytes(req) match { + case (_, bytes) if bytes.isEmpty => throw new FileNotFoundException(s"Could not find class bytes for '$path'") + case ok @ (actual @ _, bytes @ _) => ok } } - def findBytes(path: String): Array[Byte] = tryFile(path) getOrElse tryClass(path) + // data paired with actual path where it was found + private def findBytes(path: String): (String, Array[Byte]) = tryFile(path).map(data => (path, data)).getOrElse(tryClass(path)) /** Assume the string is a path and try to find the classfile it represents. */ - def tryFile(path: String): Option[Array[Byte]] = - (Try (File(path.asClassResource)) filter (_.exists) map (_.toByteArray())).toOption + private def tryFile(path: String): Option[Array[Byte]] = Try(File(path.asClassResource)).filter(_.exists).map(_.toByteArray()).toOption /** Assume the string is a fully qualified class name and try to * find the class object it represents. @@ -90,22 +87,22 @@ class JavapClass( * - a definition that is wrapped in an enclosing class * - a synthetic that is not in scope but its associated class is */ - def tryClass(path: String): Array[Byte] = { - def load(name: String) = loader classBytes name - def loadable(name: String) = loader resourceable name + private def tryClass(path: String): (String, Array[Byte]) = { + def load(name: String) = loader.classBytes(name) + def loadable(name: String) = loader.resourceable(name) // if path has an interior dollar, take it as a synthetic // if the prefix up to the dollar is a symbol in scope, // result is the translated prefix + suffix def desynthesize(s: String) = { - val i = s indexOf '$' + val i = s.indexOf('$') if (0 until s.length - 1 contains i) { - val name = s substring (0, i) - val sufx = s substring i - val tran = intp translatePath name + val name = s.substring(0, i) + val sufx = s.substring(i) + val tran = intp.translatePath(name) def loadableOrNone(strip: Boolean) = { def suffix(strip: Boolean)(x: String) = - (if (strip && (x endsWith "$")) x.init else x) + sufx - val res = tran map (suffix(strip) _) + (if (strip && x.endsWith("$")) x.init else x) + sufx + val res = tran.map(suffix(strip)(_)) if (res.isDefined && loadable(res.get)) res else None } // try loading translated+suffix @@ -131,284 +128,156 @@ class JavapClass( // just try it plain getOrElse p ) - load(q) + (q, load(q)) } +} +object JavapClass { + private final val classSuffix = ".class" - class JavapTool { - type ByteAry = Array[Byte] - type Input = Tuple2[String, Try[ByteAry]] - - implicit protected class Failer[A](a: => A) { - def orFailed[B >: A](b: => B) = if (failed) b else a - } - protected def noToolError = new JpError(s"No javap tool available: ${getClass.getName} failed to initialize.") + // We enjoy flexibility in specifying either a fully-qualified class name com.acme.Widget + // or a resource path com/acme/Widget.class; but not widget.out + implicit private class MaybeClassLike(val s: String) extends AnyVal { + def asClassName = s.stripSuffix(classSuffix).replace('/', '.') + def asClassResource = if (s.endsWith(classSuffix)) s else s.replace('.', '/') + classSuffix + } + implicit private class ClassLoaderOps(val loader: ScalaClassLoader) extends AnyVal { + /* would classBytes succeed with a nonempty array */ + def resourceable(className: String): Boolean = loader.getResource(className.asClassResource) != null + } +} +abstract class Javap(protected val intp: Repl) { + def loader: Either[String, ClassLoader] - // output filtering support - val writer = new CharArrayWriter - def written = { - writer.flush() - val w = writer.toString - writer.reset() - w - } + def task(loader: ClassLoader): Either[String, JavapTool] - def filterLines(target: String, text: String): String = { - // take Foo# as Foo#apply for purposes of filtering. - val filterOn = target.splitHashMember._2 map { s => if (s.isEmpty) "apply" else s } - var filtering = false // true if in region matching filter - // turn filtering on/off given the pattern of interest - def filterStatus(line: String, pattern: String) = { - def isSpecialized(method: String) = (method startsWith pattern+"$") && (method endsWith "$sp") - def isAnonymized(method: String) = (pattern == "$anonfun") && (method startsWith "$anonfun$") - // cheap heuristic, todo maybe parse for the java sig. - // method sigs end in paren semi - def isAnyMethod = line endsWith ");" - // take the method name between the space char and left paren. - // accept exact match or something that looks like what we might be asking for. - def isOurMethod = { - val lparen = line lastIndexOf '(' - val blank = line.lastIndexOf(' ', lparen) - if (blank < 0) false - else { - val method = line.substring(blank+1, lparen) - (method == pattern || isSpecialized(method) || isAnonymized(method)) - } - } - filtering = - if (filtering) { - // next blank line terminates section - // in non-verbose mode, next line is next method, more or less - line.trim.nonEmpty && (!isAnyMethod || isOurMethod) - } else { - isAnyMethod && isOurMethod + /** Run the tool. Option args start with "-", except that "-" itself + * denotes the last REPL result. + * The default options are "-protected -verbose". + * Byte data for filename args is retrieved with findBytes. + * @return results for invoking JpResult.show() + */ + final def apply(args: Seq[String]): List[Javap.JpResult] = + if (args.isEmpty) List(JpResult(Javap.helpText)) + else + loader match { + case Left(msg) => List(JpResult(msg)) + case Right(cl) => + task(cl) match { + case Left(msg) => List(JpResult(msg)) + case Right(tk) => new JavapClass(cl, intp, tk).apply(args) } - filtering } - // do we output this line? - def checkFilter(line: String) = filterOn map (filterStatus(line, _)) getOrElse true - val sw = new StringWriter - val pw = new PrintWriter(sw) - for { - line <- Source.fromString(text).getLines() - if checkFilter(line) - } pw println line - pw.flush() - sw.toString - } +} - import JavapTool._ - type Task = { - def call(): Boolean // true = ok - //def run(args: Array[String]): Int // all args - //def handleOptions(args: Array[String]): Unit // options, then run() or call() +object Javap { + import scala.util.Properties.isJavaAtLeast + import java.io.File + import java.net.URL + + private val javap8 = "scala.tools.nsc.interpreter.shell.Javap8" + private val javap9 = "scala.tools.nsc.interpreter.shell.Javap9" + private val javapP = "scala.tools.nsc.interpreter.shell.JavapProvider" + + // load and run a tool + def apply(intp: Repl)(targets: String*): List[JpResult] = { + def outDirIsClassPath: Boolean = intp.settings.Yreploutdir.isSetByUser && { + val outdir = intp.outputDir.file.getAbsoluteFile + intp.compilerClasspath.exists(url => url.isFile && new File(url.toURI).getAbsoluteFile == outdir) } - // result of Task.run - //object TaskResult extends Enumeration { - // val Ok, Error, CmdErr, SysErr, Abnormal = Value - //} - val TaskClass = loader.tryToInitializeClass[Task](JavapTask).orNull - // Since the tool is loaded by reflection, check for catastrophic failure. - protected def failed = TaskClass eq null - - val TaskCtor = TaskClass.getConstructor( - classOf[Writer], - classOf[JavaFileManager], - classOf[DiagnosticListener[_]], - classOf[JIterable[String]], - classOf[JIterable[String]] - ) orFailed null - - class JavaReporter extends DiagnosticListener[JavaFileObject] with Clearable { - type D = Diagnostic[_ <: JavaFileObject] - val diagnostics = new ConcurrentLinkedQueue[D] - override def report(d: Diagnostic[_ <: JavaFileObject]): Unit = { - diagnostics add d - } - override def clear() = diagnostics.clear() - /** All diagnostic messages. - * @param locale Locale for diagnostic messages, null by default. - */ - def messages(implicit locale: Locale = null) = diagnostics.asScala.map(_ getMessage locale).toList - - def reportable(): String = { - clear() - if (messages.nonEmpty) messages mkString ("", EOL, EOL) else "" - } + def create(toolName: String) = { + val loader = new ClassLoader(getClass.getClassLoader) with ScalaClassLoader + loader.create[Javap](toolName, Console.println(_))(intp) } - val reporter = new JavaReporter - - // DisassemblerTool.getStandardFileManager(reporter,locale,charset) - val defaultFileManager: JavaFileManager = - (loader.tryToLoadClass[JavaFileManager]("com.sun.tools.javap.JavapFileManager").get getMethod ( - "create", - classOf[DiagnosticListener[_]], - classOf[PrintWriter] - ) invoke (null, reporter, new PrintWriter(System.err, true))).asInstanceOf[JavaFileManager] orFailed null - - // manages named arrays of bytes, which might have failed to load - class JavapFileManager(val managed: Seq[Input])(delegate: JavaFileManager = defaultFileManager) - extends ForwardingJavaFileManager[JavaFileManager](delegate) { - import JavaFileManager.Location - import JavaFileObject.Kind - import Kind._ - import StandardLocation._ - import java.net.{URI, URISyntaxException} - - // name#fragment is OK, but otherwise fragile - def uri(name: String): URI = - try new URI(name) // new URI("jfo:" + name) - catch { case _: URISyntaxException => new URI("dummy") } - - def inputNamed(name: String): Try[ByteAry] = (managed find (_._1 == name)).get._2 - def managedFile(name: String, kind: Kind) = kind match { - case CLASS => fileObjectForInput(name, inputNamed(name), kind) - case _ => null - } - // todo: just wrap it as scala abstractfile and adapt it uniformly - def fileObjectForInput(name: String, bytes: Try[ByteAry], kind: Kind): JavaFileObject = - new SimpleJavaFileObject(uri(name), kind) { - override def openInputStream(): InputStream = new ByteArrayInputStream(bytes.get) - // if non-null, ClassWriter wrongly requires scheme non-null - override def toUri: URI = null - override def getName: String = name - // suppress - override def getLastModified: Long = -1L - } - override def getJavaFileForInput(location: Location, className: String, kind: Kind): JavaFileObject = - location match { - case CLASS_PATH => managedFile(className, kind) - case _ => null - } - override def hasLocation(location: Location): Boolean = - location match { - case CLASS_PATH => true - case _ => false - } + def advisory = { + val msg = "On JDK 9 or higher, use -nobootcp to enable :javap, or set -Yrepl-outdir to a file system path on the tool class path with -toolcp." + List(JpResult(msg)) } - def fileManager(inputs: Seq[Input]) = new JavapFileManager(inputs)() - /** Create a Showable to show tool messages and tool output, with output massage. - * @param target attempt to filter output to show region of interest - * @param filter whether to strip REPL names - */ - def showable(target: String, filter: Boolean): Showable = - new Showable { - val output = filterLines(target, s"${reporter.reportable()}${written}") - def show() = - if (filter) intp.reporter.withoutTruncating(printWriter.write(output)) - else intp.reporter.withoutUnwrapping(printWriter.write(output, 0, output.length)) + if (targets.isEmpty) List(JpResult(Javap.helpText)) + else if (!isJavaAtLeast("9")) create(javap8)(targets) + else { + var res: Option[List[JpResult]] = None + if (classOf[Repl].getClassLoader != null) { + val javap = create(javap9) + if (javap.loader.isRight) + res = Some(javap(targets)) + } + res.getOrElse { + if (outDirIsClassPath) create(javapP)(targets) + else advisory } - - // eventually, use the tool interface - def task(options: Seq[String], classes: Seq[String], inputs: Seq[Input]): Task = { - //ServiceLoader.load(classOf[javax.tools.DisassemblerTool]). - //getTask(writer, fileManager, reporter, options.asJava, classes.asJava) - val toolopts = options filter (_ != "-filter") - TaskCtor.newInstance(writer, fileManager(inputs), reporter, toolopts.asJava, classes.asJava) - .orFailed (throw new IllegalStateException) - } - // a result per input - private def applyOne(options: Seq[String], filter: Boolean, klass: String, inputs: Seq[Input]): Try[JpResult] = { - val t = - Try { - task(options, Seq(klass), inputs).call() - } map { - case true => JpResult(showable(klass, filter)) - case _ => JpResult(reporter.reportable()) - } recoverWith { - case e: java.lang.reflect.InvocationTargetException => e.getCause match { - case t: IllegalArgumentException => Success(JpResult(t.getMessage)) // bad option - case x => Failure(x) - } - } - - val cleanup = { _: Any => reporter.clear(); t} - t transform (cleanup, cleanup) } - - /** Run the tool. */ - def apply(options: Seq[String], filter: Boolean)(inputs: Seq[Input]): List[JpResult] = (inputs map { - case (klass, Success(_)) => applyOne(options, filter, klass, inputs).get - case (_, Failure(e)) => JpResult(e.toString) - }).toList orFailed List(noToolError) } - object JavapTool { - // >= 1.7 - val JavapTask = "com.sun.tools.javap.JavapTask" - - private def hasClass(cl: ScalaClassLoader, cn: String) = cl.tryToInitializeClass[AnyRef](cn).isDefined - - def isAvailable = hasClass(loader, JavapTask) - - /** Select the tool implementation for this platform. */ - def apply() = { - require(isAvailable) - new JavapTool - } + implicit private class URLOps(val url: URL) extends AnyVal { + def isFile: Boolean = url.getProtocol == "file" } -} - -object JavapClass { - - def apply( - loader: ScalaClassLoader = ScalaClassLoader.appLoader, - printWriter: PrintWriter = new PrintWriter(System.out, true), - intp: Repl - ) = new JavapClass(loader, printWriter, intp) /** Match foo#bar, both groups are optional (may be null). */ val HashSplit = "([^#]+)?(?:#(.+)?)?".r - // We enjoy flexibility in specifying either a fully-qualified class name com.acme.Widget - // or a resource path com/acme/Widget.class; but not widget.out - implicit class MaybeClassLike(val s: String) extends AnyVal { - /* private[this] final val suffix = ".class" */ - private def suffix = ".class" - def asClassName = (s stripSuffix suffix).replace('/', '.') - def asClassResource = if (s endsWith suffix) s else s.replace('.', '/') + suffix - def splitSuffix: (String, String) = if (s endsWith suffix) (s dropRight suffix.length, suffix) else (s, "") - def strippingSuffix(f: String => String): String = - if (s endsWith suffix) f(s dropRight suffix.length) else s - // e.g. Foo#bar. Foo# yields zero-length member part. - def splitHashMember: (String, Option[String]) = { - val i = s lastIndexOf '#' - if (i < 0) (s, None) - //else if (i >= s.length - 1) (s.init, None) - else (s take i, Some(s drop i+1)) + // e.g. Foo#bar. Foo# yields zero-length member part. + private def splitHashMember(s: String): Option[String] = + s.lastIndexOf('#') match { + case -1 => None + case i => Some(s.drop(i+1)) } - } - implicit class ClassLoaderOps(val loader: ScalaClassLoader) extends AnyVal { - /* would classBytes succeed with a nonempty array */ - def resourceable(className: String): Boolean = loader.getResource(className.asClassResource) != null - } - implicit class URLOps(val url: URL) extends AnyVal { - def isFile: Boolean = url.getProtocol == "file" - } -} -abstract class Javap { - /** Run the tool. Option args start with "-", except that "-" itself - * denotes the last REPL result. - * The default options are "-protected -verbose". - * Byte data for filename args is retrieved with findBytes. - * @return results for invoking JpResult.show() - */ - def apply(args: Seq[String]): List[Javap.JpResult] -} - -object Javap { - def isAvailable(cl: ScalaClassLoader = ScalaClassLoader.appLoader) = JavapClass(cl, intp = null).JavapTool.isAvailable - - def apply(path: String): Unit = apply(Seq(path)) - def apply(args: Seq[String]): Unit = JavapClass(intp=null) apply args foreach (_.show()) + // filter lines of javap output for target such as Klass#methode + def filterLines(target: String, text: String): String = { + // take Foo# as Foo#apply for purposes of filtering. + val filterOn = splitHashMember(target).map(s => if (s.isEmpty) "apply" else s) + var filtering = false // true if in region matching filter + // turn filtering on/off given the pattern of interest + def filterStatus(line: String, pattern: String) = { + def isSpecialized(method: String) = (method startsWith pattern+"$") && (method endsWith "$sp") + def isAnonymized(method: String) = (pattern == "$anonfun") && (method startsWith "$anonfun$") + // cheap heuristic, todo maybe parse for the java sig. + // method sigs end in paren semi + def isAnyMethod = line endsWith ");" + // take the method name between the space char and left paren. + // accept exact match or something that looks like what we might be asking for. + def isOurMethod = { + val lparen = line lastIndexOf '(' + val blank = line.lastIndexOf(' ', lparen) + if (blank < 0) false + else { + val method = line.substring(blank+1, lparen) + (method == pattern || isSpecialized(method) || isAnonymized(method)) + } + } + filtering = + if (filtering) { + // next blank line terminates section + // in non-verbose mode, next line is next method, more or less + line.trim.nonEmpty && (!isAnyMethod || isOurMethod) + } else { + isAnyMethod && isOurMethod + } + filtering + } + // do we output this line? + def checkFilter(line: String) = filterOn.map(filterStatus(line, _)).getOrElse(true) + stringFromWriter(pw => text.linesIterator.foreach(line => if (checkFilter(line)) pw.println(line))) + } private[interpreter] trait Showable { def show(): Unit } + /** Create a Showable to show tool messages and tool output, with output massage. + * @param filter whether to strip REPL names + */ + def showable(intp: Repl, filter: Boolean, text: String): Showable = + new Showable { + val out = intp.reporter.out + def show() = + if (filter) intp.reporter.withoutTruncating(out.write(text)) + else intp.reporter.withoutUnwrapping(out.write(text, 0, text.length)) + } + sealed trait JpResult { type ResultType def isError: Boolean @@ -436,8 +305,9 @@ object Javap { def show() = value.show() // output to tool's PrintWriter } + // split javap options from REPL's -filter flag, also take prefixes of flag names def toolArgs(args: Seq[String]): (Seq[String], Boolean) = { - val (opts, rest) = args flatMap massage partition (_ != "-filter") + val (opts, rest) = args.flatMap(massage).partition(_ != "-filter") (opts, rest.nonEmpty) } @@ -492,13 +362,212 @@ object Javap { def helpText: String = (helps map { case (name, help) => f"$name%-12.12s$help%n" }).mkString - def helper(pw: PrintWriter) = new Showable { - def show() = pw print helpText - } + def helper(pw: PrintWriter) = new Showable { def show() = pw.print(helpText) } val DefaultOptions = List("-protected", "-verbose") } -object NoJavap extends Javap { - def apply(args: Seq[String]): List[Javap.JpResult] = Nil +/** Loaded reflectively under JDK8 to locate tools.jar and load JavapTask tool. */ +class Javap8(intp0: Repl) extends Javap(intp0) { + import scala.tools.util.PathResolver + import scala.util.Properties.jdkHome + + private def findToolsJar() = PathResolver.SupplementalLocations.platformTools + + private def addToolsJarToLoader() = + findToolsJar() match { + case Some(tools) => ScalaClassLoader.fromURLs(Seq(tools.toURL), intp.classLoader) + case _ => intp.classLoader + } + override def loader = + Right(addToolsJarToLoader()).filterOrElse( + _.tryToInitializeClass[AnyRef](JavapTask.taskClassName).isDefined, + s":javap unavailable: no ${JavapTask.taskClassName} or no tools.jar at $jdkHome" + ) + override def task(loader: ClassLoader) = Right(new JavapTask(loader, intp)) +} + +/** Loaded reflectively under JDK9 to load JavapTask tool. */ +class Javap9(intp0: Repl) extends Javap(intp0) { + override def loader = + Right(new ClassLoader(intp.classLoader) with ScalaClassLoader).filterOrElse( + _.tryToInitializeClass[AnyRef](JavapTask.taskClassName).isDefined, + s":javap unavailable: no ${JavapTask.taskClassName}" + ) + override def task(loader: ClassLoader) = Right(new JavapTask(loader, intp)) +} + +/** Loaded reflectively under JDK9 to locate ToolProvider. */ +class JavapProvider(intp0: Repl) extends Javap(intp0) { + import JavapTool.Input + import Javap.{filterLines, HashSplit} + import java.util.Optional + //import java.util.spi.ToolProvider + + type ToolProvider = AnyRef { def run(out: PrintWriter, err: PrintWriter, args: Array[String]): Unit } + + override def loader = Right(getClass.getClassLoader) + + private def tool(provider: ToolProvider) = new JavapTool { + override def apply(options: Seq[String], filter: Boolean)(inputs: Seq[Input]): List[JpResult] = inputs.map { + case Input(target @ HashSplit(klass, _), actual, Success(_)) => + val more = List("-cp", intp.outputDir.file.getAbsoluteFile.toString, actual) + val s = stringFromWriter(w => provider.run(w, w, (options ++ more).toArray)) + JpResult(filterLines(target, s)) + case Input(_, _, Failure(e)) => JpResult(e.toString) + }.toList + } + + //ToolProvider.findFirst("javap") + override def task(loader: ClassLoader) = { + val provider = Class.forName("java.util.spi.ToolProvider", /*initialize=*/ true, loader) + .getDeclaredMethod("findFirst", classOf[String]) + .invoke(null, "javap").asInstanceOf[Optional[ToolProvider]] + if (provider.isPresent) + Right(tool(provider.get)) + else + Left(s":javap unavailable: provider not found") + } +} + +/** The task or tool provider. */ +abstract class JavapTool { + import JavapTool._ + def apply(options: Seq[String], filter: Boolean)(inputs: Seq[Input]): List[JpResult] +} +object JavapTool { + case class Input(target: String, actual: String, data: Try[Array[Byte]]) +} + +// Machinery to run JavapTask reflectively +class JavapTask(val loader: ScalaClassLoader, intp: Repl) extends JavapTool { + import javax.tools.{Diagnostic, DiagnosticListener, + ForwardingJavaFileManager, JavaFileManager, JavaFileObject, + SimpleJavaFileObject, StandardLocation} + import java.io.CharArrayWriter + import java.util.Locale + import java.util.concurrent.ConcurrentLinkedQueue + import scala.collection.JavaConverters._ + import scala.collection.generic.Clearable + import JavapTool._ + import Javap.{filterLines, showable} + + // output filtering support + val writer = new CharArrayWriter + def written = { + writer.flush() + val w = writer.toString + writer.reset() + w + } + + type Task = { + def call(): Boolean // true = ok + //def run(args: Array[String]): Int // all args + //def handleOptions(args: Array[String]): Unit // options, then run() or call() + } + // result of Task.run + //object TaskResult extends Enumeration { + // val Ok, Error, CmdErr, SysErr, Abnormal = Value + //} + + class JavaReporter extends DiagnosticListener[JavaFileObject] with Clearable { + type D = Diagnostic[_ <: JavaFileObject] + val diagnostics = new ConcurrentLinkedQueue[D] + override def report(d: Diagnostic[_ <: JavaFileObject]) = diagnostics.add(d) + override def clear() = diagnostics.clear() + /** All diagnostic messages. + * @param locale Locale for diagnostic messages, null by default. + */ + def messages(implicit locale: Locale = null) = diagnostics.asScala.map(_.getMessage(locale)).toList + + def reportable(): String = { + import scala.util.Properties.lineSeparator + clear() + if (messages.nonEmpty) messages.mkString("", lineSeparator, lineSeparator) else "" + } + } + val reporter = new JavaReporter + + // DisassemblerTool.getStandardFileManager(reporter,locale,charset) + val defaultFileManager: JavaFileManager = + (loader.tryToLoadClass[JavaFileManager]("com.sun.tools.javap.JavapFileManager").get getMethod ( + "create", + classOf[DiagnosticListener[_]], + classOf[PrintWriter] + ) invoke (null, reporter, new PrintWriter(System.err, true))).asInstanceOf[JavaFileManager] + + // manages named arrays of bytes, which might have failed to load + class JavapFileManager(val managed: Seq[Input])(delegate: JavaFileManager = defaultFileManager) + extends ForwardingJavaFileManager[JavaFileManager](delegate) { + import JavaFileObject.Kind + import Kind._ + import StandardLocation._ + import JavaFileManager.Location + import java.net.{URI, URISyntaxException} + import java.io.ByteArrayInputStream + + // name#fragment is OK, but otherwise fragile + def uri(name: String): URI = + try new URI(name) // new URI("jfo:" + name) + catch { case _: URISyntaxException => new URI("dummy") } + + // look up by actual class name or by target descriptor (unused?) + def inputNamed(name: String): Try[Array[Byte]] = managed.find(m => m.actual == name || m.target == name).get.data + + def managedFile(name: String, kind: Kind) = kind match { + case CLASS => fileObjectForInput(name, inputNamed(name), kind) + case _ => null + } + // todo: just wrap it as scala abstractfile and adapt it uniformly + def fileObjectForInput(name: String, bytes: Try[Array[Byte]], kind: Kind): JavaFileObject = + new SimpleJavaFileObject(uri(name), kind) { + override def openInputStream(): InputStream = new ByteArrayInputStream(bytes.get) + // if non-null, ClassWriter wrongly requires scheme non-null + override def toUri: URI = null + override def getName: String = name + // suppress + override def getLastModified: Long = -1L + } + override def getJavaFileForInput(location: Location, className: String, kind: Kind): JavaFileObject = + location match { + case CLASS_PATH => managedFile(className, kind) + case _ => null + } + override def hasLocation(location: Location): Boolean = + location match { + case CLASS_PATH => true + case _ => false + } + } + def fileManager(inputs: Seq[Input]) = new JavapFileManager(inputs)() + + // eventually, use the tool interface [Edit: which became ToolProvider] + //ServiceLoader.load(classOf[javax.tools.DisassemblerTool]). + //getTask(writer, fileManager, reporter, options.asJava, classes.asJava) + def task(options: Seq[String], classes: Seq[String], inputs: Seq[Input]): Task = + loader.create[Task](JavapTask.taskClassName, Console.println(_))(writer, fileManager(inputs), reporter, options.asJava, classes.asJava) + + /** Run the tool. */ + override def apply(options: Seq[String], filter: Boolean)(inputs: Seq[Input]): List[JpResult] = inputs.map { + case Input(target, actual, Success(_)) => + import java.lang.reflect.InvocationTargetException + try { + if (task(options, Seq(actual), inputs).call()) JpResult(showable(intp, filter, filterLines(target, s"${reporter.reportable()}${written}"))) + else JpResult(reporter.reportable()) + } catch { + case e: InvocationTargetException => e.getCause match { + case t: IllegalArgumentException => JpResult(t.getMessage) // bad option + case x => throw x + } + } finally { + reporter.clear() + } + case Input(_, _, Failure(e)) => JpResult(e.getMessage) + }.toList +} + +object JavapTask { + // introduced in JDK7 as internal API + val taskClassName = "com.sun.tools.javap.JavapTask" } diff --git a/src/repl/scala/tools/nsc/interpreter/IMain.scala b/src/repl/scala/tools/nsc/interpreter/IMain.scala index f09990f8ca42..6fca98e8ec41 100644 --- a/src/repl/scala/tools/nsc/interpreter/IMain.scala +++ b/src/repl/scala/tools/nsc/interpreter/IMain.scala @@ -118,6 +118,8 @@ class IMain(val settings: Settings, parentClassLoaderOverride: Option[ClassLoade object replOutput extends ReplOutput(settings.Yreploutdir) { } + override def outputDir = replOutput.dir + // Used in a test case. def showDirectory: String = { val writer = new StringWriter() diff --git a/src/repl/scala/tools/nsc/interpreter/Interface.scala b/src/repl/scala/tools/nsc/interpreter/Interface.scala index bdd60a616fd7..9d5abb672304 100644 --- a/src/repl/scala/tools/nsc/interpreter/Interface.scala +++ b/src/repl/scala/tools/nsc/interpreter/Interface.scala @@ -17,6 +17,7 @@ import java.io.PrintWriter import java.net.URL import scala.reflect.ClassTag +import scala.reflect.io.AbstractFile import scala.reflect.internal.util.{AbstractFileClassLoader, Position, SourceFile} import scala.tools.nsc.Settings import scala.tools.nsc.interpreter.Results.Result @@ -193,6 +194,10 @@ trait Repl extends ReplCore { // like beQuietDuring, but also turn off noisy settings. // this requires access to both settings and the global compiler def withSuppressedSettings(body: => Unit): Unit + + def compilerClasspath: Seq[URL] + + def outputDir: AbstractFile } /**