Skip to content

Commit b0a3572

Browse files
committed
Fix #2426: Use Scala-2 syntax for annotations of primary class constructors
Use Scala-2 compatible syntax for annotations of primary class constructors. In fact, we can drop Scala 2's restriction that such annotations may only have one parameter list.
1 parent 11e2526 commit b0a3572

File tree

4 files changed

+159
-23
lines changed

4 files changed

+159
-23
lines changed

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

Lines changed: 112 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))
@@ -1280,20 +1315,77 @@ object Parsers {
12801315
if (in.token == RPAREN) Nil else commaSeparated(exprInParens)
12811316

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

12881356
/** ArgumentExprs ::= ParArgumentExprs
12891357
* | [nl] BlockExpr
12901358
*/
12911359
def argumentExprs(): List[Tree] =
12921360
if (in.token == LBRACE) blockExpr() :: Nil else parArgumentExprs()
12931361

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

12991391
/** ArgumentExprss ::= {ArgumentExprs}
@@ -1305,9 +1397,17 @@ object Parsers {
13051397
}
13061398

13071399
/** ParArgumentExprss ::= {ParArgumentExprs}
1400+
*
1401+
* Special treatment for arguments of primary class constructor
1402+
* annotations. If an argument list returns `List(ParamNotArg)`
1403+
* ignore it, and return prefix parsed before that list instead.
13081404
*/
13091405
def parArgumentExprss(fn: Tree): Tree =
1310-
if (in.token == LPAREN) parArgumentExprss(Apply(fn, parArgumentExprs()))
1406+
if (in.token == LPAREN) {
1407+
val first = parArgumentExprs()
1408+
if (inClassConstrAnnots && first == ParamNotArg :: Nil) fn
1409+
else parArgumentExprss(Apply(fn, first))
1410+
}
13111411
else fn
13121412

13131413
/** BlockExpr ::= `{' (CaseClauses | Block) `}'
@@ -2094,21 +2194,15 @@ object Parsers {
20942194
*/
20952195
def classConstr(owner: Name, isCaseClass: Boolean = false): DefDef = atPos(in.lastOffset) {
20962196
val tparams = typeParamClauseOpt(ParamOwner.Class)
2097-
val cmods = constrModsOpt(owner)
2197+
val cmods = fromWithinClassConstr(constrModsOpt(owner))
20982198
val vparamss = paramClauses(owner, isCaseClass)
20992199
makeConstructor(tparams, vparamss).withMods(cmods)
21002200
}
21012201

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

21132207
/** ObjectDef ::= id TemplateOpt
21142208
*/

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

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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class Foo @deprecated("foo", "2.11.0") (x: Int)
2+
3+
class Bar @deprecated(x: Int)
4+
5+
class Baz @deprecated()
6+
7+
class C
8+
object obj extends C
9+
10+
class ann(x: C)(y: C, s: String) extends scala.annotation.Annotation
11+
12+
class Bam @ann(obj)(obj, "h")(n: String)
13+

0 commit comments

Comments
 (0)