Search code examples
scalamacrosannotationsscala-macrosscala-macro-paradise

Scala: is it possible to annotate a constructor field of a class using a macro-annotation? (macro paradise)


I am trying to annotate the constructor values of a class using macro-annotations. Assume that a macro-annotation called @identity is implemented and is used as follows in the class definition of class A:

class A(@identity val foo: String, // causes error
        val bar: String) {
@identity val foobar: String = "" // doesn't cause error
}

When just annotating foobar everything compiles just fine. However, when annotating foo I get the following compile-time error:

top-level class without companion can only expand either into an eponymous class or into a block consisting in eponymous companions

Could someone elaborate on this error and why it occurs?


Solution

  • I suspect you call a macro

      import scala.annotation.{StaticAnnotation, compileTimeOnly}
      import scala.language.experimental.macros
      import scala.reflect.macros.whitebox
    
      @compileTimeOnly("enable macro paradise to expand macro annotations")
      class identity extends StaticAnnotation {
        def macroTransform(annottees: Any*): Any = macro identity.impl
      }
    
      object identity {
        def impl(c: whitebox.Context)(annottees: c.Tree*): c.Tree = {
          import c.universe._
          println(s"$annottees")
          q"..$annottees"
        }
      }
    

    like

      class A(@identity val foo: String,
              val bar: String) {
        @identity val foobar: String = ""
      }
    
      object A
    

    Then you have error

    Warning:scalac: List(<paramaccessor> val foo: String = _, class A extends scala.AnyRef {
      <paramaccessor> val foo: String = _;
      <paramaccessor> val bar: String = _;
      def <init>(foo: String, bar: String) = {
        super.<init>();
        ()
      };
      @new identity() val foobar: String = ""
    }, object A extends scala.AnyRef {
      def <init>() = {
        super.<init>();
        ()
      }
    })
    Warning:scalac: 
    Warning:scalac: List(<paramaccessor> val foo: String = _, def <init>(foo: String, bar: String) = {
      super.<init>();
      ()
    })
    Warning:scalac: List(val foobar: String = "")
    Error:(8, 12) top-level class with companion can only expand into a block consisting in eponymous companions
      class A(@identity val foo: String,
    Error:(8, 12) foo is already defined as value foo
      class A(@identity val foo: String,
    Error:(8, 12) foo  is already defined as value foo
      class A(@identity val foo: String,
    

    The thing is that you take a class (and possibly companion object) and return not only them but also val foo so you change number/flavor of top-level definitions which is forbidden https://docs.scala-lang.org/overviews/macros/annotations.html

    Top-level expansions must retain the number of annottees, their flavors and their names, with the only exception that a class might expand into a same-named class plus a same-named module, in which case they automatically become companions as per previous rule.

    For example if we change the macro

       def impl(c: whitebox.Context)(annottees: c.Tree*): c.Tree = {
          import c.universe._
          println(s"$annottees")
          q"..${annottees.tail}" // addded tail
        }
    

    then everything will compile.