Repository: scalameta/scalameta
Pull request #3048 (2023-01-18 22:11:18)

ScalametaParser: support coloneol in refined types
For #3045.


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 b35910512..0d60b417c 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
@@ -4166,9 +4166,9 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
   private def refinement(innerType: Option[Type]): Option[Type] =
     if (!dialect.allowSignificantIndentation)
       refinementInBraces(innerType, -1)
-    else if (!token.is[KwWith])
+    else if (!token.isAny[Colon, KwWith])
       refinementInBraces(innerType, previousIndentation + 1)
-    else if (tryAhead[Indentation.Indent])
+    else if (tryAhead[Indentation.Indent] || tryAhead(in.observeIndented()))
       Some(refineWith(innerType, indented(refineStatSeq())))
     else innerType
 
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 d156d1963..b97c63c9f 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
@@ -409,11 +409,9 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
         case xs => currRef(xs)
       }
 
-    def mkOutdentsTo(
-        region: SepRegionIndented,
-        maxPointPos: Int,
-        regions: List[SepRegion]
-    )(cond: SepRegion => Boolean): TokenRef = {
+    def mkOutdentsTo(region: SepRegion, maxPointPos: Int, regions: List[SepRegion])(
+        cond: SepRegion => Boolean
+    ): TokenRef = {
       @tailrec
       def iter(ref: TokenRef, xs: List[SepRegion]): Unit = xs match {
         case (head: SepRegionIndented) :: tail =>
@@ -551,6 +549,10 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
       def getIfCanProduceLF =
         if (canProduceLF) Some(lastWhitespaceToken) else None
 
+      def isLeadingInfix() =
+        !newlines && dialect.allowInfixOperatorAfterNL &&
+          isLeadingInfixOperator(nextPos)
+
       val resOpt =
         if (next == null || !hasLF) None
         else if (!dialect.allowSignificantIndentation) getIfCanProduceLF
@@ -571,23 +573,25 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
            *     ```
            */
           def getOutdentIfNeeded() = sepRegions match {
-            case (r: SepRegionIndented) :: tail if {
+            case (r: SepRegionIndented) :: xs if {
                   if (nextIndent < r.indent)
                     r.closeOnNonCase ||
-                    !(!newlines && dialect.allowInfixOperatorAfterNL &&
-                      isLeadingInfixOperator(nextPos) && // exclude leading infix op
-                      tail.find(_.isIndented).forall(_.indent <= nextIndent)) &&
+                    !(isLeadingInfix() && // exclude leading infix op
+                      xs.find(_.isIndented).forall(_.indent <= nextIndent)) &&
                     // need to check prev.prev in case of `end match`
                     (prev.isNot[CanContinueOnNextLine] || getPrevToken(prevPos).is[soft.KwEnd])
                   else outdentOnCase(r)
                 } =>
-              Some(mkOutdentsTo(r, nextPos, tail) { x =>
-                !x.isIndented || nextIndent < x.indent || outdentOnCase(x)
-              })
+              emitOutdents(r, xs) {
+                case x: SepRegionIndented => nextIndent < x.indent || outdentOnCase(x)
+                case _ => true
+              }
             case _ => None
           }
           def outdentOnCase(r: SepRegion): Boolean =
             r.closeOnNonCase && next.isNot[KwCase] && nextIndent == r.indent
+          def emitOutdents(r: SepRegion, regions: List[SepRegion])(f: SepRegion => Boolean) =
+            Some(mkOutdentsTo(r, nextPos, regions)(f))
 
           /**
            * Indent is needed in the following cases:
@@ -604,27 +608,30 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
            * level.
            */
           def getIndentIfNeeded = {
-            val ok = nextIndent >= 0 && {
-              val (currIndent, indentOnArrow) =
-                sepRegions.headOption.fold((0, true))(r => (r.indent, r.indentOnArrow))
-
-              // !next.is[RightBrace] - braces can sometimes have -1 and we can start indent on }
-              if (nextIndent > currIndent && prev.is[RightArrow]) {
-                indentOnArrow && next.isNot[RightBrace] && !isEndMarkerIntro(nextPos)
-              } else if (nextIndent > currIndent) {
-                // if does not work with indentation in pattern matches
-                val shouldNotIndentIf =
-                  prev.is[KwIf] && sepRegions.headOption.contains(RegionArrow)
-                !shouldNotIndentIf && !next.is[RightBrace] && canStartIndent(prevPos)
-              } else
+            def exceedsIndent =
+              nextIndent > sepRegions.find(_.indent > 0).fold(0)(_.indent)
+            def emitIndent(regions: List[SepRegion], closeOnNonCase: Boolean = false) =
+              Some(mkIndent(indentPos, RegionIndent(nextIndent, closeOnNonCase) :: regions))
+            // !next.is[RightBrace] - braces can sometimes have -1 and we can start indent on }
+            prev match {
+              case _ if nextIndent < 0 || next.is[RightBrace] => None
+              case _ if prevToken.is[Indentation.Indent] => None
+              case _ if prevToken.is[Indentation.Outdent] => None
+              // if does not work with indentation in pattern matches
+              case _: KwIf if sepRegions.headOption.contains(RegionArrow) => None
+              case _: KwCatch | _: KwMatch =>
                 // always add indent for indented `match` block
                 // check the previous token to avoid infinity loop
-                prev.isAny[KwMatch, KwCatch] && !getPrevToken(prevPos).is[soft.KwEnd] &&
-                next.is[KwCase] && prevToken.isNot[Indentation.Indent]
+                val ok = next.is[KwCase] && !getPrevToken(prevPos).is[soft.KwEnd]
+                if (ok) emitIndent(sepRegions, true) else None
+              case _: RightArrow =>
+                val ok = sepRegions.headOption.forall(_.indentOnArrow) &&
+                  !isEndMarkerIntro(nextPos) && exceedsIndent
+                if (ok) emitIndent(sepRegions) else None
+              case _ =>
+                val ok = canStartIndent(prevPos) && exceedsIndent
+                if (ok) emitIndent(sepRegions) else None
             }
-            if (ok)
-              Some(mkIndent(indentPos, RegionIndent(nextIndent, prev.is[KwMatch]) :: sepRegions))
-            else None
           }
 
           getOutdentIfNeeded()


Test patch:
diff --git a/tests/shared/src/test/scala-2.13/scala/meta/tests/parsers/dotty/Scala3PositionSuite.scala b/tests/shared/src/test/scala-2.13/scala/meta/tests/parsers/dotty/Scala3PositionSuite.scala
index 5bd1a9a40..4a0385e5a 100644
--- a/tests/shared/src/test/scala-2.13/scala/meta/tests/parsers/dotty/Scala3PositionSuite.scala
+++ b/tests/shared/src/test/scala-2.13/scala/meta/tests/parsers/dotty/Scala3PositionSuite.scala
@@ -475,7 +475,7 @@ class Scala3PositionSuite extends BasePositionSuite(dialects.Scala3) {
        |Case case aa =>
        |Term.Block   @@case bb =>
        |Case case bb =>
-       |Term.Block   @@finally
+       |Term.Block   case bb =>@@
        |Term.Block cc
        |  dd
        |""".stripMargin
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/BaseDottySuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/BaseDottySuite.scala
index 8e483f475..7c05b98d2 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/BaseDottySuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/BaseDottySuite.scala
@@ -37,7 +37,10 @@ trait BaseDottySuite extends ParseSuite {
 
   final def pname(name: String): Type.Name = Type.Name(name)
   final def pparam(s: String): Type.Param =
-    Type.Param(Nil, Type.Name(s), Nil, Type.Bounds(None, None), Nil, Nil)
+    Type.Param(Nil, Type.Name(s), Nil, noBounds, Nil, Nil)
+
+  final val noBounds = Type.Bounds(None, None)
+  final def lowBound(bound: Type) = Type.Bounds(Some(bound), None)
 
   final def bool(v: Boolean) = Lit.Boolean(v)
   final def int(v: Int) = Lit.Int(v)
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 97aa3fd72..306dd96e7 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
@@ -2449,8 +2449,8 @@ class ControlSyntaxSuite extends BaseDottySuite {
            |      println(ex)
            |    case ex: Throwable =>
            |      throw ex
-           |      end try
            |  }
+           |  end try
            |}
            |""".stripMargin
       )
@@ -2499,11 +2499,12 @@ class ControlSyntaxSuite extends BaseDottySuite {
                 Case(
                   Pat.Typed(Pat.Var(tname("ex")), pname("Throwable")),
                   None,
-                  Term.Block(List(Term.Throw(tname("ex")), Term.EndMarker(tname("try"))))
+                  Term.Throw(tname("ex"))
                 )
               ),
               None
-            )
+            ),
+            Term.EndMarker(tname("try"))
           ),
           Nil
         )
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala
index 72cad5b3f..37a1c6fdd 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala
@@ -98,14 +98,47 @@ class TypeSuite extends BaseDottySuite {
   }
 
   test("coloneol-type3") {
-    runTestError[Stat](
+    runTestAssert[Stat](
       """|type A = Product:
          |  type T>: Null:
          |    type D <: Product
          |""".stripMargin,
-      """|error: ; expected but : found
-         |type A = Product:
-         |                ^""".stripMargin
+      assertLayout = Some(
+        "type A = Product { type T >: Null { type D <: Product } }"
+      )
+    )(
+      Defn.Type(
+        Nil,
+        Type.Name("A"),
+        Nil,
+        Type.Refine(
+          Some(Type.Name("Product")),
+          List(
+            Decl.Type(
+              Nil,
+              Type.Name("T"),
+              Nil,
+              Type.Bounds(
+                Some(
+                  Type.Refine(
+                    Some(Type.Name("Null")),
+                    List(
+                      Decl.Type(
+                        Nil,
+                        Type.Name("D"),
+                        Nil,
+                        Type.Bounds(None, Some(Type.Name("Product")))
+                      )
+                    )
+                  )
+                ),
+                None
+              )
+            )
+          )
+        ),
+        Type.Bounds(None, None)
+      )
     )
   }
 
@@ -142,6 +175,39 @@ class TypeSuite extends BaseDottySuite {
          |      }
          |""".stripMargin,
       assertLayout = Some("type AA = String with Int { type T >: Null { type T >: Int } }")
+    )(
+      Defn.Type(
+        Nil,
+        pname("AA"),
+        Nil,
+        Type.Refine(
+          Some(Type.With(pname("String"), pname("Int"))),
+          Decl.Type(
+            Nil,
+            pname("T"),
+            Nil,
+            lowBound(
+              Type.Refine(
+                Some(pname("Null")),
+                Decl.Type(Nil, pname("T"), Nil, lowBound(pname("Int"))) :: Nil
+              )
+            )
+          ) :: Nil
+        ),
+        noBounds
+      )
+    )
+  }
+
+  test("coloneol-followed-by-brace-indent") {
+    runTestAssert[Stat](
+      """|type AA = String with Int:
+         |    type T>: Null:
+         |        type T>: Int
+         |""".stripMargin,
+      assertLayout = Some(
+        "type AA = String with Int { type T >: Null { type T >: Int } }"
+      )
     )(
       Defn.Type(
         Nil,
@@ -173,18 +239,6 @@ class TypeSuite extends BaseDottySuite {
     )
   }
 
-  test("coloneol-followed-by-brace-indent") {
-    runTestError[Stat](
-      """|type AA = String with Int:
-         |    type T>: Null:
-         |        type T>: Int
-         |""".stripMargin,
-      """|error: ; expected but : found
-         |type AA = String with Int:
-         |                         ^""".stripMargin
-    )
-  }
-
   test("with-followed-by-brace") {
     runTestAssert[Stat](
       """|{