Skip to content

Commit

Permalink
Allow lines starting with a dot to fall outside previous indentation …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
odersky committed May 10, 2023
1 parent e614f23 commit d836de6
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 8 deletions.
2 changes: 0 additions & 2 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3599,8 +3599,6 @@ object Parsers {
}
}



/** DefDef ::= DefSig [‘:’ Type] ‘=’ Expr
* | this TypelessClauses [DefImplicitClause] `=' ConstrExpr
* DefDcl ::= DefSig `:' Type
Expand Down
27 changes: 22 additions & 5 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -611,11 +611,17 @@ object Scanners {
case r: Indented =>
insert(OUTDENT, offset)
handleNewIndentWidth(r.enclosing, ir =>
val lw = lastWidth
errorButContinue(
em"""The start of this line does not match any of the previous indentation widths.
|Indentation width of current line : $nextWidth
|This falls between previous widths: ${ir.width} and $lw"""))
if next.token == DOT
&& !nextWidth.isClose(r.indentWidth)
&& !nextWidth.isClose(ir.indentWidth)
then
ir.otherIndentWidths += nextWidth
else
val lw = lastWidth
errorButContinue(
em"""The start of this line does not match any of the previous indentation widths.
|Indentation width of current line : $nextWidth
|This falls between previous widths: ${ir.width} and $lw"""))
case r =>
if skipping then
if r.enclosing.isClosedByUndentAt(nextWidth) then
Expand Down Expand Up @@ -1666,6 +1672,17 @@ object Scanners {

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

/** Does `this` differ from `that` by not more than a single space? */
def isClose(that: IndentWidth): Boolean = this match
case Run(ch1, n1) =>
that match
case Run(ch2, n2) => ch1 == ch2 && ch1 != '\t' && (n1 - n2).abs <= 1
case Conc(l, r) => false
case Conc(l1, r1) =>
that match
case Conc(l2, r2) => l1 == l2 && r1.isClose(r2)
case _ => false

def toPrefix: String = this match {
case Run(ch, n) => ch.toString * n
case Conc(l, r) => l.toPrefix ++ r.toPrefix
Expand Down
15 changes: 14 additions & 1 deletion docs/_docs/reference/other-new-features/indentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ There are two rules:

- 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.

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.
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.

```scala
if x < 0 then
Expand All @@ -109,6 +109,19 @@ if x < 0 then
x
```

However, there is one exception to this rule: If the next line starts with a '`.`' _and_ the indentation
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:

```scala
xs.map: x =>
x + 1
.filter: x =>
x > 0
```
Here, the line starting with `.filter` does not have an indentation level matching a previous line,
but it is still accepted since it starts with a '`.`' and differs in at least two spaces from the
indentation levels of both the region that is closed and the next outer region.

Indentation tokens are only inserted in regions where newline statement separators are also inferred:
at the top-level, inside braces `{...}`, but not inside parentheses `(...)`, patterns or types.

Expand Down
18 changes: 18 additions & 0 deletions tests/neg/outdent-dot.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- Error: tests/neg/outdent-dot.scala:6:5 ------------------------------------------------------------------------------
6 | .toString // error
| ^
| The start of this line does not match any of the previous indentation widths.
| Indentation width of current line : 5 spaces
| This falls between previous widths: 2 spaces and 6 spaces
-- Error: tests/neg/outdent-dot.scala:11:3 -----------------------------------------------------------------------------
11 | .filter: x => // error
| ^
| The start of this line does not match any of the previous indentation widths.
| Indentation width of current line : 3 spaces
| This falls between previous widths: 2 spaces and 6 spaces
-- Error: tests/neg/outdent-dot.scala:13:4 -----------------------------------------------------------------------------
13 | println("foo") // error
| ^
| The start of this line does not match any of the previous indentation widths.
| Indentation width of current line : 4 spaces
| This falls between previous widths: 2 spaces and 6 spaces
13 changes: 13 additions & 0 deletions tests/neg/outdent-dot.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
def Block(f: => Int): Int = f

def bar(): String =
Block:
2 + 2
.toString // error

def foo(xs: List[Int]) =
xs.map: x =>
x + 1
.filter: x => // error
x > 0
println("foo") // error
13 changes: 13 additions & 0 deletions tests/pos/outdent-dot.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
def Block(f: => Int): Int = f

def bar(): String =
Block:
2 + 2
.toString

def foo(xs: List[Int]) =
xs.map: x =>
x + 1
.filter: x =>
x > 0
println("foo")

0 comments on commit d836de6

Please sign in to comment.