Repository: scalameta/scalameta (scala)

Issue:
Pretty print of block
Hey folks, 

I encounter incorrect behavior of pretty print from scala meta in case of blocks:

```
import scala.meta._

val a = q"val a = 1; a"

val tree = q"val b = 1; $a"

println(tree.syntax)
```

produce: 

```
{
  val b = 1
  {
    val a = 1
    a
  }
}
```
which is incompatible with scala syntax.

https://scastie.scala-lang.org/GaU07vEdSxOMuUArWk8Zyw

Pull request:
Fixes #2046 

I tried to create a complete solution that works in all cases, not just the ones reported in the issue.
I added tests for all `Stat` combinations that I could find in https://scalameta.org/docs/trees/quasiquotes.html. Let me know if there is any relevant one that I missed.

---

### Code style
In `guessNeedsLineSep` function I "expanded" the pattern matching of the stats (e.g. `Defn.Val(_, _, _, rhs) =>`), but maybe you would prefer a more concise solution.
Maybe something like this instead?
```scala
...
case t: Decl.Type => false
case t: Defn.Val => guessNeedsLineSep(t.rhs)
case t: Defn.Var => guessNeedsLineSep(t.body)
case t: Defn.Def => guessNeedsLineSep(t.body)
...
```

---

Note:
I think it would be better to not merge until the bugs I found while working on this (#3136 and #3138) are resolved.

Patch:
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 ef9afe59e5..be1d031135 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
@@ -296,6 +296,30 @@ object TreeSyntax {
       }
       !isOnlyChildOfOnlyChild(t)
     }
+    @annotation.tailrec
+    def guessNeedsLineSep(t: Tree): Boolean = t match {
+      case t: Term.ApplyUnary => guessNeedsLineSep(t.arg)
+      case _: Decl.Val | _: Decl.Var | _: Decl.Def | _: Defn.Type | _: Term.Ref | _: Member.Apply |
+          _: Term.Ascribe | _: Term.Tuple | _: Term.New | _: Term.Interpolate | _: Term.Xml |
+          _: Lit =>
+        true
+      case t: Term.Do => false
+      case t: Tree.WithBody => guessNeedsLineSep(t.body)
+      case t: Stat.WithTemplate => t.templ.self.isEmpty && t.templ.stats.isEmpty
+      case t: Term.ApplyInfix =>
+        val args = t.argClause.values
+        args.lengthCompare(1) != 0 || guessNeedsLineSep(args.head)
+      case t: Term.Return => guessNeedsLineSep(t.expr)
+      case t: Term.Throw => guessNeedsLineSep(t.expr)
+      case t: Term.If => guessNeedsLineSep(if (guessHasElsep(t)) t.elsep else t.thenp)
+      case t: Term.Try =>
+        t.finallyp match {
+          case Some(x) => guessNeedsLineSep(x)
+          case _ => t.catchp.isEmpty && guessNeedsLineSep(t.expr)
+        }
+      case t: Term.TryWithHandler => guessNeedsLineSep(t.finallyp.getOrElse(t.catchp))
+      case _ => false
+    }
 
     // Branches
     implicit def syntaxTree[T <: Tree]: Syntax[T] = Syntax {
@@ -398,10 +422,24 @@ object TreeSyntax {
       case t: Term.Tuple => m(SimpleExpr1, s("(", r(t.args, ", "), ")"))
       case t: Term.Block =>
         import Term.{Block, Function}
+        def withBlankLines(x: Seq[Stat]) = {
+          val builder = List.newBuilder[Show.Result]
+          var prevStat: Stat = null
+          x.foreach { stat =>
+            if (stat.is[Block] && null != prevStat && guessNeedsLineSep(prevStat)) builder += EOL
+            builder += i(stat)
+            prevStat = stat
+          }
+          builder.result()
+        }
         def block(pre: Show.Result, x: Seq[Stat]) = {
           val res =
             if (pre == Show.None && x.isEmpty) s("{}")
-            else s("{", w(" ", pre), r(x.map(i(_))), n("}"))
+            else {
+              val stats =
+                if (dialect.allowSignificantIndentation) x.map(i(_)) else withBlankLines(x)
+              s("{", w(" ", pre), r(stats), n("}"))
+            }
           m(SimpleExpr, res)
         }
         t match {


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/SyntacticSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/SyntacticSuite.scala
index 62f5c2ae1b..902ba72d35 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/SyntacticSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/SyntacticSuite.scala
@@ -1933,4 +1933,36 @@ class SyntacticSuite extends scala.meta.tests.parsers.ParseSuite {
     assertSyntax(tree, "\"\"\"\n\\\"\\\"\\\"\"\"\"")(tree)
   }
 
+  test("#2046 new line separator required before some Term.Block") {
+    assertEquals(
+      blockStat("{{a}; val b = 1; val c = 2; {d}; {e}}").syntax,
+      s"""|{
+          |  {
+          |    a
+          |  }
+          |  val b = 1
+          |  val c = 2
+          |
+          |  {
+          |    d
+          |  }
+          |  {
+          |    e
+          |  }
+          |}
+        """.stripMargin.trim.replace("\n", EOL)
+    )
+    assertEquals(
+      blockStat("{if (a) while (b) for (c <- d) -e; {f}}").syntax,
+      s"""|{
+          |  if (a) while (b) for (c <- d) -e
+          |
+          |  {
+          |    f
+          |  }
+          |}
+        """.stripMargin.trim.replace("\n", EOL)
+    )
+  }
+
 }
diff --git a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/TreeSyntaxSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/TreeSyntaxSuite.scala
index 2a7ab67294..a9c9ffbebb 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/TreeSyntaxSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/TreeSyntaxSuite.scala
@@ -18,7 +18,7 @@ class TreeSyntaxSuite extends scala.meta.tests.parsers.ParseSuite {
   ): Unit = {
     val stat = statStr.trim // make sure no trailing newlines
     test(s"${loc.line}: $stat") {
-      val sep = "" // XXX: after fixing TreeSyntax, modify this
+      val sep = if (needNL) "\n" else ""
       val statSyntax = Option(syntaxStr).getOrElse(stat).replace("\n", "\n  ")
 
       val expectedSyntax =