Skip to content

Commit 09e39f9

Browse files
committed
Represent Java annotations as interfaces so they can be extended
Previously we treated Java annotations as if they were classes, like Scala annotations. For example, given @interface Ann { int foo(); } we pretended it was defined as: abstract class Ann(foo: Int) extends java.lang.annotation.Annotation { def foo(): Int } We take advantage of this to type annotation trees as if they were new calls, for example `@Ann(1)` is typed as `new Ann(1)`. Pretending that annotations are classes is fine most of the time and matches what Scala 2.12 did, but it's problematic because the JVM treats annotations as interfaces. In practice this was only an issue with code trying to extend Java annotations, which would either be rejected at compile-time or miscompiled before this commit. This commit switches our representation of annotations to be trait-based instead: trait Ann(foo: Int) extends java.lang.annotation.Annotation { def foo(): Int } Classes are then free to extend annotations using the same pattern as in Scala 2.13: class Foo extends Ann {val annotationType = classOf[Retention]; def foo(): Int = 1} Notice that we still pretend these traits have constructors, this lets us type annotation trees in much the same way as before, and crucially it means that macros that depended on the exact tree shape of annotation trees can continue to work, as demonstrated by the annot-java-tree test extracted from wartremover. To prevent miscompilation issues, we disallow passing arguments to the annotation constructor in `extends` clause. The treatment of default arguments to annotations stays unchanged from 85cd1cf. Fixes scala#5690. Fixes scala#12840. Fixes scala#14199.
1 parent 9ff325c commit 09e39f9

File tree

23 files changed

+163
-36
lines changed

23 files changed

+163
-36
lines changed

compiler/src/dotty/tools/dotc/core/classfile/ClassfileConstants.scala

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -353,18 +353,15 @@ object ClassfileConstants {
353353
if (jflag == 0) base else base | translateFlag(jflag)
354354

355355
private def translateFlags(jflags: Int, baseFlags: FlagSet): FlagSet = {
356-
val nflags =
357-
if ((jflags & JAVA_ACC_ANNOTATION) == 0) jflags
358-
else jflags & ~(JAVA_ACC_ABSTRACT | JAVA_ACC_INTERFACE) // annotations are neither abstract nor interfaces
359356
var res: FlagSet = baseFlags | JavaDefined
360-
res = addFlag(res, nflags & JAVA_ACC_PRIVATE)
361-
res = addFlag(res, nflags & JAVA_ACC_PROTECTED)
362-
res = addFlag(res, nflags & JAVA_ACC_FINAL)
363-
res = addFlag(res, nflags & JAVA_ACC_SYNTHETIC)
364-
res = addFlag(res, nflags & JAVA_ACC_STATIC)
365-
res = addFlag(res, nflags & JAVA_ACC_ENUM)
366-
res = addFlag(res, nflags & JAVA_ACC_ABSTRACT)
367-
res = addFlag(res, nflags & JAVA_ACC_INTERFACE)
357+
res = addFlag(res, jflags & JAVA_ACC_PRIVATE)
358+
res = addFlag(res, jflags & JAVA_ACC_PROTECTED)
359+
res = addFlag(res, jflags & JAVA_ACC_FINAL)
360+
res = addFlag(res, jflags & JAVA_ACC_SYNTHETIC)
361+
res = addFlag(res, jflags & JAVA_ACC_STATIC)
362+
res = addFlag(res, jflags & JAVA_ACC_ENUM)
363+
res = addFlag(res, jflags & JAVA_ACC_ABSTRACT)
364+
res = addFlag(res, jflags & JAVA_ACC_INTERFACE)
368365
res
369366
}
370367

compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,7 @@ class ClassfileParser(
165165
* Updates the read pointer of 'in'. */
166166
def parseParents: List[Type] = {
167167
val superType =
168-
if (isAnnotation) {
169-
in.nextChar
170-
defn.ObjectType
171-
}
172-
else if (classRoot.symbol == defn.ComparableClass ||
168+
if (classRoot.symbol == defn.ComparableClass ||
173169
classRoot.symbol == defn.JavaCloneableClass ||
174170
classRoot.symbol == defn.JavaSerializableClass) {
175171
// Treat these interfaces as universal traits
@@ -844,7 +840,7 @@ class ClassfileParser(
844840

845841
class AnnotConstructorCompleter(classInfo: TempClassInfoType) extends LazyType {
846842
def complete(denot: SymDenotation)(using Context): Unit = {
847-
val attrs = classInfo.decls.toList.filter(sym => sym.isTerm && sym != denot.symbol)
843+
val attrs = classInfo.decls.toList.filter(sym => sym.isTerm && sym != denot.symbol && sym.name != nme.CONSTRUCTOR)
848844
val paramNames = attrs.map(_.name.asTermName)
849845
val paramTypes = attrs.map(_.info.resultType)
850846
denot.info = MethodType(paramNames, paramTypes, classRoot.typeRef)

compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ object JavaParsers {
876876
List(constructorParams), TypeTree(), EmptyTree).withMods(Modifiers(Flags.JavaDefined))
877877
val templ = makeTemplate(annotationParents, constr :: body, List(), true)
878878
val annot = atSpan(start, nameOffset) {
879-
TypeDef(name, templ).withMods(mods | Flags.Abstract)
879+
TypeDef(name, templ).withMods(mods | Flags.JavaInterface)
880880
}
881881
addCompanionObject(statics, annot)
882882
}

compiler/src/dotty/tools/dotc/typer/Checking.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,8 @@ trait Checking {
11091109
def checkParentCall(call: Tree, caller: ClassSymbol)(using Context): Unit =
11101110
if (!ctx.isAfterTyper) {
11111111
val called = call.tpe.classSymbol
1112+
if (called.derivesFrom(defn.JavaAnnotationClass))
1113+
report.error(i"${called.name} must appear without any argument to be a valid class parent because it is a Java annotation", call.srcPos)
11121114
if (caller.is(Trait))
11131115
report.error(i"$caller may not call constructor of $called", call.srcPos)
11141116
else if (called.is(Trait) && !caller.mixins.contains(called))

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2595,6 +2595,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
25952595
*/
25962596
def ensureConstrCall(cls: ClassSymbol, parent: Tree, psym: Symbol)(using Context): Tree =
25972597
if parent.isType && !cls.is(Trait) && !cls.is(JavaDefined) && psym.isClass
2598+
// Annotations are represented as traits with constructors, but should
2599+
// never be called as such outside of annotation trees.
2600+
&& !psym.derivesFrom(defn.JavaAnnotationClass)
25982601
&& (!psym.is(Trait)
25992602
|| psym.primaryConstructor.info.takesParams && !cls.superClass.isSubClass(psym))
26002603
then typed(untpd.New(untpd.TypedSplice(parent), Nil))
File renamed without changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public @interface Ann {
2+
int value();
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class Bar extends Ann(1) { // error
2+
def value = 1
3+
def annotationType = classOf[Ann]
4+
}
5+
6+
def test =
7+
// Typer errors
8+
new Ann // error
9+
new Ann(1) {} // error
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public @interface Ann {
2+
int value();
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def test =
2+
// Posttyper errors
3+
new Ann(1) // error
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public @interface Ann {
2+
int value();
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def test =
2+
// Refchecks error
3+
new Ann {} // error

tests/neg/repeatable/Test_1.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import repeatable._
66
@FirstLevel_0(Array()) // error
77
trait U
88

9-
@FirstLevel_0(Array(Plain_0(4), Plain_0(5)))
10-
@FirstLevel_0(Array(Plain_0(6), Plain_0(7)))
9+
@FirstLevel_0(Array(new Plain_0(4), new Plain_0(5)))
10+
@FirstLevel_0(Array(new Plain_0(6), new Plain_0(7)))
1111
@SecondLevel_0(Array()) // error
1212
trait T
1313

1414
@SecondLevel_0(Array())
1515
@SecondLevel_0(Array()) // error
16-
trait S
16+
trait S

tests/pos/i5690.scala

Lines changed: 0 additions & 8 deletions
This file was deleted.

tests/pos/i7467.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ class DuplicateSymbolError_DirectSuperclass extends DefaultListCellRenderer() {
55
override def getListCellRendererComponent(list: JList[_ <: Object], value: Object, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component = ???
66
}
77

8-
class DuplicateSymbolError_IndirectInterface extends DefaultListCellRenderer() {
9-
override def getListCellRendererComponent(list: JList[_], value: Object, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component = ???
10-
}
8+
// class DuplicateSymbolError_IndirectInterface extends DefaultListCellRenderer() {
9+
// override def getListCellRendererComponent(list: JList[_], value: Object, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component = ???
10+
// }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import scala.quoted.*
2+
3+
inline def checkSuppressWarnings[T]: Unit = ${ checkSuppressWarningsImpl[T] }
4+
5+
def checkSuppressWarningsImpl[T: Type](using Quotes): Expr[Unit] =
6+
import quotes.reflect.*
7+
val SuppressWarningsSymbol = TypeTree.of[SuppressWarnings].symbol
8+
val sym = TypeRepr.of[T].typeSymbol
9+
// Imitate what wartremover does, so we can avoid unintentionally breaking it:
10+
// https://github.com/wartremover/wartremover/blob/fb18e6eafe9a47823e04960aaf4ec7a9293719ef/core/src/main/scala-3/org/wartremover/WartUniverse.scala#L63-L77
11+
val actualArgs = sym
12+
.getAnnotation(SuppressWarningsSymbol)
13+
.collect {
14+
case Apply(
15+
Select(_, "<init>"),
16+
Apply(Apply(_, Typed(Repeated(values, _), _) :: Nil), Apply(_, _ :: Nil) :: Nil) :: Nil
17+
) =>
18+
// "-Yexplicit-nulls"
19+
// https://github.com/wartremover/wartremover/issues/660
20+
values.collect { case Literal(StringConstant(str)) =>
21+
str
22+
}
23+
}
24+
.toList
25+
.flatten
26+
val expectedArgs = List("a", "b")
27+
assert(actualArgs == expectedArgs,
28+
s"Expected $expectedArgs arguments for SuppressWarnings annotation of $sym but got $actualArgs")
29+
'{}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@SuppressWarnings(Array("a", "b")) class Foo
2+
3+
@main def Test =
4+
checkSuppressWarnings[Foo]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public @interface Ann_1 {
2+
int bar() default 1;
3+
int baz() default 2;
4+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// scalajs: --skip
2+
3+
class Foo extends Ann_1 {
4+
override def bar = 3
5+
override def baz = 4
6+
def annotationType = classOf[Ann_1]
7+
}
8+
9+
object Test {
10+
def main(args: Array[String]): Unit = {
11+
val x = new Foo
12+
val y: Ann_1 = x
13+
val z: Int @Ann_1(1) = 1
14+
val zz: Int @Ann_1() = 1
15+
// val x: scala.annotation.Annotation = new Ann {
16+
// // val x: java.lang.annotation.Annotation = new Ann {
17+
// def annotationType = classOf[Ann]
18+
// }
19+
// println(x)
20+
}
21+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public @interface Ann {
2+
int bar() default 1;
3+
int baz() default 2;
4+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// scalajs: --skip
2+
3+
class Foo extends Ann {
4+
override def bar = 3
5+
override def baz = 4
6+
def annotationType = classOf[Ann]
7+
}
8+
9+
object Test {
10+
def main(args: Array[String]): Unit = {
11+
val x = new Foo
12+
val y: Ann = x
13+
val z: Int @Ann(1) = 1
14+
val zz: Int @Ann() = 1
15+
// val x: scala.annotation.Annotation = new Ann {
16+
// // val x: java.lang.annotation.Annotation = new Ann {
17+
// def annotationType = classOf[Ann]
18+
// }
19+
// println(x)
20+
}
21+
}

tests/run/repeatable/Test_1.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import repeatable._
77
@Plain_0(3)
88
trait U
99

10-
@FirstLevel_0(Array(Plain_0(4), Plain_0(5)))
11-
@FirstLevel_0(Array(Plain_0(6), Plain_0(7)))
10+
@FirstLevel_0(Array(new Plain_0(4), new Plain_0(5)))
11+
@FirstLevel_0(Array(new Plain_0(6), new Plain_0(7)))
1212
trait T
1313

14-
object Test:
15-
def main(args: Array[String]) =
14+
object Test {
15+
def main(args: Array[String]) = {
1616
val annValuesU = classOf[U].getAnnotation(classOf[FirstLevel_0]).value.toList.map(_.value).sorted
1717
annValuesU.foreach(println)
1818

1919
println()
2020

2121
val annValuesT = classOf[T].getAnnotation(classOf[SecondLevel_0]).value.toList.map(_.value.toList.map(_.value).sorted).sorted
2222
annValuesT.foreach(println)
23+
}
24+
}

tests/run/t9400.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// scalajs: --skip
2+
3+
class Deprecation extends Deprecated {
4+
final val annotationType = classOf[Deprecated]
5+
6+
def forRemoval(): Boolean = false
7+
def since(): String = ""
8+
}
9+
10+
class Suppression extends SuppressWarnings {
11+
final val annotationType = classOf[SuppressWarnings]
12+
13+
def value = Array("unchecked")
14+
}
15+
16+
class Retention(runtime: Boolean) extends java.lang.annotation.Retention {
17+
final val annotationType = classOf[Retention]
18+
19+
def value =
20+
if (runtime) java.lang.annotation.RetentionPolicy.RUNTIME
21+
else java.lang.annotation.RetentionPolicy.SOURCE
22+
}
23+
24+
object Test extends App {
25+
new Deprecation
26+
new Suppression
27+
new Retention(true)
28+
}

0 commit comments

Comments
 (0)