diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index e54ea0b2b6aa..50f8f319c55e 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -246,7 +246,7 @@ object JavaParsers { def qualId(): RefTree = { var t: RefTree = atSpan(in.offset) { Ident(ident()) } - while (in.token == DOT) { + while (in.token == DOT && in.lookaheadToken == IDENTIFIER) { in.nextToken() t = atSpan(t.span.start, in.offset) { Select(t, ident()) } } @@ -343,22 +343,105 @@ object JavaParsers { annots.toList } - /** Annotation ::= TypeName [`(` AnnotationArgument {`,` AnnotationArgument} `)`] + /** Annotation ::= TypeName [`(` [AnnotationArgument {`,` AnnotationArgument}] `)`] + * AnnotationArgument ::= ElementValuePair | ELementValue + * ElementValuePair ::= Identifier `=` ElementValue + * ElementValue ::= ConstExpressionSubset + * | ElementValueArrayInitializer + * | Annotation + * ElementValueArrayInitializer ::= `{` [ElementValue {`,` ElementValue}] [`,`] `}` + * ConstExpressionSubset ::= Literal + * | QualifiedName + * | ClassLiteral + * + * We support only subset of const expressions expected in this context by java. + * If we encounter expression that we cannot parse, we do not raise parsing error, + * but instead we skip entire annotation silently. */ def annotation(): Option[Tree] = { - val id = convertToTypeId(qualId()) - // only parse annotations without arguments - if (in.token == LPAREN && in.lookaheadToken != RPAREN) { - skipAhead() - accept(RPAREN) - None - } - else { - if (in.token == LPAREN) { + object LiteralT: + def unapply(token: Token) = Option(token match { + case TRUE => true + case FALSE => false + case CHARLIT => in.name(0) + case INTLIT => in.intVal(false).toInt + case LONGLIT => in.intVal(false) + case FLOATLIT => in.floatVal(false).toFloat + case DOUBLELIT => in.floatVal(false) + case STRINGLIT => in.name.toString + case _ => null + }).map(Constant(_)) + + def classOrId(): Tree = + val id = qualId() + if in.lookaheadToken == CLASS then in.nextToken() - accept(RPAREN) + accept(CLASS) + TypeApply( + Select( + scalaDot(nme.Predef), + nme.classOf), + convertToTypeId(id) :: Nil + ) + else id + + def array(): Option[Tree] = + accept(LBRACE) + val buffer = ListBuffer[Option[Tree]]() + while in.token != RBRACE do + buffer += argValue() + if in.token == COMMA then + in.nextToken() // using this instead of repsep allows us to handle trailing commas + accept(RBRACE) + Option.unless(buffer contains None) { + Apply(scalaDot(nme.Array), buffer.flatten.toList) + } + + def argValue(): Option[Tree] = + val tree = in.token match { + case LiteralT(c) => + val tree = atSpan(in.offset)(Literal(c)) + in.nextToken() + Some(tree) + case AT => + in.nextToken() + annotation() + case IDENTIFIER => Some(classOrId()) + case LBRACE => array() + case _ => None } - Some(ensureApplied(Select(New(id), nme.CONSTRUCTOR))) + if in.token == COMMA || in.token == RBRACE || in.token == RPAREN then + tree + else + skipTo(COMMA, RBRACE, RPAREN) + None + + def annArg(): Option[Tree] = + val name = if (in.token == IDENTIFIER && in.lookaheadToken == EQUALS) + val n = ident() + accept(EQUALS) + n + else + nme.value + argValue().map(NamedArg(name, _)) + + + val id = convertToTypeId(qualId()) + val args = ListBuffer[Option[Tree]]() + if in.token == LPAREN then + in.nextToken() + if in.token != RPAREN then + args += annArg() + while in.token == COMMA do + in.nextToken() + args += annArg() + accept(RPAREN) + + Option.unless(args contains None) { + Apply( + Select(New(id), nme.CONSTRUCTOR), + args.flatten.toList + ) } } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 02a85c6fd20c..7bd34bf8a414 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -854,8 +854,24 @@ class Typer extends Namer case _ => tree } + def typedNamedArg(tree: untpd.NamedArg, pt: Type)(using Context): NamedArg = { - val arg1 = typed(tree.arg, pt) + /* Special case for resolving types for arguments of an annotation defined in Java. + * It allows that value of any type T can appear in positions where Array[T] is expected. + * For example, both `@Annot(5)` and `@Annot({5, 6}) are viable calls of the constructor + * of annotation defined as `@interface Annot { int[] value() }` + * We assume that calling `typedNamedArg` in context of Java implies that we are dealing + * with annotation contructor, as named arguments are not allowed anywhere else in Java. + */ + val arg1 = pt match { + case AppliedType(a, typ :: Nil) if ctx.isJava && a.isRef(defn.ArrayClass) => + tryAlternatively { typed(tree.arg, pt) } { + val elemTp = untpd.TypedSplice(TypeTree(typ)) + typed(untpd.JavaSeqLiteral(tree.arg :: Nil, elemTp), pt) + } + case _ => typed(tree.arg, pt) + } + assignType(cpy.NamedArg(tree)(tree.name, arg1), arg1) } @@ -1977,10 +1993,10 @@ class Typer extends Namer */ def annotContext(mdef: untpd.Tree, sym: Symbol)(using Context): Context = { def isInner(owner: Symbol) = owner == sym || sym.is(Param) && owner == sym.owner - val c = ctx.outersIterator.dropWhile(c => isInner(c.owner)).next() - c.property(ExprOwner) match { - case Some(exprOwner) if c.owner.isClass => c.exprContext(mdef, exprOwner) - case _ => c + val outer = ctx.outersIterator.dropWhile(c => isInner(c.owner)).next() + outer.property(ExprOwner) match { + case Some(exprOwner) if outer.owner.isClass => outer.exprContext(mdef, exprOwner) + case _ => outer } } diff --git a/tests/run/java-annot-params.check b/tests/run/java-annot-params.check new file mode 100644 index 000000000000..273067c04996 --- /dev/null +++ b/tests/run/java-annot-params.check @@ -0,0 +1,23 @@ +class annots.A +class annots.A +SOME STRING +VALUE OF CONST +false +13.7 +VALUE +List(a, b, c) +List() +List(SINGLE) +List(ABC) + +class annots.A +class annots.A +SOME STRING +VALUE OF CONST +false +13.7 +VALUE +List(a, b, c) +List() +List(SINGLE) +List(ABC) diff --git a/tests/run/java-annot-params/Annots_0.java b/tests/run/java-annot-params/Annots_0.java new file mode 100644 index 000000000000..48b1e8a0dc08 --- /dev/null +++ b/tests/run/java-annot-params/Annots_0.java @@ -0,0 +1,82 @@ +package annots; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@interface WithClass { + Class arg(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithClassDefaultName { + Class value(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithString { + String arg(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithReference { + String arg(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithBoolean { + boolean arg(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithFloat { + float arg(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithNested { + Nested arg(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface Nested { + String value(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithArray { + String[] value(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithEmptyArray { + String[] value(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithSingleElement { + String[] value(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface WithMultipleArgs { + int[] ints(); + float floatVal(); + Nested[] annots(); + Class clazz(); + String[] value(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface ShouldNotCrash { + String value(); +} + +@Retention(RetentionPolicy.RUNTIME) +@interface ShouldAlsoNotCrash { + String value(); + int[] ints(); +} + +class A { + static final String CONST = "VALUE OF CONST"; +} diff --git a/tests/run/java-annot-params/Test_1.scala b/tests/run/java-annot-params/Test_1.scala new file mode 100644 index 000000000000..00ee08b3c623 --- /dev/null +++ b/tests/run/java-annot-params/Test_1.scala @@ -0,0 +1,5 @@ +object Test: + def main(args: Array[String]): Unit = + annots.runTest(classOf[annots.Use_0]) + println() + annots.runTest(classOf[annots.Use_1]) \ No newline at end of file diff --git a/tests/run/java-annot-params/Use_0.java b/tests/run/java-annot-params/Use_0.java new file mode 100644 index 000000000000..5ddcb542c418 --- /dev/null +++ b/tests/run/java-annot-params/Use_0.java @@ -0,0 +1,22 @@ +package annots; + +@WithClass(arg = A.class) +@WithClassDefaultName(A.class) +@WithString(arg = "SOME STRING") +@WithReference(arg = A.CONST) +@WithBoolean(arg = false) +@WithFloat(arg = 13.7f) +@WithNested(arg = @Nested("VALUE")) +@WithArray({ "a", "b", "c" }) +@WithEmptyArray({}) +@WithSingleElement("SINGLE") +@WithMultipleArgs( + ints = {1, 2, 3, }, + annots = { @Nested("Value"), @Nested(A.CONST) }, + floatVal = 13.7f, + value = "ABC", + clazz = A.class +) +@ShouldNotCrash(false ? "A" + A.CONST : "B") +@ShouldAlsoNotCrash(value = "C", ints = { 1, 2, 3, 5 - 1 }) +public class Use_0 {} \ No newline at end of file diff --git a/tests/run/java-annot-params/Use_1.java b/tests/run/java-annot-params/Use_1.java new file mode 100644 index 000000000000..cec0aa69ea3f --- /dev/null +++ b/tests/run/java-annot-params/Use_1.java @@ -0,0 +1,21 @@ +package annots; + +@WithClass(arg = A.class) +@WithClassDefaultName(A.class) +@WithString(arg = "SOME STRING") +@WithReference(arg = A.CONST) +@WithBoolean(arg = false) +@WithFloat(arg = 13.7f) +@WithNested(arg = @Nested("VALUE")) +@WithArray({"a", "b", "c"}) +@WithEmptyArray({}) +@WithSingleElement("SINGLE") +@WithMultipleArgs( + ints = { 1, 2, 3, }, + annots = { @Nested("Value"), + @Nested(A.CONST) }, + floatVal = 13.7f, + value = "ABC", + clazz = A.class +) +public class Use_1 {} \ No newline at end of file diff --git a/tests/run/java-annot-params/run_1.scala b/tests/run/java-annot-params/run_1.scala new file mode 100644 index 000000000000..0fe66f00a2e6 --- /dev/null +++ b/tests/run/java-annot-params/run_1.scala @@ -0,0 +1,17 @@ +package annots + +def runTest(cls: Class[_]): Unit = + val params = + Option(cls.getAnnotation(classOf[WithClass])).map(_.arg) :: + Option(cls.getAnnotation(classOf[WithClassDefaultName])).map(_.value) :: + Option(cls.getAnnotation(classOf[WithString])).map(_.arg) :: + Option(cls.getAnnotation(classOf[WithReference])).map(_.arg) :: + Option(cls.getAnnotation(classOf[WithBoolean])).map(_.arg) :: + Option(cls.getAnnotation(classOf[WithFloat])).map(_.arg) :: + Option(cls.getAnnotation(classOf[WithNested])).map(_.arg.value) :: + Option(cls.getAnnotation(classOf[WithArray])).map(_.value.toList) :: + Option(cls.getAnnotation(classOf[WithEmptyArray])).map(_.value.toList) :: + Option(cls.getAnnotation(classOf[WithSingleElement])).map(_.value.toList) :: + Option(cls.getAnnotation(classOf[WithMultipleArgs])).map(_.value.toList) :: + Nil + params.flatten.foreach(println) \ No newline at end of file