summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/util/Javap.scala
blob: cbfd8fec5185b4cc408bc97d37f64f24315211b0 (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
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
/* 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 scala.tools.nsc.interpreter.IMain
import java.io.{ ByteArrayInputStream, CharArrayWriter, FileNotFoundException, InputStream,
                 PrintWriter, Writer }
import java.util.{ Locale }
import java.util.regex.Pattern
import javax.tools.{ Diagnostic, DiagnosticCollector, DiagnosticListener,
                     ForwardingJavaFileManager, JavaFileManager, JavaFileObject,
                     SimpleJavaFileObject, StandardLocation }
import scala.reflect.io.{ AbstractFile, Directory, File, Path }
import java.io.{File => JFile}
import scala.io.Source
import scala.util.{ Try, Success, Failure }
import scala.util.Properties.lineSeparator
import scala.collection.JavaConverters
import scala.collection.generic.Clearable
import java.net.URL
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,
  val printWriter: PrintWriter,
  intp: Option[IMain] = None
) extends Javap {
  import JavapTool.ToolArgs
  import JavapClass._

  lazy val tool = JavapTool()

  /** Run the tool. Option args start with "-".
   *  The default options are "-protected -verbose".
   *  Byte data for filename args is retrieved with findBytes.
   */
  def apply(args: Seq[String]): List[JpResult] = {
    val (options, claases) = args partition (s => (s startsWith "-") && s.length > 1)
    val (flags, upgraded) = upgrade(options)
    import flags.{ app, fun, help, raw }
    val targets = if (fun && !help) FunFinder(loader, intp).funs(claases) else claases
    if (help || claases.isEmpty) List(JpResult(JavapTool.helper(printWriter)))
    else if (targets.isEmpty) List(JpResult("No anonfuns found."))
    else tool(raw, upgraded)(targets map (claas => claas -> bytesFor(claas, app)))
  }

  /** Cull our tool options. */
  private def upgrade(options: Seq[String]): (ToolArgs, Seq[String]) = ToolArgs fromArgs options match {
    case (t,s) if s.nonEmpty  => (t,s)
    case (t,s)                => (t, JavapTool.DefaultOptions)
  }

  /** Find bytes. Handle "-", "-app", "Foo#bar" (by ignoring member), "#bar" (by taking "bar"). */
  private def bytesFor(path: String, app: Boolean) = Try {
    def last = intp.get.mostRecentVar  // fail if no intp
    def req = if (path == "-") last else {
      val s = path.splitHashMember
      if (s._1.nonEmpty) s._1
      else s._2 getOrElse "#"
    }
    def asAppBody(s: String) = {
      val (cls, fix) = s.splitSuffix
      s"${cls}$$delayedInit$$body${fix}"
    }
    def todo = if (app) asAppBody(req) else req
    val bytes = findBytes(todo)
    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]] =
    (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
   */
  def tryClass(path: 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 flatMap (_ 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(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(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 = if (intp.isEmpty) p else (
      // only simple names get the scope treatment
      Some(p) filter (_ contains '.')
      // take path as a Name in scope
      orElse (intp flatMap (_ translatePath p) filter loadable)
      // take path as a Name in scope and find its enclosing class
      orElse (intp flatMap (_ translateEnclosingClass p) filter loadable)
      // take path as a synthetic derived from some Name in scope
      orElse desynthesize(p)
      // just try it plain
      getOrElse p
    )
    load(q)
  }

  /** Base class for javap tool adapters for java 6 and 7. */
  abstract class JavapTool {
    type ByteAry = Array[Byte]
    type Input = Pair[String, Try[ByteAry]]

    /** Run the tool. */
    def apply(raw: Boolean, 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.")

    // output filtering support
    val writer = new CharArrayWriter
    def written = {
      writer.flush()
      val w = writer.toString
      writer.reset()
      w
    }

    /** Create a Showable with output massage.
     *  @param raw show ugly repl names
     *  @param target attempt to filter output to show region of interest
     *  @param preamble other messages to output
     */
    def showWithPreamble(raw: Boolean, target: String, preamble: String = ""): Showable = new Showable {
      // ReplStrippingWriter clips and scrubs on write(String)
      // circumvent it by write(mw, 0, mw.length) or wrap it in withoutUnwrapping
      def show() =
        if (raw && intp.isDefined) intp.get withoutUnwrapping { writeLines() }
        else writeLines()
      private def writeLines() {
        // take Foo# as Foo#apply for purposes of filtering. Useful for -fun Foo#;
        // if apply is added here, it's for other than -fun: javap Foo#, perhaps m#?
        val filterOn = target.splitHashMember._2 map { s => if (s.isEmpty) "apply" else s }
        var filtering = false   // true if in region matching filter
        // true to output
        def checkFilter(line: String) = if (filterOn.isEmpty) true else {
          // cheap heuristic, todo maybe parse for the java sig.
          // method sigs end in paren semi
          def isAnyMethod = line.endsWith(");")
          def isOurMethod = {
            val lparen = line.lastIndexOf('(')
            val blank = line.lastIndexOf(' ', lparen)
            (blank >= 0 && line.substring(blank+1, lparen) == filterOn.get)
          }
          filtering = if (filtering) {
            // next blank line terminates section
            // for -public, next line is next method, more or less
            line.trim.nonEmpty && !isAnyMethod
          } else {
            isAnyMethod && isOurMethod
          }
          filtering
        }
        for (line <- Source.fromString(preamble + written).getLines; if checkFilter(line))
          printWriter write line+lineSeparator
        printWriter.flush()
      }
    }
  }

  class JavapTool6 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
    val printWrapper = new PrintWriter(writer)
    def newPrinter(in: InputStream, env: FakeEnvironment): FakePrinter =
      PrinterCtr.newInstance(in, printWrapper, env) orFailed null
    def showable(raw: Boolean, target: String, fp: FakePrinter): Showable = {
      fp.asInstanceOf[{ def print(): Unit }].print()      // run tool and flush to buffer
      printWrapper.flush()  // just in case
      showWithPreamble(raw, target)
    }

    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(raw: Boolean, options: Seq[String])(inputs: Seq[Input]): List[JpResult] =
      (inputs map {
        case (claas, Success(ba)) => JpResult(showable(raw, claas, newPrinter(new ByteArrayInputStream(ba), newEnv(options))))
        case (_, Failure(e))      => JpResult(e.toString)
      }).toList orFailed List(noToolError)
  }

  class JavapTool7 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(raw: Boolean): String = {
        // don't filter this message if raw, since the names are likely to differ
        val container = "Binary file .* contains .*".r
        val m = if (raw) messages
                else messages filter (_ match { case container() => false case _ => true })
        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
        }
    }
    def fileManager(inputs: Seq[Input]) = new JavapFileManager(inputs)()

    // show tool messages and tool output, with output massage
    def showable(raw: Boolean, target: String): Showable = showWithPreamble(raw, target, reporter.reportable(raw))

    // 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 applyOne(raw: Boolean, options: Seq[String], claas: String, inputs: Seq[Input]): Try[JpResult] =
      Try {
        task(options, Seq(claas), inputs).call()
      } map {
        case true => JpResult(showable(raw, claas))
        case _    => JpResult(reporter.reportable(raw))
      } 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(raw: Boolean, options: Seq[String])(inputs: Seq[Input]): List[JpResult] = (inputs map {
      case (claas, Success(_))  => applyOne(raw, 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
          }
        }
      }
    }

    case class ToolArgs(raw: Boolean = false, help: Boolean = false, app: Boolean = false, fun: Boolean = false)

    object ToolArgs {
      def fromArgs(args: Seq[String]): (ToolArgs, Seq[String]) = ((ToolArgs(), Seq[String]()) /: (args flatMap massage)) {
        case ((t,others), s) => s match {
          case "-fun"   => (t copy (fun=true), others)
          case "-app"   => (t copy (app=true), others)
          case "-help"  => (t copy (help=true), others)
          case "-raw"   => (t copy (raw=true), others)
          case _        => (t, others :+ s)
        }
      }
    }

    val helps = List(
      "usage"       -> ":javap [opts] [path or class or -]...",
      "-help"       -> "Prints this help message",
      "-raw"        -> "Don't unmangle REPL names",
      "-app"        -> "Show the DelayedInit body of Apps",
      "-fun"        -> "Show anonfuns for class or Class#method",
      "-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"
    )

    // match prefixes and unpack opts, or -help on failure
    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 helper(pw: PrintWriter) = new Showable {
      def show() = helps foreach (p => pw write "%-12.12s%s%n".format(p._1,p._2))
    }

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

    def isAvailable = Seq(Env, Tool) exists (cn => hasClass(loader, cn))

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

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

    def apply() = if (isTaskable(loader)) new JavapTool7 else new JavapTool6
  }
}

object JavapClass {
  def apply(
    loader: ScalaClassLoader = ScalaClassLoader.appLoader,
    printWriter: PrintWriter = new PrintWriter(System.out, true),
    intp: Option[IMain] = None
  ) = new JavapClass(loader, printWriter, intp)

  // 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))
    }
  }
  implicit class ClassLoaderOps(val cl: ClassLoader) extends AnyVal {
    private def parentsOf(x: ClassLoader): List[ClassLoader] = if (x == null) Nil else x :: parentsOf(x.getParent)
    def parents: List[ClassLoader] = parentsOf(cl)
    /* all file locations */
    def locations = {
      def alldirs = parents flatMap (_ match {
        case ucl: ScalaClassLoader.URLClassLoader => ucl.classPathURLs
        case jcl: java.net.URLClassLoader         => jcl.getURLs
        case _ => Nil
      })
      val dirs = for (d <- alldirs; if d.getProtocol == "file") yield Path(new JFile(d.toURI))
      dirs
    }
    /* only the file location from which the given class is loaded */
    def locate(k: String): Option[Path] = {
      Try {
        val claas = try cl loadClass k catch {
          case _: NoClassDefFoundError => null    // let it snow
        }
        // cf ScalaClassLoader.originOfClass
        claas.getProtectionDomain.getCodeSource.getLocation
      } match {
        case Success(null)              => None
        case Success(loc) if loc.isFile => Some(Path(new JFile(loc.toURI)))
        case _                          => None
      }
    }
    /* would classBytes succeed with a nonempty array */
    def resourceable(className: String): Boolean = cl.getResource(className.asClassResource) != null
  }
  implicit class PathOps(val p: Path) extends AnyVal {
    import scala.tools.nsc.io.Jar
    def isJar = Jar isJarOrZip p
  }
  implicit class URLOps(val url: URL) extends AnyVal {
    def isFile: Boolean = url.getProtocol == "file"
  }
  object FunFinder {
    def apply(loader: ScalaClassLoader, intp: Option[IMain]) = new FunFinder(loader, intp)
  }
  class FunFinder(loader: ScalaClassLoader, intp: Option[IMain]) {

    // class k, candidate f without prefix
    def isFunOfClass(k: String, f: String) = {
      val p = (s"${Pattern quote k}\\$$+anonfun").r
      (p findPrefixOf f).nonEmpty
    }
    // class k, candidate f without prefix, method m
    def isFunOfMethod(k: String, m: String, f: String) = {
      val p = (s"${Pattern quote k}\\$$+anonfun\\$$${Pattern quote m}\\$$").r
      (p findPrefixOf f).nonEmpty
    }
    def isFunOfTarget(k: String, m: Option[String], f: String) =
      if (m.isEmpty) isFunOfClass(k, f)
      else isFunOfMethod(k, m.get, f)
    def listFunsInAbsFile(k: String, m: Option[String], d: AbstractFile) = {
      for (f <- d; if !f.isDirectory && isFunOfTarget(k, m, f.name)) yield f.name
    }
    // path prefix p, class k, dir d
    def listFunsInDir(p: String, k: String, m: Option[String])(d: Directory) = {
      val subdir  = Path(p)
      for (f <- (d / subdir).toDirectory.list; if f.isFile && isFunOfTarget(k, m, f.name))
        yield f.name
    }
    // path prefix p, class k, jar file f
    def listFunsInJar(p: String, k: String, m: Option[String])(f: File) = {
      import java.util.jar.JarEntry
      import scala.tools.nsc.io.Jar
      def maybe(e: JarEntry) = {
        val (path, name) = {
          val parts = e.getName split "/"
          if (parts.length < 2) ("", e.getName)
          else (parts.init mkString "/", parts.last)
        }
        if (path == p && isFunOfTarget(k, m, name)) Some(name) else None
      }
      (new Jar(f) map maybe).flatten
    }
    def loadable(name: String) = loader resourceable name
    // translated class, optional member, opt member to filter on, whether it is repl output
    def translate(s: String): (String, Option[String], Option[String], Boolean) = {
      val (k0, m0) = s.splitHashMember
      val k = k0.asClassName
      val member = m0 filter (_.nonEmpty)  // take Foo# as no member, not ""
      val filter = m0 flatMap { case "" => Some("apply") case _ => None }   // take Foo# as filter on apply
      // class is either something replish or available to loader
      // $line.$read$$etc$Foo#member
      ((intp flatMap (_ translatePath k) filter (loadable) map ((_, member, filter, true)))
      // s = "f" and $line.$read$$etc$#f is what we're after,
      // ignoring any #member (except take # as filter on #apply)
      orElse (intp flatMap (_ translateEnclosingClass k) map ((_, Some(k), filter, true)))
      getOrElse (k, member, filter, false))
    }
    /** Find the classnames of anonfuns associated with k,
     *  where k may be an available class or a symbol in scope.
     */
    def funsOf(k0: String): Seq[String] = {
      // class is either something replish or available to loader
      val (k, member, filter, isReplish) = translate(k0)
      val splat   = k split "\\."
      val name    = splat.last
      val prefix  = if (splat.length > 1) splat.init mkString "/" else ""
      val pkg     = if (splat.length > 1) splat.init mkString "." else ""
      // reconstitute an anonfun with a package
      // if filtered, add the hash back, e.g. pkg.Foo#bar, pkg.Foo$anon$1#apply
      def packaged(s: String) = {
        val p = if (pkg.isEmpty) s else s"$pkg.$s"
        val pm = filter map (p + "#" + _)
        pm getOrElse p
      }
      // is this translated path in (usually virtual) repl outdir? or loadable from filesystem?
      val fs = if (isReplish) {
        def outed(d: AbstractFile, p: Seq[String]): Option[AbstractFile] = {
          if (p.isEmpty) Option(d)
          else Option(d.lookupName(p.head, true)) flatMap (f => outed(f, p.tail))
        }
        outed(intp.get.replOutput.dir, splat.init) map { d =>
          listFunsInAbsFile(name, member, d) map packaged
        }
      } else {
        loader locate k map { w =>
          if (w.isDirectory) listFunsInDir(prefix, name, member)(w.toDirectory) map packaged
          else if (w.isJar) listFunsInJar(prefix, name, member)(w.toFile) map packaged
          else Nil
        }
      }
      fs match {
        case Some(xs) => xs.to[Seq]     // maybe empty
        case None     => Seq()          // nothing found, e.g., junk input
      }
    }
    def funs(ks: Seq[String]) = ks flatMap funsOf _
  }
}

object Javap {

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

  def apply(path: String): Unit      = apply(Seq(path))
  def apply(args: Seq[String]): Unit = 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
  }
  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) _)
  }
}