Getting Parameters from Scala Macro Annotation

2019-04-23 06:59发布

So I have an annotation on a function (DefDef). This annotation has parameters. However, I am confused on how to get the parameters from the constructor.

Usage example:

class TestMacro {
  @Foo(true)
  def foo(): String = ""
  foo
}

Here's the code for the annotation:

class Foo(b: Boolean) extends StaticAnnotation {
  def macroTransform(annottees: Any*) = macro Foo.impl
}

object Foo {
  def impl(c: whitebox.Context)(annottees: c.Tree*): c.Expr[Any] = {
    import c.universe._
    //how do I get value of `b` here???
    c.abort(c.enclosingPosition, "message")
  }
}

2条回答
2楼-- · 2019-04-23 07:47

What about this:

val b: Boolean = c.prefix.tree match {
    case q"new Foo($b)" => c.eval[Boolean](c.Expr(b))
}

For sake of completeness this is the full source:

import scala.reflect.macros.Context
import scala.language.experimental.macros
import scala.annotation.StaticAnnotation
import scala.annotation.compileTimeOnly
import scala.reflect.api.Trees
import scala.reflect.runtime.universe._

class Foo(b: Boolean) extends StaticAnnotation {
  def macroTransform(annottees: Any*) :Any = macro FooMacro.impl
}

object FooMacro {
  def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._
    val b: Boolean = c.prefix.tree match {
        case q"new Foo($b)" => c.eval[Boolean](c.Expr(b))
    }
    c.abort(c.enclosingPosition, "message")
  }
}
查看更多
冷血范
3楼-- · 2019-04-23 07:53

This is an answer that shows a variation on Federico's technique, if you want to use a static annotation that has optional named arguments. In that case, you need to consider the possible invocation expressions in the case matching statement. An optional argument might be explicitly named, it might be given without a name, or it might be not present. Each of these shows up at compile time as a separate pattern in c.prefix.tree, as shown below.

@compileTimeOnly("Must enable the Scala macro paradise compiler plugin to expand static annotations")
class noop(arg1: Int, arg2: Int = 0) extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro AnnotationMacros.noop
}

class AnnotationMacros(val c: whitebox.Context) {
  import c.universe._

  // an annotation that doesn't do anything:
  def noop(annottees: c.Expr[Any]*): c.Expr[Any] = {
    // cases for handling optional arguments
    val (arg1q, arg2q) = c.prefix.tree match {
      case q"new noop($arg1, arg2 = $arg2)" => (arg1, arg2)  // user gave named arg2
      case q"new noop($arg1, $arg2)" => (arg1, arg2)         // arg2 without name
      case q"new noop($arg1)" => (arg1, q"0")                // arg2 defaulted
      case _ => c.abort(c.enclosingPosition, "unexpected annotation pattern!")
    }

    // print out the values
    println(s"arg1= ${evalTree[Int](arg1q)}   arg2= ${evalTree[Int](arg2q)}")

    // just return the original annotee:
    annottees.length match {
      case 1 => c.Expr(q"{ ${annottees(0)} }")
      case _ => c.abort(c.enclosingPosition, "Only one annottee!")
    }
  }

  def evalTree[T](tree: Tree) = c.eval(c.Expr[T(c.untypecheck(tree.duplicate)))
}

Here is an example invocation that names arg2, and so it will match the first pattern - case q"new noop($arg1, arg2 = $arg2)" - above:

object demo {
  // I will match this pattern: case q"new noop($arg1, arg2 = $arg2)"
  @noop(1, arg2 = 2)
  trait someDeclarationToAnnotate
}

Note also that because of the way these patterns work, you have to explicitly supply the default argument value inside the macro code, which is unfortunately a bit hacky, but the final evaluated class is not available to you.

As an experiment, I tried actually creating the class by calling evalTree[scope.of.class.noop](c.prefix.tree), but the Scala compiler throws an error because it considered that a reference to the annotation inside the annotation macro code, which is illegal.

查看更多
登录 后发表回答