
scala.tools.nsc.interpreter.shell.JavapClass.scala Maven / Gradle / Ivy
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package scala.tools.nsc.interpreter
package shell
import scala.language.reflectiveCalls
import java.io.{InputStream, PrintWriter}
import scala.reflect.internal.util.ScalaClassLoader
import scala.tools.nsc.util.stringFromWriter
import scala.util.{Failure, Success, Try}
import scala.util.{Either, Left, Right}
import Javap.JpResult
/** Javap command implementation.
*/
class JavapClass(
val loader: ScalaClassLoader,
intp: Repl,
tool: JavapTool
) {
import JavapClass._
import Javap.{DefaultOptions, HashSplit, helper, toolArgs}
import JavapTool.Input
import java.io.FileNotFoundException
import scala.reflect.io.File
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 (options, filter) = {
val (opts, flag) = toolArgs(options0)
(if (opts.isEmpty) DefaultOptions else opts, flag)
}
if (options.contains("-help") || targets.isEmpty)
List(JpResult(helper(printWriter)))
else
tool(options, filter)(targets.map(targeted))
}
/** Associate the requested path with a possibly failed or empty array of bytes. */
private def targeted(path: String): Input =
bytesFor(path) match {
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").
* @return the path to use for filtering, and the byte array
*/
private def bytesFor(path: String) = Try {
val req = path match {
case "-" => intp.mostRecentVar
case HashSplit(prefix, _) if prefix != null => prefix
case HashSplit(_, member) if member != null => member
case s => s
}
findBytes(req) match {
case (_, bytes) if bytes.isEmpty => throw new FileNotFoundException(s"Could not find class bytes for '$path'")
case ok @ (actual @ _, bytes @ _) => ok
}
}
// 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.
*/
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.
* There are other symbols of interest, too:
* - a definition that is wrapped in an enclosing class
* - a synthetic that is not in scope but its associated class is
*/
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('$')
if (0 until s.length - 1 contains i) {
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 (res.isDefined && loadable(res.get)) res else None
}
// try loading translated+suffix
val res = loadableOrNone(strip = false)
// some synthetics lack a dollar, (e.g., suffix = delayedInit$body)
// so as a hack, if prefix$$suffix fails, also try prefix$suffix
if (res.isDefined) res else loadableOrNone(strip = true)
} else None
}
val p = path.asClassName // scrub any suffix
// if repl, translate the name to something replish
// (for translate, would be nicer to get the sym and ask .isClass,
// instead of translatePath and then asking did I get a class back)
val q = (
// only simple names get the scope treatment
Some(p) filter (_ contains '.')
// take path as a Name in scope
orElse (intp translatePath p filter loadable)
// take path as a Name in scope and find its enclosing class
orElse (intp translateEnclosingClass p filter loadable)
// take path as a synthetic derived from some Name in scope
orElse desynthesize(p)
// just try it plain
getOrElse p
)
(q, load(q))
}
}
object JavapClass {
private final val classSuffix = ".class"
// 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]
def task(loader: ClassLoader): Either[String, JavapTool]
/** 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)
}
}
}
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)
}
def create(toolName: String) = {
val loader = new ClassLoader(getClass.getClassLoader) with ScalaClassLoader
loader.create[Javap](toolName, Console.println(_))(intp)
}
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))
}
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
}
}
}
implicit private class URLOps(val url: URL) extends AnyVal {
def isFile: Boolean = url.getProtocol == "file"
}
/** Match foo#bar, both groups are optional (may be null). */
val HashSplit = "([^#]+)?(?:#(.+)?)?".r
// 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))
}
// 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
def value: ResultType
def show(): Unit
// todo
// def header(): String
// def fields(): List[String]
// def methods(): List[String]
// def signatures(): List[String]
}
object JpResult {
def apply(msg: String) = new JpError(msg)
def apply(res: Showable) = new JpSuccess(res)
}
class JpError(msg: String) extends JpResult {
type ResultType = String
def isError = true
def value = msg
def show() = println(msg) // makes sense for :javap, less for -Ygen-javap
}
class JpSuccess(val value: Showable) extends JpResult {
type ResultType = AnyRef
def isError = false
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")
(opts, rest.nonEmpty)
}
val helps = List(
"usage" -> ":javap [opts] [path or class or -]...",
"-help" -> "Prints this help message",
"-verbose/-v" -> "Stack size, number of locals, method args",
"-private/-p" -> "Private classes and members",
"-package" -> "Package-private classes and members",
"-protected" -> "Protected classes and members",
"-public" -> "Public classes and members",
"-l" -> "Line and local variable tables",
"-c" -> "Disassembled code",
"-s" -> "Internal type signatures",
"-sysinfo" -> "System info of class",
"-constants" -> "Static final constants",
"-filter" -> "Filter REPL machinery from output"
)
// match prefixes and unpack opts, or -help on failure
private def massage(arg: String): Seq[String] = {
require(arg startsWith "-")
// arg matches opt "-foo/-f" if prefix of -foo or exactly -f
val r = """(-[^/]*)(?:/(-.))?""".r
def maybe(opt: String, s: String): Option[String] = opt match {
// disambiguate by preferring short form
case r(lf, sf) if s == sf => Some(sf)
case r(lf, sf) if lf startsWith s => Some(lf)
case _ => None
}
def candidates(s: String) = (helps map (h => maybe(h._1, s))).flatten
// one candidate or one single-char candidate
def uniqueOf(maybes: Seq[String]) = {
def single(s: String) = s.length == 2
if (maybes.length == 1) maybes
else if ((maybes count single) == 1) maybes filter single
else Nil
}
// each optchar must decode to exactly one option
def unpacked(s: String): Try[Seq[String]] = {
val ones = (s drop 1) map { c =>
val maybes = uniqueOf(candidates(s"-$c"))
if (maybes.length == 1) Some(maybes.head) else None
}
Try(ones) filter (_ forall (_.isDefined)) map (_.flatten)
}
val res = uniqueOf(candidates(arg))
if (res.nonEmpty) res
else (unpacked(arg)
getOrElse (Seq("-help"))) // or else someone needs help
}
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) }
val DefaultOptions = List("-protected", "-verbose")
}
/** 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.jdk.CollectionConverters._
import scala.collection.mutable.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"
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy