From 3c4b0946671bbfea8d63f5b5a40f7ae2a0a8a93b Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 5 Feb 2025 16:06:48 -0800 Subject: [PATCH 1/5] Restrict outdent in parens for certain region prefixes --- .../dotty/tools/dotc/parsing/Scanners.scala | 9 ++- tests/neg/i22527.scala | 24 +++++++ tests/pos/i22527.scala | 65 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/neg/i22527.scala create mode 100644 tests/pos/i22527.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index ec246f7a3742..2380323eec7c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -765,7 +765,14 @@ object Scanners { case _ => false currentRegion match case r: Indented if isEnclosedInParens(r.outer) => - insert(OUTDENT, offset) + // For some region prefixes (COLONeol, EQUALS) only OUTDENT if COMMA at EOL + if canStartIndentTokens.contains(r.prefix) && !statCtdTokens.contains(r.prefix) then + val lookahead = LookaheadScanner() + lookahead.nextToken() + if lookahead.isAfterLineEnd then + insert(OUTDENT, offset) + else + insert(OUTDENT, offset) case _ => peekAhead() if isAfterLineEnd diff --git a/tests/neg/i22527.scala b/tests/neg/i22527.scala new file mode 100644 index 000000000000..b9d9166e8fc2 --- /dev/null +++ b/tests/neg/i22527.scala @@ -0,0 +1,24 @@ + +//rule of thumb is COLONeol was at EOL, so COMMA must be at EOL +def test: Unit = + assert( + identity: + true, "ok" // error end of statement expected but ',' found + ) + +def toss: Unit = + assert( + throw + null, "ok" // error same + ) + +def callme[A](x: => A, msg: String) = try x.toString catch case t: RuntimeException => msg + +// not all indented regions require COMMA at EOL for OUTDENT +def orElse(x: Int): Unit = + callme( + if x > 0 then + class X extends AnyRef, Serializable // error Not found: Serializable - did you mean Specializable? + true // error ',' or ')' expected, but 'true' found + else + false, "fail") diff --git a/tests/pos/i22527.scala b/tests/pos/i22527.scala new file mode 100644 index 000000000000..055ed8acfae2 --- /dev/null +++ b/tests/pos/i22527.scala @@ -0,0 +1,65 @@ + +def f: Unit = + identity( + identity: + class X extends AnyRef, Serializable + 42 + ) + +def g: Unit = + identity( + x = + class X extends AnyRef, Serializable + 27 + ) + +def test: Unit = + assert( + identity: + true, + "ok" + ) + +def toss: Unit = + assert( + throw + null, + "ok" + ) + +def callme[A](x: => A, msg: String) = try x.toString catch case t: RuntimeException => msg + +def orElse(x: Int): Unit = + callme( + if x > 0 then + true + else + false, "fail") + +def onlyIf(x: Int): Unit = + callme( + if (x > 0) + true, "fail") // warn value discard + +def h(xs: List[Int]) = + xs.foldLeft(0) + ( + (acc, x) => + acc + + x, + ) + +def sum(x: Int, y: Int, z: Int) = x+y+z + +def k(xs: List[Int], y: Int, z: Int) = + xs.foldLeft(0) + ( + (acc, x) => + sum( + x + + y + + z, + y, + z, + ) + ) From fcf0e5791ce9d3885684d9300646be4081a1869a Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 6 Feb 2025 09:41:27 -0800 Subject: [PATCH 2/5] Relax and restrict only colon regions --- .../dotty/tools/dotc/parsing/Scanners.scala | 4 ++-- tests/neg/i22527.scala | 13 ++++++------ tests/pos/i22527.scala | 21 ++++++++++++------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 2380323eec7c..20fbc6c96341 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -765,8 +765,8 @@ object Scanners { case _ => false currentRegion match case r: Indented if isEnclosedInParens(r.outer) => - // For some region prefixes (COLONeol, EQUALS) only OUTDENT if COMMA at EOL - if canStartIndentTokens.contains(r.prefix) && !statCtdTokens.contains(r.prefix) then + // For region prefix COLONeol, only OUTDENT if COMMA at EOL + if r.prefix == COLONeol then val lookahead = LookaheadScanner() lookahead.nextToken() if lookahead.isAfterLineEnd then diff --git a/tests/neg/i22527.scala b/tests/neg/i22527.scala index b9d9166e8fc2..4b80c21ff035 100644 --- a/tests/neg/i22527.scala +++ b/tests/neg/i22527.scala @@ -6,12 +6,6 @@ def test: Unit = true, "ok" // error end of statement expected but ',' found ) -def toss: Unit = - assert( - throw - null, "ok" // error same - ) - def callme[A](x: => A, msg: String) = try x.toString catch case t: RuntimeException => msg // not all indented regions require COMMA at EOL for OUTDENT @@ -22,3 +16,10 @@ def orElse(x: Int): Unit = true // error ',' or ')' expected, but 'true' found else false, "fail") + +def g: Unit = + identity( + x = + class X extends AnyRef, Serializable // error + 27 // error + ) diff --git a/tests/pos/i22527.scala b/tests/pos/i22527.scala index 055ed8acfae2..d222c3311b86 100644 --- a/tests/pos/i22527.scala +++ b/tests/pos/i22527.scala @@ -6,13 +6,6 @@ def f: Unit = 42 ) -def g: Unit = - identity( - x = - class X extends AnyRef, Serializable - 27 - ) - def test: Unit = assert( identity: @@ -26,6 +19,11 @@ def toss: Unit = null, "ok" ) +def raise: Unit = + assert( + throw + null, "ok" // ok now + ) def callme[A](x: => A, msg: String) = try x.toString catch case t: RuntimeException => msg @@ -63,3 +61,12 @@ def k(xs: List[Int], y: Int, z: Int) = z, ) ) + +object `arrow eol`: + def f(g: Int => Int, i: Int): Int = g(i) + def g(map: Int => Int): Int => Int = map + def test = + f( + g: x => + x + 1, 42 + ) From e69bf6b60f1ca22a711a9cb59051b7282a3e7c9a Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sat, 8 Feb 2025 09:00:31 -0800 Subject: [PATCH 3/5] Warn about unindented args --- .../dotty/tools/dotc/parsing/Scanners.scala | 10 ++- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- tests/neg/syntax-error-recovery.check | 6 +- tests/pos/i22527.scala | 9 ++- tests/warn/i22527.check | 22 +++++ tests/warn/i22527.scala | 81 +++++++++++++++++++ 6 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 tests/warn/i22527.check create mode 100644 tests/warn/i22527.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 20fbc6c96341..3be8b87b6171 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -651,8 +651,14 @@ object Scanners { if skipping then if r.enclosing.isClosedByUndentAt(nextWidth) then insert(OUTDENT, offset) - else if r.isInstanceOf[InBraces] && !closingRegionTokens.contains(token) then - report.warning("Line is indented too far to the left, or a `}` is missing", sourcePos()) + else + val checkable = r match + case _: InBraces => true + case InParens(LPAREN, _) => true + case _ => false + if checkable && !closingRegionTokens.contains(token) then + report.warning(s"Line is indented too far to the left, or a ${showToken(r.closedBy)} is missing", + sourcePos()) else if lastWidth < nextWidth || lastWidth == nextWidth && (lastToken == MATCH || lastToken == CATCH) && token == CASE then if canStartIndentTokens.contains(lastToken) then diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 9f4cc2086dc1..c3c8f6c2ace8 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -1053,7 +1053,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { addParamssText( addParamssText(keywordStr("extension "), leadingParamss) ~~ (defKeyword ~~ valDefText(nameIdText(tree))).close, - trailingParamss) + trailingParamss) else addParamssText(defKeyword ~~ valDefText(nameIdText(tree)), tree.paramss) diff --git a/tests/neg/syntax-error-recovery.check b/tests/neg/syntax-error-recovery.check index 83b764c3062c..9f6cbaa7bf60 100644 --- a/tests/neg/syntax-error-recovery.check +++ b/tests/neg/syntax-error-recovery.check @@ -96,7 +96,5 @@ | longer explanation available when compiling with `-explain` -- Warning: tests/neg/syntax-error-recovery.scala:61:2 ----------------------------------------------------------------- 61 | println(bam) - | ^^^^^^^ - | Alphanumeric method println is not declared infix; it should not be used as infix operator. - | Instead, use method syntax .println(...) or backticked identifier `println`. - | The latter can be rewritten automatically under -rewrite -source 3.4-migration. + | ^ + | Line is indented too far to the left, or a ')' is missing diff --git a/tests/pos/i22527.scala b/tests/pos/i22527.scala index d222c3311b86..cf67d2e750fb 100644 --- a/tests/pos/i22527.scala +++ b/tests/pos/i22527.scala @@ -1,4 +1,8 @@ +//> using options -Werror + +import annotation.* + def f: Unit = identity( identity: @@ -34,6 +38,7 @@ def orElse(x: Int): Unit = else false, "fail") +@nowarn("msg=Unit") def onlyIf(x: Int): Unit = callme( if (x > 0) @@ -57,8 +62,8 @@ def k(xs: List[Int], y: Int, z: Int) = x + y + z, - y, - z, + y, + z, ) ) diff --git a/tests/warn/i22527.check b/tests/warn/i22527.check new file mode 100644 index 000000000000..b50612e12b57 --- /dev/null +++ b/tests/warn/i22527.check @@ -0,0 +1,22 @@ +-- Warning: tests/warn/i22527.scala:60:10 ------------------------------------------------------------------------------ +60 | y, // warn + | ^ + | Line is indented too far to the left, or a ')' is missing +-- Warning: tests/warn/i22527.scala:61:10 ------------------------------------------------------------------------------ +61 | z, // warn + | ^ + | Line is indented too far to the left, or a ')' is missing +-- Warning: tests/warn/i22527.scala:72:2 ------------------------------------------------------------------------------- +72 | 42 // warn + | ^ + | Line is indented too far to the left, or a ')' is missing +-- Warning: tests/warn/i22527.scala:79:0 ------------------------------------------------------------------------------- +79 |k: // warn + |^ + |Line is indented too far to the left, or a ')' is missing +-- [E190] Potential Issue Warning: tests/warn/i22527.scala:40:6 -------------------------------------------------------- +40 | true, "fail") // warn value discard + | ^^^^ + | Discarded non-Unit value of type Boolean. Add `: Unit` to discard silently. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/warn/i22527.scala b/tests/warn/i22527.scala new file mode 100644 index 000000000000..64cd32617406 --- /dev/null +++ b/tests/warn/i22527.scala @@ -0,0 +1,81 @@ + +def f: Unit = + identity( + identity: + class X extends AnyRef, Serializable + 42 + ) + +def test: Unit = + assert( + identity: + true, + "ok" + ) + +def toss: Unit = + assert( + throw + null, + "ok" + ) +def raise: Unit = + assert( + throw + null, "ok" // ok now + ) + +def callme[A](x: => A, msg: String) = try x.toString catch case t: RuntimeException => msg + +def orElse(x: Int): Unit = + callme( + if x > 0 then + true + else + false, "fail") + +def onlyIf(x: Int): Unit = + callme( + if (x > 0) + true, "fail") // warn value discard + +def h(xs: List[Int]) = + xs.foldLeft(0) + ( + (acc, x) => + acc + + x, + ) + +def sum(x: Int, y: Int, z: Int) = x+y+z + +def k(xs: List[Int], y: Int, z: Int) = + xs.foldLeft(0) + ( + (acc, x) => + sum( + x + + y + + z, + y, // warn + z, // warn + ) + ) + +object `arrow eol`: + def f(g: Int => Int, i: Int): Int = g(i) + def g(map: Int => Int): Int => Int = map + def k(i: => Int) = i + def test = + f( + g(_ + 1), + 42 // warn + ) + def test2 = + f( + g: + x => + x + 1, +k: // warn + 42 + ) From 595a86eb9bf978db502836aad9a17b12b9eebd2e Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 12 Feb 2025 21:39:17 -0800 Subject: [PATCH 4/5] Clean up syntax in tests --- .../completion/CompletionExtensionSuite.scala | 3 +- .../completion/CompletionKeywordSuite.scala | 2 +- .../pc/tests/completion/CompletionSuite.scala | 2 +- .../tools/pc/tests/hover/HoverDocSuite.scala | 31 +++++++++---------- .../scaladoc/site/TemplateFileTests.scala | 20 ++++++------ tests/neg/i22527.scala | 5 +++ tests/pos/i22527.scala | 2 +- tests/warn/i22527.scala | 2 +- 8 files changed, 35 insertions(+), 32 deletions(-) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala index 16535381165c..e6cf096716c6 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala @@ -86,7 +86,7 @@ class CompletionExtensionSuite extends BaseCompletionSuite: @Test def `filter-by-type-old` = check( - """|package example + """|package example | |object enrichments: | implicit class A(num: Int): @@ -99,7 +99,6 @@ class CompletionExtensionSuite extends BaseCompletionSuite: """|identity: String (implicit) |""".stripMargin, // identity2 won't be available filter = _.contains("(implicit)") - ) @Test def `filter-by-type-subtype` = diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala index 6abea1209998..013bc844f216 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala @@ -59,7 +59,7 @@ class CompletionKeywordSuite extends BaseCompletionSuite: | **/ |} |""".stripMargin, - "", + "", includeCommitCharacter = true ) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 103f365f8af6..96aa208abdf8 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -1358,7 +1358,7 @@ class CompletionSuite extends BaseCompletionSuite: | val t: TT@@ |} |""".stripMargin, - "TTT[A <: Int] = List[A]" + "TTT[A <: Int] = List[A]" ) @Test def `type-lambda` = diff --git a/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverDocSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverDocSuite.scala index a82fa73865c8..3e9d8c1695e8 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverDocSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverDocSuite.scala @@ -19,20 +19,20 @@ class HoverDocSuite extends BaseHoverSuite: @Test def `doc` = check( - """object a { - | <> - |} - |""".stripMargin, - """|**Expression type**: - |```scala - |java.util.List[Int] - |``` - |**Symbol signature**: - |```scala - |final def emptyList[T](): java.util.List[T] - |``` - |Found documentation for java/util/Collections#emptyList(). - |""".stripMargin, + """|object a { + | <> + |} + |""".stripMargin, + """|**Expression type**: + |```scala + |java.util.List[Int] + |``` + |**Symbol signature**: + |```scala + |final def emptyList[T](): java.util.List[T] + |``` + |Found documentation for java/util/Collections#emptyList(). + |""".stripMargin, ) @Test def `doc-parent` = @@ -47,7 +47,6 @@ class HoverDocSuite extends BaseHoverSuite: |``` |Found documentation for scala/collection/LinearSeqOps#headOption(). |""".stripMargin, - ) @Test def `java-method` = @@ -61,7 +60,7 @@ class HoverDocSuite extends BaseHoverSuite: |``` |Found documentation for java/lang/String#substring(). |""".stripMargin - ) + ) @Test def `object` = check( diff --git a/scaladoc/test/dotty/tools/scaladoc/site/TemplateFileTests.scala b/scaladoc/test/dotty/tools/scaladoc/site/TemplateFileTests.scala index f07868ad4f44..0a855c169275 100644 --- a/scaladoc/test/dotty/tools/scaladoc/site/TemplateFileTests.scala +++ b/scaladoc/test/dotty/tools/scaladoc/site/TemplateFileTests.scala @@ -208,26 +208,26 @@ class TemplateFileTests: testTemplate( """# Hello {{ msg }}!""", ext = "md" - ) { t => + ): t => assertEquals( """
- |

Hello there!

- |
""".stripMargin, - t.resolveInner(RenderingContext(Map("msg" -> "there"))).code.trim()) - } + |

Hello there!

+ |""".stripMargin, + t.resolveInner(RenderingContext(Map("msg" -> "there"))).code.trim() + ) @Test def mixedTemplates() : Unit = testTemplate( """# Hello {{ msg }}!""", ext = "md" - ) { t => + ): t => assertEquals( """
- |

Hello there2!

- |
""".stripMargin, - t.resolveInner(RenderingContext(Map("msg" -> "there2"))).code.trim()) - } + |

Hello there2!

+ |""".stripMargin, + t.resolveInner(RenderingContext(Map("msg" -> "there2"))).code.trim() + ) @Test def htmlOnly(): Unit = diff --git a/tests/neg/i22527.scala b/tests/neg/i22527.scala index 4b80c21ff035..b9d476793634 100644 --- a/tests/neg/i22527.scala +++ b/tests/neg/i22527.scala @@ -23,3 +23,8 @@ def g: Unit = class X extends AnyRef, Serializable // error 27 // error ) + +def onlyIf(x: Int): Unit = + callme( + if (x > 0) + true, "fail") // error syntax is broken after old-style conditional diff --git a/tests/pos/i22527.scala b/tests/pos/i22527.scala index cf67d2e750fb..a8a1d436dfe2 100644 --- a/tests/pos/i22527.scala +++ b/tests/pos/i22527.scala @@ -41,7 +41,7 @@ def orElse(x: Int): Unit = @nowarn("msg=Unit") def onlyIf(x: Int): Unit = callme( - if (x > 0) + if (x > 0) then // then syntax required true, "fail") // warn value discard def h(xs: List[Int]) = diff --git a/tests/warn/i22527.scala b/tests/warn/i22527.scala index 64cd32617406..36486552bfbb 100644 --- a/tests/warn/i22527.scala +++ b/tests/warn/i22527.scala @@ -36,7 +36,7 @@ def orElse(x: Int): Unit = def onlyIf(x: Int): Unit = callme( - if (x > 0) + if x > 0 then true, "fail") // warn value discard def h(xs: List[Int]) = From 456a6ab3260e909ba93e2fd201150af8b9af6dd9 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 19 Nov 2025 21:19:05 -0800 Subject: [PATCH 5/5] No indent in named arg pattern --- .../dotty/tools/dotc/parsing/Parsers.scala | 11 +++++--- tests/pos/i24474.scala | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 tests/pos/i24474.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index f44703a562f1..852c087b871f 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -671,11 +671,14 @@ object Parsers { ts.toList else leading :: Nil - def maybeNamed(op: () => Tree): () => Tree = () => + def maybeNamed(location: Location, op: () => Tree): () => Tree = () => if isIdent && in.lookahead.token == EQUALS && sourceVersion.enablesNamedTuples then atSpan(in.offset): val name = ident() in.nextToken() + if location.inPattern && in.token == INDENT then + in.currentRegion = in.currentRegion.outer + in.nextToken() NamedArg(name, op()) else op() @@ -2958,7 +2961,7 @@ object Parsers { if isErased then isFormalParams = true if isFormalParams then binding(Modifiers()) else - val t = maybeNamed(exprInParens)() + val t = maybeNamed(Location.InParens, exprInParens)() if t.isInstanceOf[ValDef] then isFormalParams = true t commaSeparatedRest(exprOrBinding(), exprOrBinding) @@ -3411,8 +3414,8 @@ object Parsers { * | NamedPattern {‘,’ NamedPattern} * NamedPattern ::= id '=' Pattern */ - def patterns(location: Location = Location.InPattern): List[Tree] = - commaSeparated(maybeNamed(() => pattern(location))) + def patterns(location: Location = Location.InPattern): List[Tree] = // default used from Markup + commaSeparated(maybeNamed(location, () => pattern(location))) // check that patterns are all named or all unnamed is done at desugaring def patternsOpt(location: Location = Location.InPattern): List[Tree] = diff --git a/tests/pos/i24474.scala b/tests/pos/i24474.scala new file mode 100644 index 000000000000..48f3a1d1a92d --- /dev/null +++ b/tests/pos/i24474.scala @@ -0,0 +1,28 @@ +case class Foo(arg1: Int, arg2: Int) +case class Bar(arg1: Int, arg2: Option[Int]) + +object OK: + Foo(1, 2) match + case Foo(arg1 = + 5 + ) => + +object Test: + Foo(1, 2) match + case Foo(arg1 = + 5 // was: error: pattern expected + ) => + +object Nest: + Bar(1, Option(2)) match + case Bar(arg2 = + Some(value = + 42 + ) + ) => + +object Detupled: + (fst = 1, snd = 2) match + case (fst = + 5 + ) =>