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
What should be the result?
Currently the annotation transforms into
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
MyClass1and its companion but can't rewriteMyClass2or its companion. For thatMyClass2should 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.typecheckScala 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.isCaseClassHow to check if some T is a case class at compile time in Scala?
Hardly you need
ClassTags.One more complication is when
MyClass1andMyClass2are in the same scopeThen upon expansion of macro annotation for
MyClass1the scope isn't typechecked yet, so it's impossible to typecheck the tree of field definitionmc: Option[MyClass2](classMyClass2is not known yet). If the classes are in different scopes it's okThis is modified version of your code (I'm just printing the fields of the second class)