Skip to content

Commit

Permalink
Fix scala#10788: Parsing arguments for java annotations
Browse files Browse the repository at this point in the history
* A port of fixes from scala/scala#8781 with improvements.
* Some of improvements come from scala/scala#8982.
* There are small changes to typer to allow for single elements of any
  type T be used where array of T is expected inside of java annotations
  arguments as it is correct in java and is used in 2 files in scala
  standard library.
  • Loading branch information
Kordyjan authored and michelou committed Feb 5, 2021
1 parent db6f2f5 commit f7a0667
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 18 deletions.
92 changes: 79 additions & 13 deletions compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala
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 @@ -346,20 +346,86 @@ object JavaParsers {
/** Annotation ::= TypeName [`(` AnnotationArgument {`,` AnnotationArgument} `)`]
*/
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(): Tree =
accept(LBRACE)
val buffer = ListBuffer[Tree]()
while (in.token != RBRACE) {
buffer += argValue()
if (in.token == COMMA) in.nextToken() // using this instead of repsep allows us to handle trailing commas
}
Some(ensureApplied(Select(New(id), nme.CONSTRUCTOR)))
}
val ok = !buffer.contains(EmptyTree)
in.token match {
case RBRACE if ok =>
accept(RBRACE)
Apply(scalaDot(nme.Array), buffer.toList)
case _ =>
skipTo(RBRACE)
EmptyTree
}

def argValue(): Tree =
in.token match {
case LiteralT(c) =>
val tree = atSpan(in.offset)(Literal(c))
in.nextToken()
tree
case AT =>
in.nextToken()
annotation().get
case IDENTIFIER => classOrId()
case LBRACE => array()
case _ => EmptyTree
}

def annArg(): Tree =
if (in.token == IDENTIFIER && in.lookaheadToken == EQUALS)
val name = ident()
accept(EQUALS)
val argv = argValue()
NamedArg(name, argv)

else
NamedArg(nme.value, argValue())


val id = convertToTypeId(qualId())
val args = if in.token == LPAREN then
in.nextToken()
val args = repsep(annArg, COMMA)
accept(RPAREN)
args
else Nil

Some(Apply(
Select(New(id), nme.CONSTRUCTOR),
args
))
}

def modifiers(inInterface: Boolean): Modifiers = {
Expand Down
28 changes: 23 additions & 5 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Expand Up @@ -81,6 +81,15 @@ object Typer {
*/
private val DroppedEmptyArgs = new Property.Key[Unit]


/** Marker context property that indicates that typer is resolving types for arguments of
* an annotation defined in Java. This means 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() }`
*/
private[typer] val JavaAnnotationArg = Property.Key[Unit]()

/** An attachment that indicates a failed conversion or extension method
* search was tried on a tree. This will in some cases be reported in error messages
*/
Expand Down Expand Up @@ -855,7 +864,14 @@ class Typer extends Namer
}

def typedNamedArg(tree: untpd.NamedArg, pt: Type)(using Context): NamedArg = {
val arg1 = typed(tree.arg, pt)
val arg1 = if (ctx.property(JavaAnnotationArg).isDefined) {
pt match {
case AppliedType(a, typ :: Nil) if (a.isRef(defn.ArrayClass)) =>
tryAlternatively { typed(tree.arg, pt) } { typed(untpd.JavaSeqLiteral(tree.arg :: Nil, TypeTree(typ)), pt) }
case _ => typed(tree.arg, pt)
}
} else typed(tree.arg, pt)

assignType(cpy.NamedArg(tree)(tree.name, arg1), arg1)
}

Expand Down Expand Up @@ -1977,11 +1993,13 @@ 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()
val c = outer.property(ExprOwner) match {
case Some(exprOwner) if outer.owner.isClass => outer.exprContext(mdef, exprOwner)
case _ => outer
}
if (c.isJava) c.fresh.setProperty(JavaAnnotationArg, ())
else c
}

def completeAnnotations(mdef: untpd.MemberDef, sym: Symbol)(using Context): Unit = {
Expand Down
23 changes: 23 additions & 0 deletions 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)
70 changes: 70 additions & 0 deletions tests/run/java-annot-params/Annots_0.java
@@ -0,0 +1,70 @@
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();
}
class A {
static final String CONST = "VALUE OF CONST";
}
5 changes: 5 additions & 0 deletions 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])
20 changes: 20 additions & 0 deletions tests/run/java-annot-params/Use_0.java
@@ -0,0 +1,20 @@
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_0 {}
21 changes: 21 additions & 0 deletions 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 {}
17 changes: 17 additions & 0 deletions 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)

0 comments on commit f7a0667

Please sign in to comment.