From 44477d3a8f89ceddb6fe60aa41afa71c711a566f Mon Sep 17 00:00:00 2001 From: Harrison Houghton Date: Sun, 7 May 2017 18:26:20 -0400 Subject: [PATCH] Implement annotation parsing in JavaParsers. This causes the java parser to get somewhat more complex, but allows for macros and compiler plugins to correctly inspect java-defined classes for annotations. Moved a few utility methods into {Parsers,Scanners}Common as well, so I can reuse them in the java ones. This involves a change to the t4788 tests, as SAnnotation now correctly has RetentionPolicy.SOURCE, and is therefore ignored by the check in BCodeHelpers#BCAnnotGen.shouldEmitAnnotation; also, ASM Textifier emits an // invisible marker now that CAnnotation has RetentionPolicy.CLASS. I'm not sure what the chances that anyone is relying upon this behavior are, but since it does the Right Thing for separate compilation runs, they probably should not be too disappointed. The new tests are run-tests not just pos-tests at the suggestion of retronym, to ensure that everything works the same at runtime as it does at compile time. As a matter of fact, it doesn't: the compile-time universe maintains ordering of annotation values in the tree, while the runtime universe uses java reflection to get at the values and therefore cannot know of their order. The test contains one exception for that. Review by densh, retronym Fixes scala/bug#8928 Co-authored-by: Jason Zaugg Co-authored-by: Dale Wijnand --- .../scala/tools/nsc/ast/parser/Parsers.scala | 31 ++--- .../scala/tools/nsc/ast/parser/Scanners.scala | 3 + .../scala/tools/nsc/javac/JavaParsers.scala | 108 ++++++++++++++---- .../scala/tools/nsc/javac/JavaScanners.scala | 3 +- .../symtab/classfile/ClassfileParser.scala | 9 +- .../scala/tools/nsc/typechecker/Typers.scala | 5 + .../scala/tools/nsc/interactive/Global.scala | 2 +- .../scala/tools/partest/BytecodeTest.scala | 4 + .../parse-invariants/src/a/A.java | 6 + .../run/t4788-separate-compilation.check | 8 +- .../AnnWithArgs_0.java | 19 +++ .../t4788-separate-compilation/Test_2.scala | 18 +++ test/files/run/t4788.check | 8 +- test/files/run/t4788/AnnWithArgs.java | 19 +++ test/files/run/t4788/Test.scala | 18 +++ test/files/run/t8928.javaopts | 1 + test/files/run/t8928/Annotated_0.java | 19 +++ test/files/run/t8928/Annotated_1.java | 19 +++ test/files/run/t8928/Array_0.java | 14 +++ test/files/run/t8928/Checks_0.scala | 84 ++++++++++++++ test/files/run/t8928/Empty_0.java | 7 ++ test/files/run/t8928/Enum_0.java | 12 ++ test/files/run/t8928/Macros_0.scala | 16 +++ test/files/run/t8928/Nested_0.java | 13 +++ test/files/run/t8928/NoArgs_0.java | 7 ++ test/files/run/t8928/Simple_0.java | 18 +++ test/files/run/t8928/Test_1.scala | 16 +++ 27 files changed, 444 insertions(+), 43 deletions(-) create mode 100644 test/files/run/t4788-separate-compilation/AnnWithArgs_0.java create mode 100644 test/files/run/t4788/AnnWithArgs.java create mode 100644 test/files/run/t8928.javaopts create mode 100644 test/files/run/t8928/Annotated_0.java create mode 100644 test/files/run/t8928/Annotated_1.java create mode 100644 test/files/run/t8928/Array_0.java create mode 100644 test/files/run/t8928/Checks_0.scala create mode 100644 test/files/run/t8928/Empty_0.java create mode 100644 test/files/run/t8928/Enum_0.java create mode 100644 test/files/run/t8928/Macros_0.scala create mode 100644 test/files/run/t8928/Nested_0.java create mode 100644 test/files/run/t8928/NoArgs_0.java create mode 100644 test/files/run/t8928/Simple_0.java create mode 100644 test/files/run/t8928/Test_1.scala diff --git a/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala b/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala index 6ffdb1654d96..c9e158b06ca2 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala @@ -95,6 +95,23 @@ trait ParsersCommon extends ScannersCommon { self => */ @inline final def makeParens(body: => List[Tree]): Parens = Parens(inParens(if (in.token == RPAREN) Nil else body)) + + /** {{{ part { `sep` part } }}}, or if sepFirst is true, {{{ { `sep` part } }}}. */ + final def tokenSeparated[T](separator: Token, sepFirst: Boolean, part: => T): List[T] = { + val ts = new ListBuffer[T] + if (!sepFirst) + ts += part + + while (in.token == separator) { + in.nextToken() + ts += part + } + ts.toList + } + + /** {{{ tokenSeparated }}}, with the separator fixed to commas. */ + @inline final def commaSeparated[T](part: => T): List[T] = + tokenSeparated(COMMA, sepFirst = false, part) } } @@ -791,20 +808,6 @@ self => errorTypeTree } } - - /** {{{ part { `sep` part } }}},or if sepFirst is true, {{{ { `sep` part } }}}. */ - final def tokenSeparated[T](separator: Token, sepFirst: Boolean, part: => T): List[T] = { - val ts = new ListBuffer[T] - if (!sepFirst) - ts += part - - while (in.token == separator) { - in.nextToken() - ts += part - } - ts.toList - } - @inline final def commaSeparated[T](part: => T): List[T] = tokenSeparated(COMMA, sepFirst = false, part) @inline final def caseSeparated[T](part: => T): List[T] = tokenSeparated(CASE, sepFirst = true, part) def readAnnots(part: => Tree): List[Tree] = tokenSeparated(AT, sepFirst = true, part) diff --git a/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala b/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala index a95cb85f5824..7c88b61b5e11 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala @@ -40,6 +40,9 @@ trait ScannersCommon { } trait ScannerCommon extends CommonTokenData { + /** Consume and discard the next token. */ + def nextToken(): Unit + // things to fill in, in addition to buf, decodeUni which come from CharArrayReader def error(off: Offset, msg: String): Unit def incompleteInputError(off: Offset, msg: String): Unit diff --git a/src/compiler/scala/tools/nsc/javac/JavaParsers.scala b/src/compiler/scala/tools/nsc/javac/JavaParsers.scala index 9f3d66dda17d..3a991510cbb5 100644 --- a/src/compiler/scala/tools/nsc/javac/JavaParsers.scala +++ b/src/compiler/scala/tools/nsc/javac/JavaParsers.scala @@ -242,11 +242,21 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { // -------------------- specific parsing routines ------------------ - def qualId(): RefTree = { - var t: RefTree = atPos(in.currentPos) { Ident(ident()) } - while (in.token == DOT) { + def qualId(orClassLiteral: Boolean = false): Tree = { + var t: Tree = atPos(in.currentPos) { Ident(ident()) } + var done = false + while (!done && in.token == DOT) { in.nextToken() - t = atPos(in.currentPos) { Select(t, ident()) } + t = atPos(in.currentPos) { + if (orClassLiteral && in.token == CLASS) { + in.nextToken() + done = true + val tpeArg = convertToTypeId(t) + TypeApply(Select(gen.mkAttributedRef(definitions.PredefModule), nme.classOf), tpeArg :: Nil) + } else { + Select(t, ident()) + } + } } t } @@ -275,7 +285,7 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { } def typ(): Tree = { - annotations() + annotations() // TODO: fix scala/bug#9883 (JSR 308) optArrayBrackets { if (in.token == FINAL) in.nextToken() if (in.token == IDENTIFIER) { @@ -334,20 +344,73 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { } def annotations(): List[Tree] = { - //var annots = new ListBuffer[Tree] + val annots = new ListBuffer[Tree] while (in.token == AT) { in.nextToken() - annotation() + val annot = annotation() + if (annot.nonEmpty) annots += annot } - List() // don't pass on annotations for now + annots.toList } - /** Annotation ::= TypeName [`(` AnnotationArgument {`,` AnnotationArgument} `)`] + /** Annotation ::= TypeName [`(` [AnnotationArgument {`,` AnnotationArgument}] `)`] */ - def annotation() { - qualId() - if (in.token == LPAREN) { skipAhead(); accept(RPAREN) } - else if (in.token == LBRACE) { skipAhead(); accept(RBRACE) } + def annotation(): Tree = { + def annArg(): Tree = { + def annVal(): Tree = { + tryLiteral() match { + case Some(lit) => atPos(in.currentPos)(Literal(lit)) + case _ if in.token == AT => + in.nextToken() + annotation() + case _ if in.token == LBRACE => + atPos(in.pos) { + val elts = inBracesOrNil(commaSeparated(annVal())) + if (elts.exists(_.isEmpty)) EmptyTree + else Apply(ArrayModule_overloadedApply, elts: _*) + } + case _ if in.token == IDENTIFIER => + qualId(orClassLiteral = true) + } + } + + if (in.token == IDENTIFIER) { + qualId(orClassLiteral = true) match { + case name: Ident if in.token == EQUALS => + in.nextToken() + /* name = value */ + val value = annVal() + if (value.isEmpty) EmptyTree else gen.mkNamedArg(name, value) + case rhs => + /* implicit `value` arg with constant value */ + gen.mkNamedArg(nme.value, rhs) + } + } else { + /* implicit `value` arg */ + val value = annVal() + if (value.isEmpty) EmptyTree else gen.mkNamedArg(nme.value, value) + } + } + + atPos(in.pos) { + val id = convertToTypeId(qualId()) + if (in.token == LPAREN) { + val saved = new JavaTokenData {}.copyFrom(in) // prep to bail if non-literals/identifiers + accept(LPAREN) + val args = + if (in.token == RPAREN) Nil + else commaSeparated(atPos(in.pos)(annArg())) + if (in.token == RPAREN) { + accept(RPAREN) + New(id, args :: Nil) + } else { + in.copyFrom(saved) + skipAhead() + accept(RPAREN) + EmptyTree + } + } else New(id, ListOfNil) + } } def modifiers(inInterface: Boolean): Modifiers = { @@ -361,7 +424,8 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { in.token match { case AT if (in.lookaheadToken != INTERFACE) => in.nextToken() - annotation() + val annot = annotation() + if (annot.nonEmpty) annots :+= annot case PUBLIC => isPackageAccess = false in.nextToken() @@ -419,10 +483,10 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { def typeParam(): TypeDef = atPos(in.currentPos) { - annotations() + val anns = annotations() val name = identForType() val hi = if (in.token == EXTENDS) { in.nextToken() ; bound() } else EmptyTree - TypeDef(Modifiers(Flags.JAVA | Flags.DEFERRED | Flags.PARAM), name, Nil, TypeBoundsTree(EmptyTree, hi)) + TypeDef(Modifiers(Flags.JAVA | Flags.DEFERRED | Flags.PARAM, tpnme.EMPTY, anns), name, Nil, TypeBoundsTree(EmptyTree, hi)) } def bound(): Tree = @@ -446,7 +510,7 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { def formalParam(): ValDef = { if (in.token == FINAL) in.nextToken() - annotations() + val anns = annotations() var t = typ() if (in.token == DOTDOTDOT) { in.nextToken() @@ -454,7 +518,7 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { AppliedTypeTree(scalaDot(tpnme.JAVA_REPEATED_PARAM_CLASS_NAME), List(t)) } } - varDecl(in.currentPos, Modifiers(Flags.JAVA | Flags.PARAM), t, ident().toTermName) + varDecl(in.currentPos, Modifiers(Flags.JAVA | Flags.PARAM, typeNames.EMPTY, anns), t, ident().toTermName) } def optThrows() { @@ -841,7 +905,7 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { } def enumConst(enumType: Tree): (ValDef, Boolean) = { - annotations() + val anns = annotations() var hasClassBody = false val res = atPos(in.currentPos) { val name = ident() @@ -856,7 +920,7 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { skipAhead() accept(RBRACE) } - ValDef(Modifiers(Flags.JAVA_ENUM | Flags.STABLE | Flags.JAVA | Flags.STATIC), name.toTermName, enumType, blankExpr) + ValDef(Modifiers(Flags.JAVA_ENUM | Flags.STABLE | Flags.JAVA | Flags.STATIC, typeNames.EMPTY, anns), name.toTermName, enumType, blankExpr) } (res, hasClassBody) } @@ -894,10 +958,10 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { var pos = in.currentPos val pkg: RefTree = if (in.token == AT || in.token == PACKAGE) { - annotations() + annotations() // TODO: put these somewhere? pos = in.currentPos accept(PACKAGE) - val pkg = qualId() + val pkg = qualId().asInstanceOf[RefTree] accept(SEMI) pkg } else { diff --git a/src/compiler/scala/tools/nsc/javac/JavaScanners.scala b/src/compiler/scala/tools/nsc/javac/JavaScanners.scala index a25c51eaf358..391fb030831e 100644 --- a/src/compiler/scala/tools/nsc/javac/JavaScanners.scala +++ b/src/compiler/scala/tools/nsc/javac/JavaScanners.scala @@ -52,12 +52,13 @@ trait JavaScanners extends ast.parser.ScannersCommon { /** the base of a number */ var base: Int = 0 - def copyFrom(td: JavaTokenData) = { + def copyFrom(td: JavaTokenData): this.type = { this.token = td.token this.pos = td.pos this.lastPos = td.lastPos this.name = td.name this.base = td.base + this } } diff --git a/src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala b/src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala index 3eeb2fc1f9bf..1a048537c510 100644 --- a/src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala +++ b/src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala @@ -851,8 +851,15 @@ abstract class ClassfileParser(reader: ReusableInstance[ReusableDataReader]) { case tpnme.RuntimeAnnotationATTR => val numAnnots = u2 + val annots = new ListBuffer[AnnotationInfo] for (n <- 0 until numAnnots; annot <- parseAnnotation(u2)) - sym.addAnnotation(annot) + annots += annot + /* `sym.withAnnotations(annots)`, like `sym.addAnnotation(annot)`, prepends, + * so if we parsed in classfile order we would wind up with the annotations + * in reverse order in `sym.annotations`. Instead we just read them out the + * other way around, for now. TODO: sym.addAnnotation add to the end? + */ + sym.setAnnotations(sym.annotations ::: annots.toList) // TODO 1: parse runtime visible annotations on parameters // case tpnme.RuntimeParamAnnotationATTR diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala index 27b55d5d4ac5..0bc3aa346591 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala @@ -3887,6 +3887,11 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper case Typed(t, _) => tree2ConstArg(t, pt) + case tree if unit.isJava && pt.typeSymbol == ArrayClass => + /* If we get here, we have a Java array annotation argument which was passed + * as a single value, and needs to be wrapped. */ + trees2ConstArg(tree :: Nil, pt.typeArgs.head) + case tree => tryConst(tree, pt) } diff --git a/src/interactive/scala/tools/nsc/interactive/Global.scala b/src/interactive/scala/tools/nsc/interactive/Global.scala index 0594ff25ae5e..a2fad0c2dd56 100644 --- a/src/interactive/scala/tools/nsc/interactive/Global.scala +++ b/src/interactive/scala/tools/nsc/interactive/Global.scala @@ -1369,7 +1369,7 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") val symbols = Set(UnitClass, BooleanClass, ByteClass, ShortClass, IntClass, LongClass, FloatClass, - DoubleClass, NilModule, ListClass) ++ TupleClass.seq + DoubleClass, NilModule, ListClass, PredefModule) ++ TupleClass.seq ++ ArrayModule_overloadedApply.alternatives symbols.foreach(_.initialize) } diff --git a/src/partest-extras/scala/tools/partest/BytecodeTest.scala b/src/partest-extras/scala/tools/partest/BytecodeTest.scala index 309a6d49c482..35c487ba673b 100644 --- a/src/partest-extras/scala/tools/partest/BytecodeTest.scala +++ b/src/partest-extras/scala/tools/partest/BytecodeTest.scala @@ -124,6 +124,10 @@ abstract class BytecodeTest { } // loading + protected def getField(classNode: ClassNode, name: String): FieldNode = + classNode.fields.asScala.find(_.name == name) getOrElse + sys.error(s"Didn't find field '$name' in class '${classNode.name}'") + protected def getMethod(classNode: ClassNode, name: String): MethodNode = classNode.methods.asScala.find(_.name == name) getOrElse sys.error(s"Didn't find method '$name' in class '${classNode.name}'") diff --git a/test/files/presentation/parse-invariants/src/a/A.java b/test/files/presentation/parse-invariants/src/a/A.java index a04478149103..fa81c6699160 100644 --- a/test/files/presentation/parse-invariants/src/a/A.java +++ b/test/files/presentation/parse-invariants/src/a/A.java @@ -1,5 +1,10 @@ package syntax; +@NoArgs +@Empty() +@Simple(n = 1, c = '2', f = 6.7f, d = 8.9, s = "t", z = A.class, e = P.Pluto, a = @C.I("t")) +@Arrays({ @Array({0, 1, C._2}), @Array(3) }) +@Deprecated class A { transient volatile int x; strictfp void test() { @@ -11,6 +16,7 @@ synchronized void syncMethod() {} void thrower() throws Throwable {} + @Deprecated void deprecated() {} } strictfp class B {} \ No newline at end of file diff --git a/test/files/run/t4788-separate-compilation.check b/test/files/run/t4788-separate-compilation.check index 618fddfea324..38381c28a783 100644 --- a/test/files/run/t4788-separate-compilation.check +++ b/test/files/run/t4788-separate-compilation.check @@ -1,5 +1,9 @@ Some(@Ljava/lang/Deprecated;()) None -Some(@LSAnnotation;()) -Some(@LCAnnotation;()) +None +Some(@LCAnnotation;() // invisible) Some(@LRAnnotation;()) +Some(@LAnnWithArgs_0$Ann;(value="literal") // invisible) +Some(@LAnnWithArgs_0$Ann;(value="muk") // invisible) +Some(@LAnnWithArgs_0$Ann;(value="mukja") // invisible) +Some(@LAnnWithArgs_0$Ann;(value="mukja") // invisible) diff --git a/test/files/run/t4788-separate-compilation/AnnWithArgs_0.java b/test/files/run/t4788-separate-compilation/AnnWithArgs_0.java new file mode 100644 index 000000000000..898f61291d13 --- /dev/null +++ b/test/files/run/t4788-separate-compilation/AnnWithArgs_0.java @@ -0,0 +1,19 @@ +public class AnnWithArgs_0 { + public static @interface Ann { + String value(); + } + + @Ann("literal") + public static int x1 = 1; + + @Ann(AnnWithArgs_0.strS) + public static int x2 = 2; + + @Ann("muk" + "ja") + public static int x3 = 3; + + @Ann(AnnWithArgs_0.strS + "ja") + public static int x4 = 4; + + public static final String strS = "muk"; +} diff --git a/test/files/run/t4788-separate-compilation/Test_2.scala b/test/files/run/t4788-separate-compilation/Test_2.scala index b4267e948aca..b73dce374af6 100644 --- a/test/files/run/t4788-separate-compilation/Test_2.scala +++ b/test/files/run/t4788-separate-compilation/Test_2.scala @@ -18,6 +18,19 @@ object Test extends BytecodeTest { .map(_.trim) } + def annotationsForField(className: String, fieldName: String): Option[String] = { + val field = getField(loadClassNode(className, skipDebugInfo = false), fieldName) + val textifier = new Textifier + field.accept(new TraceClassVisitor(null, textifier, null)) + + val fieldString = stringFromWriter(w => textifier.print(w)) + fieldString + .split('\n') + .filterNot(_.contains("@Lscala/reflect/ScalaSignature")) + .find(_.contains("@L")) + .map(_.trim) + } + def show { // It seems like @java.lang.Deprecated shows up in both the // Deprecated attribute and RuntimeVisibleAnnotation attribute, @@ -31,5 +44,10 @@ object Test extends BytecodeTest { println(annotationsForClass("S")) println(annotationsForClass("C")) println(annotationsForClass("R")) + + println(annotationsForField("AnnWithArgs_0", "x1")) + println(annotationsForField("AnnWithArgs_0", "x2")) + println(annotationsForField("AnnWithArgs_0", "x3")) + println(annotationsForField("AnnWithArgs_0", "x4")) } } diff --git a/test/files/run/t4788.check b/test/files/run/t4788.check index 618fddfea324..e4d6fa09e575 100644 --- a/test/files/run/t4788.check +++ b/test/files/run/t4788.check @@ -1,5 +1,9 @@ Some(@Ljava/lang/Deprecated;()) None -Some(@LSAnnotation;()) -Some(@LCAnnotation;()) +None +Some(@LCAnnotation;() // invisible) Some(@LRAnnotation;()) +Some(@LAnnWithArgs$Ann;(value="literal") // invisible) +Some(@LAnnWithArgs$Ann;(value="muk") // invisible) +Some(@LAnnWithArgs$Ann;(value="mukja") // invisible) +Some(@LAnnWithArgs$Ann;(value="mukja") // invisible) diff --git a/test/files/run/t4788/AnnWithArgs.java b/test/files/run/t4788/AnnWithArgs.java new file mode 100644 index 000000000000..bc42b1f0e4e4 --- /dev/null +++ b/test/files/run/t4788/AnnWithArgs.java @@ -0,0 +1,19 @@ +public class AnnWithArgs { + public static @interface Ann { + String value(); + } + + @Ann("literal") + public static int x1 = 1; + + @Ann(AnnWithArgs.strS) + public static int x2 = 2; + + @Ann("muk" + "ja") + public static int x3 = 3; + + @Ann(AnnWithArgs.strS + "ja") + public static int x4 = 4; + + public static final String strS = "muk"; +} diff --git a/test/files/run/t4788/Test.scala b/test/files/run/t4788/Test.scala index b4267e948aca..79936dc681dc 100644 --- a/test/files/run/t4788/Test.scala +++ b/test/files/run/t4788/Test.scala @@ -18,6 +18,19 @@ object Test extends BytecodeTest { .map(_.trim) } + def annotationsForField(className: String, fieldName: String): Option[String] = { + val field = getField(loadClassNode(className, skipDebugInfo = false), fieldName) + val textifier = new Textifier + field.accept(new TraceClassVisitor(null, textifier, null)) + + val fieldString = stringFromWriter(w => textifier.print(w)) + fieldString + .split('\n') + .filterNot(_.contains("@Lscala/reflect/ScalaSignature")) + .find(_.contains("@L")) + .map(_.trim) + } + def show { // It seems like @java.lang.Deprecated shows up in both the // Deprecated attribute and RuntimeVisibleAnnotation attribute, @@ -31,5 +44,10 @@ object Test extends BytecodeTest { println(annotationsForClass("S")) println(annotationsForClass("C")) println(annotationsForClass("R")) + + println(annotationsForField("AnnWithArgs", "x1")) + println(annotationsForField("AnnWithArgs", "x2")) + println(annotationsForField("AnnWithArgs", "x3")) + println(annotationsForField("AnnWithArgs", "x4")) } } diff --git a/test/files/run/t8928.javaopts b/test/files/run/t8928.javaopts new file mode 100644 index 000000000000..a8e6bbca18ae --- /dev/null +++ b/test/files/run/t8928.javaopts @@ -0,0 +1 @@ +-Dneeds.forked.jvm diff --git a/test/files/run/t8928/Annotated_0.java b/test/files/run/t8928/Annotated_0.java new file mode 100644 index 000000000000..c3eef08d1713 --- /dev/null +++ b/test/files/run/t8928/Annotated_0.java @@ -0,0 +1,19 @@ +package test; + +// This should stay in sync with Annotated_1 to test annotations defined in the same compilation run + +@NoArgs_0 +@Simple_0(_byte = 1, _char = '2', _short = Simple_0.THREE, _int = 4, _long = 5, _float = 6.7f, _double = 8.9, _string = "ten", _class = Object.class) +@Nested_0( + inner = @Nested_0.Inner("turkey") +) +@Array_0.Repeated({ + @Array_0({8, 6, 7, 5, 3, 0, Annotated_0.NINE}), + @Array_0(6) +}) +@Enum_0(choice = Enum_0.Enum.ONE) +@Empty_0() +public class Annotated_0 { + public static final int NINE = 9; +} + diff --git a/test/files/run/t8928/Annotated_1.java b/test/files/run/t8928/Annotated_1.java new file mode 100644 index 000000000000..012fadc1087f --- /dev/null +++ b/test/files/run/t8928/Annotated_1.java @@ -0,0 +1,19 @@ +package test; + +// This should stay in sync with Annotated_0 to test annotations defined in an earlier compilation run + +@NoArgs_0 +@Simple_0(_byte = 1, _char = '2', _short = Simple_0.THREE, _int = 4, _long = 5, _float = 6.7f, _double = 8.9, _string = "ten", _class = Object.class) +@Nested_0( + inner = @Nested_0.Inner("turkey") +) +@Array_0.Repeated({ + @Array_0({8, 6, 7, 5, 3, 0, Annotated_1.NINE}), + @Array_0(6) +}) +@Enum_0(choice = Enum_0.Enum.ONE) +@Empty_0() +public class Annotated_1 { + public static final int NINE = 9; +} + diff --git a/test/files/run/t8928/Array_0.java b/test/files/run/t8928/Array_0.java new file mode 100644 index 000000000000..abc5503726b4 --- /dev/null +++ b/test/files/run/t8928/Array_0.java @@ -0,0 +1,14 @@ +package test; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(Array_0.Repeated.class) +public @interface Array_0 { + int[] value(); + + @Retention(RetentionPolicy.RUNTIME) + public @interface Repeated { + Array_0[] value(); + } +} diff --git a/test/files/run/t8928/Checks_0.scala b/test/files/run/t8928/Checks_0.scala new file mode 100644 index 000000000000..bff5cd142ea3 --- /dev/null +++ b/test/files/run/t8928/Checks_0.scala @@ -0,0 +1,84 @@ +package test + +import reflect.api.Universe + +class Checks[U <: Universe with Singleton](val universe: U, ordered: Boolean) { + import universe._ + + def check(tpe: Type): Unit = { + val ObjectTpe = typeOf[Object] + + def assertMatch(name: String, v: Tree)(pf: PartialFunction[Tree, Any]): Unit = + if (!pf.isDefinedAt(v)) throw new AssertionError(s"$name: ${showRaw(v)}") + + val anns: List[Annotation] = tpe.typeSymbol.annotations + + anns match { + case List(noArgs, simple, nested, arrays, enum, empty) => + assert(noArgs.tree.tpe =:= typeOf[NoArgs_0]) + assert(noArgs.tree.children.size == 1) + + assert(simple.tree.tpe =:= typeOf[Simple_0]) + + val parameters: List[(String, Tree)] = + simple.tree.children.tail.map { + case AssignOrNamedArg(Ident(TermName(name)), value) => + name -> value + } + + /* Runtime reflection does not preserve ordering on java annotations, + * so check only when we're in a macro universe. */ + if (ordered) { + assert(parameters.map(_._1) == List( + "_byte", "_char", "_short", "_int", "_long", "_float", "_double", "_string", "_class")) + } + + parameters.sortBy(_._1) match { + case ("_byte", byte) :: ("_char", char) + :: ("_class", clasz) :: ("_double", double) + :: ("_float", float) :: ("_int", int) + :: ("_long", long) :: ("_short", short) + :: ("_string", string) :: Nil => + + assertMatch("byte", byte) { case Literal(Constant(1)) => } + assertMatch("char", char) { case Literal(Constant('2')) => } + assertMatch("short", short) { case Literal(Constant(3)) => } + assertMatch("int", int) { case Literal(Constant(4)) => } + assertMatch("long", long) { case Literal(Constant(5L)) => } + assertMatch("float", float) { case Literal(Constant(6.7f)) => } + assertMatch("double", double) { case Literal(Constant(8.9d)) => } + assertMatch("string", string) { case Literal(Constant("ten")) => } + assertMatch("class", clasz) { case Literal(Constant(ObjectTpe)) => } + } + + assert(nested.tree.tpe =:= typeOf[Nested_0]) + nested.tree.children match { + case _ :: inner :: Nil => + assertMatch("inner", inner) { + case AssignOrNamedArg(Ident(TermName("inner")), Apply(Select(New(tpe), nme.CONSTRUCTOR), AssignOrNamedArg(Ident(TermName("value")), Literal(Constant("turkey"))) :: Nil)) + if tpe.tpe =:= typeOf[Nested_0.Inner] => + } + } + + assert(arrays.tree.tpe =:= typeOf[Array_0.Repeated]) + arrays.tree.children match { + case _ :: AssignOrNamedArg(Ident(TermName("value")), Apply(arr, fst :: snd :: Nil)) :: Nil => + assertMatch("value(0)", fst) { + case Apply(Select(New(tpe), nme.CONSTRUCTOR), AssignOrNamedArg(Ident(TermName("value")), Apply(arr, args)) :: Nil) + if ((args zip Seq(8, 6, 7, 5, 3, 0, 9)) forall { case (Literal(Constant(l)), r) => l == r }) && + tpe.tpe =:= typeOf[Array_0] => + } + assertMatch("value(1)", snd) { + case Apply(Select(New(tpe), nme.CONSTRUCTOR), AssignOrNamedArg(Ident(TermName("value")), Apply(arr, args)) :: Nil) + if ((args zip Seq(6)) forall { case (Literal(Constant(l)), r) => l == r }) && + tpe.tpe =:= typeOf[Array_0] => + } + } + assert(enum.tree.tpe =:= typeOf[Enum_0]) + + assert(empty.tree.tpe =:= typeOf[Empty_0]) + assert(empty.tree.children.size == 1) + } + } + +} \ No newline at end of file diff --git a/test/files/run/t8928/Empty_0.java b/test/files/run/t8928/Empty_0.java new file mode 100644 index 000000000000..a5f6866c3d76 --- /dev/null +++ b/test/files/run/t8928/Empty_0.java @@ -0,0 +1,7 @@ +package test; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Empty_0 {} \ No newline at end of file diff --git a/test/files/run/t8928/Enum_0.java b/test/files/run/t8928/Enum_0.java new file mode 100644 index 000000000000..ab88e7ed63c8 --- /dev/null +++ b/test/files/run/t8928/Enum_0.java @@ -0,0 +1,12 @@ +package test; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Enum_0 { + Enum choice(); + + public enum Enum { + ONE, OTHER + } +} \ No newline at end of file diff --git a/test/files/run/t8928/Macros_0.scala b/test/files/run/t8928/Macros_0.scala new file mode 100644 index 000000000000..4940e3d60ab9 --- /dev/null +++ b/test/files/run/t8928/Macros_0.scala @@ -0,0 +1,16 @@ +package test + +object Macros { + import language.experimental.macros + def apply[T]: Unit = macro impl[T] + + import reflect.macros.whitebox + def impl[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = { + import c.universe._ + + new Checks[c.universe.type](c.universe, ordered = true) + .check(weakTypeOf[T]) + + Literal(Constant(())) + } +} \ No newline at end of file diff --git a/test/files/run/t8928/Nested_0.java b/test/files/run/t8928/Nested_0.java new file mode 100644 index 000000000000..ea71d9b8cf06 --- /dev/null +++ b/test/files/run/t8928/Nested_0.java @@ -0,0 +1,13 @@ +package test; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Nested_0 { + Inner inner(); + + @Retention(RetentionPolicy.RUNTIME) + public @interface Inner { + String value(); + } +} \ No newline at end of file diff --git a/test/files/run/t8928/NoArgs_0.java b/test/files/run/t8928/NoArgs_0.java new file mode 100644 index 000000000000..e29b688c116b --- /dev/null +++ b/test/files/run/t8928/NoArgs_0.java @@ -0,0 +1,7 @@ +package test; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +public @interface NoArgs_0 {} + diff --git a/test/files/run/t8928/Simple_0.java b/test/files/run/t8928/Simple_0.java new file mode 100644 index 000000000000..b67c2bee241c --- /dev/null +++ b/test/files/run/t8928/Simple_0.java @@ -0,0 +1,18 @@ +package test; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Simple_0 { + byte _byte(); + char _char(); + short _short(); + int _int(); + long _long(); + float _float(); + double _double(); + String _string(); + Class _class(); + + short THREE = 3; +} \ No newline at end of file diff --git a/test/files/run/t8928/Test_1.scala b/test/files/run/t8928/Test_1.scala new file mode 100644 index 000000000000..1cef564ff1be --- /dev/null +++ b/test/files/run/t8928/Test_1.scala @@ -0,0 +1,16 @@ +import test._ + +object Test extends App { + Macros.apply[Annotated_0] + Macros.apply[Annotated_1] + + import reflect.runtime.universe + import universe._ + + /* consistency check with runtime universe */ + val checks = new Checks[universe.type](universe, ordered = false) + checks.check(weakTypeOf[Annotated_0]) + checks.check(weakTypeOf[Annotated_1]) + + +} \ No newline at end of file