Skip to content

Support use-site meta-annotations #16445

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

Merged
merged 16 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions compiler/src/dotty/tools/dotc/core/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package dotty.tools
package dotc
package core

import Symbols._, Types._, Contexts._, Constants._
import dotty.tools.dotc.ast.tpd, tpd.*
import Symbols._, Types._, Contexts._, Constants._, Phases.*
import ast.tpd, tpd.*
import util.Spans.Span
import printing.{Showable, Printer}
import printing.Texts.Text
import annotation.internal.sharable

import scala.annotation.internal.sharable

object Annotations {

Expand Down Expand Up @@ -87,6 +88,20 @@ object Annotations {
def sameAnnotation(that: Annotation)(using Context): Boolean =
symbol == that.symbol && tree.sameTree(that.tree)

def hasOneOfMetaAnnotation(metaSyms: Symbol*)(using Context): Boolean = atPhaseNoLater(erasurePhase) {
def recTp(tp: Type): Boolean = tp.dealiasKeepAnnots match
case AnnotatedType(parent, metaAnnot) => metaSyms.exists(metaAnnot.matches) || recTp(parent)
case _ => false
def rec(tree: Tree): Boolean = methPart(tree) match
case New(tpt) => rec(tpt)
case Select(qual, _) => rec(qual)
case Annotated(arg, metaAnnot) => metaSyms.exists(metaAnnot.tpe.classSymbol.derivesFrom) || rec(arg)
case t @ Ident(_) => recTp(t.tpe)
case Typed(expr, _) => rec(expr)
case _ => false
metaSyms.exists(symbol.hasAnnotation) || rec(tree)
}

/** Operations for hash-consing, can be overridden */
def hash: Int = System.identityHashCode(this)
def eql(that: Annotation) = this eq that
Expand Down
6 changes: 5 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,8 @@ class Definitions {
@tu lazy val UncheckedVarianceAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedVariance")
@tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile")
@tu lazy val WithPureFunsAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WithPureFuns")
@tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter")
@tu lazy val BeanSetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanSetter")
@tu lazy val FieldMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.field")
@tu lazy val GetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.getter")
@tu lazy val ParamMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.param")
Expand All @@ -1037,8 +1039,10 @@ class Definitions {
@tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable")

// A list of meta-annotations that are relevant for fields and accessors
@tu lazy val FieldAccessorMetaAnnots: Set[Symbol] =
@tu lazy val NonBeanMetaAnnots: Set[Symbol] =
Set(FieldMetaAnnot, GetterMetaAnnot, ParamMetaAnnot, SetterMetaAnnot)
@tu lazy val MetaAnnots: Set[Symbol] =
NonBeanMetaAnnots + BeanGetterMetaAnnot + BeanSetterMetaAnnot

// A list of annotations that are commonly used to indicate that a field/method argument or return
// type is not null. These annotations are used by the nullification logic in JavaNullInterop to
Expand Down
14 changes: 10 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/BeanProperties.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ class BeanProperties(thisPhase: DenotTransformer):
info = MethodType(Nil, valDef.denot.info),
coord = annot.tree.span
).enteredAfter(thisPhase).asTerm
meth.addAnnotations(valDef.symbol.annotations)
val annots = valDef.symbol.annotations.filterConserve { a =>
a.hasOneOfMetaAnnotation(defn.BeanGetterMetaAnnot)
}
meth.addAnnotations(annots)
val body: Tree = ref(valDef.symbol)
DefDef(meth, body)
DefDef(meth, body).withSpan(meth.span)

def maybeGenerateSetter(valDef: ValDef, annot: Annotation)(using Context): Option[Tree] =
Option.when(valDef.denot.asSymDenotation.flags.is(Mutable)) {
Expand All @@ -48,9 +51,12 @@ class BeanProperties(thisPhase: DenotTransformer):
info = MethodType(valDef.name :: Nil, valDef.denot.info :: Nil, defn.UnitType),
coord = annot.tree.span
).enteredAfter(thisPhase).asTerm
meth.addAnnotations(valDef.symbol.annotations)
val annots = valDef.symbol.annotations.filterConserve { a =>
a.hasOneOfMetaAnnotation(defn.BeanSetterMetaAnnot)
}
meth.addAnnotations(annots)
def body(params: List[List[Tree]]): Tree = Assign(ref(valDef.symbol), params.head.head)
DefDef(meth, body)
DefDef(meth, body).withSpan(meth.span)
}

def prefixedName(prefix: String, valName: Name) =
Expand Down
24 changes: 9 additions & 15 deletions compiler/src/dotty/tools/dotc/transform/Memoize.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package transform
import core._
import DenotTransformers._
import Contexts._
import Phases.phaseOf
import Phases.*
import SymDenotations.SymDenotation
import Denotations._
import Symbols._
Expand Down Expand Up @@ -108,29 +108,24 @@ class Memoize extends MiniPhase with IdentityDenotTransformer { thisPhase =>
if (sym.isGetter) sym.info.resultType
else /*sym.isSetter*/ sym.info.firstParamTypes.head

newSymbol(
val fieldSym = newSymbol(
owner = ctx.owner,
name = sym.name.asTermName.fieldName,
flags = Private | (if (sym.is(StableRealizable)) EmptyFlags else Mutable),
info = fieldType,
coord = tree.span
).withAnnotationsCarrying(sym, defn.FieldMetaAnnot)
.enteredAfter(thisPhase)
}

def addAnnotations(denot: Denotation): Unit =
denot match {
case fieldDenot: SymDenotation if sym.annotations.nonEmpty =>
val cpy = fieldDenot.copySymDenotation()
cpy.annotations = sym.annotations
cpy.installAfter(thisPhase)
case _ => ()
)
fieldSym.annotations = sym.annotations.filterConserve { annot =>
annot.hasOneOfMetaAnnotation(defn.FieldMetaAnnot)
|| !annot.hasOneOfMetaAnnotation(defn.MetaAnnots.toList*)
}
fieldSym.enteredAfter(thisPhase)
}

def removeUnwantedAnnotations(denot: SymDenotation, metaAnnotSym: ClassSymbol): Unit =
if (sym.annotations.nonEmpty) {
val cpy = sym.copySymDenotation()
cpy.filterAnnotations(_.symbol.hasAnnotation(metaAnnotSym))
cpy.filterAnnotations(annot => annot.hasOneOfMetaAnnotation(metaAnnotSym))
cpy.installAfter(thisPhase)
}

Expand Down Expand Up @@ -183,7 +178,6 @@ class Memoize extends MiniPhase with IdentityDenotTransformer { thisPhase =>
if isErasableBottomField(field, rhsClass) then erasedBottomTree(rhsClass)
else transformFollowingDeep(ref(field))(using ctx.withOwner(sym))
val getterDef = cpy.DefDef(tree)(rhs = getterRhs)
addAnnotations(fieldDef.denot)
removeUnwantedAnnotations(sym, defn.GetterMetaAnnot)
Thicket(fieldDef, getterDef)
else if sym.isSetter then
Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/Pickler.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package dotty.tools.dotc
package dotty.tools
package dotc
package transform

import core._
Expand Down Expand Up @@ -147,6 +148,7 @@ class Pickler extends Phase {
if unequal then
output("before-pickling.txt", previous)
output("after-pickling.txt", unpickled)
//sys.process.Process("diff -u before-pickling.txt after-pickling.txt").!
report.error(em"""pickling difference for $cls in ${cls.source}, for details:
|
| diff before-pickling.txt after-pickling.txt""")
Expand Down
11 changes: 4 additions & 7 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package dotty.tools.dotc
package dotty.tools
package dotc
package transform

import dotty.tools.dotc.ast.{Trees, tpd, untpd, desugar}
Expand Down Expand Up @@ -186,12 +187,8 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
private def removeUnwantedAnnotations(sym: Symbol, metaAnnotSym: Symbol,
metaAnnotSymBackup: Symbol, keepIfNoRelevantAnnot: Boolean)(using Context): Unit =
def shouldKeep(annot: Annotation): Boolean =
val annotSym = annot.symbol
annotSym.hasAnnotation(metaAnnotSym)
|| annotSym.hasAnnotation(metaAnnotSymBackup)
|| (keepIfNoRelevantAnnot && {
!annotSym.annotations.exists(metaAnnot => defn.FieldAccessorMetaAnnots.contains(metaAnnot.symbol))
})
annot.hasOneOfMetaAnnotation(metaAnnotSym, metaAnnotSymBackup)
|| keepIfNoRelevantAnnot && !annot.hasOneOfMetaAnnotation(defn.NonBeanMetaAnnots.toList*)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why exclude "bean" meta annotations here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs before bean getters and setters are created. So if we lose the @beanGetter/@beanSetter-annotated annotations on the regular class vals, e.g.:

@(JsonProperty @beanGetter) @BeanProperty val beanGet: String = ""

Then they'll never be copied over onto the def getBeanGet: String = ... we're about to syntheses.

Copy link
Member Author

@dwijnand dwijnand Dec 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Memoize, where we split vals into fields and re-written getters/setters, we now also drop annotations that are beanGetter/Setter annotated, from the field, and keep only @getter/@setter-annotated annotations on the (non-bean) getters/setters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 thanks

if sym.annotations.nonEmpty then
sym.filterAnnotations(shouldKeep(_))

Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/transform/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ object SymUtils:
self.isAllOf(EnumCase, butNot = JavaDefined)

def annotationsCarrying(meta: ClassSymbol)(using Context): List[Annotation] =
self.annotations.filter(_.symbol.hasAnnotation(meta))
self.annotations.filter(_.hasOneOfMetaAnnotation(meta))

def withAnnotationsCarrying(from: Symbol, meta: ClassSymbol)(using Context): self.type = {
self.addAnnotations(from.annotationsCarrying(meta))
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/util/Spans.scala
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ object Spans {
assert(isSpan)
if (this == NoCoord) NoSpan else Span(-1 - encoding)
}
override def toString = if isSpan then s"$toSpan" else s"Coord(idx=$toIndex)"
}

/** An index coordinate */
Expand Down
1 change: 1 addition & 0 deletions tests/run/beans.check
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
true
10
[@beans.LibraryAnnotation_1()]
[]
some text
other text
1 change: 1 addition & 0 deletions tests/run/beans/Test_3.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public A run() throws ReflectiveOperationException{
System.out.println(a.isY());
System.out.println(new T2().getX());

System.out.println(Arrays.asList(a.getClass().getDeclaredField("retainingAnnotation").getAnnotations()));
System.out.println(Arrays.asList(a.getClass().getMethod("getRetainingAnnotation").getAnnotations()));

System.out.println(a.getMutableOneWithLongName());
Expand Down
37 changes: 37 additions & 0 deletions tests/run/i12492.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
inspecting constructor MyTable
inspecting param aaaParam1 @MyColumnBase
inspecting param fldParam1
inspecting param getParam1
inspecting param parParam1 @MyColumnBase
inspecting field aaaField1 @MyColumnBase
inspecting field aaaParam1
inspecting field fldField1 @MyColumnBase
inspecting field fldParam1 @MyColumnBase
inspecting field getField1
inspecting field getParam1
inspecting field parField1
inspecting field parParam1
inspecting method aaaField1
inspecting method aaaParam1
inspecting method fldField1
inspecting method fldParam1
inspecting method getField1 @MyColumnBase
inspecting method getParam1 @MyColumnBase
inspecting method parField1
inspecting method parParam1
inspecting constructor MyTable2
inspecting param fldParam2
inspecting param getParam2
inspecting param parParam2 @MyColumnBase
inspecting field fldField2 @MyColumnBase
inspecting field fldParam2 @MyColumnBase
inspecting field getField2
inspecting field getParam2
inspecting field parField2
inspecting field parParam2
inspecting method fldField2
inspecting method fldParam2
inspecting method getField2 @MyColumnBase
inspecting method getParam2 @MyColumnBase
inspecting method parField2
inspecting method parParam2
5 changes: 5 additions & 0 deletions tests/run/i12492/MyColumnBase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface MyColumnBase {}
27 changes: 27 additions & 0 deletions tests/run/i12492/MyTable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import scala.annotation.meta.{ field as fld, getter as get, param as par }

type FldColumn = MyColumnBase @fld
type GetColumn = MyColumnBase @get
type ParColumn = MyColumnBase @par

class MyTable(
@(MyColumnBase ) val aaaParam1: String,
@(MyColumnBase @fld) val fldParam1: String,
@(MyColumnBase @get) val getParam1: String,
@(MyColumnBase @par) val parParam1: String,
) {
@(MyColumnBase ) val aaaField1: String = ""
@(MyColumnBase @fld) val fldField1: String = ""
@(MyColumnBase @get) val getField1: String = ""
@(MyColumnBase @par) val parField1: String = ""
}

class MyTable2(
@FldColumn val fldParam2: String,
@GetColumn val getParam2: String,
@ParColumn val parParam2: String,
) {
@FldColumn val fldField2: String = ""
@GetColumn val getField2: String = ""
@ParColumn val parField2: String = ""
}
31 changes: 31 additions & 0 deletions tests/run/i12492/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// scalajs: --skip
object Test:
def main(args: Array[String]): Unit =
go(classOf[MyTable])
go(classOf[MyTable2])

def go(cls: Class[?]): Unit =
for c <- cls.getDeclaredConstructors.sortBy(_.getName) do
c.setAccessible(true)
println(s"inspecting constructor ${c.getName}")
for p <- c.getParameters.sortBy(_.getName) do
print(s"inspecting param ${p.getName}")
for a <- p.getAnnotations.sortBy(_.annotationType.toString) do
print(s" @${a.annotationType.getName}")
println()

for (m <- cls.getDeclaredFields.sortBy(_.getName)) {
m.setAccessible(true)
print(s"inspecting field ${m.getName}")
for a <- m.getAnnotations().sortBy(_.annotationType.toString) do
print(s" @${a.annotationType.getName}")
println()
}

for (m <- cls.getDeclaredMethods.sortBy(_.getName)) {
m.setAccessible(true)
print(s"inspecting method ${m.getName}")
for a <- m.getAnnotations().sortBy(_.annotationType.toString) do
print(s" @${a.annotationType.getName}")
println()
}
30 changes: 30 additions & 0 deletions tests/run/i15318.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
inspecting constructor Bean
inspecting field beanAaa @JsonProperty
inspecting field beanGet
inspecting field beanSet
inspecting field normAaa @JsonProperty
inspecting field normFld @JsonProperty
inspecting field normGet
inspecting field normSet
inspecting method beanAaa
inspecting method beanGet
inspecting method beanSet
inspecting method beanSet_$eq
inspecting method getBeanAaa
inspecting method getBeanGet @JsonProperty
inspecting method getBeanSet
inspecting method normAaa
inspecting method normFld
inspecting method normGet @JsonProperty
inspecting method normSet
inspecting method normSet_$eq @JsonProperty
inspecting method setBeanSet @JsonProperty
inspecting constructor Bean2
inspecting field beanGet
inspecting field beanSet
inspecting method beanGet
inspecting method beanSet
inspecting method beanSet_$eq
inspecting method getBeanGet @JsonProperty
inspecting method getBeanSet
inspecting method setBeanSet @JsonProperty
21 changes: 21 additions & 0 deletions tests/run/i15318/Bean.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import scala.annotation.meta.{ field as fld, getter as get, setter as set, * }
import scala.beans.BeanProperty

type BeanGetJsonProperty = JsonProperty @beanGetter
type BeanSetJsonProperty = JsonProperty @beanSetter

class Bean {
@(JsonProperty ) val normAaa: String = ""
@(JsonProperty @fld) val normFld: String = ""
@(JsonProperty @get) val normGet: String = ""
@(JsonProperty @set) var normSet: String = ""

@(JsonProperty ) @BeanProperty val beanAaa: String = ""
@(JsonProperty @beanGetter) @BeanProperty val beanGet: String = ""
@(JsonProperty @beanSetter) @BeanProperty var beanSet: String = ""
}

class Bean2 {
@BeanGetJsonProperty @BeanProperty val beanGet: String = ""
@BeanSetJsonProperty @BeanProperty var beanSet: String = ""
}
5 changes: 5 additions & 0 deletions tests/run/i15318/JsonProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface JsonProperty {}
Loading