9
votes

TL;DR: Basically, I'm looking for the Scala equivalent of the Java:

(MyAnnotation) Thing.getClass().getAnnotations()[0]

Despite the fact that I can happily discover annotations and query based on their type, I can't seem to get from a scala.reflect.runtime.universe.Annotation into my actual type.

scala> // Declare an annotation (it seems StaticAnnotation means runtime
scala> // retention)
scala> case class MyAnnotation(x: Int, y: String) extends scala.annotation.StaticAnnotation
defined class MyAnnotation

scala> // Make a thing decorated with MyAnnotation
scala> @MyAnnotation(x=5, y="cool") case class Thing()
defined class Thing

scala> // Look at the annotation on the Thing...the runtime clearly
scala> // understands the values on it
scala> val annotation = scala.reflect.runtime.universe.typeOf[Thing].typeSymbol.asClass.annotations(0)
annotation: reflect.runtime.universe.Annotation = MyAnnotation(5, "cool")

scala> // I can sort of get at the values by index, which isn't terribly
scala> // safe
scala> annotation.scalaArgs(0)
res0: reflect.runtime.universe.Tree = 5

scala> // And what is a Tree here anyway? It certainly isn't a String (or
scala> // Int). I just want the value!
scala> annotation.scalaArgs(1)
res1: reflect.runtime.universe.Tree = "cool"

scala> // But how do I get at those values programatically?
scala> annotation.asInstanceOf[MyAnnotation]
java.lang.ClassCastException: scala.reflect.internal.AnnotationInfos$CompleteAnnotationInfo cannot be cast to MyAnnotation
        at .<init>(<console>:13)
        at .<clinit>(<console>)
        at .<init>(<console>:7)
        at .<clinit>(<console>)
        at $print(<console>)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:606)
        at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:734)
        at scala.tools.nsc.interpreter.IMain$Request.loadAndRun(IMain.scala:983)
        at scala.tools.nsc.interpreter.IMain.loadAndRunReq$1(IMain.scala:573)
        at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:604)
        at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:568)
        at scala.tools.nsc.interpreter.ILoop.reallyInterpret$1(ILoop.scala:760)
        at scala.tools.nsc.interpreter.ILoop.interpretStartingWith(ILoop.scala:805)
        at scala.tools.nsc.interpreter.ILoop.command(ILoop.scala:717)
        at scala.tools.nsc.interpreter.ILoop.processLine$1(ILoop.scala:581)
        at scala.tools.nsc.interpreter.ILoop.innerLoop$1(ILoop.scala:588)
        at scala.tools.nsc.interpreter.ILoop.loop(ILoop.scala:591)
        at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply$mcZ$sp(ILoop.scala:882)
        at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply(ILoop.scala:837)
        at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply(ILoop.scala:837)
        at scala.tools.nsc.util.ScalaClassLoader$.savingContextLoader(ScalaClassLoader.scala:135)
        at scala.tools.nsc.interpreter.ILoop.process(ILoop.scala:837)
        at scala.tools.nsc.MainGenericRunner.runTarget$1(MainGenericRunner.scala:83)
        at scala.tools.nsc.MainGenericRunner.process(MainGenericRunner.scala:96)
        at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:105)
        at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)

The joyous part about all this is I can't even use the traditional Java method even if I wanted, as Scala doesn't bother to populate the array from getAnnotations() anymore.

scala> Thing.getClass.getAnnotations.length
res2: Int = 0

Related

I think what I want is sort of in "Reflection Overview" in the section "Instantiating a Type at Runtime," but I don't see why I would have to jump through so many hoops just to get the values in an annotation. In Java, the value is just a cast away.

The question "See annotations in Scala reflection" seems related, but the question is from 2011 and applies to Scala 2.9. I'm using 2.10 and from what I can tell, a huge amount of how reflection works has changed since then.

3

3 Answers

7
votes

In their current form, Scala annotations try to combine Java compatibility (which implies constant arguments only and a very limited number of language constructs allowed in annotations) and ultimate flexibility (which implies allowing anything one could imagine in annotations).

This is manifested by the ClassfileAnnotation (compatibility) vs StaticAnnotation (flexibility) distinction. According to this idea, one can choose between Java-style reflection with limited annotations being available as objects and Scala-style reflection with fully flexible annotations being available only as abstract syntax trees (note that we can't automatically convert static annotations to runtime objects, because code stored in such annotations might contain arbitrary Scala expressions, which makes it very hard to evaluate them).

Unfortunately, this idealistic picture is broken by the fact that classfile annotations don't support runtime retention: https://issues.scala-lang.org/browse/SI-32, which means that one doesn't actually get to choose - only Scala-style reflection is supported at the moment. Hopefully, some day we'll get SI-32 fixed, but I'm unaware of any ongoing effort to make it work.

A couple months ago, I wanted to implement something like scala.reflect.Annotation.eval that would take a Scala-style annotation and evaluate it if possible. However, after a discussion during one of our reflection meetings we decided against it, because apart from an unfortunate non-generality, this API would also bring troubles to compile-time reflection (which in Scala is unified with runtime reflection).

This has been logged as an issue at https://issues.scala-lang.org/browse/SI-6423 and also discussed at https://groups.google.com/forum/#!topic/scala-internals/8v2UL-LR9yY, but with no concrete plans for improvement at the moment. Hopefully Project Palladium is going to help here, since one of its core components is a Scala interpreter, which will provide necessary generality to support Annotation.eval.

7
votes

Not the java equivalent, but with scala 2.11.6, this works to extract values out of a annotation:

case class MyAnnotationClass(id: String) extends scala.annotation.StaticAnnotation

val myAnnotatedClass: ClassSymbol = u.runtimeMirror(Thread.currentThread().getContextClassLoader).staticClass("MyAnnotatedClass")
val annotation: Option[Annotation] = myAnnotatedClass.annotations.find(_.tree.tpe =:= u.typeOf[MyAnnotationClass])
val result = annotation.flatMap { a =>
  a.tree.children.tail.collect({ case Literal(Constant(id: String)) => DoSomething(id) }).headOption
}

I understand that you might have done it already, but just in case it can help someone :)

2
votes

Without having to depend on scala-compiler, this is my version:

  def fetchAnnotations[T <: Annotation](cls : Class[_]) : List[T]= {
    import scala.reflect.runtime.universe._
    val mirror = runtimeMirror(cls.getClassLoader)
    val clsSymbol = mirror.staticClass(cls.getCanonicalName)
    val annotations = clsSymbol.annotations

    val res = ListBuffer[T]()
    for(annt : Annotation <- annotations) {
      val anntCls = annt.tree.tpe.typeSymbol.asClass
      val classMirror = mirror.reflectClass(anntCls);
      val anntType = annt.tree.tpe
      val constructor = anntType.decl(termNames.CONSTRUCTOR).asMethod;
      val constructorMirror = classMirror.reflectConstructor(constructor);

      val instance = annt.tree match {
        case Apply(c, args : List[Tree])   =>
          val res = args.collect({
            case i: Tree =>
              i match {
                case Literal(Constant(value)) =>
                  value
              }
          })
          constructorMirror(res: _*).asInstanceOf[T]
      }

      res+=(instance)
    }
    res.toList
  }

The previous code will only work when the arguments are primitive I suspect.

If we can afford depending on scala-compiler, then we can do something like:

def fetchAnnotations_toolBox[T <: Annotation](cls : Class[_]) : List[T]= {
    import scala.reflect.runtime.universe._
    val mirror = runtimeMirror(cls.getClassLoader)
    val clsSymbol = mirror.staticClass(cls.getCanonicalName)
    val annotations = clsSymbol.annotations

    val res = ListBuffer[T]()
    for(annt : Annotation <- annotations) {
      import scala.tools.reflect.ToolBox

      val toolbox = mirror.mkToolBox()
      val instance = toolbox.eval(toolbox.untypecheck(toolbox.typecheck(annt.tree))).asInstanceOf[T]
      res+=(instance)
    }
    res.toList
  }