From 660902295fe259f2d620496a2e19f6a8ad882b12 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 31 Mar 2021 12:58:56 +0200 Subject: [PATCH] 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 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. --- .../dotty/tools/dotc/parsing/Scanners.scala | 22 ++++++++++++++---- .../other-new-features/indentation.md | 23 ++++++++++++++++++- tests/run/indent.scala | 12 ++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 tests/run/indent.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 1412b5a04f47..15b3f6e63134 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -369,7 +369,7 @@ object Scanners { * token that can start an expression. * If a leading infix operator is found and the source version is `3.0-migration`, emit a change warning. */ - def isLeadingInfixOperator(inConditional: Boolean = true) = + def isLeadingInfixOperator(nextWidth: IndentWidth = indentWidth(offset), inConditional: Boolean = true) = allowLeadingInfixOperators && isOperator && (isWhitespace(ch) || ch == LF) @@ -397,6 +397,20 @@ object Scanners { assumeStartsExpr(lookahead) || lookahead.token == NEWLINE && assumeStartsExpr(lookahead.next) } + && { + currentRegion match + case r: Indented => + r.width <= nextWidth + || { + r.outer match + case null => true + case Indented(outerWidth, others, _, _) => + outerWidth < nextWidth && !others.contains(nextWidth) + case outer => + outer.indentWidth < nextWidth + } + case _ => true + } && { if migrateTo3 then val (what, previous) = @@ -512,7 +526,7 @@ object Scanners { if newlineIsSeparating && canEndStatTokens.contains(lastToken) && canStartStatTokens.contains(token) - && !isLeadingInfixOperator() + && !isLeadingInfixOperator(nextWidth) && !(lastWidth < nextWidth && isContinuing(lastToken)) then insert(if (pastBlankLine) NEWLINES else NEWLINE, lineOffset) @@ -521,7 +535,7 @@ object Scanners { || nextWidth == lastWidth && (indentPrefix == MATCH || indentPrefix == CATCH) && token != CASE then if currentRegion.isOutermost then if nextWidth < lastWidth then currentRegion = topLevelRegion(nextWidth) - else if !isLeadingInfixOperator() && !statCtdTokens.contains(lastToken) then + else if !isLeadingInfixOperator(nextWidth) && !statCtdTokens.contains(lastToken) then currentRegion match case r: Indented => currentRegion = r.enclosing @@ -1105,7 +1119,7 @@ object Scanners { putChar(ch) nextRawChar() getStringPart(multiLine) - } + } else if (ch == '$') { nextRawChar() if (ch == '$' || ch == '"') { diff --git a/docs/docs/reference/other-new-features/indentation.md b/docs/docs/reference/other-new-features/indentation.md index 7d28c821cbf3..4b1b78a9bf4e 100644 --- a/docs/docs/reference/other-new-features/indentation.md +++ b/docs/docs/reference/other-new-features/indentation.md @@ -80,8 +80,11 @@ There are two rules: ``` then else do catch finally yield match ``` - - the first token on the next line is not a + - if the first token on the next line is a [leading infix operator](../changed-features/operators.md). + then its indentation width is less then the current indentation width, + and it either matches a previous indentation width or is also less + than the enclosing indentation width. If an `` is inserted, the top element is popped from `IW`. 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 `` tokens @@ -105,6 +108,24 @@ if x < 0 then 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. +**Note:** The rules for leading infix operators above are there to make sure that +```scala + one + + two.match + case 1 => b + case 2 => c + + three +``` +is parsed as `one + (two.match ...) + three`. Also, that +```scala +if x then + a + + b + + c +else d +``` +is parsed as `if x then a + b + c else d`. + ### Optional Braces Around Template Bodies 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. diff --git a/tests/run/indent.scala b/tests/run/indent.scala new file mode 100644 index 000000000000..24cb22a5c354 --- /dev/null +++ b/tests/run/indent.scala @@ -0,0 +1,12 @@ +@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) +