Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meta-annotations written at use-site are ignored #12492

Closed
bchazalet opened this issue May 16, 2021 · 9 comments · Fixed by #16445
Closed

Meta-annotations written at use-site are ignored #12492

bchazalet opened this issue May 16, 2021 · 9 comments · Fixed by #16445

Comments

@bchazalet
Copy link

bchazalet commented May 16, 2021

Compiler version

3.0.0

Minimized code

This is an issue I've encountered while trying to port squeryl to scala 3.

In a MyColumnBase.java file:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface MyColumnBase {

    String value() default "";
    
    String name() default "";

}

and in a scala file:

import annotation.meta.field

type MyColumn = MyColumnBase @field

class MyTable(
  @MyColumn(name="BRAND_NAME")
  val brandName: String
) {
    @MyColumn(name="WEIGHT")
    val weightInGrams: Option[String] = None
}

val clasz = classOf[MyTable]

for(m <- clasz.getDeclaredFields) {
  m.setAccessible(true)
  if(m.getName == "weightInGrams" || m.getName == "brandName"){
    println(s"inspecting field ${m.getName}")
    assert(m.getAnnotations().size == 1, s"no annotation found for ${m.getName}")
  }
}

Output

Array() had size 0 instead of expected size 1 no annotation found for brandName

Expectation

I would expect to see the annotation on the field brandName. In scala 2.13, I can at least.

@bchazalet
Copy link
Author

can someone hint me as to where the parsing of the constructor params is done? I could perhaps try to understand where the problem lies in the code.

@OlivierBlanvillain
Copy link
Contributor

OlivierBlanvillain commented May 21, 2021

@bchazalet I don't think this is a parsing issue, the annotation shows up with -Xprint:typer:

sbt:scala3> run -Xprint:typer /tmp/foo.java /tmp/bar.scala
[info] running (fork) dotty.tools.dotc.Main -classpath /home/olivier/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.5/scala-library-2.13.5.jar:/home/olivier/workspace/dotty/library/../out/bootstrap/scala3-library-bootstrapped/scala-3.0.1-RC1-bin-SNAPSHOT-nonbootstrapped/scala3-library_3-3.0.1-RC1-bin-SNAPSHOT.jar -Xprint:typer /tmp/foo.java /tmp/bar.scala
result of /tmp/bar.scala after typer:
package <empty> {
  import annotation.meta.field
  class MyTable(@MyColumn(_, name = "BRAND_NAME") brandName: String) extends
    Object
  () {
    @MyColumn(_, name = "BRAND_NAME") val brandName: String
    @MyColumn(_, name = "WEIGHT") val weightInGrams: Option[String] = None
  }
  final lazy module val bar$package: bar$package$ = new bar$package$()
  final module class bar$package$() extends Object() { this: bar$package.type =>
    type MyColumn = MyColumnBase @annotation.meta.field()
    @main() def test: Unit =
      {
        val clasz: Class[MyTable] = classOf[MyTable]
        refArrayOps[java.lang.reflect.Field](clasz.getDeclaredFields()).foreach[
          Unit
        ](
          {
            def $anonfun(m: java.lang.reflect.Field): Unit =
              {
                m.setAccessible(true)
                if
                  m.getName().==("weightInGrams").||(m.getName().==("brandName")
                    )
                 then
                  {
                    println(
                      _root_.scala.StringContext.apply(
                        ["inspecting field ","" : String]*
                      ).s([m.getName() : Any]*)
                    )
                    if
                      refArrayOps[java.lang.annotation.Annotation](
                        m.getAnnotations()
                      ).size.==(1).unary_!
                     then
                      scala.runtime.Scala3RunTime.assertFailed(
                        _root_.scala.StringContext.apply(
                          ["no annotation found for ","" : String]*
                        ).s([m.getName() : Any]*)
                      )
                     else ()
                  }
                 else ()
              }
            closure($anonfun)
          }
        )
      }
  }
  final class test() extends Object() {
    <static> def main(args: Array[String]): Unit =
      try test catch
        {
          case error @ _:scala.util.CommandLineParser.ParseError =>
            scala.util.CommandLineParser.showError(error)
        }
  }
}

@bchazalet
Copy link
Author

thanks for the info @OlivierBlanvillain. Is there somewhere I can find the order of the different phases so that I can maybe try to identify in which one it gets 'lost'?

@bchazalet
Copy link
Author

bchazalet commented May 21, 2021

I found the useful -Xprint:all option. It looks like this is where it's happening:

result of /tmp/bar.scala after posttyper:
package <empty> {
  import annotation.meta.field
  @scala.annotation.internal.SourceFile("/tmp/bar.scala") class MyTable(
    @MyColumn(_, name = "BRAND_NAME") brandName: String
  ) extends Object() {
    val brandName: String
    @MyColumn(_, name = "WEIGHT") val weightInGrams: Option[String] = None
  }
  final lazy module val bar$package: bar$package$ = new bar$package$()
  @scala.annotation.internal.SourceFile("/tmp/bar.scala") final module class
    bar$package$
  () extends Object() { this: bar$package.type =>
    private def writeReplace(): AnyRef =
      new scala.runtime.ModuleSerializationProxy(classOf[bar$package.type])
    type MyColumn = MyColumnBase @annotation.meta.field()
    @main() def main(): Unit =
      {
        val clasz: Class[MyTable] = classOf[MyTable]
        refArrayOps[java.lang.reflect.Field](clasz.getDeclaredFields()).foreach[
          Unit
        ](
          {
            def $anonfun(m: java.lang.reflect.Field): Unit =
              {
                m.setAccessible(true)
                if
                  m.getName().==("weightInGrams").||(m.getName().==("brandName")
                    )
                 then
                  {
                    println(
                      _root_.scala.StringContext.apply(
                        ["inspecting field ","" : String]*
                      ).s([m.getName() : Any]*)
                    )
                    if
                      refArrayOps[java.lang.annotation.Annotation](
                        m.getAnnotations()
                      ).size.==(1).unary_!
                     then
                      scala.runtime.Scala3RunTime.assertFailed(
                        _root_.scala.StringContext.apply(
                          ["no annotation found for ","" : String]*
                        ).s([m.getName() : Any]*)
                      )
                     else ()
                  }
                 else ()
              }
            closure($anonfun)
          }
        )
      }
  }
  @scala.annotation.internal.SourceFile("/tmp/bar.scala") final class main()
     extends
   Object() {
    <static> def main(args: Array[String]): Unit =
      try main() catch
        {
          case error @ _:scala.util.CommandLineParser.ParseError =>
            scala.util.CommandLineParser.showError(error)
        }
  }
}

The annotation doesn't disappear, it's still there on the constructor param but it is not set on the val brandName: String unlike what happens for weightInGrams.

@smarter
Copy link
Member

smarter commented May 21, 2021

The issue is that we look for meta-annotations on the symbol definition (https://github.com/lampepfl/dotty/blob/a956774180d124adc98527863b919c35d5f9db92/compiler/src/dotty/tools/dotc/transform/PostTyper.scala#L158, https://github.com/lampepfl/dotty/blob/a956774180d124adc98527863b919c35d5f9db92/compiler/src/dotty/tools/dotc/transform/Memoize.scala#L112), whereas here the meta-annotation @field is part of the type (hidden behind an alias here, but it could just as well be written @(MyColumnBase @field) val brandName: String).

@smarter smarter changed the title java annotation on constructor field is lost Meta-annotations written at use-site are ignored May 21, 2021
@bchazalet
Copy link
Author

@smarter does that explain why it works for weightInGrams? The meta-annotation is part of the type in this case too, but the annotation is present on the private field anyway.

@smarter
Copy link
Member

smarter commented May 21, 2021

In that case the meta-annotation isn't needed since we're directly annotating a field (from https://www.scala-lang.org/api/current/scala/annotation/meta/index.html):

By default, annotations on (val-, var- or plain) constructor parameters end up on the parameter, not on any other entity. Annotations on fields by default only end up on the field.

@pjfanning
Copy link
Contributor

pjfanning commented Dec 5, 2021

I'm not sure if this test case is related but annotation in this example seems to be ignored by the Scala3 compiler (while it is handled by Scala2 compiler).

import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.module.scala.DefaultScalaModule

import scala.annotation.meta.getter

object TestJsonValue {
  case class ValueClass(@(JsonValue @getter) value: String)
}

val clz = classOf[TestJsonValue.ValueClass]
val mtd = clz.getMethod("value")
println(mtd.toString + " " + mtd.getAnnotations.toSeq)

Scala 2.13.7: https://scastie.scala-lang.org/0JhMWXbvQPKbcNZjqALHAg (final println shows the 1 expected annotation)
Scala 3.1.0: https://scastie.scala-lang.org/IiOpcSSYSUSYDZh6csEb9w (final println shows 0 annotations)

@smarter
Copy link
Member

smarter commented Dec 5, 2021

Same thing yes.

lrytz added a commit that referenced this issue Dec 14, 2022
magro added a commit to inoio/solrs that referenced this issue Jan 8, 2023
Workaround issue with annotations covered by
scala/scala3#12492, fixed with
scala/scala3#16445

This commit can be reverted as soon as the fix is released (according to
https://github.com/lampepfl/dotty/releases this is not yet the case)
magro added a commit to inoio/solrs that referenced this issue Jan 8, 2023
Workaround issue with annotations covered by
scala/scala3#12492, fixed with
scala/scala3#16445

This commit can be reverted as soon as the fix is released (according to
https://github.com/lampepfl/dotty/releases this is not yet the case)
magro added a commit to inoio/solrs that referenced this issue Jan 8, 2023
Workaround issue with annotations covered by
scala/scala3#12492, fixed with
scala/scala3#16445

This commit can be reverted as soon as the fix is released (according to
https://github.com/lampepfl/dotty/releases this is not yet the case)
@Kordyjan Kordyjan added this to the 3.3.0 milestone Aug 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants