flavian flavian - 1 month ago 10
Scala Question

Scala macros: Derive type signature from a ValDef

A very straightforward macro compat backed macro annotation.

def impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._

annottees.map(_.tree) match {
case (classDef @ q"$mods class $tpname[..$tparams] $ctorMods(...$params) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
:: Nil if mods.hasFlag(Flag.CASE) =>
val name = tpname.toTermName
val typeName = tpname.toTypeName
val res = q"""
$classDef
object $name {
..${doStuff(c)(typeName, name, params.head)}
}
"""
c.Expr[Any](res)

case _ => c.abort(c.enclosingPosition, "Invalid annotation target, this must be a case class")
}
}


So all very simple straightforward fun. The bit causing problems originates from the
$params
above, which are just
List[List[ValDef]]
, namely somehow the type signature is getting lost.

def accessors(c: blackbox.Context)(
params: Seq[c.universe.ValDef]
): Iterable[(c.universe.TermName, c.universe.TypeName)] = {
import c.universe._

params.map {
case ValDef(mods: Modifiers, name: TermName, tpt: Tree, rhs: Tree) => {
// tpt.tpe = kaboom, null pointer
name -> TypeName(tpt.tpe.typeSymbol.fullName)
}
}
}


The
tpe
on
ValDef
comes back as
null
, so the defs are not typed, but I need the type signature of the params to achieve what I want. How can I get at the type signature of the params without it blowing up?

Ironically,
showCode(tpt)
does produce the right type string, so this can be worked around with
TypeName(tpt.toString)
, but I'm not sure why
tpe
is not accessible.

Answer

The correct way to do this is to evaluate the type arguments using c.typepcheck in c.TypeMode as follows:

  /**
    * Retrieves the accessor fields on a case class and returns an iterable of tuples of the form Name -> Type.
    * For every single field in a case class, a reference to the string name and string type of the field are returned.
    *
    * Example:
    *
    * {{{
    *   case class Test(id: UUID, name: String, age: Int)
    *
    *   accessors(Test) = Iterable("id" -> "UUID", "name" -> "String", age: "Int")
    * }}}
    *
    * @param params The list of params retrieved from the case class.
    * @return An iterable of tuples where each tuple encodes the string name and string type of a field.
    */
  def accessors(
    params: Seq[ValDef]
  ): Iterable[Accessor] = {
    params.map {
      case ValDef(_, name: TermName, tpt: Tree, _) => {
        Accessor(
          name,
          TypeName(tpt.toString),
          c.typecheck(tq"$tpt", c.TYPEmode).tpe
        )
      }
    }
  }

In this case Accessor is a custom case class that has to be defined inside a scope where import c.universe._ is available:

  case class Accessor(
    name: TermName,
    tpe: TypeName,
    paramType: Type
  ) {
    def symbol = paramType.typeSymbol
  }