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
MacroExpansionTreeLiteralTreeFunctionTreeNoTree
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()
Look up for comprehension desugaring
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(
// { (<unknown>) =>
//
// }
// ).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>) =>
//
// }
// )
// )
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
OriginalTreerepresents synthetic trees whose range match exactly the enclosingSyntheticrange.OriginalSubTreerepresents trees whose range is smaller than the enclosingSyntheticrange.
This change avoids the need for the SemanticDB Synthetic type.
