Search code examples
scalametaprogrammingscala-macros

How to convert scala code block to string?


I need to implement a testing function which checks compile-time error information for the "splain" plugin, part of this function needs to convert a codeblock into a string, e.g.:

def convert(fn: => Unit): String

// for testing

val code = convert {
  object I extends Seq {}
}

assert(code == "object I extends Seq {}")

Is this possible using standard scala features? Thanks a lot for your advice.

This function will enable verifications of compile-time messages of complex code that needs to be indexed and refactored by IDE often


Solution

  • Yes, it's possible.

    Li Haoyi's macro Text from sourcecode

    def text[T: c.WeakTypeTag](c: Compat.Context)(v: c.Expr[T]): c.Expr[sourcecode.Text[T]] = {
      import c.universe._
      val fileContent = new String(v.tree.pos.source.content)
      val start = v.tree.collect {
        case treeVal => treeVal.pos match {
          case NoPosition ⇒ Int.MaxValue
          case p ⇒ p.startOrPoint
        }
      }.min
      val g = c.asInstanceOf[reflect.macros.runtime.Context].global
      val parser = g.newUnitParser(fileContent.drop(start))
      parser.expr()
      val end = parser.in.lastOffset
      val txt = fileContent.slice(start, start + end)
      val tree = q"""${c.prefix}(${v.tree}, $txt)"""
      c.Expr[sourcecode.Text[T]](tree)
    }
    

    does almost what you want:

    def convert[A](fn: => Text[A]): String = fn.source
    
    convert(10 + 20 +
      30
    )
    
    //10 + 20 +
    //  30
    

    Unfortunately,

    if you have multiple statements in a {} block, sourcecode.Text will only capture the source code for the last expression that gets returned.

    And since { object I extends Seq {} } is actually { object I extends Seq {}; () } the macro will not work in this case.

    So let's write our own simple macro

    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    def convert(fn: => Any): String = macro convertImpl
    
    def convertImpl(c: blackbox.Context)(fn: c.Tree): c.Tree = {
      import c.universe._
    
      val pos = fn.pos
      val res = new String(pos.source.content).slice(pos.start, pos.end)
    
      Literal(Constant(res))
    }
    

    Usage:

    trait Seq
    
    convert {
      val i: Int = 1
      object I extends Seq {}
      10 + 20 + 30
      convert(1)
    }
    
    //{
    //    val i: Int = 1
    //    object I extends Seq {}
    //    10 + 20 + 30
    //    convert(1)
    //  }
    

    Notice that arguments of def macros are typechecked before macro expansion (so convert { val i: Int = "a" }, convert { object I extends XXX } without defined XXX, convert { (; } etc. will not compile).