Skip to content

Commit 26e45c9

Browse files
committed
Enforce indentation off-side rule
Ref lampepfl/dotty 10671 Currently class A: val x = 1 val y = 2 is admitted. This becomes confusing since this looseness ends up not catching things like class Test: test("hello") assert(1 == 1) where `assert(1 == 1)` is actually not passed into `test(...)(...)`. Following the convention of indentation language like Python, we should reject superfluous indentation, which can be defined as having two or more indentation widths within a region.
1 parent 830d1b8 commit 26e45c9

File tree

2 files changed

+99
-50
lines changed

2 files changed

+99
-50
lines changed

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

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,14 @@ object Parsers {
439439
finally staged = saved
440440
}
441441

442+
private var strictIndent = false
443+
private def withStrictIndent[T](body: => T): T = {
444+
val saved = strictIndent
445+
strictIndent = true
446+
try body
447+
finally strictIndent = saved
448+
}
449+
442450
/* ---------- TREE CONSTRUCTION ------------------------------------------- */
443451

444452
/** Convert tree to formal parameter list
@@ -1281,16 +1289,21 @@ object Parsers {
12811289
in.sourcePos())
12821290
patch(source, Span(in.offset), " ")
12831291

1284-
def possibleTemplateStart(isNew: Boolean = false): Unit =
1292+
def possibleTemplateStart[A](isNew: Boolean = false)(rest: => A): A =
12851293
in.observeColonEOL()
1286-
if in.token == COLONEOL || in.token == WITHEOL then
1287-
if in.lookahead.isIdent(nme.end) then in.token = NEWLINE
1294+
val indented =
1295+
if in.token == COLONEOL || in.token == WITHEOL then
1296+
if in.lookahead.isIdent(nme.end) then in.token = NEWLINE
1297+
else
1298+
in.nextToken()
1299+
if in.token != INDENT && in.token != LBRACE then
1300+
syntaxErrorOrIncomplete(i"indented definitions expected, ${in}")
1301+
true
12881302
else
1289-
in.nextToken()
1290-
if in.token != INDENT && in.token != LBRACE then
1291-
syntaxErrorOrIncomplete(i"indented definitions expected, ${in}")
1292-
else
1293-
newLineOptWhenFollowedBy(LBRACE)
1303+
newLineOptWhenFollowedBy(LBRACE)
1304+
false
1305+
if indented then withStrictIndent(rest)
1306+
else rest
12941307

12951308
def checkEndMarker[T <: Tree](stats: ListBuffer[T]): Unit =
12961309

@@ -2309,19 +2322,22 @@ object Parsers {
23092322
def newExpr(): Tree =
23102323
val start = in.skipToken()
23112324
def reposition(t: Tree) = t.withSpan(Span(start, in.lastOffset))
2312-
possibleTemplateStart()
2313-
val parents =
2314-
if in.isNestedStart then Nil
2315-
else constrApps(commaOK = false)
2316-
colonAtEOLOpt()
2317-
possibleTemplateStart(isNew = true)
2318-
parents match {
2319-
case parent :: Nil if !in.isNestedStart =>
2320-
reposition(if (parent.isType) ensureApplied(wrapNew(parent)) else parent)
2321-
case _ =>
2322-
New(reposition(templateBodyOpt(emptyConstructor, parents, Nil)))
2325+
possibleTemplateStart() {
2326+
val parents =
2327+
if in.isNestedStart then Nil
2328+
else constrApps(commaOK = false)
2329+
colonAtEOLOpt()
2330+
possibleTemplateStart(isNew = true) {
2331+
parents match {
2332+
case parent :: Nil if !in.isNestedStart =>
2333+
reposition(if (parent.isType) ensureApplied(wrapNew(parent)) else parent)
2334+
case _ =>
2335+
New(reposition(templateBodyOpt(emptyConstructor, parents, Nil)))
2336+
}
2337+
}
23232338
}
23242339

2340+
23252341
/** ExprsInParens ::= ExprInParens {`,' ExprInParens}
23262342
*/
23272343
def exprsInParensOpt(): List[Tree] =
@@ -3660,12 +3676,13 @@ object Parsers {
36603676
tokenSeparated(COMMA, () => convertToTypeId(qualId()))
36613677
}
36623678
else Nil
3663-
possibleTemplateStart()
3664-
if (isEnum) {
3665-
val (self, stats) = withinEnum(templateBody())
3666-
Template(constr, parents, derived, self, stats)
3679+
possibleTemplateStart() {
3680+
if (isEnum) {
3681+
val (self, stats) = withinEnum(templateBody())
3682+
Template(constr, parents, derived, self, stats)
3683+
}
3684+
else templateBodyOpt(constr, parents, derived)
36673685
}
3668-
else templateBodyOpt(constr, parents, derived)
36693686
}
36703687

36713688
/** TemplateOpt = [Template]
@@ -3675,12 +3692,13 @@ object Parsers {
36753692
if in.token == EXTENDS || isIdent(nme.derives) then
36763693
template(constr)
36773694
else
3678-
possibleTemplateStart()
3679-
if in.isNestedStart then
3680-
template(constr)
3681-
else
3682-
checkNextNotIndented()
3683-
Template(constr, Nil, Nil, EmptyValDef, Nil)
3695+
possibleTemplateStart() {
3696+
if in.isNestedStart then
3697+
template(constr)
3698+
else
3699+
checkNextNotIndented()
3700+
Template(constr, Nil, Nil, EmptyValDef, Nil)
3701+
}
36843702

36853703
/** TemplateBody ::= [nl] `{' TemplateStatSeq `}'
36863704
* EnumBody ::= [nl] ‘{’ [SelfType] EnumStat {semi EnumStat} ‘}’
@@ -3705,10 +3723,11 @@ object Parsers {
37053723
/** with Template, with EOL <indent> interpreted */
37063724
def withTemplate(constr: DefDef, parents: List[Tree]): Template =
37073725
if in.token != WITHEOL then accept(WITH)
3708-
possibleTemplateStart() // consumes a WITHEOL token
3709-
val (self, stats) = templateBody()
3710-
Template(constr, parents, Nil, self, stats)
3711-
.withSpan(Span(constr.span.orElse(parents.head.span).start, in.lastOffset))
3726+
possibleTemplateStart() { // consumes a WITHEOL token
3727+
val (self, stats) = templateBody()
3728+
Template(constr, parents, Nil, self, stats)
3729+
.withSpan(Span(constr.span.orElse(parents.head.span).start, in.lastOffset))
3730+
}
37123731

37133732
/* -------- STATSEQS ------------------------------------------- */
37143733

@@ -3778,6 +3797,7 @@ object Parsers {
37783797
def templateStatSeq(): (ValDef, List[Tree]) = checkNoEscapingPlaceholders {
37793798
var self: ValDef = EmptyValDef
37803799
val stats = new ListBuffer[Tree]
3800+
var firstStatIndent: Option[IndentWidth] = None
37813801
if (isExprIntro && !isDefIntro(modifierTokens)) {
37823802
val first = expr1()
37833803
if (in.token == ARROW) {
@@ -3793,13 +3813,22 @@ object Parsers {
37933813
in.nextToken()
37943814
}
37953815
else {
3816+
val indent = in.indentWidth(in.offset)
3817+
if (firstStatIndent.isEmpty) firstStatIndent = Some(indent)
3818+
else ()
37963819
stats += first
37973820
acceptStatSepUnlessAtEnd(stats)
37983821
}
37993822
}
38003823
var exitOnError = false
38013824
while (!isStatSeqEnd && !exitOnError) {
38023825
setLastStatOffset()
3826+
val indent = in.indentWidth(in.offset)
3827+
if (firstStatIndent.isEmpty) firstStatIndent = Some(indent)
3828+
else if (strictIndent && firstStatIndent != Some(indent))
3829+
syntaxErrorOrIncomplete(s"unexpected indent: found $indent expected ${firstStatIndent.get}")
3830+
else ()
3831+
38033832
if (in.token == IMPORT)
38043833
stats ++= importClause(IMPORT, mkImport())
38053834
else if (in.token == EXPORT)
@@ -3919,18 +3948,19 @@ object Parsers {
39193948
else
39203949
val pkg = qualId()
39213950
var continue = false
3922-
possibleTemplateStart()
3923-
if in.token == EOF then
3924-
ts += makePackaging(start, pkg, List())
3925-
else if in.isNestedStart then
3926-
ts += inDefScopeBraces(makePackaging(start, pkg, topStatSeq()), rewriteWithColon = true)
3927-
continue = true
3928-
else
3929-
acceptStatSep()
3930-
ts += makePackaging(start, pkg, topstats())
3931-
if continue then
3932-
acceptStatSepUnlessAtEnd(ts)
3933-
ts ++= topStatSeq()
3951+
possibleTemplateStart() {
3952+
if in.token == EOF then
3953+
ts += makePackaging(start, pkg, List())
3954+
else if in.isNestedStart then
3955+
ts += inDefScopeBraces(makePackaging(start, pkg, topStatSeq()), rewriteWithColon = true)
3956+
continue = true
3957+
else
3958+
acceptStatSep()
3959+
ts += makePackaging(start, pkg, topstats())
3960+
if continue then
3961+
acceptStatSepUnlessAtEnd(ts)
3962+
ts ++= topStatSeq()
3963+
}
39343964
}
39353965
else
39363966
ts ++= topStatSeq(outermost = true)

compiler/test/dotty/tools/dotc/parsing/IndentTest.scala

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package parsing
55
import ast.untpd._
66
import org.junit.Test
77

8-
class IndentTest extends ScannerTest:
8+
class IndentTest extends ParserTest:
99

1010
@Test
1111
def parseBraces: Unit =
@@ -14,15 +14,25 @@ class IndentTest extends ScannerTest:
1414
| val x = 1
1515
| val y = 2
1616
|}""".stripMargin
17-
assert(scanTextEither(code).isRight)
17+
assert(parseTextEither(code).isRight)
1818

1919
@Test
2020
def parseIndents: Unit =
2121
val code = s"""
2222
|class A:
2323
| val x = 1
24+
| val y = 2
2425
|""".stripMargin
25-
assert(scanTextEither(code).isRight)
26+
assert(parseTextEither(code).isRight)
27+
28+
@Test
29+
def innerClassIndents: Unit =
30+
val code = s"""
31+
|class A:
32+
| class B:
33+
| val x = 1
34+
|""".stripMargin
35+
assert(parseTextEither(code).isRight)
2636

2737
@Test
2838
def superfluousIndents: Unit =
@@ -31,4 +41,13 @@ class IndentTest extends ScannerTest:
3141
| val x = 1
3242
| val y = 2
3343
|""".stripMargin
34-
assert(scanTextEither(code).isLeft)
44+
assert(parseTextEither(code).isLeft)
45+
46+
@Test
47+
def superfluousIndents2: Unit =
48+
val code = s"""
49+
|class Test:
50+
| test("hello")
51+
| assert(1 == 1)
52+
|""".stripMargin
53+
assert(parseTextEither(code).isLeft)

0 commit comments

Comments
 (0)