Repository: scalameta/scalameta (scala)

Issue:
; expected but finally found
## Configuration (required) ##

```
version = 3.7.17
runner.dialect = scala3
```

## Command-line parameters (required) ##

When I run scalafmt via CLI like this: `scalafmt -c .scalafmt.conf Repo.scala`

## Steps

Given code like this:
```scala
def get() =
  val iter = ???
  val arena = ???
  try
    try Right(f())
    catch case NonFatal(ex) => Left(???)
    finally iter.close()
  finally arena.close()
```

## Problem

Scalafmt formats code like this:
```scala
Repo.scala:8: error: [dialect scala3] ; expected but finally found
  finally arena.close()
```

## Expectation

I would like the formatted output to look like this:
```scala
def get() =
  val iter = ???
  val arena = ???
  try
    try Right(f())
    catch case NonFatal(ex) => Left(???)
    finally iter.close()
  finally arena.close()
```

## Workaround

Exclude files like this.

Pull request:
Since we allow `finally` to be at the same indent as the `try` body, we could end up with `finally` terminating both the previous `catch` as well as the outer `try`.

Instead, let's introduce a new `RegionTry` and ensure that this region is not removed before the `finally` keyword, so that we won't end up with an exposed outer `RegionTry`.

Fixes #3532.

Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala
index 2dd1f27bc1..f274d107c4 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala
@@ -515,10 +515,12 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
       case _: KwObject | _: KwClass | _: KwTrait | _: KwPackage | _: KwNew
           if dialect.allowSignificantIndentation =>
         currRef(RegionTemplateMark :: sepRegions)
+      case _: KwTry =>
+        currRef(RegionTry :: dropRegionLine(sepRegions))
       case _: KwMatch if !isPrevEndMarker() =>
         getCaseIntro(sepRegions)
       case _: KwCatch =>
-        getCaseIntro(sepRegions)
+        getCaseIntro(dropWhile(sepRegions)(_ ne RegionTry))
       case _: KwCase if !next.isClassOrObject =>
         def expr() = new RegionCaseExpr(countIndent(currPos))
         currRef(dropRegionLine(sepRegions) match {
@@ -543,9 +545,9 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
           // otherwise, these two regions would have been removed already
           case (_: RegionCaseBody) :: (r: RegionIndent) :: rs =>
             val ref = mkOutdentTo(r, currPos, rs)
-            ref.next = currRef(rs)
+            ref.next = currRef(dropUntil(rs)(_ eq RegionTry))
             ref
-          case _ => currRef(sepRegions)
+          case rs => currRef(dropUntil(rs)(_ eq RegionTry))
         }
       case _: LeftBrace =>
         val lbRegions = sepRegions match {
@@ -720,6 +722,12 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
           case RegionTemplateMark :: xs => if (blankBraceOr(!derives(next))) strip(xs) else None
           case RegionTemplateInherit :: xs =>
             if (blankBraceOr(!derives(next) && !derives(prev))) strip(xs) else None
+          case RegionTry :: xs
+              if !next.isAny[KwCatch, KwFinally] && isLeadingInfix != LeadingInfix.Yes =>
+            strip(xs)
+          case (_: RegionCaseBody) :: xs
+              if !next.isAny[KwCase, KwFinally] && isLeadingInfix != LeadingInfix.Yes =>
+            strip(xs)
           case Nil | (_: CanProduceLF) :: _ => Some(rs)
           case _ => None
         }
@@ -760,6 +768,8 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
         else if (!dialect.allowSignificantIndentation)
           getInfixLFIfNeeded().orElse(getIfCanProduceLF(sepRegionsOrig))
         else {
+          def noOutdent(sepRegions: List[SepRegion]) =
+            sepRegions.find(_.isIndented).forall(_.indent <= nextIndent)
 
           /**
            * Outdent is needed in following cases:
@@ -776,21 +786,24 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
            */
           def getOutdentIfNeeded(sepRegions: List[SepRegion]) =
             mkOutdentsOpt(nextPos, sepRegions) {
-              case (rc: RegionCaseBody) :: (r: RegionIndent) :: rs if (next match {
-                    case _: KwCase => nextIndent < r.indent
-                    case _: KwFinally => nextIndent <= r.indent
-                    case _ => nextIndent < r.indent || nextIndent == r.indent && rc.arrow.ne(prev)
-                  }) =>
-                OutdentInfo(r, rs)
-              case (r: RegionIndentTry) :: rs if (next match {
-                    case _: KwCatch | _: KwFinally => nextIndent <= r.indent
-                    case _ => nextIndent < r.indent
-                  }) =>
-                OutdentInfo(r, rs)
+              case (rc: RegionCaseBody) :: (r: RegionIndent) :: rs =>
+                if (nextIndent > r.indent) null
+                else if (next.is[KwFinally]) OutdentInfo(r, rs, noOutdent(rs))
+                else if (nextIndent < r.indent || rc.arrow.ne(prev) && !next.is[KwCase])
+                  OutdentInfo(r, rs)
+                else null
+              case (r: RegionIndent) :: (rs @ RegionTry :: xs) =>
+                if (nextIndent < r.indent ||
+                  nextIndent == r.indent && next.isAny[KwCatch, KwFinally]) {
+                  val done = noOutdent(xs)
+                  OutdentInfo(r, if (done) rs else xs, done)
+                } else null
+              case RegionTry :: rs =>
+                if (noOutdent(rs)) null else OutdentInfo(null, rs)
               case (_: RegionNonDelimNonIndented) :: rs if (prev match {
                     // [then]  else  do  [catch]  finally  yield  [match]
                     case _: KwThen | _: KwCatch | _: KwMatch => false
-                    case _ => rs.find(_.isIndented).exists(_.indent >= nextIndent)
+                    case _ => !noOutdent(rs)
                   }) =>
                 OutdentInfo(null, rs)
               case (r: SepRegionIndented) :: _ if nextIndent >= r.indent =>
@@ -883,7 +896,7 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
                   case _ => emitIndent(sepRegions)
                 }
               case _: KwTry =>
-                emitIndentWith(new RegionIndentTry(nextIndent), sepRegions)
+                emitIndent(sepRegions)
               case _ =>
                 dropRegionLine(sepRegions) match {
                   case RegionForBraces :: rs if prev.is[RightBrace] =>
@@ -1012,6 +1025,13 @@ object ScannerTokens {
     loop(str.length - 1, 0)
   }
 
+  @tailrec
+  private def dropWhile(regions: List[SepRegion])(f: SepRegion => Boolean): List[SepRegion] =
+    regions match {
+      case head :: tail => if (f(head)) dropWhile(tail)(f) else regions
+      case _ => Nil
+    }
+
   @tailrec
   private def dropUntil(regions: List[SepRegion])(f: SepRegion => Boolean): List[SepRegion] =
     regions match {
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/SepRegion.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/SepRegion.scala
index 40b532890a..e9236454f7 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/SepRegion.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/SepRegion.scala
@@ -33,7 +33,7 @@ case object RegionBracket extends RegionDelimNonIndented
 case class RegionBrace(override val indent: Int) extends RegionDelimNonIndented with CanProduceLF
 
 case object RegionCaseMark extends RegionNonDelimNonIndented
-final class RegionCaseExpr(override val indent: Int) extends RegionNonDelimNonIndented
+final class RegionCaseExpr(override val indent: Int) extends SepRegionNonIndented
 final class RegionCaseBody(override val indent: Int, val arrow: Token)
     extends SepRegionNonIndented with CanProduceLF
 
@@ -181,4 +181,4 @@ case object RegionForOther extends RegionFor {
   def isTerminatingTokenRequired(): Boolean = true
 }
 
-final class RegionIndentTry(override val indent: Int) extends SepRegionIndented
+case object RegionTry extends SepRegionNonIndented with CanProduceLF
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
index 0bc1e38017..5d5d9bb70a 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
@@ -470,12 +470,19 @@ object TreeSyntax {
           )
         )
       case t: Term.Try =>
+        val showExpr = p(Expr, t.expr)
+        val needParensAroundExpr = t.expr match {
+          case e: Term.Try if e.finallyp.isEmpty =>
+            if (e.catchp.isEmpty) t.catchp.nonEmpty || t.finallyp.isDefined
+            else t.catchp.isEmpty && t.finallyp.isDefined
+          case _ => false
+        }
         m(
           Expr1,
           s(
             kw("try"),
             " ",
-            p(Expr, t.expr),
+            if (needParensAroundExpr) s("(", i(showExpr), n(")")) else showExpr,
             if (t.catchp.nonEmpty) s(" ", kw("catch"), " {", t.catchp, n("}"))
             else s(""),
             t.finallyp


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ControlSyntaxSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ControlSyntaxSuite.scala
index af755b02af..ef6a525126 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ControlSyntaxSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ControlSyntaxSuite.scala
@@ -600,6 +600,285 @@ class ControlSyntaxSuite extends BaseDottySuite {
     )
   }
 
+  test("#3532 nested try-finally on same line") {
+    val code =
+      """|try try Right(f()) finally iter.close()
+         |finally arena.close()
+         |""".stripMargin
+    val layout = "try try Right(f()) finally iter.close() finally arena.close()"
+    val tree = Term.Try(
+      Term.Try(
+        Term.Apply(tname("Right"), List(Term.Apply(tname("f"), Nil))),
+        Nil,
+        Some(Term.Apply(Term.Select(tname("iter"), tname("close")), Nil))
+      ),
+      Nil,
+      Some(Term.Apply(Term.Select(tname("arena"), tname("close")), Nil))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3532 nested try-finally without catch") {
+    val code =
+      """|try
+         |  try Right(f())
+         |  finally iter.close()
+         |finally arena.close()
+         |""".stripMargin
+    val layout = "try try Right(f()) finally iter.close() finally arena.close()"
+    val tree = Term.Try(
+      Term.Try(
+        Term.Apply(tname("Right"), List(Term.Apply(tname("f"), Nil))),
+        Nil,
+        Some(Term.Apply(Term.Select(tname("iter"), tname("close")), Nil))
+      ),
+      Nil,
+      Some(Term.Apply(Term.Select(tname("arena"), tname("close")), Nil))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3532 nested try-finally, oneline catch") {
+    val code =
+      """|try
+         |  try Right(f())
+         |  catch case NonFatal(ex) => Left(???)
+         |  finally iter.close()
+         |finally arena.close()
+         |""".stripMargin
+    val layout =
+      """|try try Right(f()) catch {
+         |  case NonFatal(ex) =>
+         |    Left(???)
+         |} finally iter.close() finally arena.close()
+         |""".stripMargin
+    val tree = Term.Try(
+      Term.Try(
+        Term.Apply(tname("Right"), List(Term.Apply(tname("f"), Nil))),
+        Case(
+          Pat.Extract(tname("NonFatal"), List(Pat.Var(tname("ex")))),
+          None,
+          Term.Apply(tname("Left"), List(tname("???")))
+        ) :: Nil,
+        Some(Term.Apply(Term.Select(tname("iter"), tname("close")), Nil))
+      ),
+      Nil,
+      Some(Term.Apply(Term.Select(tname("arena"), tname("close")), Nil))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3532 nested try-finally, catch with non-indented case") {
+    val code =
+      """|try
+         |  try Right(f())
+         |  catch
+         |  case NonFatal(ex) => Left(???)
+         |  finally iter.close()
+         |finally arena.close()
+         |""".stripMargin
+    val layout =
+      """|try try Right(f()) catch {
+         |  case NonFatal(ex) =>
+         |    Left(???)
+         |} finally iter.close() finally arena.close()
+         |""".stripMargin
+    val tree = Term.Try(
+      Term.Try(
+        Term.Apply(tname("Right"), List(Term.Apply(tname("f"), Nil))),
+        Case(
+          Pat.Extract(tname("NonFatal"), List(Pat.Var(tname("ex")))),
+          None,
+          Term.Apply(tname("Left"), List(tname("???")))
+        ) :: Nil,
+        Some(Term.Apply(Term.Select(tname("iter"), tname("close")), Nil))
+      ),
+      Nil,
+      Some(Term.Apply(Term.Select(tname("arena"), tname("close")), Nil))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3532 nested try-finally, catch with indented case") {
+    val code =
+      """|try
+         |  try Right(f())
+         |  catch
+         |    case NonFatal(ex) => Left(???)
+         |  finally iter.close()
+         |finally arena.close()
+         |""".stripMargin
+    val layout =
+      """|try try Right(f()) catch {
+         |  case NonFatal(ex) =>
+         |    Left(???)
+         |} finally iter.close() finally arena.close()
+         |""".stripMargin
+    val tree = Term.Try(
+      Term.Try(
+        Term.Apply(tname("Right"), List(Term.Apply(tname("f"), Nil))),
+        Case(
+          Pat.Extract(tname("NonFatal"), List(Pat.Var(tname("ex")))),
+          None,
+          Term.Apply(tname("Left"), List(tname("???")))
+        ) :: Nil,
+        Some(Term.Apply(Term.Select(tname("iter"), tname("close")), Nil))
+      ),
+      Nil,
+      Some(Term.Apply(Term.Select(tname("arena"), tname("close")), Nil))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3532 nested try, inner with catch, outer with finally") {
+    val code =
+      """|try
+         |  try Right(f())
+         |  catch
+         |    case NonFatal(ex) => Left(???)
+         |finally arena.close()
+         |""".stripMargin
+    val layout =
+      """|try (
+         |  try Right(f()) catch {
+         |    case NonFatal(ex) =>
+         |      Left(???)
+         |  }
+         |) finally arena.close()
+         |""".stripMargin
+    val tree = Term.Try(
+      Term.Try(
+        Term.Apply(tname("Right"), List(Term.Apply(tname("f"), Nil))),
+        Case(
+          Pat.Extract(tname("NonFatal"), List(Pat.Var(tname("ex")))),
+          None,
+          Term.Apply(tname("Left"), List(tname("???")))
+        ) :: Nil,
+        None
+      ),
+      Nil,
+      Some(Term.Apply(Term.Select(tname("arena"), tname("close")), Nil))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3532 nested try-finally, try with multiline non-indented expr 1") {
+    val code =
+      """|try
+         |  try { 1 + 2 }
+         |    + 3
+         |  finally iter.close()
+         |finally arena.close()
+         |""".stripMargin
+    val layout =
+      """|try try {
+         |  1 + 2
+         |} + 3 finally iter.close() finally arena.close()
+         |""".stripMargin
+    val tree = Term.Try(
+      Term.Try(
+        Term.ApplyInfix(
+          Term.Block(List(Term.ApplyInfix(int(1), tname("+"), Nil, List(int(2))))),
+          tname("+"),
+          Nil,
+          List(int(3))
+        ),
+        Nil,
+        Some(Term.Apply(Term.Select(tname("iter"), tname("close")), Nil))
+      ),
+      Nil,
+      Some(Term.Apply(Term.Select(tname("arena"), tname("close")), Nil))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3532 nested try-finally, try with multiline non-indented expr 2") {
+    val code =
+      """|try
+         |  try { 1 + 2 }
+         |    .foo
+         |  finally iter.close()
+         |finally arena.close()
+         |""".stripMargin
+    val layout =
+      """|try try {
+         |  1 + 2
+         |}.foo finally iter.close() finally arena.close()
+         |""".stripMargin
+    val tree = Term.Try(
+      Term.Try(
+        Term.Select(
+          Term.Block(Term.ApplyInfix(int(1), tname("+"), Nil, List(int(2))) :: Nil),
+          tname("foo")
+        ),
+        Nil,
+        Some(Term.Apply(Term.Select(tname("iter"), tname("close")), Nil))
+      ),
+      Nil,
+      Some(Term.Apply(Term.Select(tname("arena"), tname("close")), Nil))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3532 nested try-finally, try with multiline non-indented expr 3") {
+    val code =
+      """|try
+         |  try { 1 + 2 }
+         |    (foo)
+         |  finally iter.close()
+         |finally arena.close()
+         |""".stripMargin
+    val error =
+      """|<input>:5: error: ; expected but finally found
+         |finally arena.close()
+         |^""".stripMargin
+    runTestError[Stat](code, error)
+  }
+
+  test("#3532 nested bare try, outer without catch") {
+    val code =
+      """|try
+         |  try 1
+         |    + 2
+         |finally foo
+         |""".stripMargin
+    val layout =
+      """|try (
+         |  try 1 + 2
+         |) finally foo
+         |""".stripMargin
+    val tree = Term.Try(
+      Term.Try(Term.ApplyInfix(int(1), tname("+"), Nil, List(int(2))), Nil, None),
+      Nil,
+      Some(tname("foo"))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3532 nested bare try, outer with catch") {
+    val code =
+      """|try
+         |  try 1
+         |    + 2
+         |catch
+         |  case NonFatal(ex) => 3
+         |finally foo
+         |""".stripMargin
+    val layout =
+      """|try (
+         |  try 1 + 2
+         |) catch {
+         |  case NonFatal(ex) => 3
+         |} finally foo
+         |""".stripMargin
+    val tree = Term.Try(
+      Term.Try(Term.ApplyInfix(int(1), tname("+"), Nil, List(int(2))), Nil, None),
+      List(Case(Pat.Extract(tname("NonFatal"), List(Pat.Var(tname("ex")))), None, int(3))),
+      Some(tname("foo"))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
   // --------------------------
   // FOR
   // --------------------------