Skip to content

Compilation order triggers appearance of unexpected annotations on class members #12536

Open
@danielleontiev

Description

@danielleontiev
Having the following project structure
.
├── README.md
├── build.sbt
├── core
│   └── src
│       └── main
│           └── scala
│               ├── Main.scala
│               ├── bar
│               │   └── Bar.scala
│               ├── baz
│               │   └── description.scala
│               └── foo
│                   └── Foo.scala
├── generic
│   └── src
│       └── main
│           └── scala
│               └── Gen.scala
└── project
    ├── build.properties
    └── project

13 directories, 8 files

Main.scala

import bar.Bar
import foo.Foo

object Main {

  Bar.bar(null)

  Gen.derive[Foo]
}

Bar.scala

package bar

import foo.Foo

object Bar {

  def bar(foo: Foo) = foo.foo
}

description.scala

package baz

import scala.annotation.StaticAnnotation

class description(val text: String) extends StaticAnnotation

Foo.scala

package foo

import baz.description

case class Foo(
    @description("foo")
    foo: Int
)

Gen.scala

import scala.language.experimental.macros
import scala.reflect.macros._

object Gen {
  def derive[T]: Unit = macro gen[T]

  def gen[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
    import c.universe._

    val classMembers = weakTypeOf[T].typeSymbol.asType.toType.members

    val annotationsOnConstructorParams =
      classMembers
        .filter(_.isConstructor)
        .flatMap(_.asMethod.paramLists.flatten)
        .flatMap(_.annotations)

    annotationsOnConstructorParams.foreach(_.tree) // touch annotations to evaluate lazy info

    println(
      s"Annotations on constructor: $annotationsOnConstructorParams"
    )

    classMembers
      .filter(_.toString.contains("foo")) // Take only "foo" TermSymbol and MethodSymbol
      .foreach { s =>
        val annotations = s.annotations
        annotations.foreach(_.tree) // touch annotations to evaluate lazy info
        println(s"Annotations: $annotations, is method? - ${s.isMethod}")
      /*
          The output of the program shows the following:

          Annotations on constructor: List(baz.description("foo"))
          Annotations: List(<notype>), is method? - false
          Annotations: List(<notype>), is method? - true

          Moreover, these annotations are the instances of
          scala.reflect.internal.AnnotationInfos.UnmappableAnnotation
       */

      /*
          On the other hand, commenting out line 6 in Main.scala and running ;clean;compile shows

          Annotations on constructor: List(baz.description("foo"))
          Annotations: List(), is method? - false
          Annotations: List(), is method? - true
       */
      }

    q"()"
  }
}

build.sbt

ThisBuild / version := "0.1.0-SNAPSHOT"

ThisBuild / scalaVersion := "2.13.8"

lazy val root = (project in file("."))
  .settings(
    name := "annotations-bug"
  )
  .aggregate(core, generic)

lazy val core = (project in file("core"))
  .settings(
    name := "core"
  )
  .dependsOn(generic)

lazy val generic = (project in file("generic")).settings(
  name := "generic",
  libraryDependencies ++= Seq(
    "org.scala-lang" % "scala-reflect" % scalaVersion.value
  )
)

build.properties

sbt.version = 1.6.2

It seems that compilation order can change the list of annotations that can be seen from the macro. It the example above there are two key parts - macro module and the ordinary code.

In the macro module annotations collection is performed and annotations from constructor parameters, method symbols and term symbols are printed. According to the documentation (if I understand it correctly), @description annotation should appear only on constructor parameters and not on the class fields or getters of these fields. But running the example shows that unexpectedly some annotations appear on both class field and getter. Moreover, these annotations are the instances of scala.reflect.internal.AnnotationInfos.UnmappableAnnotation, which, in case of using in the output tree, fails the compilation.

The triggering point for the behavior is line 6 in Main.scala. Commenting it out and compiling project from scratch will print only expected one annotation on constructor parameter.

The code in the example available here but also copy-pasted to the issue for easier referencing. I would like to say thank you to @vladislavsheludchenkov for the great help in chasing the issue and writing minimal reproducer for it.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions