Repository: scalameta/scalameta (scala)

Issue:
Parser fails when multiline and one condition wrapped in parenthesis
I was trying to fix it, but it touches whitespace issues which I am not super confident about:
```

  test("issue-metals") {
    runTestAssert(
      """|
         |for (m <- decls
         |    if oneCond
         |      && (cond)
         |      && satisfiable) {}
         |
         |""".stripMargin,
      Some("for (m <- decls; if oneCond && cond && satisfiable) {}")
    )(Term.For(
      List(
        Enumerator.Generator(Pat.Var(Term.Name("m")), Term.Name("decls")),
        Enumerator.Guard(Term.ApplyInfix(
          Term.ApplyInfix(
            Term.Name("oneCond"),
            Term.Name("&&"),
            Type.ArgClause(Nil),
            Term.ArgClause(List(Term.Name("cond")), None)
          ),
          Term.Name("&&"),
          Type.ArgClause(Nil),
          Term.ArgClause(List(Term.Name("satisfiable")), None)
        ))
      ),
      Term.Block(Nil)
    ))
  }
```

Pull request description:
Fixes #3713.

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 4a9b8e8dcf..c98cbaad93 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
@@ -591,9 +591,11 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
               case _: LeftArrow => RegionForOther :: rs
               case _ => rs
             }
-          case RegionForBraces if prev.is[RightBrace] => rs
+          case r: RegionFor if r.isClosingConditionToken(prev) => rs
           case _ => if (prevToken.is[AtEOL] || curr.is[CloseDelim]) rs else regions
         }
+      case RegionParen :: RegionForMaybeParens :: rs if curr.is[LeftArrow] =>
+        RegionParen :: RegionForParens :: rs
       case _ => regions
     }
 
@@ -645,7 +647,7 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
           case RegionTry :: xs
               if !next.isAny[KwCatch, KwFinally] && isLeadingInfix != LeadingInfix.Yes => strip(xs)
           case Nil | (_: CanProduceLF) :: _ => Some(rs)
-          case RegionParen :: RegionForMaybeParens :: _ => Some(rs)
+          case RegionParen :: RegionForParens :: _ => Some(rs)
           case _ => None
         }
         val ok = lastNewlinePos >= 0 && mightStartStat(next, closeDelimOK = true) &&
@@ -655,7 +657,7 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
 
       // https://dotty.epfl.ch/docs/reference/changed-features/operators.html#syntax-change-1
       lazy val isLeadingInfix = sepRegionsOrig match {
-        case Nil | (_: CanProduceLF) :: _
+        case Nil | (_: CanProduceLF) :: _ | RegionParen :: RegionForParens :: _
             if !newlines && lastNewlinePos >= 0 && dialect.allowInfixOperatorAfterNL &&
               next.isSymbolicInfixOperator => isLeadingInfixArg(nextPos + 1, nextIndent)
         case _ => LeadingInfix.No
@@ -803,8 +805,8 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
                 }
               case _: KwTry => emitIndent(sepRegions)
               case _ => dropRegionLine(sepRegions) match {
-                  case RegionForBraces :: rs if prev.is[RightBrace] =>
-                    val ko = RegionForBraces.isTerminatingToken(next)
+                  case (r: RegionFor) :: rs if r.isClosingConditionToken(prev) =>
+                    val ko = r.isTerminatingToken(next)
                     if (ko) None else emitIndent(rs)
                   case RegionForMaybeParens :: rs if prev.is[RightParen] =>
                     val ko = next.is[LeftArrow] || RegionForMaybeParens.isTerminatingToken(next)
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 e9236454f7..15dea95277 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
@@ -170,15 +170,23 @@ object RegionFor {
 sealed trait RegionFor extends RegionControl {
   def isControlKeyword(token: Token): Boolean = token.is[Token.KwFor]
   def isTerminatingToken(token: Token): Boolean = token.isAny[Token.KwDo, Token.KwYield]
+  def isClosingConditionToken(token: Token): Boolean
 }
 case object RegionForMaybeParens extends RegionFor {
   def isTerminatingTokenRequired(): Boolean = false
+  def isClosingConditionToken(token: Token): Boolean = false
+}
+case object RegionForParens extends RegionFor {
+  def isTerminatingTokenRequired(): Boolean = false
+  def isClosingConditionToken(token: Token): Boolean = token.is[Token.RightParen]
 }
 case object RegionForBraces extends RegionFor {
   def isTerminatingTokenRequired(): Boolean = false
+  def isClosingConditionToken(token: Token): Boolean = token.is[Token.RightBrace]
 }
 case object RegionForOther extends RegionFor {
   def isTerminatingTokenRequired(): Boolean = true
+  def isClosingConditionToken(token: Token): Boolean = false
 }
 
 case object RegionTry extends SepRegionNonIndented with CanProduceLF


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 f31063e882..32be8fd275 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
@@ -1148,6 +1148,34 @@ class ControlSyntaxSuite extends BaseDottySuite {
     ))
   }
 
+  test("#3713 code in parens within enums") {
+    val code = """|for (m <- decls
+                  |    if oneCond
+                  |      && (cond)
+                  |      && satisfiable) {}
+                  |
+                  |""".stripMargin
+    val layout = "for (m <- decls; if oneCond && cond && satisfiable) {}"
+    val tree = Term.For(
+      List(
+        Enumerator.Generator(Pat.Var(Term.Name("m")), Term.Name("decls")),
+        Enumerator.Guard(Term.ApplyInfix(
+          Term.ApplyInfix(
+            Term.Name("oneCond"),
+            Term.Name("&&"),
+            Type.ArgClause(Nil),
+            Term.ArgClause(List(Term.Name("cond")), None)
+          ),
+          Term.Name("&&"),
+          Type.ArgClause(Nil),
+          Term.ArgClause(List(Term.Name("satisfiable")), None)
+        ))
+      ),
+      Term.Block(Nil)
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
   // --------------------------
   // WHILE
   // --------------------------