Repository: scalameta/scalameta (scala)

Issue:
New line needed but no way to represent it
When I create a Val that is assigned the yield of a for-yield and then in the same block we also have a Select in a Block for example it will be wrapped in { } which will be perceived by the compiler as an extension of the yield. This means the AST is not bidirectional and when it is parsed back it will just generate one large Val: 
A basic example below, when put back into the AST explorer will only show one Val

`  val tree: Term.Block = Term.Block(
    List(
      Defn.Val(
        Nil,
        List(Pat.Var(Term.Name("a"))),
        None,
        Term.ForYield(
          List(
            Enumerator
              .Generator(Pat.Var(Term.Name("i")), Term.ApplyInfix(Lit.Int(1), Term.Name("to"), Nil, List(Lit.Int(10))))
          ),
          Term.Apply(Term.Name("i"), List(Lit.Int(2)))
        )
      ),
      Term.Apply(Term.Select(Term.Block(List(Term.Name("a"))), Term.Name("this")), Nil)
    )
  )`


If we could add a semicolon after yield result or a new line after val it all works fine, but I don't see a way of representing this in the AST. Can you please advise if it is possible and if not can it be implemented?

Pull request:
In the logic deciding whether to insert a newline between two stats, checking if the next stat is a block is insufficient, as it could be an expression (select or infix chain) which *starts* with a block.

Instead, let's turn it into a Show.Result and check if it starts with a brace.

Fixes #4202.

Patch:
diff --git a/scalameta/common/shared/src/main/scala/scala/meta/prettyprinters/Show.scala b/scalameta/common/shared/src/main/scala/scala/meta/prettyprinters/Show.scala
index 8094909ad6..f994166fd8 100644
--- a/scalameta/common/shared/src/main/scala/scala/meta/prettyprinters/Show.scala
+++ b/scalameta/common/shared/src/main/scala/scala/meta/prettyprinters/Show.scala
@@ -50,34 +50,44 @@ private[meta] object Show {
       sb.toString
     }
     final def isEmpty: Boolean = this eq None
+    def headChar: Option[Char]
   }
 
   final case object None extends Result {
     override def desc: String = "None"
+    def headChar: Option[Char] = Option.empty
   }
   final case class Str(value: String) extends Result {
     override def desc: String = s"Str($value)"
+    def headChar: Option[Char] = value.headOption
   }
   final case class Sequence(xs: Result*) extends Result {
     override def desc: String = s"Sequence(#${xs.length})"
+    def headChar: Option[Char] = xs.view.flatMap(_.headChar).headOption
   }
   final case class Repeat(xs: Seq[Result], sep: String) extends Result {
     override def desc: String = s"Repeat(#${xs.length}, s=$sep)"
+    def headChar: Option[Char] = xs.view.flatMap(_.headChar).headOption
   }
   final case class Indent(res: Result) extends Result {
     override def desc: String = s"Indent(r=${res.desc})"
+    def headChar: Option[Char] = res.headChar
   }
   final case class Newline(res: Result) extends Result {
     override def desc: String = s"Newline(r=${res.desc})"
+    def headChar: Option[Char] = Option.empty
   }
   final case class Meta(data: Any, res: Result) extends Result {
     override def desc: String = s"Meta(d=$data, r=${res.desc})"
+    def headChar: Option[Char] = res.headChar
   }
   final case class Wrap(prefix: String, res: Result, suffix: String) extends Result {
     override def desc: String = s"Wrap(p=$prefix, r=${res.desc}, s=$suffix)"
+    def headChar: Option[Char] = prefix.headOption.orElse(res.headChar)
   }
   final case class Function(fn: StringBuilder => Result) extends Result {
     override def desc: String = s"Function(...)"
+    def headChar: Option[Char] = Option.empty
   }
 
   def apply[T](f: T => Result): Show[T] = new Show[T] {
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 c73a9c8cd5..033adcc373 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
@@ -1078,15 +1078,14 @@ object TreeSyntax {
     implicit def syntaxCases: Syntax[Seq[CaseTree]] = Syntax(cases => r(cases.map(i(_))))
 
     private def printStats(stats: Seq[Stat]) = r {
-      val builder = List.newBuilder[Show.Result]
       var prevStat: Stat = null
-      stats.foreach { stat =>
-        if (stat.is[Term.Block] && null != prevStat && guessNeedsLineSep(prevStat)) builder +=
-          System.lineSeparator
-        builder += i(stat)
+      stats.map { stat =>
+        val showStat = i(stat)
+        val needNL = null != prevStat && showStat.headChar.contains('{') &&
+          guessNeedsLineSep(prevStat)
         prevStat = stat
+        if (needNL) n(showStat) else showStat
       }
-      builder.result()
     }
 
     private def printCaps(captures: List[Term.Ref], prefix: String = ""): Show.Result =


Test patch:
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 ea2e3af926..197a07a124 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
@@ -16,23 +16,21 @@ class TreeSyntaxSuite extends scala.meta.tests.parsers.ParseSuite {
       loc: munit.Location
   ): Unit = {
     val stat = statStr.trim // make sure no trailing newlines
-    test(s"${loc.line}: $stat") {
-      val sep = if (needNL) "\n" else ""
+    def testWithSuffix(suffix: String): Unit = test(s"${loc.line}: $stat [$suffix]") {
       val statSyntax = Option(syntaxStr).getOrElse(stat).replace("\n", "\n  ")
 
+      val indentedSuffix = suffix.replace("\n", "\n  ")
       val expectedSyntax =
         s"""|{
-            |  $statSyntax$sep
-            |  {
-            |    a
-            |  }
+            |  $statSyntax${if (needNL) "\n" else ""}
+            |  $indentedSuffix
             |}""".stripMargin.lf2nl
 
-      val treeWithSemi = templStat(s"{$stat;{a}}")
+      val treeWithSemi = templStat(s"{$stat;$suffix}")
       val treeWithSemiStructure = treeWithSemi.structure
       assertNoDiff(TreeSyntax.reprint(treeWithSemi).toString, expectedSyntax)
 
-      def getTreeWithNL() = templStat(s"{$stat\n{a}}")
+      def getTreeWithNL() = templStat(s"{$stat\n$suffix}")
       if (needNL) scala.util.Try(getTreeWithNL())
         .foreach(treeWithNL => assertNotEquals(treeWithNL.structure, treeWithSemiStructure))
       else {
@@ -41,6 +39,23 @@ class TreeSyntaxSuite extends scala.meta.tests.parsers.ParseSuite {
         assertNoDiff(treeWithNL.structure, treeWithSemiStructure)
       }
     }
+    testWithSuffix(
+      """|{
+         |  a
+         |}""".stripMargin
+    )
+    testWithSuffix(
+      """|{
+         |  a
+         |} + {
+         |  b
+         |}""".stripMargin
+    )
+    testWithSuffix(
+      """|{
+         |  a
+         |}.b""".stripMargin
+    )
   }
 
   private def testBlockAddNL(t: String, expected: String = null)(implicit loc: munit.Location) =