Skip to content

Commit a113884

Browse files
authored
Merge pull request #11117 from Kordyjan/java-annotations
Support for parsing annotation arguments from java
2 parents 8ed6b59 + e057a3e commit a113884

File tree

8 files changed

+287
-18
lines changed

8 files changed

+287
-18
lines changed

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

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ object JavaParsers {
246246

247247
def qualId(): RefTree = {
248248
var t: RefTree = atSpan(in.offset) { Ident(ident()) }
249-
while (in.token == DOT) {
249+
while (in.token == DOT && in.lookaheadToken == IDENTIFIER) {
250250
in.nextToken()
251251
t = atSpan(t.span.start, in.offset) { Select(t, ident()) }
252252
}
@@ -343,22 +343,105 @@ object JavaParsers {
343343
annots.toList
344344
}
345345

346-
/** Annotation ::= TypeName [`(` AnnotationArgument {`,` AnnotationArgument} `)`]
346+
/** Annotation ::= TypeName [`(` [AnnotationArgument {`,` AnnotationArgument}] `)`]
347+
* AnnotationArgument ::= ElementValuePair | ELementValue
348+
* ElementValuePair ::= Identifier `=` ElementValue
349+
* ElementValue ::= ConstExpressionSubset
350+
* | ElementValueArrayInitializer
351+
* | Annotation
352+
* ElementValueArrayInitializer ::= `{` [ElementValue {`,` ElementValue}] [`,`] `}`
353+
* ConstExpressionSubset ::= Literal
354+
* | QualifiedName
355+
* | ClassLiteral
356+
*
357+
* We support only subset of const expressions expected in this context by java.
358+
* If we encounter expression that we cannot parse, we do not raise parsing error,
359+
* but instead we skip entire annotation silently.
347360
*/
348361
def annotation(): Option[Tree] = {
349-
val id = convertToTypeId(qualId())
350-
// only parse annotations without arguments
351-
if (in.token == LPAREN && in.lookaheadToken != RPAREN) {
352-
skipAhead()
353-
accept(RPAREN)
354-
None
355-
}
356-
else {
357-
if (in.token == LPAREN) {
362+
object LiteralT:
363+
def unapply(token: Token) = Option(token match {
364+
case TRUE => true
365+
case FALSE => false
366+
case CHARLIT => in.name(0)
367+
case INTLIT => in.intVal(false).toInt
368+
case LONGLIT => in.intVal(false)
369+
case FLOATLIT => in.floatVal(false).toFloat
370+
case DOUBLELIT => in.floatVal(false)
371+
case STRINGLIT => in.name.toString
372+
case _ => null
373+
}).map(Constant(_))
374+
375+
def classOrId(): Tree =
376+
val id = qualId()
377+
if in.lookaheadToken == CLASS then
358378
in.nextToken()
359-
accept(RPAREN)
379+
accept(CLASS)
380+
TypeApply(
381+
Select(
382+
scalaDot(nme.Predef),
383+
nme.classOf),
384+
convertToTypeId(id) :: Nil
385+
)
386+
else id
387+
388+
def array(): Option[Tree] =
389+
accept(LBRACE)
390+
val buffer = ListBuffer[Option[Tree]]()
391+
while in.token != RBRACE do
392+
buffer += argValue()
393+
if in.token == COMMA then
394+
in.nextToken() // using this instead of repsep allows us to handle trailing commas
395+
accept(RBRACE)
396+
Option.unless(buffer contains None) {
397+
Apply(scalaDot(nme.Array), buffer.flatten.toList)
398+
}
399+
400+
def argValue(): Option[Tree] =
401+
val tree = in.token match {
402+
case LiteralT(c) =>
403+
val tree = atSpan(in.offset)(Literal(c))
404+
in.nextToken()
405+
Some(tree)
406+
case AT =>
407+
in.nextToken()
408+
annotation()
409+
case IDENTIFIER => Some(classOrId())
410+
case LBRACE => array()
411+
case _ => None
360412
}
361-
Some(ensureApplied(Select(New(id), nme.CONSTRUCTOR)))
413+
if in.token == COMMA || in.token == RBRACE || in.token == RPAREN then
414+
tree
415+
else
416+
skipTo(COMMA, RBRACE, RPAREN)
417+
None
418+
419+
def annArg(): Option[Tree] =
420+
val name = if (in.token == IDENTIFIER && in.lookaheadToken == EQUALS)
421+
val n = ident()
422+
accept(EQUALS)
423+
n
424+
else
425+
nme.value
426+
argValue().map(NamedArg(name, _))
427+
428+
429+
val id = convertToTypeId(qualId())
430+
val args = ListBuffer[Option[Tree]]()
431+
if in.token == LPAREN then
432+
in.nextToken()
433+
if in.token != RPAREN then
434+
args += annArg()
435+
while in.token == COMMA do
436+
in.nextToken()
437+
args += annArg()
438+
accept(RPAREN)
439+
440+
Option.unless(args contains None) {
441+
Apply(
442+
Select(New(id), nme.CONSTRUCTOR),
443+
args.flatten.toList
444+
)
362445
}
363446
}
364447

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -854,8 +854,24 @@ class Typer extends Namer
854854
case _ => tree
855855
}
856856

857+
857858
def typedNamedArg(tree: untpd.NamedArg, pt: Type)(using Context): NamedArg = {
858-
val arg1 = typed(tree.arg, pt)
859+
/* Special case for resolving types for arguments of an annotation defined in Java.
860+
* It allows that value of any type T can appear in positions where Array[T] is expected.
861+
* For example, both `@Annot(5)` and `@Annot({5, 6}) are viable calls of the constructor
862+
* of annotation defined as `@interface Annot { int[] value() }`
863+
* We assume that calling `typedNamedArg` in context of Java implies that we are dealing
864+
* with annotation contructor, as named arguments are not allowed anywhere else in Java.
865+
*/
866+
val arg1 = pt match {
867+
case AppliedType(a, typ :: Nil) if ctx.isJava && a.isRef(defn.ArrayClass) =>
868+
tryAlternatively { typed(tree.arg, pt) } {
869+
val elemTp = untpd.TypedSplice(TypeTree(typ))
870+
typed(untpd.JavaSeqLiteral(tree.arg :: Nil, elemTp), pt)
871+
}
872+
case _ => typed(tree.arg, pt)
873+
}
874+
859875
assignType(cpy.NamedArg(tree)(tree.name, arg1), arg1)
860876
}
861877

@@ -1977,10 +1993,10 @@ class Typer extends Namer
19771993
*/
19781994
def annotContext(mdef: untpd.Tree, sym: Symbol)(using Context): Context = {
19791995
def isInner(owner: Symbol) = owner == sym || sym.is(Param) && owner == sym.owner
1980-
val c = ctx.outersIterator.dropWhile(c => isInner(c.owner)).next()
1981-
c.property(ExprOwner) match {
1982-
case Some(exprOwner) if c.owner.isClass => c.exprContext(mdef, exprOwner)
1983-
case _ => c
1996+
val outer = ctx.outersIterator.dropWhile(c => isInner(c.owner)).next()
1997+
outer.property(ExprOwner) match {
1998+
case Some(exprOwner) if outer.owner.isClass => outer.exprContext(mdef, exprOwner)
1999+
case _ => outer
19842000
}
19852001
}
19862002

tests/run/java-annot-params.check

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class annots.A
2+
class annots.A
3+
SOME STRING
4+
VALUE OF CONST
5+
false
6+
13.7
7+
VALUE
8+
List(a, b, c)
9+
List()
10+
List(SINGLE)
11+
List(ABC)
12+
13+
class annots.A
14+
class annots.A
15+
SOME STRING
16+
VALUE OF CONST
17+
false
18+
13.7
19+
VALUE
20+
List(a, b, c)
21+
List()
22+
List(SINGLE)
23+
List(ABC)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package annots;
2+
3+
import java.lang.annotation.*;
4+
5+
@Retention(RetentionPolicy.RUNTIME)
6+
@interface WithClass {
7+
Class<?> arg();
8+
}
9+
10+
@Retention(RetentionPolicy.RUNTIME)
11+
@interface WithClassDefaultName {
12+
Class<?> value();
13+
}
14+
15+
@Retention(RetentionPolicy.RUNTIME)
16+
@interface WithString {
17+
String arg();
18+
}
19+
20+
@Retention(RetentionPolicy.RUNTIME)
21+
@interface WithReference {
22+
String arg();
23+
}
24+
25+
@Retention(RetentionPolicy.RUNTIME)
26+
@interface WithBoolean {
27+
boolean arg();
28+
}
29+
30+
@Retention(RetentionPolicy.RUNTIME)
31+
@interface WithFloat {
32+
float arg();
33+
}
34+
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@interface WithNested {
37+
Nested arg();
38+
}
39+
40+
@Retention(RetentionPolicy.RUNTIME)
41+
@interface Nested {
42+
String value();
43+
}
44+
45+
@Retention(RetentionPolicy.RUNTIME)
46+
@interface WithArray {
47+
String[] value();
48+
}
49+
50+
@Retention(RetentionPolicy.RUNTIME)
51+
@interface WithEmptyArray {
52+
String[] value();
53+
}
54+
55+
@Retention(RetentionPolicy.RUNTIME)
56+
@interface WithSingleElement {
57+
String[] value();
58+
}
59+
60+
@Retention(RetentionPolicy.RUNTIME)
61+
@interface WithMultipleArgs {
62+
int[] ints();
63+
float floatVal();
64+
Nested[] annots();
65+
Class<?> clazz();
66+
String[] value();
67+
}
68+
69+
@Retention(RetentionPolicy.RUNTIME)
70+
@interface ShouldNotCrash {
71+
String value();
72+
}
73+
74+
@Retention(RetentionPolicy.RUNTIME)
75+
@interface ShouldAlsoNotCrash {
76+
String value();
77+
int[] ints();
78+
}
79+
80+
class A {
81+
static final String CONST = "VALUE OF CONST";
82+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object Test:
2+
def main(args: Array[String]): Unit =
3+
annots.runTest(classOf[annots.Use_0])
4+
println()
5+
annots.runTest(classOf[annots.Use_1])
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package annots;
2+
3+
@WithClass(arg = A.class)
4+
@WithClassDefaultName(A.class)
5+
@WithString(arg = "SOME STRING")
6+
@WithReference(arg = A.CONST)
7+
@WithBoolean(arg = false)
8+
@WithFloat(arg = 13.7f)
9+
@WithNested(arg = @Nested("VALUE"))
10+
@WithArray({ "a", "b", "c" })
11+
@WithEmptyArray({})
12+
@WithSingleElement("SINGLE")
13+
@WithMultipleArgs(
14+
ints = {1, 2, 3, },
15+
annots = { @Nested("Value"), @Nested(A.CONST) },
16+
floatVal = 13.7f,
17+
value = "ABC",
18+
clazz = A.class
19+
)
20+
@ShouldNotCrash(false ? "A" + A.CONST : "B")
21+
@ShouldAlsoNotCrash(value = "C", ints = { 1, 2, 3, 5 - 1 })
22+
public class Use_0 {}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package annots;
2+
3+
@WithClass(arg = A.class)
4+
@WithClassDefaultName(A.class)
5+
@WithString(arg = "SOME STRING")
6+
@WithReference(arg = A.CONST)
7+
@WithBoolean(arg = false)
8+
@WithFloat(arg = 13.7f)
9+
@WithNested(arg = @Nested("VALUE"))
10+
@WithArray({"a", "b", "c"})
11+
@WithEmptyArray({})
12+
@WithSingleElement("SINGLE")
13+
@WithMultipleArgs(
14+
ints = { 1, 2, 3, },
15+
annots = { @Nested("Value"),
16+
@Nested(A.CONST) },
17+
floatVal = 13.7f,
18+
value = "ABC",
19+
clazz = A.class
20+
)
21+
public class Use_1 {}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package annots
2+
3+
def runTest(cls: Class[_]): Unit =
4+
val params =
5+
Option(cls.getAnnotation(classOf[WithClass])).map(_.arg) ::
6+
Option(cls.getAnnotation(classOf[WithClassDefaultName])).map(_.value) ::
7+
Option(cls.getAnnotation(classOf[WithString])).map(_.arg) ::
8+
Option(cls.getAnnotation(classOf[WithReference])).map(_.arg) ::
9+
Option(cls.getAnnotation(classOf[WithBoolean])).map(_.arg) ::
10+
Option(cls.getAnnotation(classOf[WithFloat])).map(_.arg) ::
11+
Option(cls.getAnnotation(classOf[WithNested])).map(_.arg.value) ::
12+
Option(cls.getAnnotation(classOf[WithArray])).map(_.value.toList) ::
13+
Option(cls.getAnnotation(classOf[WithEmptyArray])).map(_.value.toList) ::
14+
Option(cls.getAnnotation(classOf[WithSingleElement])).map(_.value.toList) ::
15+
Option(cls.getAnnotation(classOf[WithMultipleArgs])).map(_.value.toList) ::
16+
Nil
17+
params.flatten.foreach(println)

0 commit comments

Comments
 (0)