I'm trying to create a Scala macro which defines a single a Class argument, and which modifies the class to which it is attached based on the implementation of the Class which is provided as an argument.
//Simple class with a few arguments
class A(a: String, b: String)
//Definition of this class should be modified based on class definition of A
@parameterized(classOf[A])
class B
I've managed to create a simple macro that is able to extract the argument from the annotation, resulting in a TypeName object that contains a string representation of the full class name.
The problem is now that I need to access the definition of A from the macro implementation (specifically, I want to see what the constructor arguments are).
Is there a way to access/create a TypeTag[A] in some way? Is there a way I can access the AST of class A?
To illustrate what I'm trying to achieve, this is what I currently have as a macro definition:
object parameterizedMacro {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
import Flag._
//Extract the parameter type which was provided as an argument (rather hacky way of getting this info)
val parameterType = c.macroApplication match {
case Apply(Select(Apply(_, List(
TypeApply(Ident(TermName("classOf")), List(Ident(TypeName(parameterType))))
)) , _), _) => parameterType
case _ =>
sys.error("Could not match @parameterized arguments. Was a class provided?")
}
//Should generate method list based on the code of parameterType
val methods = ???
val result = {
annottees.map(_.tree).toList match {
case q"object $name extends ..$parents { ..$body }" :: Nil =>
q"""
object $name extends ..$parents {
..${methods}
..$body
}
"""
case q"class $name (..$args) extends ..$parents { ..$body }" :: Nil =>
q"""
class $name (..$args) extends ..$parents {
..${methods}
..$body
}
"""
}
}
c.Expr[Any](result)
}
}
It took a lot of trial and error, as I didn't manage to find a lot of documentation on the various classes, but I did end up managing to achieve what I wanted to do.
To acquire the parameter, I ended up having to put the fully qualified name in the annotation instead of the original TypeOf[Name]
.
@parameterized(the.fully.qualified.Name)
Which I could then get using
val parameterType = c.macroApplication match {
case Apply(Select(Apply(_, List(
parameterType
)) , _), _) => parameterType
case _ =>
error("Could not match @parameterized arguments. Was a class provided?")
}
val fullClassName = parameterType.toString()
Using TypeOf
as mentioned in the original post didn't work for me as I didnt manage to get from the TypeApply
to the correct fully qualified name. This doesn't really make the whole thing insecure as everything is checked at compile time, but it does look a bit less like standard Scala.
Once I had the fully qualified name, I could use the universe's mirror (The documentation on mirrors helped here) to get a ClassSymbol, from which it is possible to get the class constructor arguments:
val constructorArguments = {
val clazz = c.mirror.staticClass(fullClassName) //Get ClassSymbol
val clazzInfo = clazz.info //Turn ClassSymbol into Type
val constructor = clazzInfo.member(termNames.CONSTRUCTOR) //Get constructor member Symbol
val constructorMethod = constructor.asMethod //Turn into MethodSymbol
val parametersList = constructorMethod.paramLists //Finally extract list of parameters
if (parametersList.size != 1)
error("Expected only a single constructor in " + fullClassName)
val parameters = parametersList.head
for (parameter <- parameters) yield {
val term = parameter.asTerm //parameter is a term
(term.name.toString, term.typeSignature)
}
}