SemanticTree
Source:
SemanticTree.scala
SemanticTree
is a sealed data structure that encodes tree nodes that are
generated by the compiler from inferred type parameters, implicit arguments,
implicit conversions, inferred .apply
and for-comprehensions.
sealed abstract class SemanticTree extends Product with Serializable {
final override def toString: String = Pretty.pretty(this).render(80)
final def toString(width: Int): String = Pretty.pretty(this).render(width)
final def isEmpty: Boolean = this == NoTree
final def nonEmpty: Boolean = !isEmpty
}
final case class IdTree(info: SymbolInformation) extends SemanticTree { def symbol: Symbol = info.symbol }
final case class SelectTree(qualifier: SemanticTree, id: IdTree) extends SemanticTree
final case class ApplyTree(function: SemanticTree, arguments: List[SemanticTree]) extends SemanticTree
final case class TypeApplyTree(function: SemanticTree, typeArguments: List[SemanticType]) extends SemanticTree
final case class FunctionTree(parameters: List[IdTree], body: SemanticTree) extends SemanticTree
final case class LiteralTree(constant: Constant) extends SemanticTree
final case class MacroExpansionTree(beforeExpansion: SemanticTree, tpe: SemanticType) extends SemanticTree
final case class OriginalSubTree(tree: scala.meta.Tree) extends SemanticTree
final case class OriginalTree(tree: scala.meta.Tree) extends SemanticTree
case object NoTree extends SemanticTree
Cookbook
All code examples in this document assume you have the following imports in scope
import scalafix.v1._
import scala.meta._
The variable doc
in the code examples is an implicit instance of
scalafix.v1.SemanticDoc
.
Look up inferred type parameter
Consider the following program.
Option.apply(1) // inferred type parameter
Use Tree.synthetic
on the qualifier Option.apply
to get the inferred type
parameters
doc.tree.traverse {
// Option.apply
case option @ Term.Select(Term.Name("Option"), Term.Name("apply")) =>
println("synthetic = " + option.synthetics)
println("structure = " + option.synthetics.structure)
}
// synthetic = List(*[Int])
// structure = List(
// TypeApplyTree(
// OriginalTree(Term.Select(Term.Name("Option"), Term.Name("apply"))),
// List(TypeRef(NoType, Symbol("scala/Int#"), List()))
// )
// )
The asterisk *
represents an OriginalTree
node that matches the enclosing
non-synthetic tree, which is List
in this example.
The .synthetics
method is only available on Term
nodes, using the method on
other tree nodes such as types results in compilation error
doc.tree.traverse {
case app @ Type.Name("App") =>
println(".synthetic = " + app.synthetics)
}
// error: value synthetics is not a member of scala.meta.Type.Name
// println(".synthetic = " + app.synthetics)
// ^^^^^^^^^^^^^^
Look up symbol of semantic tree
Consider the following program.
val add: Int => Int = _ + 1
add(2) // inferred: add.apply(2)
Option[Int](2) // inferred: Option.apply[Int](2)
Use Tree.synthetics
in combination with SemanticTree.symbol
to get the symbol
of those inferred .apply
method calls.
doc.tree.traverse {
case Term.Apply.After_4_6_0(add @ q"add", Term.ArgClause(List(q"2"), _)) =>
println("add(2)")
println("synthetic = " + add.synthetics)
println("symbol = " + add.synthetics.flatMap(_.symbol).structure)
println("structure = " + add.synthetics.structure)
case Term.ApplyType.After_4_6_0(option @ q"Option", Term.ArgClause(List(t"Int"), _)) =>
println("Option[Int]")
println("synthetic = " + option.synthetics)
println("symbol = " + option.synthetics.flatMap(_.symbol).structure)
println("structure = " + option.synthetics.structure)
}
// add(2)
// synthetic = List(*.apply)
// symbol = List(Symbol("scala/Function1#apply()."))
// structure = List(
// SelectTree(
// OriginalTree(Term.Name("add")),
// IdTree(SymbolInformation(scala/Function1#apply(). => abstract method apply(v1: T1): R))
// )
// )
The .symbol
method returns nothing for the following semantic trees
MacroExpansionTree
LiteralTree
FunctionTree
NoTree
Look up implicit argument
Consider the following program.
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def run(implicit message: String) = println(message)
implicit val message = "hello world"
Future.apply[Int](3) // implicit argument: global
Main.run // implicit argument: message
Use Tree.synthetics
to look up an implicit argument for any Term
node.
doc.tree.traverse {
case term: Term if term.synthetics.nonEmpty =>
println("term = " + term.syntax)
println("synthetics = " + term.synthetics)
println("structure = " + term.synthetics.structure)
}
// term = Future.apply[Int](3)
// synthetics = List(*(global))
// structure = List(
// ApplyTree(
// OriginalTree(Term.Apply(
// Term.ApplyType(
// Term.Select(Term.Name("Future"), Term.Name("apply")),
// Type.ArgClause(List(Type.Name("Int")))
// ),
// Term.ArgClause(List(Lit.Int(3)), None)
// )),
// List(
// IdTree(SymbolInformation(scala/concurrent/ExecutionContext.Implicits.global(). => final implicit method global: ExecutionContext))
// )
// )
// )
// term = Main.run
// synthetics = List(*(message))
// structure = List(
// ApplyTree(
// OriginalTree(Term.Select(Term.Name("Main"), Term.Name("run"))),
// List(
// IdTree(SymbolInformation(_empty_/Main.message. => implicit val method message: String))
// )
// )
// )
Look up inferred type parameters for infix operators
Infix operators such as a ++ b
can have type parameters like a ++[Int] b
.
Consider the following program.
List(1) ++ List(2)
Use the Term.ApplyInfix.syntheticOperators
to look up inferred type parameters
of infix operators.
doc.tree.traverse {
case concat @ Term.ApplyInfix.After_4_6_0(_, Term.Name("++"), _, _) =>
println(".syntheticOperators = " + concat.syntheticOperators)
println(".structure = " + concat.syntheticOperators.structure)
}
// .syntheticOperators = List(*[Int])
// .structure = List(
// TypeApplyTree(
// OriginalTree(Term.ApplyInfix(
// Term.Apply(Term.Name("List"), Term.ArgClause(List(Lit.Int(1)), None)),
// Term.Name("++"),
// Type.ArgClause(List()),
// Term.ArgClause(
// List(
// Term.Apply(Term.Name("List"), Term.ArgClause(List(Lit.Int(2)), None))
// ),
// None
// )
// )),
// List(TypeRef(NoType, Symbol("scala/Int#"), List()))
// )
// )
The .syntheticOperators
method is only available for Term.ApplyInfix
nodes,
using the method on other node types results in a compilation error
doc.tree.traverse {
case concat @ Term.Name("++") =>
println(".syntheticOperator = " + concat.syntheticOperator)
}
Beware that looking up synthetics for the infix operator name returns nothing
doc.tree.traverse {
case concat @ Term.Name("++") =>
println(".synthetics = " + concat.synthetics)
}
// .synthetics = List()
for
comprehension desugaring
Look up Consider the following program.
val numbers = for {
i <- List(1, 2)
j <- 1.to(i)
} yield i + j
for (number <- numbers) println(number)
Use Tree.synthetics
on the tree node Term.ForYield
to inspect the desugared
version of the for { .. } yield
expression
doc.tree.traverse {
case forYield: Term.ForYield =>
println(".synthetics = " + forYield.synthetics)
}
// .synthetics = List(orig(List(1, 2)).flatMap[Int](
// { (i) => orig(1.to(i)).map[Int]({ (j) => orig(i + j) }) }
// ))
The orig(List(1, 2))
and orig(1.to(i)
parts represent OriginalSubTree
nodes that match non-synthetic tree nodes from the original for-comprehension.
Known limitations
The SemanticTree
data structure does not encode all possible synthetic code
that may get generated at compile-time. See
scalameta/scalameta#1711
if you are interested in contributing to improve the situation.
for
patterns
For comprehensions that use patterns produce incomplete semantic trees.
for {
(a, b) <- List(1 -> 2) // pattern
} yield a + b
Observe the empty withFilter
body and <unknown>
parameter symbol.
doc.tree.traverse {
case forYield: Term.ForYield =>
println(forYield.synthetics)
}
// List(
// orig(List(1 -> 2)).withFilter(
// { (check$ifrefutable$1) =>
//
// }
// ).map[Int](
// { (<unknown>) =>
//
// }
// )
// )
for
assignments
For comprehensions that use assignments produce incomplete semantic trees.
for {
a <- List(1)
b = a + 1 // assignment
} yield a + b
Observe the <unknown>
parameter symbol to the final call to map
.
doc.tree.traverse {
case forYield: Term.ForYield =>
println(forYield.synthetics)
}
// List(
// orig(List(1)).map[Tuple2[Int,Int]](
// { (a) =>
// orig(b = a + 1)
// }
// ).map[Int](
// { (<unknown>) =>
// orig(a + b)
// }
// )
// )
Macros
Macros can expand into arbitrary code including new definitions such as methods and classes. Semantic trees only encode tree nodes that are generated by the compiler through offical language features like type inference and implicit search.
SemanticDB
The structure of SemanticTree
mirrors SemanticDB Tree
. Consult the
SemanticDB specification for more details about synthetics:
The SemanticTree
data structure diverges from SemanticDB Tree
in few minor
details.
SemanticTree
instead of Tree
Scalafix uses the name SemanticTree
instead of Tree
in order to avoid
ambiguous references with scala.meta.Tree
when importing the two packages
together.
import scalafix.v1._
import scala.meta._
OriginalTree
vs. OriginalSubTree
Scalafix has two kinds of original trees, OriginalTree
and OriginalSubTree
,
while the SemanticDB specification has only one OriginalTree
. The difference
between the two is that
OriginalTree
represents synthetic trees whose range match exactly the enclosingSynthetic
range.OriginalSubTree
represents trees whose range is smaller than the enclosingSynthetic
range.
This change avoids the need for the SemanticDB Synthetic
type.