Skip to content

Commit

Permalink
Support use-site meta-annotations (#16445)
Browse files Browse the repository at this point in the history
Fixes #12492
Fixes #15318
  • Loading branch information
lrytz authored Dec 14, 2022
2 parents 5929a50 + ff68dc3 commit d3070f6
Show file tree
Hide file tree
Showing 19 changed files with 246 additions and 53 deletions.
23 changes: 20 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,22 @@ object Annotations {
def sameAnnotation(that: Annotation)(using Context): Boolean =
symbol == that.symbol && tree.sameTree(that.tree)

def hasOneOfMetaAnnotation(metaSyms: Set[Symbol], orNoneOf: Set[Symbol] = Set.empty)(using Context): Boolean = atPhaseNoLater(erasurePhase) {
def go(metaSyms: Set[Symbol]) =
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)
go(metaSyms) || orNoneOf.nonEmpty && !go(orNoneOf)
}

/** 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 @@ -1023,6 +1023,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 @@ -1039,8 +1041,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
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,15 @@ object SymDenotations {
final def filterAnnotations(p: Annotation => Boolean)(using Context): Unit =
annotations = annotations.filterConserve(p)

def annotationsCarrying(meta: Set[Symbol], orNoneOf: Set[Symbol] = Set.empty)(using Context): List[Annotation] =
annotations.filterConserve(_.hasOneOfMetaAnnotation(meta, orNoneOf = orNoneOf))

def copyAndKeepAnnotationsCarrying(phase: DenotTransformer, meta: Set[Symbol], orNoneOf: Set[Symbol] = Set.empty)(using Context): Unit =
if annotations.nonEmpty then
val cpy = copySymDenotation()
cpy.annotations = annotationsCarrying(meta, orNoneOf = orNoneOf)
cpy.installAfter(phase)

/** Optionally, the annotation matching the given class symbol */
final def getAnnotation(cls: Symbol)(using Context): Option[Annotation] =
dropOtherAnnotations(annotations, cls) match {
Expand Down
13 changes: 6 additions & 7 deletions compiler/src/dotty/tools/dotc/transform/BeanProperties.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import core._
import ast.tpd._
import Annotations._
import Contexts._
import Symbols.newSymbol
import Symbols.*
import SymUtils.*
import Decorators._
import Flags._
import Names._
Expand All @@ -23,8 +24,6 @@ class BeanProperties(thisPhase: DenotTransformer):
} ::: origBody)

def generateAccessors(valDef: ValDef)(using Context): List[Tree] =
import Symbols.defn

def generateGetter(valDef: ValDef, annot: Annotation)(using Context) : Tree =
val prefix = if annot matches defn.BooleanBeanPropertyAnnot then "is" else "get"
val meth = newSymbol(
Expand All @@ -34,9 +33,9 @@ class BeanProperties(thisPhase: DenotTransformer):
info = MethodType(Nil, valDef.denot.info),
coord = annot.tree.span
).enteredAfter(thisPhase).asTerm
meth.addAnnotations(valDef.symbol.annotations)
.withAnnotationsCarrying(valDef.symbol, defn.BeanGetterMetaAnnot)
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 +47,9 @@ 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)
.withAnnotationsCarrying(valDef.symbol, defn.BeanSetterMetaAnnot)
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
25 changes: 4 additions & 21 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 @@ -114,26 +114,10 @@ class Memoize extends MiniPhase with IdentityDenotTransformer { thisPhase =>
flags = Private | (if (sym.is(StableRealizable)) EmptyFlags else Mutable),
info = fieldType,
coord = tree.span
).withAnnotationsCarrying(sym, defn.FieldMetaAnnot)
).withAnnotationsCarrying(sym, defn.FieldMetaAnnot, orNoneOf = defn.MetaAnnots)
.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 _ => ()
}

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

val NoFieldNeeded = Lazy | Deferred | JavaDefined | Inline

def erasedBottomTree(sym: Symbol) =
Expand Down Expand Up @@ -183,8 +167,7 @@ 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)
sym.copyAndKeepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot))
Thicket(fieldDef, getterDef)
else if sym.isSetter then
if (!sym.is(ParamAccessor)) { val Literal(Constant(())) = tree.rhs: @unchecked } // This is intended as an assertion
Expand All @@ -210,7 +193,7 @@ class Memoize extends MiniPhase with IdentityDenotTransformer { thisPhase =>
then Literal(Constant(()))
else Assign(ref(field), adaptToField(field, ref(tree.termParamss.head.head.symbol)))
val setterDef = cpy.DefDef(tree)(rhs = transformFollowingDeep(initializer)(using ctx.withOwner(sym)))
removeUnwantedAnnotations(sym, defn.SetterMetaAnnot)
sym.copyAndKeepAnnotationsCarrying(thisPhase, Set(defn.SetterMetaAnnot))
setterDef
else
// Curiously, some accessors from Scala2 have ' ' suffixes.
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
22 changes: 7 additions & 15 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 @@ -156,12 +157,14 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
checkInferredWellFormed(tree.tpt)
if sym.is(Method) then
if sym.isSetter then
removeUnwantedAnnotations(sym, defn.SetterMetaAnnot, NoSymbol, keepIfNoRelevantAnnot = false)
sym.copyAndKeepAnnotationsCarrying(thisPhase, Set(defn.SetterMetaAnnot))
else
if sym.is(Param) then
removeUnwantedAnnotations(sym, defn.ParamMetaAnnot, NoSymbol, keepIfNoRelevantAnnot = true)
sym.copyAndKeepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
else if sym.is(ParamAccessor) then
sym.copyAndKeepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot))
else
removeUnwantedAnnotations(sym, defn.GetterMetaAnnot, defn.FieldMetaAnnot, keepIfNoRelevantAnnot = !sym.is(ParamAccessor))
sym.copyAndKeepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
if sym.isScala2Macro && !ctx.settings.XignoreScala2Macros.value then
if !sym.owner.unforcedDecls.exists(p => !p.isScala2Macro && p.name == sym.name && p.signature == sym.signature)
// Allow scala.reflect.materializeClassTag to be able to compile scala/reflect/package.scala
Expand All @@ -183,17 +186,6 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
=> Checking.checkAppliedTypesIn(tree)
case _ =>

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))
})
if sym.annotations.nonEmpty then
sym.filterAnnotations(shouldKeep(_))

private def transformSelect(tree: Select, targs: List[Tree])(using Context): Tree = {
val qual = tree.qualifier
Expand Down
7 changes: 2 additions & 5 deletions compiler/src/dotty/tools/dotc/transform/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,8 @@ object SymUtils:
def isEnumCase(using Context): Boolean =
self.isAllOf(EnumCase, butNot = JavaDefined)

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

def withAnnotationsCarrying(from: Symbol, meta: ClassSymbol)(using Context): self.type = {
self.addAnnotations(from.annotationsCarrying(meta))
def withAnnotationsCarrying(from: Symbol, meta: Symbol, orNoneOf: Set[Symbol] = Set.empty)(using Context): self.type = {
self.addAnnotations(from.annotationsCarrying(Set(meta), orNoneOf))
self
}

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()
}
Loading

0 comments on commit d3070f6

Please sign in to comment.