Skip to content

Support for parsing annotation arguments from java #11117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe merge this pattern match with the one above, to make the logic more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done it that way because I wanted to avoid repeating typed(tree.arg, pt) three times, as it may be a source of errors when the logic would be changed. On the other hand, merging those two patterns is much more readable.


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)