Reputation: 346
I have a method in my project with macros (whitebox), trying to verify and extract type arguments from a return type of a MethodSymbol
.
Here is the code (placed inside some class with import c.universe._
):
private object ImplMethod {
def apply(m: MethodSymbol): ImplMethod = {
println(m.returnType)
val respType = m.returnType match {
case tq"scala.concurrent.Future[scala.util.Either[String, ${resp}]]" => resp
case _ =>
c.abort(c.enclosingPosition, s"Method ${m.name} in type ${m.owner} does not have required result type Future[Either[String, ?]]")
}
???
}
}
During the compilation it says Warning:scalac: scala.concurrent.Future[Either[String, Int]]
which is right, however right after that it stops on the c.abort
call which means the pattern doesn't match the type.
I even tried to debug it in REPL, but here is what I got:
val tq"scala.concurrent.Future[$a]" = typeOf[scala.concurrent.Future[Int]]
scala.MatchError: scala.concurrent.Future[Int] (of class
scala.reflect.internal.Types$ClassArgsTypeRef)
... 28 elided
I tried this many times already but always ended up with handling those types as String
s which is very unclear.
Thanks for reply!
Upvotes: 0
Views: 857
Reputation: 23788
I'd be grateful if someone showed an example of deconstructing Type
using quasiquotes and pattern-matching. For now it seems to me that Type
and quasiquotes are parts of different universes (not in Scala internals sense) and can't interact. The best way I know to do something like this is code like this:
val string = typeOf[String].dealias
val future = typeOf[scala.concurrent.Future[_]].typeConstructor
val either = typeOf[scala.util.Either[_, _]].typeConstructor
val respType = (for {
f <- Some(m.returnType.dealias) if f.typeConstructor == future // Future[_]
e <- f.typeArgs.headOption if e.typeConstructor == either // Future[Either[_,_]]
ea <- Some(e.typeArgs) if ea.head.dealias == string // Future[Either[String,_]]
} yield ea(1))
.getOrElse(c.abort(c.enclosingPosition, s"Method ${m.name} in type ${m.owner} does not have required result type Future[Either[String, ?]]"))
I use Some
to wrap Type
into Option
and use for-comprehension syntax that IMHO makes it easier to understand what's going on, much easier than what you could get if you tried to use usual (non-quasiquotes-based) pattern matching on Type
.
Update: Where tq""
works at all?`
From my experience the only context in which you can use tq""
to deconstruct type is in Macro annotations that can be used to annotate whole classes or method definitions. Consider following example:
import scala.concurrent.Future
import scala.util.Either
class Test {
@CheckReturnTypeMacroAnnotation
def foo1(): scala.concurrent.Future[scala.util.Either[String, Short]] = ???
@CheckReturnTypeMacroAnnotation
def foo2(): Future[Either[String, Int]] = ???
@CheckReturnTypeMacroAnnotation
def foo3(): scala.concurrent.Future[Either[String, Long]] = ???
@CheckReturnTypeMacroAnnotation
def foo4(): Future[scala.util.Either[String, Double]] = ???
@CheckReturnTypeMacroAnnotation
def fooBad() = scala.concurrent.Future.failed[scala.util.Either[String, Short]](new RuntimeException("Fake"))
}
We want CheckReturnTypeMacroAnnotation
to ensure that the return type is of form scala.concurrent.Future[scala.util.Either[String, ?]]
. We may implement CheckReturnTypeMacroAnnotation
as
import scala.language.experimental.macros
import scala.annotation.{StaticAnnotation, compileTimeOnly}
@compileTimeOnly("enable macro to expand macro annotations")
class CheckReturnTypeMacroAnnotation extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro CheckReturnTypeMacro.process
}
object CheckReturnTypeMacro {
import scala.reflect.macros._
import scala.reflect.macros.whitebox.Context
def process(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val methodDef = annottees.map(_.tree).toList match {
case x :: Nil => x
case _ => c.abort(c.enclosingPosition, "Method definition is expected")
}
//c.warning(c.enclosingPosition, s"methodDef ${methodDef.getClass} => $methodDef")
val returnType = methodDef match {
case q"$mods def $name[..$tparams](...$paramss): $tpt = $body" => tpt
case _ => c.abort(c.enclosingPosition, "Method definition is expected")
}
//c.warning(NoPosition, s"returnType ${returnType.getClass} => $returnType")
val respType = returnType match {
case tq"scala.concurrent.Future[scala.util.Either[String, ${resp}]]" =>
c.warning(c.enclosingPosition, s"1 resp ${resp.getClass} => $resp")
resp
case tq"Future[Either[String, ${resp}]]" =>
c.warning(c.enclosingPosition, s"2 resp ${resp.getClass} => $resp")
resp
case tq"scala.concurrent.Future[Either[String, ${resp}]]" =>
c.warning(c.enclosingPosition, s"3 resp ${resp.getClass} => $resp")
resp
case tq"Future[scala.util.Either[String, ${resp}]]" =>
c.warning(c.enclosingPosition, s"4 resp ${resp.getClass} => $resp")
resp
case _ =>
c.abort(c.enclosingPosition, s"Method does not have required result type Future[Either[String, ?]]")
}
c.Expr[Any](methodDef) //this is in fact a no-op macro. it only does verification of return type
}
}
Note however how you have to handle various cases with different but similar tq""
patterns and that fooBad
that does not explicitly specify return type will fail anyway. The output of an attempt to compile Test
with this macro will generate an output like this:
Warning:(18, 8) 1 resp class scala.reflect.internal.Trees$Ident => Short
@CheckReturnTypeMacroAnnotation
Warning:(21, 8) 2 resp class scala.reflect.internal.Trees$Ident => Int
@CheckReturnTypeMacroAnnotation
Warning:(24, 8) 3 resp class scala.reflect.internal.Trees$Ident => Long
@CheckReturnTypeMacroAnnotation
Warning:(27, 8) 4 resp class scala.reflect.internal.Trees$Ident => Double
@CheckReturnTypeMacroAnnotation
Error:(31, 8) Method does not have required result type Future[Either[String, ?]]
@CheckReturnTypeMacroAnnotation
Note how all 4 cases actually present in the output and that fooBad
has failed. The issue seems to come from the fact that macro is run before typechecker. Unfortunately I'm not aware of a way to make this pattern matching really work. I looked into reify
and c.typecheck
but had no luck.
Upvotes: 2