scala.tools.nsc.ast.TreeBrowsers.scala Maven / Gradle / Ivy
The newest version!
/*
* 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
package tools.nsc
package ast
import scala.language.implicitConversions
import java.awt.{List => _, _}
import java.awt.event._
import java.io.{StringWriter, Writer}
import javax.swing._
import javax.swing.event.TreeModelListener
import javax.swing.tree._
import java.util.concurrent.CountDownLatch
import scala.annotation.{nowarn, tailrec}
/**
* Tree browsers can show the AST in a graphical and interactive
* way, useful for debugging and understanding.
*
* @author Iulian Dragos
*/
abstract class TreeBrowsers {
val global: Global
import global._
import nme.EMPTY
import TreeBrowsers._
val borderSize = 10
def create(): SwingBrowser = new SwingBrowser()
/** Pseudo tree class, so that all JTree nodes are treated uniformly */
case class ProgramTree(units: List[UnitTree]) extends Tree {
override def toString: String = "Program"
}
/** Pseudo tree class, so that all JTree nodes are treated uniformly */
case class UnitTree(unit: CompilationUnit) extends Tree {
override def toString: String = unit.toString
}
/**
* Java Swing pretty printer for Scala abstract syntax trees.
*/
class SwingBrowser {
def browse(pName: String, units: Iterator[CompilationUnit]): Unit =
browse(pName, units.toList)
/** print the whole program */
def browse(pName: String, units: List[CompilationUnit]): Unit = {
val latch = new CountDownLatch(1)
SwingUtilities.invokeAndWait {() =>
val unitList = units.map(UnitTree(_))
val tm = new ASTTreeModel(ProgramTree(unitList))
val frame = new BrowserFrame(pName)
frame.setTreeModel(tm)
frame.createFrame(latch)
}
// wait for the frame to be closed
latch.await()
}
}
/** Tree model for abstract syntax trees */
class ASTTreeModel(val program: ProgramTree) extends TreeModel {
var listeners: List[TreeModelListener] = Nil
/** Add a listener to this tree */
def addTreeModelListener(l: TreeModelListener): Unit =
listeners = l :: listeners
/** Return the index'th child of parent */
def getChild(parent: AnyRef, index: Int): AnyRef =
packChildren(parent)(index)
/** Return the number of children this 'parent' has */
def getChildCount(parent: AnyRef): Int =
packChildren(parent).length
/** Return the index of the given child */
def getIndexOfChild(parent: AnyRef, child: AnyRef): Int =
packChildren(parent) indexOf child
/** Return the root node */
def getRoot(): AnyRef = program
/** Test whether the given node is a leaf */
def isLeaf(node: AnyRef): Boolean = packChildren(node).isEmpty
def removeTreeModelListener(l: TreeModelListener): Unit =
listeners = listeners filterNot (_ == l)
/** we ignore this message for now */
def valueForPathChanged(path: TreePath, newValue: AnyRef) = ()
/**
* Return a list of children for the given node.
*/
def packChildren(t: AnyRef): List[AnyRef] = TreeInfo.children(t.asInstanceOf[Tree])
}
/**
* A window that can host the Tree widget and provide methods for
* displaying information
*
* @author Iulian Dragos
*/
class BrowserFrame(phaseName: String = "unknown") {
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel")
}
catch {
case _: Throwable => UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName())
}
val frame = new JFrame("Scala AST after " + phaseName + " phase")
frame.setJMenuBar(new ASTMenuBar())
val topLeftPane = new JPanel(new BorderLayout())
val topRightPane = new JPanel(new BorderLayout())
val bottomPane = new JPanel(new BorderLayout())
var splitPane: JSplitPane = _
var treeModel: ASTTreeModel = _
var jTree: JTree = _
val textArea: JTextArea = new JTextArea(30, 120)
textArea.setBorder(BorderFactory.createEmptyBorder(borderSize, borderSize, borderSize, borderSize))
val infoPanel = new TextInfoPanel()
private def setExpansionState(root: JTree, expand: Boolean): Unit = {
def _setExpansionState(root: JTree, path: TreePath): Unit = {
val last = path.getLastPathComponent
for (i <- 0 until root.getModel.getChildCount(last)) {
val child = root.getModel.getChild(last, i)
val childPath = path pathByAddingChild child
_setExpansionState(root, childPath)
}
if (expand) {jTree expandPath path}
else {jTree collapsePath path}
}
_setExpansionState(root, new TreePath(root.getModel.getRoot))
}
def expandAll(subtree: JTree) = setExpansionState(subtree, expand = true)
def collapseAll(subtree: JTree) = setExpansionState(subtree, expand = false)
/** Create a frame that displays the AST.
*
* @param latch The latch is used in order to stop the compilation thread
* until the user is done with the tree inspection. Swing creates its
* own threads when the frame is packed, and therefore execution
* would continue. However, this is not what we want, as the tree and
* especially symbols/types would change while the window is visible.
*/
def createFrame(latch: CountDownLatch): Unit = {
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
frame.addWindowListener(new WindowAdapter() {
/** Release the lock, so compilation may resume after the window is closed. */
override def windowClosed(e: WindowEvent): Unit = latch.countDown()
})
jTree = new JTree(treeModel) {
/** Return the string for a tree node. */
override def convertValueToText(value: Any, sel: Boolean,
exp: Boolean, leaf: Boolean,
row: Int, hasFocus: Boolean) = {
val (cls, name) = TreeInfo.treeName(value.asInstanceOf[Tree])
if (name != EMPTY)
cls + "[" + name + "]"
else
cls
}
}
jTree.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() {
def valueChanged(e: javax.swing.event.TreeSelectionEvent): Unit = {
textArea.setText(e.getPath().getLastPathComponent().toString)
infoPanel.update(e.getPath().getLastPathComponent())
}
})
val topSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, topLeftPane, topRightPane)
topSplitPane.setResizeWeight(0.5)
jTree.setBorder(
BorderFactory.createEmptyBorder(borderSize, borderSize, borderSize, borderSize))
topLeftPane.add(new JScrollPane(jTree), BorderLayout.CENTER)
topRightPane.add(new JScrollPane(infoPanel), BorderLayout.CENTER)
bottomPane.add(new JScrollPane(textArea), BorderLayout.CENTER)
textArea.setFont(new Font("monospaced", Font.PLAIN, 14))
textArea.setEditable(false)
splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, topSplitPane, bottomPane)
frame.getContentPane().add(splitPane)
frame.pack()
frame.setVisible(true)
splitPane.setDividerLocation(0.5)
}
class ASTMenuBar extends JMenuBar {
val menuKey = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(): @nowarn("cat=deprecation") // deprecated since JDK 10, replacement only available in 10+
val shiftKey = InputEvent.SHIFT_DOWN_MASK
val jmFile = new JMenu("File")
// val jmiSaveImage = new JMenuItem(
// new AbstractAction("Save Tree Image") {
// putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_S, menuKey, false))
// override def actionPerformed(e: ActionEvent) {
// //TODO
// }
// }
// )
// jmFile add jmiSaveImage
def closeWindow() = frame.getToolkit().getSystemEventQueue().postEvent(
new WindowEvent(frame, WindowEvent.WINDOW_CLOSING))
val jmiCancel = new JMenuItem (
new AbstractAction("Cancel Compilation") {
putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Q, menuKey + shiftKey, false))
override def actionPerformed(e: ActionEvent): Unit = {
closeWindow()
global.currentRun.cancel()
}
}
)
jmFile add jmiCancel
val jmiExit = new JMenuItem (
new AbstractAction("Exit") {
putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Q, menuKey, false))
override def actionPerformed(e: ActionEvent) = closeWindow()
}
)
jmFile add jmiExit
add(jmFile)
val jmView = new JMenu("View")
val jmiExpand = new JMenuItem(
new AbstractAction("Expand All Nodes") {
putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_E, menuKey, false))
override def actionPerformed(e: ActionEvent): Unit = {
expandAll(jTree)
}
}
)
jmView add jmiExpand
val jmiCollapse = new JMenuItem(
new AbstractAction("Collapse All Nodes") {
putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_L, menuKey, false))
override def actionPerformed(e: ActionEvent): Unit = {
collapseAll(jTree)
}
}
)
jmView add jmiCollapse
val jmiGoto = new JMenuItem(
new AbstractAction("Go to unit") {
putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_N, menuKey, false))
override def actionPerformed(actionEvent: ActionEvent): Unit = {
val query = JOptionPane.showInputDialog("Go to unit:", frame.getOwner)
if (query ne null) { // "Cancel" returns null
val units = treeModel.program.units
units.find(_.unit.source.file.name startsWith query) foreach { unit =>
// skip through 1-ary trees
def expando(tree: Tree): List[Tree] = tree.children match {
case only :: Nil => only :: expando(only)
case _ => tree :: Nil
}
val path = new TreePath((treeModel.getRoot :: unit :: expando(unit.unit.body)).toArray[AnyRef]) // targ necessary to disambiguate Object and Object[] ctors
jTree.expandPath(path)
jTree.setSelectionPath(path)
}
}
}
}
)
jmView add jmiGoto
add(jmView)
}
def setTreeModel(tm: ASTTreeModel): Unit = treeModel = tm
}
/**
* Present detailed information about the selected tree node.
*/
class TextInfoPanel extends JTextArea(20, 50) {
setBorder(BorderFactory.createEmptyBorder(borderSize, borderSize, borderSize, borderSize))
setEditable(false)
setFont(new Font("monospaced", Font.PLAIN, 12))
def update(v: AnyRef): Unit = {
val t: Tree = v.asInstanceOf[Tree]
val str = new StringBuilder()
var buf = new StringWriter()
t match {
case ProgramTree(_) => ()
case UnitTree(_) => ()
case _ =>
str.append("tree.id: ").append(t.id)
str.append("\ntree.pos: ").append(t.pos)
str.append(TreeInfo.attachments(t, "tree"))
str.append("\nSymbol: ").append(TreeInfo.symbolText(t))
str.append("\nSymbol owner: ").append(
if ((t.symbol ne null) && t.symbol != NoSymbol)
t.symbol.owner.toString
else
"NoSymbol has no owner")
if ((t.symbol ne null) && t.symbol.isType) {
str.append("\ntermSymbol: " + t.symbol.tpe.termSymbol
+ "\ntypeSymbol: " + t.symbol.tpe.typeSymbol)
if (t.symbol.isTypeSkolem)
str.append("\nSkolem of: " + t.symbol.deSkolemize)
}
str.append("\nSymbol tpe: ")
if (t.symbol ne null) {
str.append(t.symbol.tpe).append("\n")
buf = new StringWriter()
TypePrinter.toDocument(t.symbol.tpe).format(getWidth() / getColumnWidth(), buf)
str.append(buf.toString)
}
str.append("\n\nSymbol info: \n")
TreeInfo.symbolTypeDoc(t).format(getWidth() / getColumnWidth(), buf)
str.append(buf.toString)
str.append("\n\nSymbol Attributes: \n").append(TreeInfo.symbolAttributes(t))
str.append("\ntree.tpe: ")
if (t.tpe ne null) {
str.append(t.tpe.toString).append("\n")
buf = new StringWriter()
TypePrinter.toDocument(t.tpe).format(getWidth() / getColumnWidth(), buf)
str.append(buf.toString)
}
}
setText(str.toString)
}
}
/** Computes different information about a tree node. It
* is used as central place to do all pattern matching against
* Tree.
*/
object TreeInfo {
/** Return the case class name and the Name, if the node defines one */
def treeName(t: Tree): (String, Name) = ((t.productPrefix, t match {
case UnitTree(unit) => newTermName("" + unit)
case Super(_, mix) => newTermName("mix: " + mix)
case This(qual) => qual
case Select(_, selector) => selector
case Ident(name) => name
case SelectFromTypeTree(_, selector) => selector
case x: DefTree => x.name
case _ => EMPTY
}))
/** Return a list of children for the given tree node */
def children(t: Tree): List[Tree] = t match {
case ProgramTree(units) =>
units
case UnitTree(unit) =>
List(unit.body)
case DocDef(comment, definition) =>
List(definition)
case ClassDef(mods, name, tparams, impl) => {
var children: List[Tree] = List()
children = tparams ::: children
mods.annotations ::: impl :: children
}
case PackageDef(pid, stats) =>
stats
case ModuleDef(mods, name, impl) =>
mods.annotations ::: List(impl)
case ValDef(mods, name, tpe, rhs) =>
mods.annotations ::: List(tpe, rhs)
case DefDef(mods, name, tparams, vparams, tpe, rhs) =>
mods.annotations ::: tpe :: rhs :: vparams.flatten ::: tparams
case TypeDef(mods, name, tparams, rhs) =>
mods.annotations ::: rhs :: tparams // @M: was List(rhs, lobound)
case Import(expr, selectors) =>
List(expr)
case CaseDef(pat, guard, body) =>
List(pat, guard, body)
case Template(parents, self, body) =>
parents ::: List(self) ::: body
case LabelDef(name, params, rhs) =>
params ::: List(rhs)
case Block(stats, expr) =>
stats ::: List(expr)
case Alternative(trees) =>
trees
case Bind(name, rhs) =>
List(rhs)
case UnApply(fun, args) =>
fun :: args
case Match(selector, cases) =>
selector :: cases
case Function(vparams, body) =>
vparams ::: List(body)
case Assign(lhs, rhs) =>
List(lhs, rhs)
case If(cond, thenp, elsep) =>
List(cond, thenp, elsep)
case Return(expr) =>
List(expr)
case Throw(expr) =>
List(expr)
case New(init) =>
List(init)
case Typed(expr, tpe) =>
List(expr, tpe)
case TypeApply(fun, args) =>
List(fun) ::: args
case Apply(fun, args) =>
List(fun) ::: args
case ApplyDynamic(qual, args) =>
List(qual) ::: args
case Super(qualif, mix) =>
List(qualif)
case This(_) =>
Nil
case Select(qualif, _) =>
List(qualif)
case Ident(name) =>
Nil
case Literal(value) =>
Nil
case TypeTree() =>
Nil
case Annotated(annot, arg) =>
annot :: List(arg)
case SingletonTypeTree(ref) =>
List(ref)
case SelectFromTypeTree(qualif, _) =>
List(qualif)
case CompoundTypeTree(templ) =>
List(templ)
case AppliedTypeTree(tpe, args) =>
tpe :: args
case TypeBoundsTree(lo, hi) =>
List(lo, hi)
case ExistentialTypeTree(tpt, whereClauses) =>
tpt :: whereClauses
case Try(block, catches, finalizer) =>
block :: catches ::: List(finalizer)
case ArrayValue(elemtpt, elems) =>
elemtpt :: elems
case EmptyTree =>
Nil
case Star(t) =>
List(t)
case x => throw new MatchError(x)
}
/** Return a textual representation of this t's symbol */
def symbolText(t: Tree): String = {
val prefix =
if (t.hasSymbolField) "[has] "
else if (t.isDef) "[defines] "
else ""
prefix + t.symbol
}
/** Return t's symbol type */
def symbolTypeDoc(t: Tree): Document = {
val s = t.symbol
if (s ne null)
TypePrinter.toDocument(s.info)
else
DocNil
}
/** Return a textual representation of (some of) the symbol's
* attributes */
def symbolAttributes(t: Tree): String = {
val s = t.symbol
if ((s ne null) && (s != NoSymbol)) {
val str = new StringBuilder(s.flagString)
if (s.isStaticMember) str ++= " isStatic "
str ++= " annotations: "
str ++= s.annotations.mkString("", " ", "")
if (s.isTypeSkolem) {
str ++= "\ndeSkolemized annotations: "
str ++= s.deSkolemize.annotations.mkString("", " ", "")
}
str ++= attachments(s, "")
str.toString
}
else ""
}
def attachments(t: Attachable, pre: String): String = {
if (t.attachments.isEmpty) ""
else t.attachments.all.mkString(s"\n$pre attachments:\n ","\n ","")
}
}
object TypePrinter {
///////////////// Document pretty printer ////////////////
implicit def view(n: String): Document = DocText(n)
def toDocument(sym: Symbol): Document =
toDocument(sym.info)
def symsToDocument(syms: List[Symbol]): Document = syms match {
case Nil => DocNil
case s :: Nil => Document.group(toDocument(s))
case _ =>
Document.group(
syms.tail.foldLeft (toDocument(syms.head) :: ", ") (
(d: Document, s2: Symbol) => toDocument(s2) :: ", " :/: d) )
}
def toDocument(ts: List[Type]): Document = ts match {
case Nil => DocNil
case t :: Nil => Document.group(toDocument(t))
case _ =>
Document.group(
ts.tail.foldLeft (toDocument(ts.head) :: ", ") (
(d: Document, t2: Type) => toDocument(t2) :: ", " :/: d) )
}
def toDocument(t: Type): Document = t match {
case ErrorType => "ErrorType()"
case WildcardType => "WildcardType()"
case NoType => "NoType()"
case NoPrefix => "NoPrefix()"
case ThisType(s) => "ThisType(" + s.nameString + ")"
case SingleType(pre, sym) =>
Document.group(
Document.nest(4, "SingleType(" :/:
toDocument(pre) :: ", " :/: sym.nameString :: ")")
)
case ConstantType(value) =>
"ConstantType(" + value + ")"
case TypeRef(pre, sym, args) =>
Document.group(
Document.nest(4, "TypeRef(" :/:
toDocument(pre) :: ", " :/:
sym.nameString :: ", " :/:
"[ " :: toDocument(args) ::"]" :: ")")
)
case TypeBounds(lo, hi) =>
Document.group(
Document.nest(4, "TypeBounds(" :/:
toDocument(lo) :: ", " :/:
toDocument(hi) :: ")")
)
case RefinedType(parents, _) =>
Document.group(
Document.nest(4, "RefinedType(" :/:
toDocument(parents) :: ")")
)
case ClassInfoType(parents, _, clazz) =>
Document.group(
Document.nest(4,"ClassInfoType(" :/:
toDocument(parents) :: ", " :/:
clazz.nameString :: ")")
)
case MethodType(params, result) =>
Document.group(
Document.nest(4, "MethodType(" :/:
Document.group("(" :/:
symsToDocument(params) :/:
"), ") :/:
toDocument(result) :: ")")
)
case NullaryMethodType(result) =>
Document.group(
Document.nest(4,"NullaryMethodType(" :/:
toDocument(result) :: ")")
)
case PolyType(tparams, result) =>
Document.group(
Document.nest(4,"PolyType(" :/:
Document.group("(" :/:
symsToDocument(tparams) :/:
"), ") :/:
toDocument(result) :: ")")
)
case AnnotatedType(annots, tp) =>
Document.group(
Document.nest(4, "AnnotatedType(" :/:
annots.mkString("[", ",", "]") :/:
"," :/: toDocument(tp) :: ")")
)
case ExistentialType(tparams, result) =>
Document.group(
Document.nest(4, "ExistentialType(" :/:
Document.group("(" :/: symsToDocument(tparams) :/: "), ") :/:
toDocument(result) :: ")"))
case ImportType(expr) =>
"ImportType(" + expr.toString + ")"
case SuperType(thistpe, supertpe) =>
Document.group(
Document.nest(4, "SuperType(" :/:
toDocument(thistpe) :/: ", " :/:
toDocument(supertpe) ::")"))
case ErasedValueType(clazz, erased) =>
Document.group(
Document.nest(4, "ErasedValueType(" :/:
clazz.nameString :/: ", " :/:
toDocument(erased) :: ")"))
case _ =>
abort("Unknown case: " + t.toString +", "+ t.getClass)
}
}
}
object TreeBrowsers {
case object DocNil extends Document
case object DocBreak extends Document
case class DocText(txt: String) extends Document
case class DocGroup(doc: Document) extends Document
case class DocNest(indent: Int, doc: Document) extends Document
case class DocCons(hd: Document, tl: Document) extends Document
/**
* A basic pretty-printing library, based on Lindig's strict version
* of Wadler's adaptation of Hughes' pretty-printer.
*
* @author Michel Schinz
*/
sealed abstract class Document {
def ::(hd: Document): Document = DocCons(hd, this)
def ::(hd: String): Document = DocCons(DocText(hd), this)
def :/:(hd: Document): Document = hd :: DocBreak :: this
def :/:(hd: String): Document = hd :: DocBreak :: this
/**
* Format this document on `writer` and try to set line
* breaks so that the result fits in `width` columns.
*/
def format(width: Int, writer: Writer): Unit = {
type FmtState = (Int, Boolean, Document)
@tailrec
def fits(w: Int, state: List[FmtState]): Boolean = state match {
case _ if w < 0 => false
case List() => true
case (_, _, DocNil) :: z => fits(w, z)
case (i, b, DocCons(h, t)) :: z => fits(w, (i, b, h) :: (i, b, t) :: z)
case (_, _, DocText(t)) :: z => fits(w - t.length(), z)
case (i, b, DocNest(ii, d)) :: z => fits(w, (i + ii, b, d) :: z)
case (_, false, DocBreak) :: z => fits(w - 1, z)
case (_, true, DocBreak) :: _ => true
case (i, _, DocGroup(d)) :: z => fits(w, (i, false, d) :: z)
}
def spaces(n: Int): Unit = {
var rem = n
while (rem >= 16) { writer.write(" ") ; rem -= 16 }
if (rem >= 8) { writer.write(" ") ; rem -= 8 }
if (rem >= 4) { writer.write(" ") ; rem -= 4 }
if (rem >= 2) { writer.write(" ") ; rem -= 2 }
if (rem == 1) { writer.write(" ") }
}
@tailrec
def fmt(k: Int, state: List[FmtState]): Unit = state match {
case List() => ()
case (_, _, DocNil) :: z => fmt(k, z)
case (i, b, DocCons(h, t)) :: z => fmt(k, (i, b, h) :: (i, b, t) :: z)
case (_, _, DocText(t)) :: z => writer.write(t) ; fmt(k + t.length(), z)
case (i, b, DocNest(ii, d)) :: z => fmt(k, (i + ii, b, d) :: z)
case (i, true, DocBreak) :: z => writer.write("\n") ; spaces(i) ; fmt(i, z)
case (_, false, DocBreak) :: z => writer.write(" ") ; fmt(k + 1, z)
case (i, _, DocGroup(d)) :: z => fmt(k, (i, !fits(width - k, (i, false, d) :: z), d) :: z)
case _ => ()
}
fmt(0, (0, false, DocGroup(this)) :: Nil)
}
}
object Document {
/** The empty document */
def empty = DocNil
/** A break, which will either be turned into a space or a line break */
def break = DocBreak
/** A document consisting of some text literal */
def text(s: String): Document = DocText(s)
/**
* A group, whose components will either be printed with all breaks
* rendered as spaces, or with all breaks rendered as line breaks.
*/
def group(d: Document): Document = DocGroup(d)
/** A nested document, which will be indented as specified. */
def nest(i: Int, d: Document): Document = DocNest(i, d)
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy