Skip to content

Commit d5a5b47

Browse files
committed
Allow lines starting with a dot to fall outside previous indentation widths
If line following some indented code starts with a '`.`' and its indentation width is different from the indentation widths of the two neighboring regions by more than a single space, the line accepted even if it does not match a previous indentation width.
1 parent 8020c77 commit d5a5b47

File tree

6 files changed

+87
-14
lines changed

6 files changed

+87
-14
lines changed

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3080,8 +3080,8 @@ object Parsers {
30803080
/* -------- PARAMETERS ------------------------------------------- */
30813081

30823082
/** DefParamClauses ::= DefParamClause { DefParamClause } -- and two DefTypeParamClause cannot be adjacent
3083-
* DefParamClause ::= DefTypeParamClause
3084-
* | DefTermParamClause
3083+
* DefParamClause ::= DefTypeParamClause
3084+
* | DefTermParamClause
30853085
* | UsingParamClause
30863086
*/
30873087
def typeOrTermParamClauses(
@@ -3179,7 +3179,7 @@ object Parsers {
31793179
* UsingClsTermParamClause::= ‘(’ ‘using’ [‘erased’] (ClsParams | ContextTypes) ‘)’
31803180
* ClsParams ::= ClsParam {‘,’ ClsParam}
31813181
* ClsParam ::= {Annotation}
3182-
*
3182+
*
31833183
* TypelessClause ::= DefTermParamClause
31843184
* | UsingParamClause
31853185
*
@@ -3557,13 +3557,12 @@ object Parsers {
35573557
}
35583558
}
35593559

3560-
35613560

35623561
/** DefDef ::= DefSig [‘:’ Type] ‘=’ Expr
35633562
* | this TypelessClauses [DefImplicitClause] `=' ConstrExpr
35643563
* DefDcl ::= DefSig `:' Type
35653564
* DefSig ::= id [DefTypeParamClause] DefTermParamClauses
3566-
*
3565+
*
35673566
* if clauseInterleaving is enabled:
35683567
* DefSig ::= id [DefParamClauses] [DefImplicitClause]
35693568
*/
@@ -3602,8 +3601,8 @@ object Parsers {
36023601
val mods1 = addFlag(mods, Method)
36033602
val ident = termIdent()
36043603
var name = ident.name.asTermName
3605-
val paramss =
3606-
if in.featureEnabled(Feature.clauseInterleaving) then
3604+
val paramss =
3605+
if in.featureEnabled(Feature.clauseInterleaving) then
36073606
// If you are making interleaving stable manually, please refer to the PR introducing it instead, section "How to make non-experimental"
36083607
typeOrTermParamClauses(ParamOwner.Def, numLeadParams = numLeadParams)
36093608
else
@@ -3613,7 +3612,7 @@ object Parsers {
36133612
joinParams(tparams, vparamss)
36143613

36153614
var tpt = fromWithinReturnType { typedOpt() }
3616-
3615+
36173616
if (migrateTo3) newLineOptWhenFollowedBy(LBRACE)
36183617
val rhs =
36193618
if in.token == EQUALS then

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -610,11 +610,17 @@ object Scanners {
610610
case r: Indented =>
611611
insert(OUTDENT, offset)
612612
handleNewIndentWidth(r.enclosing, ir =>
613-
val lw = lastWidth
614-
errorButContinue(
615-
em"""The start of this line does not match any of the previous indentation widths.
616-
|Indentation width of current line : $nextWidth
617-
|This falls between previous widths: ${ir.width} and $lw"""))
613+
if next.token == DOT
614+
&& !nextWidth.isClose(r.indentWidth)
615+
&& !nextWidth.isClose(ir.indentWidth)
616+
then
617+
ir.otherIndentWidths += nextWidth
618+
else
619+
val lw = lastWidth
620+
errorButContinue(
621+
em"""The start of this line does not match any of the previous indentation widths.
622+
|Indentation width of current line : $nextWidth
623+
|This falls between previous widths: ${ir.width} and $lw"""))
618624
case r =>
619625
if skipping then
620626
if r.enclosing.isClosedByUndentAt(nextWidth) then
@@ -1665,6 +1671,17 @@ object Scanners {
16651671

16661672
def < (that: IndentWidth): Boolean = this <= that && !(that <= this)
16671673

1674+
/** Does `this` differ from `that` by not more than a single space? */
1675+
def isClose(that: IndentWidth): Boolean = this match
1676+
case Run(ch1, n1) =>
1677+
that match
1678+
case Run(ch2, n2) => ch1 == ch2 && ch1 != '\t' && (n1 - n2).abs <= 1
1679+
case Conc(l, r) => false
1680+
case Conc(l1, r1) =>
1681+
that match
1682+
case Conc(l2, r2) => l1 == l2 && r1.isClose(r2)
1683+
case _ => false
1684+
16681685
def toPrefix: String = this match {
16691686
case Run(ch, n) => ch.toString * n
16701687
case Conc(l, r) => l.toPrefix ++ r.toPrefix

docs/_docs/reference/other-new-features/indentation.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ There are two rules:
100100

101101
- An `<outdent>` is finally inserted in front of a comma that follows a statement sequence starting with an `<indent>` if the indented region is itself enclosed in parentheses.
102102

103-
It is an error if the indentation width of the token following an `<outdent>` does not match the indentation of some previous line in the enclosing indentation region. For instance, the following would be rejected.
103+
It is generally an error if the indentation width of the token following an `<outdent>` does not match the indentation of some previous line in the enclosing indentation region. For instance, the following would be rejected.
104104

105105
```scala
106106
if x < 0 then
@@ -109,6 +109,19 @@ if x < 0 then
109109
x
110110
```
111111

112+
However, there is one exception to this rule: If the next line starts with a '`.`' _and_ the indentation
113+
width is different from the indentation widths of the two neighboring regions by more than a single space, the line accepted. For instance, the following is OK:
114+
115+
```scala
116+
xs.map: x =>
117+
x + 1
118+
.filter: x =>
119+
x > 0
120+
```
121+
Here, the line starting with `.filter` does not have an indentation level matching a previous line,
122+
but it is still accepted since it starts with a '`.`' and differs in at least two spaces from the
123+
indentation levels of both the region that is closed and the next outer region.
124+
112125
Indentation tokens are only inserted in regions where newline statement separators are also inferred:
113126
at the top-level, inside braces `{...}`, but not inside parentheses `(...)`, patterns or types.
114127

tests/neg/outdent-dot.check

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- Error: tests/neg/outdent-dot.scala:6:5 ------------------------------------------------------------------------------
2+
6 | .toString // error
3+
| ^
4+
| The start of this line does not match any of the previous indentation widths.
5+
| Indentation width of current line : 5 spaces
6+
| This falls between previous widths: 2 spaces and 6 spaces
7+
-- Error: tests/neg/outdent-dot.scala:11:3 -----------------------------------------------------------------------------
8+
11 | .filter: x => // error
9+
| ^
10+
| The start of this line does not match any of the previous indentation widths.
11+
| Indentation width of current line : 3 spaces
12+
| This falls between previous widths: 2 spaces and 6 spaces
13+
-- Error: tests/neg/outdent-dot.scala:13:4 -----------------------------------------------------------------------------
14+
13 | println("foo") // error
15+
| ^
16+
| The start of this line does not match any of the previous indentation widths.
17+
| Indentation width of current line : 4 spaces
18+
| This falls between previous widths: 2 spaces and 6 spaces

tests/neg/outdent-dot.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
def Block(f: => Int): Int = f
2+
3+
def bar(): String =
4+
Block:
5+
2 + 2
6+
.toString // error
7+
8+
def foo(xs: List[Int]) =
9+
xs.map: x =>
10+
x + 1
11+
.filter: x => // error
12+
x > 0
13+
println("foo") // error

tests/pos/outdent-dot.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
def Block(f: => Int): Int = f
2+
3+
def bar(): String =
4+
Block:
5+
2 + 2
6+
.toString
7+
8+
def foo(xs: List[Int]) =
9+
xs.map: x =>
10+
x + 1
11+
.filter: x =>
12+
x > 0
13+
println("foo")

0 commit comments

Comments
 (0)