3

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

tribbloid
  • 4,026
  • 14
  • 64
  • 103
  • Which Scala version? Maybe you can achieve something with macros. Though I'm not sure you've given enough details about the context of why you need to do this ? – Gaël J Oct 02 '21 at 06:36

1 Answers1

3

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).

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Thanks a lot! If I got lucky it will be in the test code of the next version of scalac – tribbloid Oct 03 '21 at 21:52
  • It works, but with a caveat: The code must be parsed & typechecked. Or an error will be thrown before lihaoyi's macro can be invoked – tribbloid Dec 19 '21 at 20:37
  • 1
    @tribbloid In Scala all def macros are expanded after their arguments are typechecked. – Dmytro Mitin Dec 19 '21 at 21:41
  • @DmytroMitin, do you know a way to replace variables defined outside the convert method by their values? Like: `val i = 5; convert(10 + i) // 10 + 5` – Maxence Cramet Apr 07 '22 at 16:28
  • @MaxenceCramet Yes https://stackoverflow.com/questions/63132189 https://stackoverflow.com/questions/13767582 https://stackoverflow.com/questions/60806283 https://stackoverflow.com/questions/59387459 – Dmytro Mitin Aug 31 '22 at 19:59