Skip to content

Commit a7608fe

Browse files
Backport "Better error messages for missing commas and more" to LTS (#20746)
Backports #18785 to the LTS branch. PR submitted by the release tooling. [skip ci]
2 parents 7efd518 + a96e975 commit a7608fe

18 files changed

+174
-87
lines changed

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

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -555,10 +555,29 @@ object Parsers {
555555
accept(tok)
556556
try body finally accept(tok + 1)
557557

558+
/** Same as enclosed, but if closing token is missing, add `,` to the expected tokens
559+
* in the error message provided the next token could have followed a `,`.
560+
*/
561+
def enclosedWithCommas[T](tok: Token, body: => T): T =
562+
accept(tok)
563+
val closing = tok + 1
564+
val isEmpty = in.token == closing
565+
val ts = body
566+
if in.token != closing then
567+
val followComma =
568+
if tok == LPAREN then canStartExprTokens3 else canStartTypeTokens
569+
val prefix = if !isEmpty && followComma.contains(in.token) then "',' or " else ""
570+
syntaxErrorOrIncomplete(ExpectedTokenButFound(closing, in.token, prefix))
571+
if in.token == closing then in.nextToken()
572+
ts
573+
558574
def inParens[T](body: => T): T = enclosed(LPAREN, body)
559575
def inBraces[T](body: => T): T = enclosed(LBRACE, body)
560576
def inBrackets[T](body: => T): T = enclosed(LBRACKET, body)
561577

578+
def inParensWithCommas[T](body: => T): T = enclosedWithCommas(LPAREN, body)
579+
def inBracketsWithCommas[T](body: => T): T = enclosedWithCommas(LBRACKET, body)
580+
562581
def inBracesOrIndented[T](body: => T, rewriteWithColon: Boolean = false): T =
563582
if in.token == INDENT then
564583
val rewriteToBraces = in.rewriteNoIndent
@@ -970,6 +989,17 @@ object Parsers {
970989
isArrowIndent()
971990
else false
972991

992+
/** Can the next lookahead token start an operand as defined by
993+
* leadingOperandTokens, or is postfix ops enabled?
994+
* This is used to decide whether the current token can be an infix operator.
995+
*/
996+
def nextCanFollowOperator(leadingOperandTokens: BitSet): Boolean =
997+
leadingOperandTokens.contains(in.lookahead.token)
998+
|| in.postfixOpsEnabled
999+
|| in.lookahead.token == COLONop
1000+
|| in.lookahead.token == EOF // important for REPL completions
1001+
|| ctx.mode.is(Mode.Interactive) // in interactive mode the next tokens might be missing
1002+
9731003
/* --------- OPERAND/OPERATOR STACK --------------------------------------- */
9741004

9751005
var opStack: List[OpInfo] = Nil
@@ -1050,7 +1080,11 @@ object Parsers {
10501080
then recur(top)
10511081
else top
10521082

1053-
recur(first)
1083+
val res = recur(first)
1084+
if isIdent(nme.raw.STAR) && !followingIsVararg() then
1085+
syntaxError(em"spread operator `*` not allowed here; must come last in a parameter list")
1086+
in.nextToken()
1087+
res
10541088
end infixOps
10551089

10561090
/* -------- IDENTIFIERS AND LITERALS ------------------------------------------- */
@@ -1659,7 +1693,7 @@ object Parsers {
16591693
/** FunParamClause ::= ‘(’ TypedFunParam {‘,’ TypedFunParam } ‘)’
16601694
*/
16611695
def funParamClause(): List[ValDef] =
1662-
inParens(commaSeparated(() => typedFunParam(in.offset, ident())))
1696+
inParensWithCommas(commaSeparated(() => typedFunParam(in.offset, ident())))
16631697

16641698
def funParamClauses(): List[List[ValDef]] =
16651699
if in.token == LPAREN then funParamClause() :: funParamClauses() else Nil
@@ -1671,7 +1705,8 @@ object Parsers {
16711705

16721706
def infixTypeRest(t: Tree): Tree =
16731707
infixOps(t, canStartInfixTypeTokens, refinedTypeFn, Location.ElseWhere, ParseKind.Type,
1674-
isOperator = !followingIsVararg() && !isPureArrow)
1708+
isOperator = !followingIsVararg() && !isPureArrow
1709+
&& nextCanFollowOperator(canStartInfixTypeTokens))
16751710

16761711
/** RefinedType ::= WithType {[nl] Refinement} [`^` CaptureSet]
16771712
*/
@@ -1807,7 +1842,7 @@ object Parsers {
18071842
else
18081843
def singletonArgs(t: Tree): Tree =
18091844
if in.token == LPAREN && in.featureEnabled(Feature.dependent)
1810-
then singletonArgs(AppliedTypeTree(t, inParens(commaSeparated(singleton))))
1845+
then singletonArgs(AppliedTypeTree(t, inParensWithCommas(commaSeparated(singleton))))
18111846
else t
18121847
singletonArgs(simpleType1())
18131848

@@ -1823,7 +1858,7 @@ object Parsers {
18231858
def simpleType1() = simpleTypeRest {
18241859
if in.token == LPAREN then
18251860
atSpan(in.offset) {
1826-
makeTupleOrParens(inParens(argTypes(namedOK = false, wildOK = true)))
1861+
makeTupleOrParens(inParensWithCommas(argTypes(namedOK = false, wildOK = true)))
18271862
}
18281863
else if in.token == LBRACE then
18291864
atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) }
@@ -1976,7 +2011,8 @@ object Parsers {
19762011
/** TypeArgs ::= `[' Type {`,' Type} `]'
19772012
* NamedTypeArgs ::= `[' NamedTypeArg {`,' NamedTypeArg} `]'
19782013
*/
1979-
def typeArgs(namedOK: Boolean, wildOK: Boolean): List[Tree] = inBrackets(argTypes(namedOK, wildOK))
2014+
def typeArgs(namedOK: Boolean, wildOK: Boolean): List[Tree] =
2015+
inBracketsWithCommas(argTypes(namedOK, wildOK))
19802016

19812017
/** Refinement ::= `{' RefineStatSeq `}'
19822018
*/
@@ -2413,7 +2449,8 @@ object Parsers {
24132449

24142450
def postfixExprRest(t: Tree, location: Location): Tree =
24152451
infixOps(t, in.canStartExprTokens, prefixExpr, location, ParseKind.Expr,
2416-
isOperator = !(location.inArgs && followingIsVararg()))
2452+
isOperator = !(location.inArgs && followingIsVararg())
2453+
&& nextCanFollowOperator(canStartInfixExprTokens))
24172454

24182455
/** PrefixExpr ::= [PrefixOperator'] SimpleExpr
24192456
* PrefixOperator ::= ‘-’ | ‘+’ | ‘~’ | ‘!’ (if not backquoted)
@@ -2472,7 +2509,7 @@ object Parsers {
24722509
placeholderParams = param :: placeholderParams
24732510
atSpan(start) { Ident(pname) }
24742511
case LPAREN =>
2475-
atSpan(in.offset) { makeTupleOrParens(inParens(exprsInParensOrBindings())) }
2512+
atSpan(in.offset) { makeTupleOrParens(inParensWithCommas(exprsInParensOrBindings())) }
24762513
case LBRACE | INDENT =>
24772514
canApply = false
24782515
blockExpr()
@@ -2577,15 +2614,15 @@ object Parsers {
25772614
/** ParArgumentExprs ::= `(' [‘using’] [ExprsInParens] `)'
25782615
* | `(' [ExprsInParens `,'] PostfixExpr `*' ')'
25792616
*/
2580-
def parArgumentExprs(): (List[Tree], Boolean) = inParens {
2581-
if in.token == RPAREN then
2582-
(Nil, false)
2583-
else if isIdent(nme.using) then
2584-
in.nextToken()
2585-
(commaSeparated(argumentExpr), true)
2586-
else
2587-
(commaSeparated(argumentExpr), false)
2588-
}
2617+
def parArgumentExprs(): (List[Tree], Boolean) =
2618+
inParensWithCommas:
2619+
if in.token == RPAREN then
2620+
(Nil, false)
2621+
else if isIdent(nme.using) then
2622+
in.nextToken()
2623+
(commaSeparated(argumentExpr), true)
2624+
else
2625+
(commaSeparated(argumentExpr), false)
25892626

25902627
/** ArgumentExprs ::= ParArgumentExprs
25912628
* | [nl] BlockExpr
@@ -2920,7 +2957,8 @@ object Parsers {
29202957
def infixPattern(): Tree =
29212958
infixOps(
29222959
simplePattern(), in.canStartExprTokens, simplePatternFn, Location.InPattern, ParseKind.Pattern,
2923-
isOperator = in.name != nme.raw.BAR && !followingIsVararg())
2960+
isOperator = in.name != nme.raw.BAR && !followingIsVararg()
2961+
&& nextCanFollowOperator(canStartPatternTokens))
29242962

29252963
/** SimplePattern ::= PatVar
29262964
* | Literal
@@ -2941,7 +2979,7 @@ object Parsers {
29412979
case USCORE =>
29422980
wildcardIdent()
29432981
case LPAREN =>
2944-
atSpan(in.offset) { makeTupleOrParens(inParens(patternsOpt())) }
2982+
atSpan(in.offset) { makeTupleOrParens(inParensWithCommas(patternsOpt())) }
29452983
case QUOTE =>
29462984
simpleExpr(Location.InPattern)
29472985
case XMLSTART =>
@@ -2987,7 +3025,7 @@ object Parsers {
29873025
* | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’
29883026
*/
29893027
def argumentPatterns(): List[Tree] =
2990-
inParens(patternsOpt(Location.InPatternArgs))
3028+
inParensWithCommas(patternsOpt(Location.InPatternArgs))
29913029

29923030
/* -------- MODIFIERS and ANNOTATIONS ------------------------------------------- */
29933031

@@ -3176,7 +3214,7 @@ object Parsers {
31763214
* HkTypeParamClause ::= ‘[’ HkTypeParam {‘,’ HkTypeParam} ‘]’
31773215
* HkTypeParam ::= {Annotation} [‘+’ | ‘-’] (id [HkTypePamClause] | ‘_’) TypeBounds
31783216
*/
3179-
def typeParamClause(ownerKind: ParamOwner): List[TypeDef] = inBrackets {
3217+
def typeParamClause(ownerKind: ParamOwner): List[TypeDef] = inBracketsWithCommas {
31803218

31813219
def checkVarianceOK(): Boolean =
31823220
val ok = ownerKind != ParamOwner.Def && ownerKind != ParamOwner.TypeParam
@@ -3315,7 +3353,7 @@ object Parsers {
33153353
}
33163354

33173355
// begin termParamClause
3318-
inParens {
3356+
inParensWithCommas {
33193357
if in.token == RPAREN && !prefix && !impliedMods.is(Given) then Nil
33203358
else
33213359
val clause =

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,13 @@ object Tokens extends TokensCommon {
221221

222222
final val openParensTokens = BitSet(LBRACE, LPAREN, LBRACKET)
223223

224-
final val canStartExprTokens3: TokenSet =
225-
atomicExprTokens
224+
final val canStartInfixExprTokens =
225+
atomicExprTokens
226226
| openParensTokens
227-
| BitSet(INDENT, QUOTE, IF, WHILE, FOR, NEW, TRY, THROW)
227+
| BitSet(QUOTE, NEW)
228+
229+
final val canStartExprTokens3: TokenSet =
230+
canStartInfixExprTokens | BitSet(INDENT, IF, WHILE, FOR, TRY, THROW)
228231

229232
final val canStartExprTokens2: TokenSet = canStartExprTokens3 | BitSet(DO)
230233

@@ -233,6 +236,8 @@ object Tokens extends TokensCommon {
233236

234237
final val canStartTypeTokens: TokenSet = canStartInfixTypeTokens | BitSet(LBRACE)
235238

239+
final val canStartPatternTokens = atomicExprTokens | openParensTokens | BitSet(USCORE, QUOTE)
240+
236241
final val templateIntroTokens: TokenSet = BitSet(CLASS, TRAIT, OBJECT, ENUM, CASECLASS, CASEOBJECT)
237242

238243
final val dclIntroTokens: TokenSet = BitSet(DEF, VAL, VAR, TYPE, GIVEN)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,7 +1169,7 @@ extends ReferenceMsg(ForwardReferenceExtendsOverDefinitionID) {
11691169
|"""
11701170
}
11711171

1172-
class ExpectedTokenButFound(expected: Token, found: Token)(using Context)
1172+
class ExpectedTokenButFound(expected: Token, found: Token, prefix: String = "")(using Context)
11731173
extends SyntaxMsg(ExpectedTokenButFoundID) {
11741174

11751175
private def foundText = Tokens.showToken(found)
@@ -1178,7 +1178,7 @@ extends SyntaxMsg(ExpectedTokenButFoundID) {
11781178
val expectedText =
11791179
if (Tokens.isIdentifier(expected)) "an identifier"
11801180
else Tokens.showToken(expected)
1181-
i"""${expectedText} expected, but ${foundText} found"""
1181+
i"""$prefix$expectedText expected, but $foundText found"""
11821182

11831183
def explain(using Context) =
11841184
if (Tokens.isIdentifier(expected) && Tokens.isKeyword(found))

tests/neg/cc-only-defs.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ trait Test {
77

88
val b: ImpureFunction1[Int, Int] // now OK
99

10-
val a: {z} String // error
11-
} // error
10+
val a: {z} String // error // error
11+
}

tests/neg/i14564.check

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,4 @@
1-
-- [E018] Syntax Error: tests/neg/i14564.scala:5:28 --------------------------------------------------------------------
2-
5 |def test = sum"${ List(42)* }" // error // error
3-
| ^
4-
| expression expected but '}' found
5-
|
6-
| longer explanation available when compiling with `-explain`
7-
-- [E008] Not Found Error: tests/neg/i14564.scala:5:26 -----------------------------------------------------------------
8-
5 |def test = sum"${ List(42)* }" // error // error
9-
| ^^^^^^^^^
10-
| value * is not a member of List[Int], but could be made available as an extension method.
11-
|
12-
| One of the following imports might make progress towards fixing the problem:
13-
|
14-
| import scala.math.Fractional.Implicits.infixFractionalOps
15-
| import scala.math.Integral.Implicits.infixIntegralOps
16-
| import scala.math.Numeric.Implicits.infixNumericOps
17-
|
1+
-- Error: tests/neg/i14564.scala:5:26 ----------------------------------------------------------------------------------
2+
5 |def test = sum"${ List(42)* }" // error
3+
| ^
4+
| spread operator `*` not allowed here; must come last in a parameter list

tests/neg/i14564.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import language.postfixOps as _
22

33
extension (sc: StringContext) def sum(xs: Int*): String = xs.sum.toString
44

5-
def test = sum"${ List(42)* }" // error // error
5+
def test = sum"${ List(42)* }" // error
66

tests/neg/i18734.check

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-- [E040] Syntax Error: tests/neg/i18734.scala:7:8 ---------------------------------------------------------------------
2+
7 | Foo(1 2) // error
3+
| ^
4+
| ',' or ')' expected, but integer literal found
5+
-- [E040] Syntax Error: tests/neg/i18734.scala:9:8 ---------------------------------------------------------------------
6+
9 | Foo(x y) // error
7+
| ^
8+
| ',' or ')' expected, but identifier found
9+
-- [E040] Syntax Error: tests/neg/i18734.scala:11:8 --------------------------------------------------------------------
10+
11 | Foo(1 b = 2) // error
11+
| ^
12+
| ',' or ')' expected, but identifier found
13+
-- [E040] Syntax Error: tests/neg/i18734.scala:16:4 --------------------------------------------------------------------
14+
16 | b = 2 // error
15+
| ^
16+
| ',' or ')' expected, but identifier found
17+
-- [E040] Syntax Error: tests/neg/i18734.scala:19:32 -------------------------------------------------------------------
18+
19 | val f: (Int, Int) => Int = (x y) => x + y // error
19+
| ^
20+
| ',' or ')' expected, but identifier found
21+
-- [E040] Syntax Error: tests/neg/i18734.scala:23:10 -------------------------------------------------------------------
22+
23 | bar[Int String](1 2) // error // error
23+
| ^^^^^^
24+
| ',' or ']' expected, but identifier found
25+
-- [E040] Syntax Error: tests/neg/i18734.scala:23:20 -------------------------------------------------------------------
26+
23 | bar[Int String](1 2) // error // error
27+
| ^
28+
| ',' or ')' expected, but integer literal found

tests/neg/i18734.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
case class Foo(a: Int, b: Int)
2+
3+
object Bar:
4+
val x = 1
5+
val y = 2
6+
7+
Foo(1 2) // error
8+
9+
Foo(x y) // error
10+
11+
Foo(1 b = 2) // error
12+
13+
// Or
14+
Foo(
15+
a = 1
16+
b = 2 // error
17+
)
18+
19+
val f: (Int, Int) => Int = (x y) => x + y // error
20+
21+
def bar[X, Y](x: X, y: Y) = ???
22+
23+
bar[Int String](1 2) // error // error
24+
25+

tests/neg/i4453.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
class x0 { var x0 == _ * // error: _* can be used only for last argument // error: == cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method
2-
// error '=' expected, but eof found
1+
class x0 { var x0 == _ * // error spread operator `*` not allowed here // error '=' expected
2+
// error: == cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method

tests/neg/i5498-postfixOps.check

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@
44
| expression expected but end of statement found
55
|
66
| longer explanation available when compiling with `-explain`
7-
-- [E018] Syntax Error: tests/neg/i5498-postfixOps.scala:6:37 ----------------------------------------------------------
8-
6 | Seq(1, 2).filter(List(1,2) contains) // error: usage of postfix operator // error
9-
| ^
10-
| expression expected but ')' found
11-
|
12-
| longer explanation available when compiling with `-explain`
7+
-- [E040] Syntax Error: tests/neg/i5498-postfixOps.scala:6:29 ----------------------------------------------------------
8+
6 | Seq(1, 2).filter(List(1,2) contains) // error: usage of postfix operator // error // error (type error) // error (type error)
9+
| ^^^^^^^^
10+
| ',' or ')' expected, but identifier found
1311
-- [E172] Type Error: tests/neg/i5498-postfixOps.scala:6:0 -------------------------------------------------------------
14-
6 | Seq(1, 2).filter(List(1,2) contains) // error: usage of postfix operator // error
12+
6 | Seq(1, 2).filter(List(1,2) contains) // error: usage of postfix operator // error // error (type error) // error (type error)
1513
|^
1614
|No given instance of type scala.concurrent.duration.DurationConversions.Classifier[Null] was found for parameter ev of method second in trait DurationConversions
15+
-- [E007] Type Mismatch Error: tests/neg/i5498-postfixOps.scala:6:24 ---------------------------------------------------
16+
6 | Seq(1, 2).filter(List(1,2) contains) // error: usage of postfix operator // error // error (type error) // error (type error)
17+
| ^
18+
| Found: (1 : Int)
19+
| Required: Boolean
20+
|
21+
| longer explanation available when compiling with `-explain`
22+
-- [E007] Type Mismatch Error: tests/neg/i5498-postfixOps.scala:6:26 ---------------------------------------------------
23+
6 | Seq(1, 2).filter(List(1,2) contains) // error: usage of postfix operator // error // error (type error) // error (type error)
24+
| ^
25+
| Found: (2 : Int)
26+
| Required: Boolean
27+
|
28+
| longer explanation available when compiling with `-explain`

tests/neg/i5498-postfixOps.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import scala.concurrent.duration.*
33
def test() = {
44
1 second // error: usage of postfix operator
55

6-
Seq(1, 2).filter(List(1,2) contains) // error: usage of postfix operator // error
6+
Seq(1, 2).filter(List(1,2) contains) // error: usage of postfix operator // error // error (type error) // error (type error)
77
}

tests/neg/i6059.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
def I0(I1: Int ) = I1
2-
val I1 = I0(I0 i2) => // error
2+
val I1 = I0(I0 i2) => // error // error
33
true

0 commit comments

Comments
 (0)