Skip to content

Commit 6609022

Browse files
committed
Better handling of leading infix operators in indented code
```code @main def Test = val x = false val y = 1 val result = x || y.match case 1 => false case 3 => false case _ => true || !x assert(result) ``` In this code, the last `|| !x` was seen as a part of the previous case, so the code was parsed as ```scala @main def Test = val x = false val y = 1 val result = x || y.match case 1 => false case 3 => false case _ => true || !x assert(result) ``` This is highly surprising and unintuitive. The fix will insert an <outdent> token instead if the leading infix operator is too far to the left. Too far means: (1) left of the current indentation region, (2) and not to the right of any outer indentation widths. (2) allows to still parse code like this ```scala if xyz then one + two + three ``` Here, the width of the indentation region after `then` is 4, but the `+` operator is to the right of the outer indentation width of 0., so the indentation region is not closed. In other words, we do not close an indentation region if the result would not be legal, since it matches none of the previous indentation widths.
1 parent f01c14d commit 6609022

File tree

3 files changed

+52
-5
lines changed

3 files changed

+52
-5
lines changed

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ object Scanners {
369369
* token that can start an expression.
370370
* If a leading infix operator is found and the source version is `3.0-migration`, emit a change warning.
371371
*/
372-
def isLeadingInfixOperator(inConditional: Boolean = true) =
372+
def isLeadingInfixOperator(nextWidth: IndentWidth = indentWidth(offset), inConditional: Boolean = true) =
373373
allowLeadingInfixOperators
374374
&& isOperator
375375
&& (isWhitespace(ch) || ch == LF)
@@ -397,6 +397,20 @@ object Scanners {
397397
assumeStartsExpr(lookahead)
398398
|| lookahead.token == NEWLINE && assumeStartsExpr(lookahead.next)
399399
}
400+
&& {
401+
currentRegion match
402+
case r: Indented =>
403+
r.width <= nextWidth
404+
|| {
405+
r.outer match
406+
case null => true
407+
case Indented(outerWidth, others, _, _) =>
408+
outerWidth < nextWidth && !others.contains(nextWidth)
409+
case outer =>
410+
outer.indentWidth < nextWidth
411+
}
412+
case _ => true
413+
}
400414
&& {
401415
if migrateTo3 then
402416
val (what, previous) =
@@ -512,7 +526,7 @@ object Scanners {
512526
if newlineIsSeparating
513527
&& canEndStatTokens.contains(lastToken)
514528
&& canStartStatTokens.contains(token)
515-
&& !isLeadingInfixOperator()
529+
&& !isLeadingInfixOperator(nextWidth)
516530
&& !(lastWidth < nextWidth && isContinuing(lastToken))
517531
then
518532
insert(if (pastBlankLine) NEWLINES else NEWLINE, lineOffset)
@@ -521,7 +535,7 @@ object Scanners {
521535
|| nextWidth == lastWidth && (indentPrefix == MATCH || indentPrefix == CATCH) && token != CASE then
522536
if currentRegion.isOutermost then
523537
if nextWidth < lastWidth then currentRegion = topLevelRegion(nextWidth)
524-
else if !isLeadingInfixOperator() && !statCtdTokens.contains(lastToken) then
538+
else if !isLeadingInfixOperator(nextWidth) && !statCtdTokens.contains(lastToken) then
525539
currentRegion match
526540
case r: Indented =>
527541
currentRegion = r.enclosing
@@ -1105,7 +1119,7 @@ object Scanners {
11051119
putChar(ch)
11061120
nextRawChar()
11071121
getStringPart(multiLine)
1108-
}
1122+
}
11091123
else if (ch == '$') {
11101124
nextRawChar()
11111125
if (ch == '$' || ch == '"') {

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,11 @@ There are two rules:
8080
```
8181
then else do catch finally yield match
8282
```
83-
- the first token on the next line is not a
83+
- if the first token on the next line is a
8484
[leading infix operator](../changed-features/operators.md).
85+
then its indentation width is less then the current indentation width,
86+
and it either matches a previous indentation width or is also less
87+
than the enclosing indentation width.
8588

8689
If an `<outdent>` is inserted, the top element is popped from `IW`.
8790
If the indentation width of the token on the next line is still less than the new current indentation width, step (2) repeats. Therefore, several `<outdent>` tokens
@@ -105,6 +108,24 @@ if x < 0 then
105108
Indentation tokens are only inserted in regions where newline statement separators are also inferred:
106109
at the top-level, inside braces `{...}`, but not inside parentheses `(...)`, patterns or types.
107110

111+
**Note:** The rules for leading infix operators above are there to make sure that
112+
```scala
113+
one
114+
+ two.match
115+
case 1 => b
116+
case 2 => c
117+
+ three
118+
```
119+
is parsed as `one + (two.match ...) + three`. Also, that
120+
```scala
121+
if x then
122+
a
123+
+ b
124+
+ c
125+
else d
126+
```
127+
is parsed as `if x then a + b + c else d`.
128+
108129
### Optional Braces Around Template Bodies
109130

110131
The Scala grammar uses the term _template body_ for the definitions of a class, trait, or object that are normally enclosed in braces. The braces around a template body can also be omitted by means of the following rule.

tests/run/indent.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@main def Test =
2+
val x = false
3+
val y = 1
4+
val result =
5+
x
6+
|| y.match
7+
case 1 => false
8+
case 3 => false
9+
case _ => true
10+
|| !x
11+
assert(result)
12+

0 commit comments

Comments
 (0)