Scalameta Tree Guide

This notebook is based on the Scalameta Tree Guide from the Scalameta documenation.

A core functionality of Scalameta is syntax trees, which enable you to read, analyze, transform and generate Scala programs at a level of abstraction. In this guide, you will learn how to

  • parse source code into syntax trees
  • construct new syntax trees
  • pattern match syntax trees
  • traverse syntax trees
  • transform syntax trees

Installation

Add a dependency to Scalameta in your build to get started. Scalameta supports Scala 2.11, Scala 2.12, Scala.js and Scala Native.

sbt

// build.sbt
libraryDependencies += "org.scalameta" %% "scalameta" % "4.2.3"

// For Scala.js, Scala Native
libraryDependencies += "org.scalameta" %%% "scalameta" % "4.2.3"

Maven Central

All code examples assume you have the following import

import scala.meta._

import scalafix.v1._

Ammonite REPL or Jupyter Notebook with Almond

A great way to experiment with Scalameta is to use the Ammonite REPL or Almond.

In [1]:
// Ammonite REPL
import $ivy.`org.scalameta::scalameta:4.2.3`, scala.meta._
Out[1]:
import $ivy.$                               , scala.meta._

ScalaFiddle

You can try out Scalameta online with the ScalaFiddle playground.

What is a syntax tree?

Syntax trees are a representation of source code that makes it easier to programmatically analyze programs. Scalameta has syntax trees that represent Scala programs.

Scalameta trees are lossless, meaning that they represent Scala programs in sufficient to go from text to trees and vice-versa. Lossless syntax trees are great for fine-grained analysis of source code, which is useful for a range of applications including formatting, refactoring, linting and documentation tools

Parse trees

Scalameta comes with a parser to produce syntax trees from Scala source code. You can parse trees from a variety of sources into different kinds of tree nodes.

From strings

The simplest way to parse source code is from a string. As long as you have import scala.meta._ in your scope, you can use the parse[Source] extension method

In [2]:
val program = """object Main extends App { print("Hello!") }"""
val tree = program.parse[Source].get
Out[2]:
program: String = "object Main extends App { print(\"Hello!\") }"
tree: Source = Source(
  List(
    Defn.Object(
      List(),
      Term.Name("Main"),
      Template(
        List(),
        List(Init(Type.Name("App"), , List())),
        Self(, None),
        List(Term.Apply(Term.Name("print"), List(Lit.String("Hello!"))))
      )
    )
  )
)

Once parsed, you can print the tree back into its original source code

In [3]:
println(tree.syntax)
object Main extends App { print("Hello!") }

The problem with parsing from strings it that error messages don't include a filename

In [4]:
println(
  "object Main {".parse[Source]
)
<input>:1: error: } expected but end of file found
object Main {
             ^

To make error messages more helpful it's recommended to always use virtual files when possible, as explained below.

From files

To parse a file into a tree it's recommended to first read the file contents into a string and then construct a virtual file

In [5]:
val path = java.nio.file.Paths.get("example.scala")
val bytes = java.nio.file.Files.readAllBytes(path)
val text = new String(bytes, "UTF-8")
val input = Input.VirtualFile(path.toString, text)
val exampleTree = input.parse[Source].get
Out[5]:
path: java.nio.file.Path = example.scala
bytes: Array[Byte] = Array(
  111,
  98,
  106,
  101,
  99,
  116,
  32,
  69,
  120,
  97,
  109,
  112,
  108,
  101,
  32,
  101,
  120,
  116,
  101,
  110,
  100,
  115,
  32,
  65,
  112,
  112,
  32,
  123,
  32,
  112,
  114,
  105,
  110,
  116,
  108,
  110,
  40,
  34,
...
text: String = """object Example extends App { println("Hello from a file!") }
"""
input: inputs.Input.VirtualFile = VirtualFile(
  "example.scala",
  """object Example extends App { println("Hello from a file!") }
"""
)
exampleTree: Source = Source(
  List(
    Defn.Object(
      List(),
      Term.Name("Example"),
      Template(
        List(),
        List(Init(Type.Name("App"), , List())),
        Self(, None),
        List(
          Term.Apply(
            Term.Name("println"),
            List(Lit.String("Hello from a file!"))
          )
        )
      )
    )
  )
)
In [6]:
print(exampleTree.syntax)
object Example extends App { println("Hello from a file!") }

The difference between text.parse[Source] and input.parse[Source] is that the filename appear in error messages for Input.VirtualFile.

In [7]:
println(
  Input.VirtualFile("example.scala", "object Main {").parse[Source]
)
example.scala:1: error: } expected but end of file found
object Main {
             ^

From expressions

To parse a simple expressions such as a + b use parse[Stat] The name Stat stands for "statement".

In [8]:
println("a + b".parse[Stat].get.structure)
Term.ApplyInfix(Term.Name("a"), Term.Name("+"), Nil, List(Term.Name("b")))

If we try to parse an expression with parse[Source] we get an error because a + b is not valid at the top-level for Scala programs

In [9]:
println("a + b".parse[Source])
<input>:1: error: expected class or object definition
a + b
^

The same solution can be used to parse other tree nodes such as types

In [10]:
println("A with B".parse[Type].get.structure)
Type.With(Type.Name("A"), Type.Name("B"))

If we use parse[Stat] to parse types we get an error

In [11]:
println("A with B".parse[Stat])
<input>:1: error: end of file expected but with found
A with B
  ^

From programs with multiple top-level statements

To parse programs with multiple top-level statements such as build.sbt files or Ammonite scripts we use the Sbt1 dialect. By default, we get an error when using parse[Source].

In [12]:
val buildSbt = """
val core = project
val cli = project.dependsOn(core)
"""
Out[12]:
buildSbt: String = """
val core = project
val cli = project.dependsOn(core)
"""
In [13]:
println(buildSbt.parse[Source])
<input>:2: error: expected class or object definition
val core = project
^

This error happens because vals are not allowed as top-level statements in normal Scala programs. To fix this problem, wrap the input with dialects.Sbt1

In [14]:
println(dialects.Sbt1(buildSbt).parse[Source].get.stats)
List(val core = project, val cli = project.dependsOn(core))

The same solution works for virtual files

In [15]:
println(
  dialects.Sbt1(
    Input.VirtualFile("build.sbt", buildSbt)
  ).parse[Source].get.stats
)
List(val core = project, val cli = project.dependsOn(core))

The difference between dialects.Sbt1(input) and parse[Stat] is that parse[Stat] does not allow multiple top-level statements

In [16]:
println(buildSbt.parse[Stat])
<input>:3: error: end of file expected but val found
val cli = project.dependsOn(core)
^

Note that dialects.Sbt1 does not accept programs with package declarations

In [17]:
println(
  dialects.Sbt1("package library; object Main").parse[Source]
)
<input>:1: error: illegal start of definition
package library; object Main
^

Construct trees

Sometimes we need to dynamically construct syntax trees instead of parsing them from source code. There are two primary ways to construct trees: normal constructors and quasiquotes.

With normal constructors

Normal tree constructors as plain functions

In [18]:
println(Term.Apply(Term.Name("function"), List(Term.Name("argument"))))
function(argument)

Although normal constructors are verbose, they give most flexibility when constructing trees.

To learn tree node names you can use .structure on existing tree nodes

In [19]:
println("function(argument)".parse[Stat].get.structure)
Term.Apply(Term.Name("function"), List(Term.Name("argument")))

The output of structure is safe to copy-paste into programs.

Another good way to learn the structure of trees is AST Explorer.

With quasiquotes

Quasiquotes are string interpolators that expand at compile-time into normal constructor calls

In [20]:
println(q"function(argument)".structure)
Term.Apply(Term.Name("function"), List(Term.Name("argument")))

You can write multiline quasiquotes to construct large programs

In [21]:
println(
  q"""
  object Example extends App {
    println(42)
  }
  """.structure
)
Defn.Object(Nil, Term.Name("Example"), Template(Nil, List(Init(Type.Name("App"), Name(""), Nil)), Self(Name(""), None), List(Term.Apply(Term.Name("println"), List(Lit.Int(42))))))

It's important to keep in mind that quasiquotes expand at compile-time into the same program as if you had written normal constructors by hand. This means for example that formatting details or comments are not preserved

In [22]:
println(q"function  (    argument   ) // comment")
function(argument)

Quasiquotes can be composed together like normal string interpolators with dollar splices $

In [23]:
val left = q"Left()"
val right = q"Right()"
println(q"$left + $right")
Left() + Right()
Out[23]:
left: Term.Apply = Term.Apply(Term.Name("Left"), List())
right: Term.Apply = Term.Apply(Term.Name("Right"), List())

A list of trees can be inserted into a quasiquote with double dots ..$

In [24]:
val arguments = List(q"arg1", q"arg2")
println(q"function(..$arguments)")
function(arg1, arg2)
Out[24]:
arguments: List[Term.Name] = List(Term.Name("arg1"), Term.Name("arg2"))

A curried argument argument lists can be inserted into a quasiquotes with triple dots ...$

In [25]:
val arguments2 = List(q"arg3", q"arg4")
val allArguments = List(arguments, arguments2)
println(q"function(...$allArguments)")
function(arg1, arg2)(arg3, arg4)
Out[25]:
arguments2: List[Term.Name] = List(Term.Name("arg3"), Term.Name("arg4"))
allArguments: List[List[Term.Name]] = List(
  List(Term.Name("arg1"), Term.Name("arg2")),
  List(Term.Name("arg3"), Term.Name("arg4"))
)

A common mistake is to splice an empty type parameter list into type application nodes . Imagine we have a list of type arguments that happens to be empty

In [26]:
val typeArguments = List.empty[Type]
Out[26]:
typeArguments: List[Type] = List()

If we directly splice the lists into a type application we get a cryptic error message "invariant failed"

In [27]:
q"function[..$typeArguments]()"
org.scalameta.invariants.InvariantFailedException: invariant failed:
when verifying targs.!=(null).&&(targs.nonEmpty)
found that targs.nonEmpty is false
where targs = List()
  org.scalameta.invariants.InvariantFailedException$.raise(Exceptions.scala:15)
  scala.meta.Term$ApplyType$.internal$49(Trees.scala:82)
  scala.meta.Term$ApplyType$.apply(Trees.scala:82)
  ammonite.$sess.cmd26$Helper.<init>(cmd26.sc:1)
  ammonite.$sess.cmd26$.<init>(cmd26.sc:7)
  ammonite.$sess.cmd26$.<clinit>(cmd26.sc:-1)

The quasiquote above is equivalent to calling the normal constructor Type.ApplyType(.., typeArguments). Scalameta trees perform strict runtime validation for invariants such as "type application arguments must be non-empty". To fix this problem, guard the splice against the length of the list

In [28]:
println(
  (if (typeArguments.isEmpty) q"function()"
   else q"function[..$typeArguments]()").structure
)
Term.Apply(Term.Name("function"), Nil)

To learn more about quasiquotes, consult the quasiquote spec.

Pattern match trees

Use pattern matching to target interesting tree nodes and deconstruct them. A core design principle of Scalameta trees is that tree pattern matching is the dual of tree construction. If you know how to construct a tree, you know how to de-construct it.

With normal constructors

Normal constructors work in pattern position the same way they work in regular term position.

In [29]:
"function(arg1, arg2)".parse[Term].get match {
  case Term.Apply(function, List(arg1, arg2)) =>
    println("1 " + function)
    println("2 " + arg1)
    println("3 " + arg2)
}
1 function
2 arg1
3 arg2

Repeated fields are always List[T], so you can safely deconstruct trees with the List(arg1, arg2) syntax or if you prefer the arg1 :: arg2 :: Nil syntax. There is no need to use Seq(arg1, arg2) or arg1 +: arg2 +: Nil.

With quasiquotes

Quasiquotes expand at compile-time and work the same way in pattern position as in term position.

In [30]:
Term.Apply(
  Term.Name("function"),
  List(Term.Name("arg1"), Term.Name("arg2"))
) match {
  case q"$function(..$args)" =>
    println("1 " + function)
    println("2 " + args)
}
1 function
2 List(arg1, arg2)

Use triple dollar splices ...$ to extract curried argument lists

In [31]:
"function(arg1, arg2)(arg3, arg4)".parse[Term].get match {
  case q"$function(...$args)" =>
    println("1 " + function)
    println("2 " + args)
}
1 function
2 List(List(arg1, arg2), List(arg3, arg4))

Pattern matching with quasiquotes is generally discouraged because it's easy to write patterns that result in unintended match errors.

In [32]:
q"final val x = 2" match {
  case q"val x = 2" => // boom!
}
scala.MatchError: final val x = 2 (of class scala.meta.Defn$Val$DefnValImpl)
  ammonite.$sess.cmd31$Helper.<init>(cmd31.sc:1)
  ammonite.$sess.cmd31$.<init>(cmd31.sc:7)
  ammonite.$sess.cmd31$.<clinit>(cmd31.sc:-1)

To fix this pattern, we specify that the final modifier should be ignored using $_

In [33]:
q"final val x = 2" match {
  case q"$_ val x = 2" => println("OK")
}
OK

Compare trees for equality

Scalameta trees use reference equality by default, which may result in surprising behavior. A common mistake is to use == between parsed syntax trees and quasiquotes

In [34]:
"true".parse[Term].get == q"true"
Out[34]:
res33: Boolean = false

Comparing trees by == is the same as comparing them with eq. Even identical quasiquotes produce different references

In [35]:
q"true" == q"true"
Out[35]:
res34: Boolean = false

Equality checks with == will only return true when the reference is the same.j

In [36]:
{ val treeReference = q"true"
  treeReference == treeReference }
Out[36]:
treeReference: Lit.Boolean = Lit.Boolean(true)
res35_1: Boolean = true

The idiomatic way to compare trees for structural equality is to use pattern matching

In [37]:
q"true" match { case q"true" => println("YAY!") }
YAY!

If you can't use pattern matching to compare trees by structural equality, you can use .structure

In [38]:
q"true".structure == q"true".structure
Out[38]:
res37: Boolean = true

The .structure method produces large strings for large programs, which may become prohibitively slow. The Scalameta contrib module contains a more efficient isEqual helper method to compare trees structurally.

In [38]:
import scala.meta.contrib._
q"true".isEqual(q"true")
cmd38.sc:1: object contrib is not a member of package meta
import scala.meta.contrib._
                  ^cmd38.sc:2: value isEqual is not a member of meta.Lit.Boolean
val res38_1 = q"true".isEqual(q"true")
                      ^Compilation Failed
Compilation Failed

Traverse trees

Scalameta includes utilities to recursively visit tree nodes for both simple and advanced use-cases. Simple use-cases have high-level APIs that require minimal ceremony while advanced use-cases use lower-level APIs that typically involve more side-effects.

Simple traversals

Use .traverse to visit every tree node and perform a side-effect, similarly to .foreach

In [39]:
q"val x = 2".traverse {
  case node =>
    println(s"${node.productPrefix}: $node")
}
Defn.Val: val x = 2
Pat.Var: x
Term.Name: x
Lit.Int: 2

Use .collect to visit every tree node and collect a value instead of performing a side-effect

In [40]:
q"val x = 2".collect {
  case node => node.productPrefix -> node.toString
}
Out[40]:
res39: List[(String, String)] = List(
  ("Defn.Val", "val x = 2"),
  ("Pat.Var", "x"),
  ("Term.Name", "x"),
  ("Lit.Int", "2")
)

The methods .traverse and .collect don't support customizing the recursion. For more fine-grained control over which tree nodes to visit implement a custom Traverser.

Custom traversals

Extend Traverser if you need to implement a custom tree traversal

In [41]:
val traverser = new Traverser {
  override def apply(tree: Tree): Unit = tree match {
    case Pat.Var(name) =>
      println(s"stop: $name")
    case node =>
      println(s"${node.productPrefix}: $node")
      super.apply(node)
  }
}
Out[41]:
traverser: Traverser = [email protected]

The super.apply(node) call continues the recursion, so in this case we will recursively visit all nodes except children of Pat.Var nodes.

In [42]:
traverser(q"val x = 2")
Defn.Val: val x = 2
stop: x
Lit.Int: 2

There is no .collect equivalent for custom traversals. To collect a value, it's recommended to use List.newBuilder[T] for the type you are interested in and append values inside the apply method.

Transform trees

Scalameta includes utilities to transform trees for simple and advanced use-cases.

Transformed trees do not preserve comments and formatting details when pretty-printed. Look into Scalafix if you need to implement fine-grained refactorings that preserve comments and formatting details.

Simple transformations

Use .transform to visit every tree node and transform interesting tree nodes.

In [43]:
println(
  q"val x = 2".transform { case q"2" => q"42" }
)
val x = 42

The contract of .transform is that it will recursively visit all tree nodes, including the transformed trees. Due to this behavior, a common mistake is to introduce infinite recursion in .transform

In [44]:
q"a + b".transform {
  case name @ Term.Name("b") => q"function($name)"
}.toString
// [error] java.lang.StackOverflowError
// at scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$2$.apply(Api.scala:10)
// at scala.meta.transversers.Transformer.apply(Transformer.scala:4)
java.lang.StackOverflowError
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Transformer.apply(Transformer.scala:4)
  scala.meta.transversers.Api$XtensionCollectionLikeUI$transformer$1$.apply(Api.scala:10)
  scala.meta.transversers.Api$XtensionCollectionLikeUI.transform(Api.scala:13)
  ammonite.$sess.cmd43$Helper.<init>(cmd43.sc:1)
  ammonite.$sess.cmd43$.<init>(cmd43.sc:7)
  ammonite.$sess.cmd43$.<clinit>(cmd43.sc:-1)

The best solution to fix this problem is to implement a custom transformer to gain fine-grained control over the recursion.

Custom transformations

Extend Transformer if you need to implement a custom tree transformation

In [45]:
val transformer = new Transformer {
  override def apply(tree: Tree): Tree = tree match {
    case name @ Term.Name("b") => q"function($name)"
    case node => super.apply(node)
  }
}
Out[45]:
transformer: Transformer = [email protected]

By avoiding the call to super.transform in the first case, we prevent a stack overflow.

In [46]:
println(
  transformer(q"a + b")
)
a + function(b)