12

I've the following java Annotation defined

   @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.FIELD})
   @Retention(RetentionPolicy.RUNTIME)
   public @interface MyAnnotation { 
     String value() default "";
   }

And I've the following scala case class defined:

@Prefer("xyz")
case class TestAnno(arg1 : String, @Prefer("abc") agr2 : String)

I'm able see the annotation defined at case class level using reflection but I'm not able to access the annotation defined for arg2 member of case class TestAnno. De-compiling the code I see that neither the variable declaration or it's scala accessor seem to have the annotation. Only the constructor definition and copy method seem to retain the annotation for the parameters as defined in case class declaration.

Is there some other way to force the scala compiler to generate annotations for the fields declared in case class or I have to read the constructor and use a library such as ASM ByteCode Library or ParaNamer to find which parameters have which annotations? Need a solution which will work primarily for Scala case classes.

cracked_all
  • 1,331
  • 1
  • 11
  • 26

3 Answers3

17

You just need to do the following :

case class TestAnno(arg1 : String, @(Prefer @field)("abc") agr2 : String)

More info here http://www.scala-lang.org/api/current/#scala.annotation.meta.package

Alex Archambault
  • 985
  • 1
  • 8
  • 16
Quentin
  • 3,150
  • 4
  • 24
  • 34
  • 1
    Depending on how you access these parameters by reflection, you may have to add `@setter`, `@getter`, or `@param` instead or along with `@field`. (You can try with all of them, and remove them one-by-one.) – Alex Archambault Jul 23 '14 at 13:21
8

Quentin's solution worked, but IMHO it's too much boilerplate for the user.

You can read annotations on the constructor arguments with the standard reflection API. I needed this for a macro implementation.

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.annotation.StaticAnnotation
final class min(i: Long) extends StaticAnnotation

case class Foo(@min(1) c: String)
import scala.reflect.runtime.universe._
symbolOf[Foo].asClass.primaryConstructor.typeSignature.paramLists.head.head.annotations

// Exiting paste mode, now interpreting.

import scala.annotation.StaticAnnotation
defined class min
defined class Foo
import scala.reflect.runtime.universe._
res0: List[reflect.runtime.universe.Annotation] = List(min(1L))
Dimitri
  • 1,786
  • 14
  • 22
2

I did some searching around and have come with with two solutions. Comments, Suggesstions, improvements are welcome, I've marked this answer as wiki. The first one is based on ScalaBeans and ParaNamer.

  def valNamesWithAnnotations[T <: AnyRef](obj : T)(implicit m : Manifest[T]) : List[(String, List[java.lang.annotation.Annotation])] = {
    val descriptor = descriptorOf(obj.getClass)

    val c = descriptor.beanType.erasure

    val constructor: Option[Constructor[_]] = {
      if (c.getConstructors().isEmpty) None
      else Some(c.getConstructors()(0).asInstanceOf[Constructor[_]])
    }

    val paranamer = new BytecodeReadingParanamer
    val ctorParameterNames = constructor.map(paranamer.lookupParameterNames(_)).getOrElse(scala.Array[String]()).toList
    val ctorParamAnnos = constructor.getOrElse(sys.error("Cannot find constructor entry for class " + c.getName)).getParameterAnnotations

    val builder = List.newBuilder[(String, List[java.lang.annotation.Annotation])]
    val paramIter = ctorParameterNames.iterator
    val annoIter = ctorParamAnnos.iterator

    while(paramIter.hasNext && annoIter.hasNext ) {
      builder += ((paramIter.next, annoIter.next.toList))
    }

    builder.result
  }

The second approach uses ScalaSignatures and is based on this SO answer:

  def valNamesWithAnnotations[C: ClassManifest] : List[(String, List[java.lang.annotation.Annotation])] = {
    val cls = classManifest[C].erasure
    val ctors = cls.getConstructors

    assert(ctors.size == 1, "Class " + cls.getName + " should have only one constructor")
    val sig = ScalaSigParser.parse(cls).getOrElse(sys.error("No ScalaSig for class " + cls.getName + ", make sure it is a top-level case class"))

    val classSymbol = sig.parseEntry(0).asInstanceOf[ClassSymbol]
    assert(classSymbol.isCase, "Class " + cls.getName + " is not a case class")

    val tableSize = sig.table.size
    val ctorIndex = (1 until tableSize).find { i =>
      sig.parseEntry(i) match {
        case m @ MethodSymbol(SymbolInfo("<init>", owner, _, _, _, _), _) => owner match {
          case sym: SymbolInfoSymbol if sym.index == 0 => true
          case _ => false
        }
        case _ => false
      }
    }.getOrElse(sys.error("Cannot find constructor entry in ScalaSig for class " + cls.getName))

    val paramsListBuilder = List.newBuilder[String]
    for (i <- (ctorIndex + 1) until tableSize) {
      sig.parseEntry(i) match {
        case MethodSymbol(SymbolInfo(name, owner, _, _, _, _), _) => owner match {
          case sym: SymbolInfoSymbol if sym.index == ctorIndex => paramsListBuilder += name
          case _ =>
        }
        case _ =>
      }
    }

    val paramAnnoArr = ctors(0).getParameterAnnotations
    val builder = List.newBuilder[(String, List[java.lang.annotation.Annotation])]

    val paramIter = paramsListBuilder.result.iterator
    val annoIter = paramAnnoArr.iterator

    while(paramIter.hasNext && annoIter.hasNext ) {
      builder += ((paramIter.next, annoIter.next.toList))
    }

    builder.result
  }
Community
  • 1
  • 1
cracked_all
  • 1,331
  • 1
  • 11
  • 26