From 547ed4368225965d83221dbe92da0e9f66ee33ca Mon Sep 17 00:00:00 2001 From: adampauls Date: Thu, 17 Feb 2022 08:24:25 -0800 Subject: [PATCH] Better error recovery in comma-separated lists --- .../dotty/tools/dotc/parsing/Parsers.scala | 65 +++++++++++-------- tests/neg/comma-separated-errors.check | 36 ++++++++++ tests/neg/comma-separated-errors.scala | 15 +++++ tests/neg/i1679.scala | 2 +- 4 files changed, 89 insertions(+), 29 deletions(-) create mode 100644 tests/neg/comma-separated-errors.check create mode 100644 tests/neg/comma-separated-errors.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 366ed459a6a0..7fdbb30ebf39 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -553,19 +553,28 @@ object Parsers { def inDefScopeBraces[T](body: => T, rewriteWithColon: Boolean = false): T = inBracesOrIndented(body, rewriteWithColon) - /** part { `separator` part } - */ - def tokenSeparated[T](separator: Int, part: () => T): List[T] = { + /** part { `,` part } + * @param expectedEnd If set to something other than [[EMPTY]], + * assume this comma separated list must be followed by this token. + * If the parser consumes a `part` that is not followed by a comma or this expected + * token, issue a syntax error and try to recover at the next safe point. + */ + def commaSeparated[T](part: () => T, expectedEnd: Token = EMPTY): List[T] = { val ts = new ListBuffer[T] += part() - while (in.token == separator) { + while (in.token == COMMA) { in.nextToken() ts += part() } + if (expectedEnd != EMPTY && in.token != expectedEnd) { + // As a side effect, will skip to the nearest safe point, which might be a comma + syntaxErrorOrIncomplete(ExpectedTokenButFound(expectedEnd, in.token)) + if (in.token == COMMA) { + ts ++= commaSeparated(part, expectedEnd) + } + } ts.toList } - def commaSeparated[T](part: () => T): List[T] = tokenSeparated(COMMA, part) - def inSepRegion[T](f: Region => Region)(op: => T): T = val cur = in.currentRegion in.currentRegion = f(cur) @@ -1509,7 +1518,7 @@ object Parsers { /** FunParamClause ::= ‘(’ TypedFunParam {‘,’ TypedFunParam } ‘)’ */ def funParamClause(): List[ValDef] = - inParens(commaSeparated(() => typedFunParam(in.offset, ident()))) + inParens(commaSeparated(() => typedFunParam(in.offset, ident()), RPAREN)) def funParamClauses(): List[List[ValDef]] = if in.token == LPAREN then funParamClause() :: funParamClauses() else Nil @@ -1622,7 +1631,7 @@ object Parsers { else def singletonArgs(t: Tree): Tree = if in.token == LPAREN && in.featureEnabled(Feature.dependent) - then singletonArgs(AppliedTypeTree(t, inParens(commaSeparated(singleton)))) + then singletonArgs(AppliedTypeTree(t, inParens(commaSeparated(singleton, RPAREN)))) else t singletonArgs(simpleType1()) @@ -1638,7 +1647,7 @@ object Parsers { def simpleType1() = simpleTypeRest { if in.token == LPAREN then atSpan(in.offset) { - makeTupleOrParens(inParens(argTypes(namedOK = false, wildOK = true))) + makeTupleOrParens(inParens(argTypes(namedOK = false, wildOK = true, RPAREN))) } else if in.token == LBRACE then atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) } @@ -1722,7 +1731,7 @@ object Parsers { * | NamedTypeArg {`,' NamedTypeArg} * NamedTypeArg ::= id `=' Type */ - def argTypes(namedOK: Boolean, wildOK: Boolean): List[Tree] = { + def argTypes(namedOK: Boolean, wildOK: Boolean, expectedEnd: Token): List[Tree] = { def argType() = { val t = typ() @@ -1739,7 +1748,7 @@ object Parsers { val rest = if (in.token == COMMA) { in.nextToken() - commaSeparated(arg) + commaSeparated(arg, expectedEnd) } else Nil first :: rest @@ -1752,7 +1761,7 @@ object Parsers { case firstArg => otherArgs(firstArg, () => argType()) } - else commaSeparated(() => argType()) + else commaSeparated(() => argType(), expectedEnd) } /** FunArgType ::= Type | `=>' Type @@ -1781,7 +1790,7 @@ object Parsers { /** TypeArgs ::= `[' Type {`,' Type} `]' * NamedTypeArgs ::= `[' NamedTypeArg {`,' NamedTypeArg} `]' */ - def typeArgs(namedOK: Boolean, wildOK: Boolean): List[Tree] = inBrackets(argTypes(namedOK, wildOK)) + def typeArgs(namedOK: Boolean, wildOK: Boolean): List[Tree] = inBrackets(argTypes(namedOK, wildOK, RBRACKET)) /** Refinement ::= `{' RefineStatSeq `}' */ @@ -2145,7 +2154,7 @@ object Parsers { var mods1 = mods if isErased then mods1 = addModifier(mods1) try - commaSeparated(() => binding(mods1)) + commaSeparated(() => binding(mods1), RPAREN) finally accept(RPAREN) else { @@ -2376,7 +2385,7 @@ object Parsers { /** ExprsInParens ::= ExprInParens {`,' ExprInParens} */ def exprsInParensOpt(): List[Tree] = - if (in.token == RPAREN) Nil else commaSeparated(exprInParens) + if (in.token == RPAREN) Nil else commaSeparated(exprInParens, RPAREN) /** ParArgumentExprs ::= `(' [‘using’] [ExprsInParens] `)' * | `(' [ExprsInParens `,'] PostfixExpr `*' ')' @@ -2386,9 +2395,9 @@ object Parsers { (Nil, false) else if isIdent(nme.using) then in.nextToken() - (commaSeparated(argumentExpr), true) + (commaSeparated(argumentExpr, RPAREN), true) else - (commaSeparated(argumentExpr), false) + (commaSeparated(argumentExpr, RPAREN), false) } /** ArgumentExprs ::= ParArgumentExprs @@ -2532,7 +2541,7 @@ object Parsers { if (leading == LBRACE || in.token == CASE) enumerators() else { - val pats = patternsOpt() + val pats = patternsOpt(EMPTY) val pat = if (in.token == RPAREN || pats.length > 1) { wrappedEnums = false @@ -2724,7 +2733,7 @@ object Parsers { case USCORE => wildcardIdent() case LPAREN => - atSpan(in.offset) { makeTupleOrParens(inParens(patternsOpt())) } + atSpan(in.offset) { makeTupleOrParens(inParens(patternsOpt(RPAREN))) } case QUOTE => simpleExpr(Location.InPattern) case XMLSTART => @@ -2759,17 +2768,17 @@ object Parsers { /** Patterns ::= Pattern [`,' Pattern] */ - def patterns(location: Location = Location.InPattern): List[Tree] = - commaSeparated(() => pattern(location)) + def patterns(expectedEnd: Token = EMPTY, location: Location = Location.InPattern): List[Tree] = + commaSeparated(() => pattern(location), expectedEnd) - def patternsOpt(location: Location = Location.InPattern): List[Tree] = - if (in.token == RPAREN) Nil else patterns(location) + def patternsOpt(expectedEnd: Token, location: Location = Location.InPattern): List[Tree] = + if (in.token == RPAREN) Nil else patterns(expectedEnd, location) /** ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ * | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’ */ def argumentPatterns(): List[Tree] = - inParens(patternsOpt(Location.InPatternArgs)) + inParens(patternsOpt(RPAREN, Location.InPatternArgs)) /* -------- MODIFIERS and ANNOTATIONS ------------------------------------------- */ @@ -2950,7 +2959,7 @@ object Parsers { TypeDef(name, lambdaAbstract(hkparams, bounds)).withMods(mods) } } - commaSeparated(() => typeParam()) + commaSeparated(() => typeParam(), RBRACKET) } def typeParamClauseOpt(ownerKind: ParamOwner.Value): List[TypeDef] = @@ -2959,7 +2968,7 @@ object Parsers { /** ContextTypes ::= FunArgType {‘,’ FunArgType} */ def contextTypes(ofClass: Boolean, nparams: Int, impliedMods: Modifiers): List[ValDef] = - val tps = commaSeparated(funArgType) + val tps = commaSeparated(funArgType, RPAREN) var counter = nparams def nextIdx = { counter += 1; counter } val paramFlags = if ofClass then Private | Local | ParamAccessor else Param @@ -3063,7 +3072,7 @@ object Parsers { !impliedMods.is(Given) || startParamTokens.contains(in.token) || isIdent && (in.name == nme.inline || in.lookahead.isColon()) - if isParams then commaSeparated(() => param()) + if isParams then commaSeparated(() => param(), RPAREN) else contextTypes(ofClass, nparams, impliedMods) checkVarArgsRules(clause) clause @@ -3755,7 +3764,7 @@ object Parsers { val derived = if (isIdent(nme.derives)) { in.nextToken() - tokenSeparated(COMMA, () => convertToTypeId(qualId())) + commaSeparated(() => convertToTypeId(qualId())) } else Nil possibleTemplateStart() diff --git a/tests/neg/comma-separated-errors.check b/tests/neg/comma-separated-errors.check new file mode 100644 index 000000000000..3b74c2ab29c2 --- /dev/null +++ b/tests/neg/comma-separated-errors.check @@ -0,0 +1,36 @@ +-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:3:21 ---------------------------------------------------- +3 | def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error + | ^ + | ')' expected, but integer literal found +-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:3:26 ---------------------------------------------------- +3 | def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error + | ^^^ + | ':' expected, but identifier found +-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:3:42 ---------------------------------------------------- +3 | def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error + | ^ + | ')' expected, but integer literal found +-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:3:47 ---------------------------------------------------- +3 | def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error + | ^ + | ':' expected, but '=' found +-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:11:16 --------------------------------------------------- +11 | case Plus(4 1) => // error + | ^ + | ')' expected, but integer literal found +-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:12:16 --------------------------------------------------- +12 | case Plus(4 5 6 7, 1, 2 3) => // error // error + | ^ + | ')' expected, but integer literal found +-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:12:28 --------------------------------------------------- +12 | case Plus(4 5 6 7, 1, 2 3) => // error // error + | ^ + | ')' expected, but integer literal found +-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:14:12 --------------------------------------------------- +14 | val x: A[T=Int, T=Int] = ??? // error // error + | ^ + | ']' expected, but '=' found +-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:14:19 --------------------------------------------------- +14 | val x: A[T=Int, T=Int] = ??? // error // error + | ^ + | ']' expected, but '=' found diff --git a/tests/neg/comma-separated-errors.scala b/tests/neg/comma-separated-errors.scala new file mode 100644 index 000000000000..8eb7965cd3e9 --- /dev/null +++ b/tests/neg/comma-separated-errors.scala @@ -0,0 +1,15 @@ +class A[T] +object o { + def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error + + case class Plus(a: Int, b: Int) + + object Plus { + def unapply(r: Int): Plus = Plus(r - 1, 1) + } + 5 match { + case Plus(4 1) => // error + case Plus(4 5 6 7, 1, 2 3) => // error // error + } + val x: A[T=Int, T=Int] = ??? // error // error +} diff --git a/tests/neg/i1679.scala b/tests/neg/i1679.scala index cadeb85dc8db..6ca81cea6406 100644 --- a/tests/neg/i1679.scala +++ b/tests/neg/i1679.scala @@ -1,5 +1,5 @@ class A[T] object o { // Testing compiler crash, this test should be modified when named type argument are completely implemented - val x: A[T=Int, T=Int] = ??? // error: ']' expected, but '=' found // error + val x: A[T=Int, T=Int] = ??? // error: ']' expected, but '=' found // error: ']' expected, but '=' found }