Skip to content

Commit

Permalink
Merge pull request #11117 from Kordyjan/java-annotations
Browse files Browse the repository at this point in the history
Support for parsing annotation arguments from java
  • Loading branch information
Kordyjan committed Feb 3, 2021
2 parents 8ed6b59 + e057a3e commit a113884
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 18 deletions.
109 changes: 96 additions & 13 deletions compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()) }
}
Expand Down Expand Up @@ -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
)
}
}

Expand Down
26 changes: 21 additions & 5 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}
}

Expand Down
23 changes: 23 additions & 0 deletions tests/run/java-annot-params.check
Original file line number Diff line number Diff line change
@@ -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)
82 changes: 82 additions & 0 deletions tests/run/java-annot-params/Annots_0.java
Original file line number Diff line number Diff line change
@@ -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";
}
5 changes: 5 additions & 0 deletions tests/run/java-annot-params/Test_1.scala
Original file line number Diff line number Diff line change
@@ -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])
22 changes: 22 additions & 0 deletions tests/run/java-annot-params/Use_0.java
Original file line number Diff line number Diff line change
@@ -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 {}
21 changes: 21 additions & 0 deletions tests/run/java-annot-params/Use_1.java
Original file line number Diff line number Diff line change
@@ -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 {}
17 changes: 17 additions & 0 deletions tests/run/java-annot-params/run_1.scala
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit a113884

Please sign in to comment.