summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/util/Javap.scala
blob: 89c5969087bc18e3acf2259afcd70ffa10ad5283 (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
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
/* 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.collection.generic.Clearable
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
    if (claases.nonEmpty) tool(options)(claases map (claas => claas -> bytesFor(claas)))
    else List(JpResult(":javap [-lcsvp] [path1 path2 ...]"))
  }

  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 =
      if (path.endsWith(".class")) path
      else path.replace('.', '/') + ".class"
    (Try (File(file)) filter (_.exists) map (_.toByteArray)).toOption
  }
  /** 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)) => JpResult(showable(newPrinter(new ByteArrayInputStream(ba), newEnv(options))))
      case (_, Failure(e))  => JpResult(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

  class JavaReporter extends DiagnosticListener[JavaFileObject] with Clearable {
    import scala.collection.mutable.{ ArrayBuffer, SynchronizedBuffer }
    type D = Diagnostic[_ <: JavaFileObject]
    val diagnostics = new ArrayBuffer[D] with SynchronizedBuffer[D]
    override def report(d: Diagnostic[_ <: JavaFileObject]) {
      diagnostics += 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 map (_ getMessage locale)).toList

    def reportable: String = {
      import Properties.lineSeparator
      //val container = "Binary file .* contains .*".r
      //val m = messages filter (_ match { case container() => false case _ => true })
      val m = messages
      clear()
      if (m.nonEmpty) m 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] 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 = reporter.reportable
    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
  private def apply1(options: Seq[String], claas: String, inputs: Seq[Input]): Try[JpResult] =
    Try {
      task(options, Seq(claas), inputs).call()
    } map {
      case true => JpResult(showable())
      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)
      }
    } lastly {
      reporter.clear
    }
  override def apply(options: Seq[String])(inputs: Seq[Input]): List[JpResult] = (inputs map {
    case (claas, Success(_))  => apply1(options, claas, inputs).get
    case (_, Failure(e))      => JpResult(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)

  implicit class Lastly[A](val t: Try[A]) extends AnyVal {
    private def effect[X](last: =>Unit)(a: X): Try[A] = { last; t }
    def lastly(last: =>Unit): Try[A] = t transform (effect(last) _, effect(last) _)
  }
}

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