Passing parameters to scalameta paradise macro

372 Views Asked by At

I am trying to create a macro annotation but I need to pass it parameters.

class ToPojo(param1: String, param2: String) extends StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
        ...
  }
}

which is used as

@ToPojo("p1", "p2")
case class Entity(a: Int, m: Map[Long, Boolean])

The problem with above code is that the Entity gets to the apply as defn already with the annotation stripped - hence I can not get the parameters from there. Also the param1 and param2 fields are not accessible from the apply method since it gets inlined.

Could you please point me to the easiest way to overcome this using scala meta? I thought about using two annotations

@ToPojo
@ToPojoParams("p1", "p2")
case class Entity(a: Int, m: Map[Long, Boolean])

But thats hacky and ugly.

Thanks a lot

1

There are 1 best solutions below

0
On BEST ANSWER

As described in the ScalaMeta Paradise description under How do I pass an argument to the macro annotation? section

You match on this as a Scalameta tree

package scalaworld.macros

import scala.meta._

class Argument(arg: Int) extends scala.annotation.StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    // `this` is a scala.meta tree.
    println(this.structure)
    val arg = this match {
      // The argument needs to be a literal like `1` or a string like `"foobar"`.
      // You can't pass in a variable name.
      case q"new $_(${Lit.Int(arg)})" => arg
      // Example if you have more than one argument.
      case q"new $_(${Lit.Int(arg)}, ${Lit.String(foo)})" => arg
      case _ => ??? // default value
    }
    println(s"Arg is $arg")
    defn.asInstanceOf[Stat]
  }
}   

Note that this code is a bit naive and doesn't handle named arguments. If you have many arguments, writing all possible combinations of usual and named arguments is boring. So you may try something like this:

package examples

import scala.collection.immutable
import scala.meta._

class MyMacro(p1: String, p2: Int) extends scala.annotation.StaticAnnotation {


  inline def apply(defn: Any): Any = meta {
    val params = Params.extractParams(this)
    //some implementation
    ... 
  }

}


case class Params(p1: String, p2: Int) {
  def update(name: String, value: Any): Params = name match {
    case "p1" => copy(p1 = value.asInstanceOf[String])
    case "p2" => copy(p2 = value.asInstanceOf[Int])
    case _ => ???
  }
}

object Params {
  private val paramsNames = List("p1", "p2")

  def extractParams(tree: Stat): Params = {
    val args: immutable.Seq[Term.Arg] = tree.asInstanceOf[Term.New].templ.parents.head.asInstanceOf[Term.Apply].args

    args.zipWithIndex.foldLeft(Params(null, 0))((acc, argAndIndex) => argAndIndex._1 match {
      case q"${Lit(value)}" => acc.update(paramsNames(argAndIndex._2), value)
      case q"${Term.Arg.Named(name, Lit(value))}" => acc.update(name.value, value)
    })
  }
}