Search code examples
scalacode-generationscala-macrosscala-macro-paradise

Accessing class definition by name from scala macro


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

Solution

  • 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)
      }
    }