Scala annotations are not found - scala

I have a case class with annotated fields, like this:
case class Foo(#alias("foo") bar: Int)
I have a macro that processes the declaration of this class:
val (className, access, fields, bases, body) = classDecl match {
case q"case class $n $m(..$ps) extends ..$bs { ..$ss }" => (n, m, ps, bs, ss)
case _ => abort
}
Later, I search for the aliased fields, as follows:
val aliases = fields.asInstanceOf[List[ValDef]].flatMap {
field => field.symbol.annotations.collect {
//deprecated version:
//case annotation if annotation.tpe <:< cv.weakTypeOf[alias] =>
case annotation if annotation.tree.tpe <:< c.weakTypeOf[alias] =>
//deprecated version:
//annotation.scalaArgs.head match {
annotation.tree.children.tail.head match {
case Literal(Constant(param: String)) => (param, field.name)
}
}
}
However, the list of aliases ends up being empty. I have determined that field.symbol.annotations.size is, in fact, 0, despite the annotation clearly sitting on the field.
Any idea of what's wrong?
EDIT
Answering the first two comments:
(1) I tried mods.annotations, but that didn't work. That actually returns List[Tree] instead of List[Annotation], returned by symbol.annotations. Perhaps I didn't modify the code correctly, but the immediate effect was an exception during macro expansion. I'll try to play with it some more.
(2) The class declaration is grabbed while processing an annotation macro slapped on the case class.
The complete code follows. The usage is illustrated in the test code further below.
package com.xxx.util.macros
import scala.collection.immutable.HashMap
import scala.language.experimental.macros
import scala.annotation.StaticAnnotation
import scala.reflect.macros.whitebox
trait Mapped {
def $(key: String) = _vals.get(key)
protected def +=(key: String, value: Any) =
_vals += ((key, value))
private var _vals = new HashMap[String, Any]
}
class alias(val key: String) extends StaticAnnotation
class aliased extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro aliasedMacro.impl
}
object aliasedMacro {
def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val (classDecl, compDecl) = annottees.map(_.tree) match {
case (clazz: ClassDef) :: Nil => (clazz, None)
case (clazz: ClassDef) :: (comp: ModuleDef) :: Nil => (clazz, Some(comp))
case _ => abort(c, "#aliased must annotate a class")
}
val (className, access, fields, bases, body) = classDecl match {
case q"case class $n $m(..$ps) extends ..$bs { ..$ss }" => (n, m, ps, bs, ss)
case _ => abort(c, "#aliased is only supported on case class")
}
val mappings = fields.asInstanceOf[List[ValDef]].flatMap {
field => field.symbol.annotations.collect {
case annotation if annotation.tree.tpe <:< c.weakTypeOf[alias] =>
annotation.tree.children.tail.head match {
case Literal(Constant(param: String)) =>
q"""this += ($param, ${field.name})"""
}
}
}
val classCode = q"""
case class $className $access(..$fields) extends ..$bases {
..$body; ..$mappings
}"""
c.Expr(compDecl match {
case Some(compCode) => q"""$compCode; $classCode"""
case None => q"""$classCode"""
})
}
protected def abort(c: whitebox.Context, message: String) =
c.abort(c.enclosingPosition, message)
}
The test code:
package test.xxx.util.macros
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
import org.junit.runner.RunWith
import com.xxx.util.macros._
#aliased
case class Foo(#alias("foo") foo: Int,
#alias("BAR") bar: String,
baz: String) extends Mapped
#RunWith(classOf[JUnitRunner])
class MappedTest extends FunSuite {
val foo = 13
val bar = "test"
val obj = Foo(foo, bar, "extra")
test("field aliased with its own name") {
assertResult(Some(foo))(obj $ "foo")
}
test("field aliased with another string") {
assertResult(Some(bar))(obj $ "BAR")
assertResult(None)(obj $ "bar")
}
test("unaliased field") {
assertResult(None)(obj $ "baz")
}
}

Thanks for the suggestions! In the end, using field.mods.annotations did help. This is how:
val mappings = fields.asInstanceOf[List[ValDef]].flatMap {
field => field.mods.annotations.collect {
case Apply(Select(New(Ident(TypeName("alias"))), termNames.CONSTRUCTOR),
List(Literal(Constant(param: String)))) =>
q"""this += ($param, ${field.name})"""
}
}

Related

Scala whitebox macro how to check if class fields are of type of a case class

I am trying to generate a case class from a given case class that strips of Option from the fields. It needs to this recursively, so if the field itself is a case class then it must remove Option from it's fields as well.
So far I managed to it for where no fields are not a case class. But for recursion I need to get the ClassTag for the field if it's a case class. But I have no idea how I can do this. Seems like all I can access is the syntax tree before type check (I guess makes sense considering the final source code isn't formed yet). But I am wondering if it's possible to achieve this in some way.
Here is my code and the missing part as comment.
import scala.annotation.StaticAnnotation
import scala.collection.mutable
import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros
import scala.annotation.compileTimeOnly
class RemoveOptionFromFields extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro RemoveOptionFromFields.impl
}
object RemoveOptionFromFields {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
def modifiedClass(classDecl: ClassDef, compDeclOpt: Option[ModuleDef]) = {
val result = classDecl match {
case q"case class $className(..$fields) extends ..$parents { ..$body }" =>
val fieldsWithoutOption = fields.map {
case ValDef(mods, name, tpt, rhs) =>
tpt.children match {
case List(first, second) if first.toString() == "Option" =>
// Check if `second` is a case class?
// Get it's fields if so
val innerType = tpt.children(1)
ValDef(mods, name, innerType, rhs)
case _ =>
ValDef(mods, name, tpt, rhs)
}
}
val withOptionRemovedFromFieldsClassDecl = q"case class WithOptionRemovedFromFields(..$fieldsWithoutOption)"
val newCompanionDecl = compDeclOpt.fold(
q"""
object ${className.toTermName} {
$withOptionRemovedFromFieldsClassDecl
}
"""
) {
compDecl =>
val q"object $obj extends ..$bases { ..$body }" = compDecl
q"""
object $obj extends ..$bases {
..$body
$withOptionRemovedFromFieldsClassDecl
}
"""
}
q"""
$classDecl
$newCompanionDecl
"""
}
c.Expr[Any](result)
}
annottees.map(_.tree) match {
case (classDecl: ClassDef) :: Nil => modifiedClass(classDecl, None)
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifiedClass(classDecl, Some(compDecl))
case _ => c.abort(c.enclosingPosition, "This annotation only supports classes")
}
}
}
Not sure I understand what kind of recursion you need. Suppose we have two case classes: the 1st annotated (referring the 2nd) and the 2nd not annotated
#RemoveOptionFromFields
case class MyClass1(mc: Option[MyClass2])
case class MyClass2(i: Option[Int])
What should be the result?
Currently the annotation transforms into
case class MyClass1(mc: Option[MyClass2])
object MyClass1 {
case class WithOptionRemovedFromFields(mc: Class2)
}
case class MyClass2(i: Option[Int])
if the field itself is a case class then it must remove Option from it's fields as well.
Macro annotation can rewrite only class and its companion, it can't rewrite different classes. In my example with 2 classes the annotation can modify MyClass1 and its companion but can't rewrite MyClass2 or its companion. For that MyClass2 should be annotated itself.
In a scope macro annotations are expanded before type checking of this scope. So upon rewriting trees are untyped. If you need some trees to be typed (so that you can find their symbols) you can use c.typecheck
Scala macros: What is the difference between typed (aka typechecked) and untyped Trees
To check that some class is a case class you can use symbol.isClass && symbol.asClass.isCaseClass
How to check if some T is a case class at compile time in Scala?
Hardly you need ClassTags.
One more complication is when MyClass1 and MyClass2 are in the same scope
#RemoveOptionFromFields
case class MyClass1(mc: Option[MyClass2])
case class MyClass2(i: Option[Int])
Then upon expansion of macro annotation for MyClass1 the scope isn't typechecked yet, so it's impossible to typecheck the tree of field definition mc: Option[MyClass2] (class MyClass2 is not known yet). If the classes are in different scopes it's ok
{
#RemoveOptionFromFields
case class MyClass1(mc: Option[MyClass2])
}
case class MyClass2(i: Option[Int])
This is modified version of your code (I'm just printing the fields of the second class)
import scala.annotation.StaticAnnotation
import scala.reflect.macros.blackbox
import scala.language.experimental.macros
import scala.annotation.compileTimeOnly
#compileTimeOnly("enable macro annotations")
class RemoveOptionFromFields extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro RemoveOptionFromFields.impl
}
object RemoveOptionFromFields {
def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
import c.universe._
def modifiedClass(classDecl: ClassDef, compDeclOpt: Option[ModuleDef]) = {
classDecl match {
case q"$mods class $className[..$tparams] $ctorMods(..$fields) extends { ..$earlydefns } with ..$parents { $self => ..$body }"
if mods.hasFlag(Flag.CASE) =>
val fieldsWithoutOption = fields.map {
case field#q"$mods val $name: $tpt = $rhs" =>
tpt match {
case tq"$first[..${List(second)}]" =>
val firstType = c.typecheck(tq"$first", mode = c.TYPEmode, silent = true) match {
case EmptyTree => println(s"can't typecheck $first while expanding #RemoveOptionFromFields for $className"); NoType
case t => t.tpe
}
if (firstType <:< typeOf[Option[_]].typeConstructor) {
val secondSymbol = c.typecheck(tq"$second", mode = c.TYPEmode, silent = true) match {
case EmptyTree => println(s"can't typecheck $second while expanding #RemoveOptionFromFields for $className"); NoSymbol
case t => t.symbol
}
if (secondSymbol.isClass && secondSymbol.asClass.isCaseClass) {
val secondClassFields = secondSymbol.typeSignature.decls.toList.filter(s => s.isMethod && s.asMethod.isCaseAccessor)
secondClassFields.foreach(s =>
c.typecheck(q"$s", silent = true) match {
case EmptyTree => println(s"can't typecheck $s while expanding #RemoveOptionFromFields for $className")
case t => println(s"field ${t.symbol} of type ${t.tpe}, subtype of Option: ${t.tpe <:< typeOf[Option[_]]}")
}
)
}
q"$mods val $name: $second = $rhs"
} else field
case _ =>
field
}
}
val withOptionRemovedFromFieldsClassDecl = q"case class WithOptionRemovedFromFields(..$fieldsWithoutOption)"
val newCompanionDecl = compDeclOpt.fold(
q"""
object ${className.toTermName} {
$withOptionRemovedFromFieldsClassDecl
}
"""
) {
compDecl =>
val q"$mods object $obj extends { ..$earlydefns } with ..$bases { $self => ..$body }" = compDecl
q"""
$mods object $obj extends { ..$earlydefns } with ..$bases { $self =>
..$body
$withOptionRemovedFromFieldsClassDecl
}
"""
}
q"""
$classDecl
$newCompanionDecl
"""
}
}
annottees match {
case (classDecl: ClassDef) :: Nil => modifiedClass(classDecl, None)
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifiedClass(classDecl, Some(compDecl))
case _ => c.abort(c.enclosingPosition, "This annotation only supports classes")
}
}
}

Implementing Enumeratum support for Swagger

I'm using Swagger to annotate my API, and in our API we rely a lot on enumeratum. If I don't do anything, swagger won't recognize it and just call it object.
For example, I have this code that works:
sealed trait Mode extends EnumEntry
object Mode extends Enum[Mode] {
override def values = findValues
case object Initial extends Mode
case object Delta extends Mode
}
#ApiModel
case class Foobar(
#ApiModelProperty(dataType = "string", allowedValues = "Initial,Delta")
mode: Mode
)
However, I would like to avoid repeating the values as some of my types have many more than this example; I don't want to manually keep that in sync.
The problem is that the #ApiModel wants a constant in reference, so I can't do something like reference = Mode.values.mkString(",").
I did try a macro with macro paradise, typically so I can write:
#EnumeratumApiModel(Mode)
sealed trait Mode extends EnumEntry
object Mode extends Enum[Mode] {
override def values = findValues
case object Initial extends Mode
case object Delta extends Mode
}
...but it doesn't work because the macro pass can't access the Mode object.
What solution do I have to avoid repeating the values in the annotation?
This includes code so is too big for a comment.
I tried, that wouldn't work because the #ApiModel annotation wants a String constant as a value (and not a reference to a constant)
This piece of code compiles just fine for me (notice how you should avoid explicitly specifying the type):
import io.swagger.annotations._
import enumeratum._
#ApiModel(reference = Mode.reference)
sealed trait Mode extends EnumEntry
object Mode extends Enum[Mode] {
final val reference = "enum(Initial,Delta)" // this works!
//final val reference: String = "enum(Initial,Delta)" // surprisingly this doesn't!
override def values = findValues
case object Initial extends Mode
case object Delta extends Mode
}
So it seems to be enough to have another macro that would generate such reference string and I assume you already have one (or you can create one basing on the code of EnumMacros.findValuesImpl).
Update
Here is some code for POC that this can actually work. First you start with following macro annotation:
import scala.language.experimental.macros
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.reflect.macros.whitebox.Context
import scala.collection.immutable._
#compileTimeOnly("enable macro to expand macro annotations")
class SwaggerEnumContainer extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro SwaggerEnumMacros.genListString
}
#compileTimeOnly("enable macro to expand macro annotations")
class SwaggerEnumValue(val readOnly: Boolean = false, val required: Boolean = false) extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro SwaggerEnumMacros.genParamAnnotation
}
class SwaggerEnumMacros(val c: Context) {
import c.universe._
def genListString(annottees: c.Expr[Any]*): c.Expr[Any] = {
val result = annottees.map(_.tree).toList match {
case (xxx#q"object $name extends ..$parents { ..$body }") :: Nil =>
val enclosingObject = xxx.asInstanceOf[ModuleDef]
val q"${tq"$pname[..$ptargs]"}(...$pargss)" = parents.head
val enumTraitIdent = ptargs.head.asInstanceOf[Ident]
val subclassSymbols: List[TermName] = enclosingObject.impl.body.foldLeft(List.empty[TermName])((list, innerTree) => {
innerTree match {
case innerObj: ModuleDefApi =>
val innerParentIdent = innerObj.impl.parents.head.asInstanceOf[Ident]
if (enumTraitIdent.name.equals(innerParentIdent.name))
innerObj.name :: list
else
list
case _ => list
}
})
val reference = subclassSymbols.map(n => n.encodedName.toString).mkString(",")
q"""
object $name extends ..$parents {
final val allowableValues = $reference
..$body
}
"""
}
c.Expr[Any](result)
}
def genParamAnnotation(annottees: c.Expr[Any]*): c.Expr[Any] = {
val annotationParams: AnnotationParams = extractAnnotationParameters(c.prefix.tree)
val baseSwaggerAnnot =
q""" new ApiModelProperty(
dataType = "string",
allowableValues = Mode.allowableValues
) """.asInstanceOf[Apply] // why I have to force cast?
val swaggerAnnot: c.universe.Apply = annotationParams.addArgsTo(baseSwaggerAnnot)
annottees.map(_.tree).toList match {
// field definition
case List(param: ValDef) => c.Expr[Any](decorateValDef(param, swaggerAnnot))
// field in a case class = constructor param
case (param: ValDef) :: (rest#(_ :: _)) => decorateConstructorVal(param, rest, swaggerAnnot)
case _ => c.abort(c.enclosingPosition, "SwaggerEnumValue is expected to be used for value definitions")
}
}
def decorateValDef(valDef: ValDef, swaggerAnnot: Apply): ValDef = {
val q"$mods val $name: $tpt = $rhs" = valDef
val newMods: Modifiers = mods.mapAnnotations(al => swaggerAnnot :: al)
q"$newMods val $name: $tpt = $rhs"
}
def decorateConstructorVal(annottee: c.universe.ValDef, expandees: List[Tree], swaggerAnnot: Apply): c.Expr[Any] = {
val q"$_ val $tgtName: $_ = $_" = annottee
val outputs = expandees.map {
case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => {
// paramss is a 2d array so map inside map
val newParams: List[List[ValDef]] = paramss.map(_.map({
case valDef: ValDef if valDef.name == tgtName => decorateValDef(valDef, swaggerAnnot)
case otherParam => otherParam
}))
q"$mods class $tpname[..$tparams] $ctorMods(...$newParams) extends { ..$earlydefns } with ..$parents { $self => ..$stats }"
}
case otherTree => otherTree
}
c.Expr[Any](Block(outputs, Literal(Constant(()))))
}
case class AnnotationParams(readOnly: Boolean, required: Boolean) {
def customCopy(name: String, value: Any) = {
name match {
case "readOnly" => copy(readOnly = value.asInstanceOf[Boolean])
case "required" => copy(required = value.asInstanceOf[Boolean])
case _ => c.abort(c.enclosingPosition, s"Unknown parameter '$name'")
}
}
def addArgsTo(annot: Apply): Apply = {
val additionalArgs: List[AssignOrNamedArg] = List(
AssignOrNamedArg(q"readOnly", q"$readOnly"),
AssignOrNamedArg(q"required", q"$required")
)
Apply(annot.fun, annot.args ++ additionalArgs)
}
}
private def extractAnnotationParameters(tree: Tree): AnnotationParams = tree match {
case ap: Apply =>
val argNames = Array("readOnly", "required")
val defaults = AnnotationParams(readOnly = false, required = false)
ap.args.zipWithIndex.foldLeft(defaults)((acc, argAndIndex) => argAndIndex match {
case (lit: Literal, index: Int) => acc.customCopy(argNames(index), c.eval(c.Expr[Any](lit)))
case (namedArg: AssignOrNamedArg, _: Int) =>
val q"$name = $lit" = namedArg
acc.customCopy(name.asInstanceOf[Ident].name.toString, c.eval(c.Expr[Any](lit)))
case _ => c.abort(c.enclosingPosition, "Failed to parse annotation params: " + argAndIndex)
})
}
}
And then you can do this:
sealed trait Mode extends EnumEntry
#SwaggerEnumContainer
object Mode extends Enum[Mode] {
override def values = findValues
case object Initial extends Mode
case object Delta extends Mode
}
#ApiModel
case class Foobar(#ApiModelProperty(dataType = "string", allowableValues = Mode.allowableValues) mode: Mode)
Or you can do this which I think is a bit cleaner
#ApiModel
case class Foobar2(
#SwaggerEnumValue mode: Mode,
#SwaggerEnumValue(true) mode2: Mode,
#SwaggerEnumValue(required = true) mode3: Mode,
i: Int, s: String = "abc") {
#SwaggerEnumValue
val modeField: Mode = Mode.Delta
}
Note that this is still only a POC. Known deficiencies include:
#SwaggerEnumContainer can't handle case when some fake allowableValues is already defined with some fake value (which might be nicer for IDE)
#SwaggerEnumValue only supports two attributes from the range available in the original #ApiModelProperty

Scalameta: Identify particular annotations

I want to auto-generate REST API models in Scala using scalameta annotation macros. Specifically, given:
#Resource case class User(
#get id : Int,
#get #post #patch name : String,
#get #post email : String,
registeredOn : Long
)
I want to generate:
object User {
case class Get(id: Int, name: String, email: String)
case class Post(name: String, email: String)
case class Patch(name: Option[String])
}
trait UserRepo {
def getAll: Seq[User.Get]
def get(id: Int): User.Get
def create(request: User.Post): User.Get
def replace(id: Int, request: User.Put): User.Get
def update(id: Int, request: User.Patch): User.Get
def delete(id: Int): User.Get
}
I have something working here: https://github.com/pathikrit/metarest
Specifically I am doing this:
import scala.collection.immutable.Seq
import scala.collection.mutable
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.meta._
class get extends StaticAnnotation
class put extends StaticAnnotation
class post extends StaticAnnotation
class patch extends StaticAnnotation
#compileTimeOnly("#metarest.Resource not expanded")
class Resource extends StaticAnnotation {
inline def apply(defn: Any): Any = meta {
val (cls: Defn.Class, companion: Defn.Object) = defn match {
case Term.Block(Seq(cls: Defn.Class, companion: Defn.Object)) => (cls, companion)
case cls: Defn.Class => (cls, q"object ${Term.Name(cls.name.value)} {}")
case _ => abort("#metarest.Resource must annotate a class")
}
val paramsWithAnnotation = for {
Term.Param(mods, name, decltype, default) <- cls.ctor.paramss.flatten
seenMods = mutable.Set.empty[String]
modifier <- mods if seenMods.add(modifier.toString)
(tpe, defArg) <- modifier match {
case mod"#get" | mod"#put" | mod"#post" => Some(decltype -> default)
case mod"#patch" =>
val optDeclType = decltype.collect({case tpe: Type => targ"Option[$tpe]"})
val defaultArg = default match {
case Some(term) => q"Some($term)"
case None => q"None"
}
Some(optDeclType -> Some(defaultArg))
case _ => None
}
} yield modifier -> Term.Param(Nil, name, tpe, defArg)
val models = paramsWithAnnotation
.groupBy(_._1.toString)
.map({case (verb, pairs) =>
val className = Type.Name(verb.stripPrefix("#").capitalize)
val classParams = pairs.map(_._2)
q"case class $className[..${cls.tparams}] (..$classParams)"
})
val newCompanion = companion.copy(
templ = companion.templ.copy(stats = Some(
companion.templ.stats.getOrElse(Nil) ++ models
))
)
Term.Block(Seq(cls, newCompanion))
}
}
I am unhappy with the following snip of code:
modifier match {
case mod"#get" | mod"#put" | mod"#post" => ...
case mod"#patch" => ...
case _ => None
}
The above code does "stringly" pattern matching on the annotations I have. Is there anyway to re-use the exact annotations I have to pattern match for these:
class get extends StaticAnnotation
class put extends StaticAnnotation
class post extends StaticAnnotation
class patch extends StaticAnnotation
It's possible to replace the mod#get stringly typed annotation with a get() extractor using a bit of runtime reflection (at compile time).
In addition, let's say we also want to allow users to fully qualify the annotation with #metarest.get or #_root_.metarest.get
All the following code examples assume import scala.meta._. The tree structure of #get, #metarest.get and #_root_.metarest.get are
# mod"#get".structure
res4: String = """ Mod.Annot(Ctor.Ref.Name("get"))
"""
# mod"#metarest.get".structure
res5: String = """
Mod.Annot(Ctor.Ref.Select(Term.Name("metarest"), Ctor.Ref.Name("get")))
"""
# mod"#_root_.metarest.get".structure
res6: String = """
Mod.Annot(Ctor.Ref.Select(Term.Select(Term.Name("_root_"), Term.Name("metarest")), Ctor.Ref.Name("get")))
"""
The selectors are either Ctor.Ref.Select or Term.Select and the names are either Term.Name or Ctor.Ref.Name.
Let's first create a custom selector extractor
object Select {
def unapply(tree: Tree): Option[(Term, Name)] = tree match {
case Term.Select(a, b) => Some(a -> b)
case Ctor.Ref.Select(a, b) => Some(a -> b)
case _ => None
}
}
Then create a few helper utilities
object ParamAnnotation {
/* isSuffix(c, a.b.c) // true
* isSuffix(b.c, a.b.c) // true
* isSuffix(a.b.c, a.b.c) // true
* isSuffix(_root_.a.b.c, a.b.c) // true
* isSuffix(d.c, a.b.c) // false
*/
def isSuffix(maybeSuffix: Term, fullName: Term): Boolean =
(maybeSuffix, fullName) match {
case (a: Name, b: Name) => a.value == b.value
case (Select(q"_root_", a), b: Name) => a.value == b.value
case (a: Name, Select(_, b)) => a.value == b.value
case (Select(aRest, a), Select(bRest, b)) =>
a.value == b.value && isSuffix(aRest, bRest)
case _ => false
}
// Returns true if `mod` matches the tree structure of `#T`
def modMatchesType[T: ClassTag](mod: Mod): Boolean = mod match {
case Mod.Annot(term: Term.Ref) =>
isSuffix(term, termRefForType[T])
case _ => false
}
// Parses `T.getClass.getName` into a Term.Ref
// Uses runtime reflection, but this happens only at compile time.
def termRefForType[T](implicit ev: ClassTag[T]): Term.Ref =
ev.runtimeClass.getName.parse[Term].get.asInstanceOf[Term.Ref]
}
With this setup, we can add a companion object to the get definition with an
unapply boolean extractor
class get extends StaticAnnotation
object get {
def unapply(mod: Mod): Boolean = ParamAnnotation.modMatchesType[get](mod)
}
Doing the same for post and put, we can now write
// before
case mod"#get" | mod"#put" | mod"#post" => Some(decltype -> default)
// after
case get() | put() | post() => Some(decltype -> default)
Note that this approach will still not work if the user renames for example get on import
import metarest.{get => GET}
I would recommend aborting if an annotation does not match what you expected
// before
case _ => None
// after
case unexpected => abort("Unexpected modifier $unexpected. Expected one of: put, get post")
PS. The object get { def unapply(mod: Mod): Boolean = ... } part is boilerplate that could be generated by some #ParamAnnotation macro annotation, for example #ParamAnnotion class get extends StaticAnnotation

Scala compile time macro, filter argument list based on (parent) type

I have a method/constructor that takes a bunch of parameters.
Using a scala macro I can ofcourse extract the Tree representing the type of those parameters.
But I cannot find out how to convert this tree to something "useful", i.e. that I can get the parent types of, check if it is a primitive, etc.
Lets say I have a concrete type C and if want all parameters that inherit from C or are subtypes of Seq[C].
For a bit of context:
case cd#q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$_ } with ..$_ { $self => ..$stats }" :: tail =>
// extract everything that is subtype of C or Seq[C]
val cs = paramss.head.map {
case q"$mods val $name: $tpt = $default" => ???
}
Everything that goes in a macro should be typechecked, right? So $tpt should have a "type"?
How do I get it and what exactly do I get back?
This is how I determine if a type inherits from Iterable and not from Map, and if a type inherits from Map
val iterableType = typeOf[Iterable[_]].typeSymbol
val mapType = typeOf[Map[_, _]].typeSymbol
def isIterable(tpe: Type): Boolean = tpe.baseClasses.contains(iterableType) && !isMap(tpe)
def isMap(tpe: Type): Boolean = tpe.baseClasses.contains(mapType)
I use this on e.g. the fields in a class's primary constructor:
// tpe is a Type
val fields = tpe.declarations.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor => m
}.get.paramss.head
val iterableFields = fields.filter(f => isIterable(f.typeSignature))
val mapFields = fields.filter(f => isMap(f.typeSignature))
I haven't used them in anger, but it looks like you're using macro annotations.
No, you get untyped annotees.
http://docs.scala-lang.org/overviews/macros/annotations.html
macro annottees are untyped, so that we can change their signatures
Given a client:
package thingy
class C
#testThing class Thingy(val c: C, i: Int) { def j = 2*i }
object Test extends App {
Console println Thingy(42).stuff
}
then then the macro implementation sees:
val t = (annottees map (c typecheck _.tree)) match {
case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$_ } with ..$_ { $self => ..$stats }" :: _ =>
val p # q"$mods val $name: ${tpt: Type} = $default" = paramss.head.head
Console println showRaw(tpt)
Console println tpt.baseClasses
toEmit
}
where the tree has been typechecked explicitly and the type is annotated in the extraction:
case q"val $name: ${tpt: Type} = ???" =>
I realize my spelling of annotee differs from the official docs. I just can't figure out why it should be different from devotee.
Here is the toy skeleton code, for other beginners:
package thingy
import scala.annotation.StaticAnnotation
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
class testThing extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro testThing.impl
}
object testThing {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val toEmit = q"""{
class Thingy(i: Int) {
def stuff = println(i)
}
object Thingy {
def apply(x: Int) = new Thingy(x)
}
()
}"""
def dummy = Literal(Constant(()))
//val t = (annottees map (_.tree)) match {
val t = (annottees map (c typecheck _.tree)) match {
case Nil =>
c.abort(c.enclosingPosition, "No test target")
dummy
case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$_ } with ..$_ { $self => ..$stats }" :: _ =>
//val p # q"$mods val $name: $tpt = $default" = paramss.head.head
val p # q"$mods val $name: ${tpt: Type} = $default" = paramss.head.head
Console println showRaw(tpt)
Console println tpt.baseClasses
toEmit
/*
case (classDeclaration: ClassDef) :: Nil =>
println("No companion provided")
toEmit
case (classDeclaration: ClassDef) :: (companionDeclaration: ModuleDef) :: Nil =>
println("Companion provided")
toEmit
*/
case _ => c.abort(c.enclosingPosition, "Invalid test target")
dummy
}
c.Expr[Any](t)
}
}

Insert one method invocation into many other methods

I need to add a method to some other methods in the application. There are quite a lot of them. Is there any sensible way to do that except the straightforward one -- inserting the method call into each method manually?
See this answer for implementation with def macros.
Implementation with macro annotations:
import scala.reflect.macros.Context
import scala.language.experimental.macros
def wrappedImpl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
import scala.reflect.internal.Flags._
object DefMods{
def unapply(mods: Modifiers): Option[Modifiers] = {
val fs = mods.flags.asInstanceOf[Long]
if ((fs & DEFERRED) == 0 && (fs & STABLE) == 0) Some(mods)
else None
}
}
def wrap(t: Tree) = t match {
case DefDef(DefMods(mods), name, tparams, vparams, tpt, body) if name != nme.CONSTRUCTOR =>
DefDef(mods, name, tparams, vparams, tpt,
q"""
println("before")
val res = $body
println("after")
res""")
case x => x
}
def transform(t: Tree) = t match {
case ClassDef(mods, name, tparams, Template(parents, self, methods)) =>
ClassDef(mods, name, tparams, Template(parents, self, methods.map{wrap(_)}))
case x => x
}
c.Expr[Any](Block(annottees.map(_.tree).map(transform(_)).toList, Literal(Constant(()))))
}
import scala.annotation.StaticAnnotation
class wrapped extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro wrappedImpl
}
You'll have to use a compiler plugin for quasiquotes.
Usage:
#wrapped class Test {
def test() = println("test")
}
scala> new Test().test()
before
test
after