summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/util/Javap.scala
blob: 6f5f4f6ed464ee40d5896219f181bafc1f6acd01 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/* NSC -- new Scala compiler
 * Copyright 2005-2013 LAMP/EPFL
 * @author Paul Phillips
 */

package scala.tools
package util

import java.lang.{ ClassLoader => JavaClassLoader, Iterable => JIterable }
import scala.tools.nsc.util.ScalaClassLoader
import java.io.{ ByteArrayInputStream, CharArrayWriter, FileNotFoundException, InputStream,
                 PrintWriter, Writer }
import java.util.{ Locale }
import javax.tools.{ Diagnostic, DiagnosticCollector, DiagnosticListener,
                     ForwardingJavaFileManager, JavaFileManager, JavaFileObject,
                     SimpleJavaFileObject, StandardLocation }
import scala.tools.nsc.io.File
import scala.util.{ Properties, Try, Success, Failure }
import scala.collection.JavaConverters
import scala.language.reflectiveCalls

import Javap._

trait Javap {
  def loader: ScalaClassLoader
  def printWriter: PrintWriter
  def apply(args: Seq[String]): List[JpResult]
  def tryFile(path: String): Option[Array[Byte]]
  def tryClass(path: String): Array[Byte]
}

object NoJavap extends Javap {
  def loader: ScalaClassLoader                   = getClass.getClassLoader
  def printWriter: PrintWriter                   = new PrintWriter(System.err, true)
  def apply(args: Seq[String]): List[JpResult]   = Nil
  def tryFile(path: String): Option[Array[Byte]] = None
  def tryClass(path: String): Array[Byte]        = Array()
}

class JavapClass(
  val loader: ScalaClassLoader = ScalaClassLoader.appLoader,
  val printWriter: PrintWriter = new PrintWriter(System.out, true)
) extends Javap {

  lazy val tool = JavapTool(loader, printWriter)

  /** Run the tool. Option args start with "-".
   *  The default options are "-protected -verbose".
   *  Byte data for filename args is retrieved with findBytes.
   *  If the filename does not end with ".class", javap will
   *  insert a banner of the form:
   *  `Binary file dummy contains simple.Complex`.
   */
  def apply(args: Seq[String]): List[JpResult] = {
    val (optional, claases) = args partition (_ startsWith "-")
    val options = if (optional.nonEmpty) optional else JavapTool.DefaultOptions
    tool(options)(claases map (claas => claas -> bytesFor(claas)))
  }

  private def bytesFor(path: String) = Try {
    val bytes = findBytes(path)
    if (bytes.isEmpty) throw new FileNotFoundException(s"Could not find class bytes for '${path}'")
    else bytes
  }

  def findBytes(path: String): Array[Byte] = tryFile(path) getOrElse tryClass(path)

  /** Assume the string is a path and try to find the classfile
   *  it represents.
   */
  def tryFile(path: String): Option[Array[Byte]] = {
    val file = File(
      if (path.endsWith(".class")) path
      else path.replace('.', '/') + ".class"
    )
    if (!file.exists) None
    else try Some(file.toByteArray) catch { case x: Exception => None }
  }
  /** Assume the string is a fully qualified class name and try to
   *  find the class object it represents.
   */
  def tryClass(path: String): Array[Byte] = loader classBytes {
    if (path endsWith ".class") (path dropRight 6).replace('/', '.')
    else path
  }
}

abstract class JavapTool {
  type ByteAry = Array[Byte]
  type Input = Pair[String, Try[ByteAry]]
  def apply(options: Seq[String])(inputs: Seq[Input]): List[JpResult]
  // Since the tool is loaded by reflection, check for catastrophic failure.
  protected def failed: Boolean
  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.")
}

class JavapTool6(loader: ScalaClassLoader, printWriter: PrintWriter) extends JavapTool {
  import JavapTool._
  val EnvClass     = loader.tryToInitializeClass[FakeEnvironment](Env).orNull
  val PrinterClass = loader.tryToInitializeClass[FakePrinter](Printer).orNull
  override protected def failed = (EnvClass eq null) || (PrinterClass eq null)

  val PrinterCtr = PrinterClass.getConstructor(classOf[InputStream], classOf[PrintWriter], EnvClass) orFailed null
  def newPrinter(in: InputStream, env: FakeEnvironment): FakePrinter =
    PrinterCtr.newInstance(in, printWriter, env) orFailed null
  def showable(fp: FakePrinter) = new Showable {
    def show() = fp.asInstanceOf[{ def print(): Unit }].print()
  }

  lazy val parser = new JpOptions
  def newEnv(opts: Seq[String]): FakeEnvironment = {
    def result = {
      val env: FakeEnvironment = EnvClass.newInstance()
      parser(opts) foreach { case (name, value) =>
        val field = EnvClass getDeclaredField name
        field setAccessible true
        field.set(env, value.asInstanceOf[AnyRef])
      }
      env
    }
    result orFailed null
  }

  override def apply(options: Seq[String])(inputs: Seq[Input]): List[JpResult] =
    (inputs map {
      case (_, Success(ba)) => new JpSuccess(showable(newPrinter(new ByteArrayInputStream(ba), newEnv(options))))
      case (_, Failure(e))  => new JpError(e.toString)
    }).toList orFailed List(noToolError)
}

class JavapTool7(loader: ScalaClassLoader, printWriter: PrintWriter) extends JavapTool {

  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()
  }
  // result of Task.run
  //object TaskResult extends Enumeration {
  //  val Ok, Error, CmdErr, SysErr, Abnormal = Value
  //}
  val TaskClaas = loader.tryToInitializeClass[Task](JavapTool.Tool).orNull
  override protected def failed = TaskClaas eq null

  val TaskCtor  = TaskClaas.getConstructor(
    classOf[Writer],
    classOf[JavaFileManager],
    classOf[DiagnosticListener[_]],
    classOf[JIterable[String]],
    classOf[JIterable[String]]
  ) orFailed null

  val reporter = new DiagnosticCollector[JavaFileObject]

  // 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 JavaFileObject.Kind
    import Kind._
    import StandardLocation._
    import JavaFileManager.Location
    import java.net.URI
    def uri(name: String): URI = new URI(name) // new URI("jfo:" + name)

    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
      }
  }
  val writer = new CharArrayWriter
  def fileManager(inputs: Seq[Input]) = new JavapFileManager(inputs)()
  def showable(): Showable = {
    val written = {
      writer.flush()
      val w = writer.toString
      writer.reset()
      w
    }
    val msgs = {
      import Properties.lineSeparator
      val m = reporter.messages
      if (m.nonEmpty) m mkString ("", lineSeparator, lineSeparator)
      else ""
    }
    new Showable {
      def show() = {
        val mw = msgs + written
        printWriter.write(mw, 0, mw.length) // ReplStrippingWriter clips on write(String) if truncating
        printWriter.flush()
      }
    }
  }
  // eventually, use the tool interface
  def task(options: Seq[String], claases: Seq[String], inputs: Seq[Input]): Task = {
    //ServiceLoader.load(classOf[javax.tools.DisassemblerTool]).
    //getTask(writer, fileManager, reporter, options.asJava, claases.asJava)
    import JavaConverters.asJavaIterableConverter
    TaskCtor.newInstance(writer, fileManager(inputs), reporter, options.asJava, claases.asJava)
      .orFailed (throw new IllegalStateException)
  }
  // a result per input
  override def apply(options: Seq[String])(inputs: Seq[Input]): List[JpResult] = (inputs map {
    case (claas, Success(ba)) =>
      if (task(options, Seq(claas), inputs).call()) new JpSuccess(showable())
      else new JpError(reporter.messages mkString ",")
    case (_, Failure(e))      => new JpError(e.toString)
  }).toList orFailed List(noToolError)
}

object JavapTool {
  // >= 1.7
  val Tool    = "com.sun.tools.javap.JavapTask"

  // < 1.7
  val Env     = "sun.tools.javap.JavapEnvironment"
  val Printer = "sun.tools.javap.JavapPrinter"
  // "documentation"
  type FakeEnvironment = AnyRef
  type FakePrinter = AnyRef

  // support JavapEnvironment
  class JpOptions {
    private object Access {
      final val PRIVATE = 0
      final val PROTECTED = 1
      final val PACKAGE = 2
      final val PUBLIC = 3
    }
    private val envActionMap: Map[String, (String, Any)] = {
      val map = Map(
        "-l"         -> (("showLineAndLocal", true)),
        "-c"         -> (("showDisassembled", true)),
        "-s"         -> (("showInternalSigs", true)),
        "-verbose"   -> (("showVerbose", true)),
        "-private"   -> (("showAccess", Access.PRIVATE)),
        "-package"   -> (("showAccess", Access.PACKAGE)),
        "-protected" -> (("showAccess", Access.PROTECTED)),
        "-public"    -> (("showAccess", Access.PUBLIC)),
        "-all"       -> (("showallAttr", true))
      )
      map ++ List(
        "-v" -> map("-verbose"),
        "-p" -> map("-private")
      )
    }
    def apply(opts: Seq[String]): Seq[(String, Any)] = {
      opts flatMap { opt =>
        envActionMap get opt match {
          case Some(pair) => List(pair)
          case _          =>
            val charOpts = opt.tail.toSeq map ("-" + _)
            if (charOpts forall (envActionMap contains _))
              charOpts map envActionMap
            else Nil
        }
      }
    }
  }

  val DefaultOptions = List("-protected", "-verbose")

  def isAvailable(cl: ScalaClassLoader = ScalaClassLoader.appLoader) = Seq(Env, Tool) exists (cn => hasClass(cl, cn))

  private def hasClass(cl: ScalaClassLoader, cn: String) = cl.tryToInitializeClass[AnyRef](cn).isDefined

  private def isTaskable(cl: ScalaClassLoader) = hasClass(cl, Tool)

  def apply(cl: ScalaClassLoader, pw: PrintWriter) =
    if (isTaskable(cl)) new JavapTool7(cl, pw) else new JavapTool6(cl, pw)

  /** A richer [[javax.tools.DiagnosticCollector]]. */
  implicit class JavaReporter(val c: DiagnosticCollector[JavaFileObject]) extends AnyVal {
    import scala.collection.JavaConverters.iterableAsScalaIterableConverter
    /** All diagnostics in the collector. */
    def diagnostics: Iterable[Diagnostic[_ <: JavaFileObject]] = c.getDiagnostics.asScala
    /** All diagnostic messages.
     *  @param locale Locale for diagnostic messages, null by default.
     */
    def messages(implicit locale: Locale = null) = (diagnostics map (_ getMessage locale)).toList
    /*
    import Diagnostic.Kind.ERROR
    private def isErr(d: Diagnostic[_]) = d.getKind == ERROR
    /** Count the errors. */
    def errorCount: Int = diagnostics count isErr
    /** Error diagnostics in the collector. */
    def errors = (diagnostics filter isErr).toList
    */
  }
}

object Javap {

  def isAvailable(cl: ScalaClassLoader = ScalaClassLoader.appLoader) = JavapTool.isAvailable(cl)

  def apply(path: String): Unit      = apply(Seq(path))
  def apply(args: Seq[String]): Unit = new JavapClass() apply args foreach (_.show())

  trait Showable {
    def show(): Unit
  }

  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]
  }
  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
  }
}