Skip to content

Commit abad0ad

Browse files
authored
Merge pull request #2432 from dotty-staging/fix-#2426
Fix #2426: Use Scala-2 syntax for annotations of class constructors
2 parents d524e28 + 0725a61 commit abad0ad

File tree

7 files changed

+173
-54
lines changed

7 files changed

+173
-54
lines changed

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

Lines changed: 114 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,41 @@ object Parsers {
294294
} finally inFunReturnType = saved
295295
}
296296

297+
/** A placeholder for dummy arguments that should be re-parsed as parameters */
298+
val ParamNotArg = EmptyTree
299+
300+
/** A flag indicating we are parsing in the annotations of a primary
301+
* class constructor
302+
*/
303+
private var inClassConstrAnnots = false
304+
305+
private def fromWithinClassConstr[T](body: => T): T = {
306+
val saved = inClassConstrAnnots
307+
try {
308+
inClassConstrAnnots = true
309+
body
310+
} finally {
311+
inClassConstrAnnots = saved
312+
if (lookaheadTokens.nonEmpty) {
313+
in.insertTokens(lookaheadTokens.toList)
314+
lookaheadTokens.clear()
315+
}
316+
}
317+
}
318+
319+
/** Lookahead tokens for the case of annotations in class constructors.
320+
* We store tokens in lookahead as long as they can form a valid prefix
321+
* of a class parameter clause.
322+
*/
323+
private var lookaheadTokens = new ListBuffer[TokenData]
324+
325+
/** Copy current token to end of lookahead */
326+
private def saveLookahead() = {
327+
val lookahead = new TokenData{}
328+
lookahead.copyFrom(in)
329+
lookaheadTokens += lookahead
330+
}
331+
297332
def migrationWarningOrError(msg: String, offset: Int = in.offset) =
298333
if (in.isScala2Mode)
299334
ctx.migrationWarning(msg, source atPos Position(offset))
@@ -1279,20 +1314,79 @@ object Parsers {
12791314
if (in.token == RPAREN) Nil else commaSeparated(exprInParens)
12801315

12811316
/** ParArgumentExprs ::= `(' [ExprsInParens] `)'
1282-
* | `(' [ExprsInParens `,'] PostfixExpr `:' `_' `*' ')' \
1283-
*/
1284-
def parArgumentExprs(): List[Tree] =
1285-
inParens(if (in.token == RPAREN) Nil else commaSeparated(argumentExpr))
1317+
* | `(' [ExprsInParens `,'] PostfixExpr `:' `_' `*' ')'
1318+
*
1319+
* Special treatment for arguments of primary class constructor
1320+
* annotations. All empty argument lists `(` `)` following the first
1321+
* get represented as `List(ParamNotArg)` instead of `Nil`, indicating that
1322+
* the token sequence should be interpreted as an empty parameter clause
1323+
* instead. `ParamNotArg` can also be produced when parsing the first
1324+
* argument (see `classConstrAnnotExpr`).
1325+
*
1326+
* The method affects `lookaheadTokens` as a side effect.
1327+
* If the argument list parses as `List(ParamNotArg)`, `lookaheadTokens`
1328+
* contains the tokens that need to be replayed to parse the parameter clause.
1329+
* Otherwise, `lookaheadTokens` is empty.
1330+
*/
1331+
def parArgumentExprs(first: Boolean = false): List[Tree] = {
1332+
if (inClassConstrAnnots) {
1333+
assert(lookaheadTokens.isEmpty)
1334+
saveLookahead()
1335+
accept(LPAREN)
1336+
val args =
1337+
if (in.token == RPAREN)
1338+
if (first) Nil // first () counts as annotation argument
1339+
else ParamNotArg :: Nil
1340+
else {
1341+
openParens.change(LPAREN, +1)
1342+
try commaSeparated(argumentExpr)
1343+
finally openParens.change(LPAREN, -1)
1344+
}
1345+
if (args == ParamNotArg :: Nil)
1346+
in.adjustSepRegions(RPAREN) // simulate `)` without requiring it
1347+
else {
1348+
lookaheadTokens.clear()
1349+
accept(RPAREN)
1350+
}
1351+
args
1352+
}
1353+
else
1354+
inParens(if (in.token == RPAREN) Nil else commaSeparated(argumentExpr))
1355+
}
12861356

12871357
/** ArgumentExprs ::= ParArgumentExprs
12881358
* | [nl] BlockExpr
12891359
*/
12901360
def argumentExprs(): List[Tree] =
12911361
if (in.token == LBRACE) blockExpr() :: Nil else parArgumentExprs()
12921362

1293-
val argumentExpr = () => exprInParens() match {
1294-
case a @ Assign(Ident(id), rhs) => cpy.NamedArg(a)(id, rhs)
1295-
case e => e
1363+
val argumentExpr = () => {
1364+
val arg =
1365+
if (inClassConstrAnnots && lookaheadTokens.nonEmpty) classConstrAnnotExpr()
1366+
else exprInParens()
1367+
arg match {
1368+
case arg @ Assign(Ident(id), rhs) => cpy.NamedArg(arg)(id, rhs)
1369+
case arg => arg
1370+
}
1371+
}
1372+
1373+
/** Handle first argument of an argument list to an annotation of
1374+
* a primary class constructor. If the current token either cannot
1375+
* start an expression or is an identifier and is followed by `:`,
1376+
* stop parsing the rest of the expression and return `EmptyTree`,
1377+
* indicating that we should re-parse the expression as a parameter clause.
1378+
* Otherwise parse as normal.
1379+
*/
1380+
def classConstrAnnotExpr() = {
1381+
if (in.token == IDENTIFIER) {
1382+
saveLookahead()
1383+
postfixExpr() match {
1384+
case Ident(_) if in.token == COLON => ParamNotArg
1385+
case t => expr1Rest(t, Location.InParens)
1386+
}
1387+
}
1388+
else if (isExprIntro) exprInParens()
1389+
else ParamNotArg
12961390
}
12971391

12981392
/** ArgumentExprss ::= {ArgumentExprs}
@@ -1304,9 +1398,17 @@ object Parsers {
13041398
}
13051399

13061400
/** ParArgumentExprss ::= {ParArgumentExprs}
1401+
*
1402+
* Special treatment for arguments of primary class constructor
1403+
* annotations. If an argument list returns `List(ParamNotArg)`
1404+
* ignore it, and return prefix parsed before that list instead.
13071405
*/
13081406
def parArgumentExprss(fn: Tree): Tree =
1309-
if (in.token == LPAREN) parArgumentExprss(Apply(fn, parArgumentExprs()))
1407+
if (in.token == LPAREN) {
1408+
val args = parArgumentExprs(first = !fn.isInstanceOf[Trees.Apply[_]])
1409+
if (inClassConstrAnnots && args == ParamNotArg :: Nil) fn
1410+
else parArgumentExprss(Apply(fn, args))
1411+
}
13101412
else fn
13111413

13121414
/** BlockExpr ::= `{' (CaseClauses | Block) `}'
@@ -2093,21 +2195,15 @@ object Parsers {
20932195
*/
20942196
def classConstr(owner: Name, isCaseClass: Boolean = false): DefDef = atPos(in.lastOffset) {
20952197
val tparams = typeParamClauseOpt(ParamOwner.Class)
2096-
val cmods = constrModsOpt(owner)
2198+
val cmods = fromWithinClassConstr(constrModsOpt(owner))
20972199
val vparamss = paramClauses(owner, isCaseClass)
20982200
makeConstructor(tparams, vparamss).withMods(cmods)
20992201
}
21002202

2101-
/** ConstrMods ::= AccessModifier
2102-
* | Annotation {Annotation} (AccessModifier | `this')
2203+
/** ConstrMods ::= {Annotation} [AccessModifier]
21032204
*/
2104-
def constrModsOpt(owner: Name): Modifiers = {
2105-
val mods = modifiers(accessModifierTokens, annotsAsMods())
2106-
if (mods.hasAnnotations && !mods.hasFlags)
2107-
if (in.token == THIS) in.nextToken()
2108-
else syntaxError(AnnotatedPrimaryConstructorRequiresModifierOrThis(owner), mods.annotations.last.pos)
2109-
mods
2110-
}
2205+
def constrModsOpt(owner: Name): Modifiers =
2206+
modifiers(accessModifierTokens, annotsAsMods())
21112207

21122208
/** ObjectDef ::= id TemplateOpt
21132209
*/

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

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ object Scanners {
6565
*/
6666
var errOffset: Offset = NoOffset
6767

68-
6968
/** Generate an error at the given offset */
7069
def error(msg: String, off: Offset = offset) = {
7170
ctx.error(msg, source atPos Position(off))
@@ -217,11 +216,42 @@ object Scanners {
217216

218217
private class TokenData0 extends TokenData
219218

220-
/** we need one token lookahead and one token history
219+
/** The scanner itself needs one token lookahead and one token history
221220
*/
222221
val next : TokenData = new TokenData0
223222
private val prev : TokenData = new TokenData0
224223

224+
/** The parser can also add more lookahead tokens via `insertTokens`.
225+
* Tokens beyond `next` are stored in `following`.
226+
*/
227+
private var following: List[TokenData] = Nil
228+
229+
/** Push a copy of token data `td` to `following` */
230+
private def pushCopy(td: TokenData) = {
231+
val copy = new TokenData0
232+
copy.copyFrom(td)
233+
following = copy :: following
234+
}
235+
236+
/** If following is empty, invalidate token data `td` by setting
237+
* `td.token` to `EMPTY`. Otherwise pop head of `following` into `td`.
238+
*/
239+
private def popCopy(td: TokenData) =
240+
if (following.isEmpty) td.token = EMPTY
241+
else {
242+
td.copyFrom(following.head)
243+
following = following.tail
244+
}
245+
246+
/** Insert tokens `tds` in front of current token */
247+
def insertTokens(tds: List[TokenData]) = {
248+
if (next.token != EMPTY) pushCopy(next)
249+
pushCopy(this)
250+
following = tds ++ following
251+
popCopy(this)
252+
if (following.nonEmpty) popCopy(next)
253+
}
254+
225255
/** a stack of tokens which indicates whether line-ends can be statement separators
226256
* also used for keeping track of nesting levels.
227257
* We keep track of the closing symbol of a region. This can be
@@ -310,7 +340,7 @@ object Scanners {
310340
if (token == ERROR) adjustSepRegions(STRINGLIT)
311341
} else {
312342
this copyFrom next
313-
next.token = EMPTY
343+
popCopy(next)
314344
}
315345

316346
/** Insert NEWLINE or NEWLINES if

compiler/src/dotty/tools/dotc/reporting/diagnostic/ErrorMessageID.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public enum ErrorMessageID {
5050
ExpectedTokenButFoundID,
5151
MixedLeftAndRightAssociativeOpsID,
5252
CantInstantiateAbstractClassOrTraitID,
53-
AnnotatedPrimaryConstructorRequiresModifierOrThisID,
53+
DUMMY_AVAILABLE_1,
5454
OverloadedOrRecursiveMethodNeedsResultTypeID,
5555
RecursiveValueNeedsResultTypeID,
5656
CyclicReferenceInvolvingID,

compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,21 +1138,6 @@ object messages {
11381138
|""".stripMargin
11391139
}
11401140

1141-
case class AnnotatedPrimaryConstructorRequiresModifierOrThis(cls: Name)(implicit ctx: Context)
1142-
extends Message(AnnotatedPrimaryConstructorRequiresModifierOrThisID) {
1143-
val kind = "Syntax"
1144-
val msg = hl"""${"private"}, ${"protected"}, or ${"this"} expected for annotated primary constructor"""
1145-
val explanation =
1146-
hl"""|When using annotations with a primary constructor of a class,
1147-
|the annotation must be followed by an access modifier
1148-
|(${"private"} or ${"protected"}) or ${"this"}.
1149-
|
1150-
|For example:
1151-
| ${"class Sample @deprecated this(param: Parameter) { ..."}
1152-
| ^^^^
1153-
|""".stripMargin
1154-
}
1155-
11561141
case class OverloadedOrRecursiveMethodNeedsResultType(tree: Names.TermName)(implicit ctx: Context)
11571142
extends Message(OverloadedOrRecursiveMethodNeedsResultTypeID) {
11581143
val kind = "Syntax"

compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -198,21 +198,6 @@ class ErrorMessagesTests extends ErrorMessagesTest {
198198
assertTrue("expected trait", isTrait)
199199
}
200200

201-
@Test def constructorModifier =
202-
checkMessagesAfter("frontend") {
203-
"""
204-
|class AnotherClass @deprecated ()
205-
""".stripMargin
206-
}
207-
.expect { (ictx, messages) =>
208-
implicit val ctx: Context = ictx
209-
val defn = ictx.definitions
210-
211-
assertMessageCount(1, messages)
212-
val AnnotatedPrimaryConstructorRequiresModifierOrThis(cls) :: Nil = messages
213-
assertEquals("AnotherClass", cls.show)
214-
}
215-
216201
@Test def overloadedMethodNeedsReturnType =
217202
checkMessagesAfter("frontend") {
218203
"""

docs/docs/internals/syntax.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,7 @@ TmplDef ::= ([‘case’ | `enum'] ‘class’ | trait’) ClassDef
335335
| `enum' EnumDef
336336
ClassDef ::= id ClassConstr TemplateOpt ClassDef(mods, name, tparams, templ)
337337
ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsParamClauses with DefDef(_, <init>, Nil, vparamss, EmptyTree, EmptyTree) as first stat
338-
ConstrMods ::= AccessModifier
339-
| Annotation {Annotation} (AccessModifier | ‘this’)
338+
ConstrMods ::= {Annotation} [AccessModifier]
340339
ObjectDef ::= id TemplateOpt ModuleDef(mods, name, template) // no constructor
341340
EnumDef ::= id ClassConstr [`extends' [ConstrApps]] EnumDef(mods, name, tparams, template)
342341
[nl] ‘{’ EnumCaseStat {semi EnumCaseStat} ‘}’

tests/pos/i2426.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
class Foo @deprecated("foo", "2.11.0") (x: Int)
2+
3+
class Bar @deprecated(x: Int)
4+
5+
class Baz1 @deprecated(implicit c: C)
6+
class Baz2 @deprecated()(implicit c: C)
7+
class Baz3 @deprecated()()(implicit c: C)
8+
9+
object Test {
10+
implicit val c: C = obj
11+
new Baz1
12+
new Baz2
13+
new Baz3()
14+
}
15+
16+
class D(implicit x: C)
17+
18+
class C
19+
object obj extends C
20+
21+
class ann(x: C)(y: C, s: String) extends scala.annotation.Annotation
22+
23+
class Bam @ann(obj)(obj, "h")(n: String)
24+

0 commit comments

Comments
 (0)