Repository: scalameta/scalameta (scala)

Issue:
can't parse poorly-indented chained match expressions without braces
Scala 3.3.1-RC1

`.scalafmt.conf`
```
version = 3.7.10
runner.dialect = Scala3
indent.significant = 2
```

```scala
object small:
  val value =
    Nil match
    case Nil => "empty"
    case _   => "nonempty"
  match
    case "empty"    => 0
    case "nonempty" => 1

```

The code is compilable but scala fmt fails with error:
`Scalafmt parse error (small.scala:95): [dialect scala3] ; expected but match found`

Pull request description:
An expression which starts with an indent doesn't have to be indented but instead could start with an indented part and continue past it. Fixes #3261.

Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
index 938f1dfe63..00488a6e12 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
@@ -1420,14 +1420,6 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
         )
     }
 
-  private def exprMaybeIndented(): Term = {
-    if (token.is[Indentation.Indent]) {
-      blockExpr()
-    } else {
-      expr()
-    }
-  }
-
   private def tryAcceptWithOptLF[T: TokenClassifier]: Boolean = {
     acceptOpt[T] || {
       val ok = token.is[LF] && tryAhead[T]
@@ -1466,12 +1458,12 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
       if (token.is[LeftParen])
         condExprInParens[KwThen]
       else
-        try exprMaybeIndented()
+        try expr()
         finally acceptAfterOptNL[KwThen]
 
-    val thenp = exprMaybeIndented()
+    val thenp = expr()
     if (tryAcceptWithOptLF[KwElse]) {
-      Term.If(cond, thenp, exprMaybeIndented(), mods)
+      Term.If(cond, thenp, expr(), mods)
     } else if (token.is[Semicolon] && tryAhead[KwElse]) {
       next(); Term.If(cond, thenp, expr(), mods)
     } else {
@@ -1521,17 +1513,17 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
       case KwTry() =>
         next()
         val body: Term = token match {
-          case _: Indentation.Indent => block()
           case _ if dialect.allowTryWithAnyExpr => expr()
           case _: LeftParen => inParensOnOpen(expr())
           case _: LeftBrace => block()
+          case _: Indentation.Indent => block()
           case _ => expr()
         }
         def caseClausesOrExpr = caseClausesIfAny().toRight(blockWithinDelims())
 
         def finallyopt =
           if (tryAcceptWithOptLF[KwFinally]) {
-            Some(exprMaybeIndented())
+            Some(expr())
           } else {
             None
           }
@@ -1557,9 +1549,9 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
           if (token.is[LeftParen])
             condExprInParens[KwDo]
           else
-            try if (token.is[Indentation.Indent]) block() else expr()
+            try expr()
             finally acceptAfterOptNL[KwDo]
-        Term.While(cond, exprMaybeIndented())
+        Term.While(cond, expr())
       case KwDo() if dialect.allowDoWhile =>
         next()
         val body = expr()
@@ -1588,11 +1580,11 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
 
         newLinesOpt()
         if (acceptOpt[KwDo]) {
-          Term.For(enums, exprMaybeIndented())
+          Term.For(enums, expr())
         } else if (acceptOpt[KwYield]) {
-          Term.ForYield(enums, exprMaybeIndented())
+          Term.ForYield(enums, expr())
         } else {
-          Term.For(enums, exprMaybeIndented())
+          Term.For(enums, expr())
         }
       case KwReturn() =>
         next()
@@ -3520,7 +3512,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     val tpOpt: Option[Type] = typedOpt()
 
     if (acceptOpt[Equals]) {
-      val rhs = exprMaybeIndented()
+      val rhs = expr()
       if (rhs.is[Term.Placeholder] && (tpOpt.isEmpty || isVal || !lhs.forall(_.is[Pat.Var])))
         syntaxError("unbound placeholder parameter", at = token)
       if (isVal)
@@ -3609,7 +3601,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     }
 
     if (acceptOpt[Equals]) {
-      Defn.GivenAlias(mods, sigName, paramClauseGroup, decltype, exprMaybeIndented())
+      Defn.GivenAlias(mods, sigName, paramClauseGroup, decltype, expr())
     } else if (token.isAny[KwWith, LeftParen]) {
       val inits = parents()
       val (slf, stats) = {
@@ -3735,7 +3727,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     } else {
       accept[Equals]
       val isMacro = acceptOpt[KwMacro]
-      val rhs = exprMaybeIndented()
+      val rhs = expr()
       if (isMacro) Defn.Macro(mods, name, paramClauses, restype, rhs)
       else Defn.Def(mods, name, paramClauses, restype, rhs)
     }


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/SignificantIndentationSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/SignificantIndentationSuite.scala
index 5c1aadee65..355d24f3a8 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/SignificantIndentationSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/SignificantIndentationSuite.scala
@@ -2913,4 +2913,407 @@ class SignificantIndentationSuite extends BaseDottySuite {
     }
   }
 
+  test("#3261 chained `match` with outdent, no block, in assign") {
+    val code =
+      """|object small:
+         |  val value =
+         |    Nil match
+         |    case Nil => "empty"
+         |    case _   => "nonempty"
+         |  match
+         |    case "empty"    => 0
+         |    case "nonempty" => 1
+         |""".stripMargin
+    val layout =
+      """|object small {
+         |  val value = (Nil match {
+         |    case Nil => "empty"
+         |    case _ => "nonempty"
+         |  }) match {
+         |    case "empty" => 0
+         |    case "nonempty" => 1
+         |  }
+         |}
+         |""".stripMargin
+    runTestAssert[Stat](code, Some(layout)) {
+      Defn.Object(
+        Nil,
+        tname("small"),
+        tpl(
+          Defn.Val(
+            Nil,
+            List(Pat.Var(tname("value"))),
+            None,
+            Term.Match(
+              Term.Match(
+                tname("Nil"),
+                List(
+                  Case(tname("Nil"), None, str("empty")),
+                  Case(Pat.Wildcard(), None, str("nonempty"))
+                ),
+                Nil
+              ),
+              List(
+                Case(str("empty"), None, int(0)),
+                Case(str("nonempty"), None, int(1))
+              ),
+              Nil
+            )
+          ) :: Nil
+        )
+      )
+    }
+  }
+
+  test("#3261 chained `match` with outdent, block, in assign") {
+    val code =
+      """|object small:
+         |  val value =
+         |    foo()
+         |    Nil match
+         |    case Nil => "empty"
+         |    case _   => "nonempty"
+         |  match
+         |    case "empty"    => 0
+         |    case "nonempty" => 1
+         |""".stripMargin
+    val layout =
+      """|object small {
+         |  val value = {
+         |    foo()
+         |    Nil match {
+         |      case Nil => "empty"
+         |      case _ => "nonempty"
+         |    }
+         |  } match {
+         |    case "empty" => 0
+         |    case "nonempty" => 1
+         |  }
+         |}
+         |""".stripMargin
+    runTestAssert[Stat](code, Some(layout)) {
+      Defn.Object(
+        Nil,
+        tname("small"),
+        tpl(
+          Defn.Val(
+            Nil,
+            List(Pat.Var(tname("value"))),
+            None,
+            Term.Match(
+              Term.Block(
+                List(
+                  Term.Apply(tname("foo"), Term.ArgClause(Nil, None)),
+                  Term.Match(
+                    tname("Nil"),
+                    List(
+                      Case(tname("Nil"), None, str("empty")),
+                      Case(Pat.Wildcard(), None, str("nonempty"))
+                    ),
+                    Nil
+                  )
+                )
+              ),
+              List(
+                Case(str("empty"), None, int(0)),
+                Case(str("nonempty"), None, int(1))
+              ),
+              Nil
+            )
+          ) :: Nil
+        )
+      )
+    }
+  }
+
+  test("#3261 chained `match` with outdent, no block, in if cond") {
+    val code =
+      """|object small:
+         |  if
+         |    Nil match
+         |    case Nil => "empty"
+         |    case _   => "nonempty"
+         |  match
+         |    case "empty"    => true
+         |    case "nonempty" => false
+         |  then
+         |    bar
+         |""".stripMargin
+    val layout =
+      """|object small {
+         |  if ((Nil match {
+         |    case Nil => "empty"
+         |    case _ => "nonempty"
+         |  }) match {
+         |    case "empty" => true
+         |    case "nonempty" => false
+         |  }) bar
+         |}
+         |""".stripMargin
+    runTestAssert[Stat](code, Some(layout)) {
+      Defn.Object(
+        Nil,
+        tname("small"),
+        tpl(
+          Term.If(
+            Term.Match(
+              Term.Match(
+                tname("Nil"),
+                List(
+                  Case(tname("Nil"), None, str("empty")),
+                  Case(Pat.Wildcard(), None, str("nonempty"))
+                ),
+                Nil
+              ),
+              List(
+                Case(str("empty"), None, bool(true)),
+                Case(str("nonempty"), None, bool(false))
+              ),
+              Nil
+            ),
+            tname("bar"),
+            Lit.Unit(),
+            Nil
+          ) :: Nil
+        )
+      )
+    }
+  }
+
+  test("#3261 chained `match` with outdent, block, in if cond") {
+    val code =
+      """|object small:
+         |  if
+         |    foo()
+         |    Nil match
+         |    case Nil => "empty"
+         |    case _   => "nonempty"
+         |  match
+         |    case "empty"    => true
+         |    case "nonempty" => false
+         |  then
+         |    bar
+         |""".stripMargin
+    val layout =
+      """|object small {
+         |  if ({
+         |    foo()
+         |    Nil match {
+         |      case Nil => "empty"
+         |      case _ => "nonempty"
+         |    }
+         |  } match {
+         |    case "empty" => true
+         |    case "nonempty" => false
+         |  }) bar
+         |}
+         |""".stripMargin
+    runTestAssert[Stat](code, Some(layout)) {
+      Defn.Object(
+        Nil,
+        tname("small"),
+        tpl(
+          Term.If(
+            Term.Match(
+              Term.Block(
+                List(
+                  Term.Apply(tname("foo"), Nil),
+                  Term.Match(
+                    tname("Nil"),
+                    List(
+                      Case(tname("Nil"), None, str("empty")),
+                      Case(Pat.Wildcard(), None, str("nonempty"))
+                    ),
+                    Nil
+                  )
+                )
+              ),
+              List(
+                Case(str("empty"), None, bool(true)),
+                Case(str("nonempty"), None, bool(false))
+              ),
+              Nil
+            ),
+            tname("bar"),
+            Lit.Unit(),
+            Nil
+          ) :: Nil
+        )
+      )
+    }
+  }
+
+  test("#3261 chained `match` with outdent, no block, in try") {
+    val code =
+      """|object small:
+         |  try
+         |    Nil match
+         |    case Nil => "empty"
+         |    case _   => "nonempty"
+         |  match
+         |    case "empty"    => 0
+         |    case "nonempty" => 1
+         |  catch
+         |    case e => e.getMessage()
+         |""".stripMargin
+    val layout =
+      """|object small {
+         |  try (Nil match {
+         |    case Nil => "empty"
+         |    case _ => "nonempty"
+         |  }) match {
+         |    case "empty" => 0
+         |    case "nonempty" => 1
+         |  } catch {
+         |    case e =>
+         |      e.getMessage()
+         |  }
+         |}
+         |""".stripMargin
+    runTestAssert[Stat](code, Some(layout)) {
+      Defn.Object(
+        Nil,
+        tname("small"),
+        tpl(
+          Term.Try(
+            Term.Match(
+              Term.Match(
+                tname("Nil"),
+                List(
+                  Case(tname("Nil"), None, str("empty")),
+                  Case(Pat.Wildcard(), None, str("nonempty"))
+                ),
+                Nil
+              ),
+              List(
+                Case(str("empty"), None, int(0)),
+                Case(str("nonempty"), None, int(1))
+              ),
+              Nil
+            ),
+            Case(
+              Pat.Var(tname("e")),
+              None,
+              Term.Apply(Term.Select(tname("e"), tname("getMessage")), Nil)
+            ) :: Nil,
+            None
+          ) :: Nil
+        )
+      )
+    }
+  }
+
+  test("#3261 chained `match` with outdent, block, in try") {
+    val code =
+      """|object small:
+         |  try
+         |    foo()
+         |    Nil match
+         |    case Nil => "empty"
+         |    case _   => "nonempty"
+         |  match
+         |    case "empty"    => 0
+         |    case "nonempty" => 1
+         |  catch
+         |    case e => e.getMessage()
+         |""".stripMargin
+    val layout =
+      """|object small {
+         |  try {
+         |    foo()
+         |    Nil match {
+         |      case Nil => "empty"
+         |      case _ => "nonempty"
+         |    }
+         |  } match {
+         |    case "empty" => 0
+         |    case "nonempty" => 1
+         |  } catch {
+         |    case e =>
+         |      e.getMessage()
+         |  }
+         |}
+         |""".stripMargin
+    runTestAssert[Stat](code, Some(layout)) {
+      Defn.Object(
+        Nil,
+        tname("small"),
+        tpl(
+          Term.Try(
+            Term.Match(
+              Term.Block(
+                List(
+                  Term.Apply(tname("foo"), Term.ArgClause(Nil, None)),
+                  Term.Match(
+                    tname("Nil"),
+                    List(
+                      Case(tname("Nil"), None, str("empty")),
+                      Case(Pat.Wildcard(), None, str("nonempty"))
+                    ),
+                    Nil
+                  )
+                )
+              ),
+              List(
+                Case(str("empty"), None, int(0)),
+                Case(str("nonempty"), None, int(1))
+              ),
+              Nil
+            ),
+            Case(
+              Pat.Var(tname("e")),
+              None,
+              Term.Apply(Term.Select(tname("e"), tname("getMessage")), Nil)
+            ) :: Nil,
+            None
+          ) :: Nil
+        )
+      )
+    }
+  }
+
+  test("#3261 partial func, in try") {
+    val code =
+      """|object small:
+         |  try
+         |    case Nil => "empty"
+         |    case _   => "nonempty"
+         |  catch
+         |    case e => e.getMessage()
+         |""".stripMargin
+    val layout =
+      """|object small {
+         |  try {
+         |    case Nil => "empty"
+         |    case _ => "nonempty"
+         |  } catch {
+         |    case e =>
+         |      e.getMessage()
+         |  }
+         |}
+         |""".stripMargin
+    runTestAssert[Stat](code, Some(layout)) {
+      Defn.Object(
+        Nil,
+        tname("small"),
+        tpl(
+          Term.Try(
+            Term.PartialFunction(
+              List(
+                Case(tname("Nil"), None, str("empty")),
+                Case(Pat.Wildcard(), None, str("nonempty"))
+              )
+            ),
+            Case(
+              Pat.Var(tname("e")),
+              None,
+              Term.Apply(Term.Select(tname("e"), tname("getMessage")), Nil)
+            ) :: Nil,
+            None
+          ) :: Nil
+        )
+      )
+    }
+  }
+
 }