Repository: scalameta/scalameta (scala)

Issue:
Type alias definition using 'with' keyword show error message, but can be run successfully.
### Describe the bug

```
type A = AnyRef with
  type T>: Null
```
The above is accepted by Scala 3.2.0/3.1.3 REPL:
```
scala> type A = AnyRef with
     |   type T>: Null
     | 
// defined alias type A = AnyRef{T >: Null}
```

But in VS Code, there is a red indicator under the 'with' keyword, and the problem panel display "identifier expected but indent found scalameta". Even so, I can successfully launch the code in VS Code that contain the above definition.


### Expected behavior

It should not display the error message.

### Operating system

Linux

### Editor/Extension

VS Code

### Version of Metals

v1.19.0

### Extra context or search terms

_No response_

Pull request:
Fixes https://github.com/scalameta/scalameta/issues/2853

Previously, we would not accept `type T = AnyRef with<Indentation>`. Now, we accept the statements in after indentation as refinements.

Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/LazyTokenIterator.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/LazyTokenIterator.scala
index 3f410211f6..cb2911c7ab 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/LazyTokenIterator.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/LazyTokenIterator.scala
@@ -133,6 +133,10 @@ private[parsers] class LazyTokenIterator private (
       foundIndentation
   }
 
+  def previousIndentation: Int = {
+    sepRegions.lift(1).fold(0)(_.indent)
+  }
+
   def observeOutdented(): Boolean = {
     if (!dialect.allowSignificantIndentation) false
     else {
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 691a1c0e29..3f5c3554df 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
@@ -180,6 +180,10 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     in.currentIndentation
   }
 
+  private def previousIndentation: Int = {
+    in.previousIndentation
+  }
+
   def token = in.token
   def next() = in.next()
   def nextTwice() = { next(); next() }
@@ -880,13 +884,35 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
 
     def compoundTypeRest(typ: Type): Type = {
       val startPos = typ.startTokenPos
-      var t = typ
-      while (acceptOpt[KwWith]) {
-        val rhs = annotType()
-        t = atPos(startPos, rhs)(Type.With(t, rhs))
+
+      @tailrec
+      def gatherWithTypes(previousType: Type): Type = {
+        if (acceptOpt[KwWith]) {
+          /* Indentation means a refinement and we cannot join
+           * refinements this way so stop looping.
+           */
+          if (token.is[Indentation.Indent]) {
+            autoPos(Type.Refine(Some(previousType), indented(refineStatSeq())))
+          } else {
+            val rhs = annotType()
+            val t = autoEndPos(startPos)(Type.With(previousType, rhs))
+            gatherWithTypes(t)
+          }
+        } else {
+          previousType
+        }
       }
-      if (isAfterOptNewLine[LeftBrace]) refinement(innerType = Some(t))
-      else t
+
+      val t = gatherWithTypes(typ)
+      val wasLF = token.is[LF]
+
+      // the indentation needs to be higher than the containing one
+      def canAddBracesRefinement =
+        !dialect.allowSignificantIndentation || !wasLF || currentIndentation > previousIndentation
+
+      if (isAfterOptNewLine[LeftBrace] && canAddBracesRefinement) {
+        refinement(innerType = Some(t))
+      } else t
     }
 
     def annotType(): Type = annotTypeRest(simpleType())
@@ -4211,7 +4237,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
   def refineStatSeq(): List[Stat] = listBy[Stat] { stats =>
     while (!token.is[StatSeqEnd]) {
       refineStat().foreach(stats += _)
-      if (token.isNot[RightBrace]) acceptStatSep()
+      if (token.isNot[RightBrace] && token.isNot[Indentation.Outdent]) acceptStatSep()
     }
   }
 
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/TokenIterator.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/TokenIterator.scala
index 5c9da75d90..36f33b6636 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/TokenIterator.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/TokenIterator.scala
@@ -8,6 +8,7 @@ trait TokenIterator {
   def prevTokenPos: Int
   def tokenPos: Int
   def currentIndentation: Int
+  def previousIndentation: Int
   def token: Token
   def fork: TokenIterator
   def observeIndented(): Boolean


Test patch:
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 e5ac43931b..87486b962c 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
@@ -7,7 +7,7 @@ import Name.Anonymous
 import scala.meta.parsers.ParseException
 import scala.meta.tests.parsers.ParseSuite
 
-class TypeSuite extends ParseSuite {
+class TypeSuite extends BaseDottySuite {
 
   private def assertTpe(expr: String)(tree: Tree)(implicit dialect: Dialect): Unit = {
     assertTree(tpe(expr))(tree)
@@ -15,6 +15,166 @@ class TypeSuite extends ParseSuite {
 
   import scala.meta.dialects.Scala3
 
+  test("with-type") {
+    runTestAssert[Stat](
+      """|type A = AnyRef with
+         |  type T>: Null
+         |""".stripMargin,
+      assertLayout = Some("type A = AnyRef { type T >: Null }")
+    )(
+      Defn.Type(
+        Nil,
+        Type.Name("A"),
+        Nil,
+        Type.Refine(
+          Some(Type.Name("AnyRef")),
+          List(Decl.Type(Nil, Type.Name("T"), Nil, Type.Bounds(Some(Type.Name("Null")), None)))
+        ),
+        Type.Bounds(None, None)
+      )
+    )
+  }
+
+  test("with-type2") {
+    runTestAssert[Stat](
+      """|type A = AnyRef with Product with
+         |  type T>: Null
+         |""".stripMargin,
+      assertLayout = Some("type A = AnyRef with Product { type T >: Null }")
+    )(
+      Defn.Type(
+        Nil,
+        Type.Name("A"),
+        Nil,
+        Type.Refine(
+          Some(Type.With(Type.Name("AnyRef"), Type.Name("Product"))),
+          List(Decl.Type(Nil, Type.Name("T"), Nil, Type.Bounds(Some(Type.Name("Null")), None)))
+        ),
+        Type.Bounds(None, None)
+      )
+    )
+  }
+
+  test("with-type3") {
+    runTestAssert[Stat](
+      """|type A = Product with
+         |  type T>: Null
+         |  with
+         |    type D <: 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)
+      )
+    )
+  }
+
+  test("with-type-error") {
+    runTestError[Stat](
+      """|type A = Product with
+         |  type T>: Null
+         | with
+         |    type D <: Product
+         |""".stripMargin,
+      "error: ; expected but with found"
+    )
+  }
+
+  test("with-indent-error") {
+
+    // latter type should be ignored despite indentation
+    runTestAssert[Stat](
+      """|type A = Product
+         |  type T>: Null
+         |""".stripMargin,
+      assertLayout = Some("type A = Product")
+    )(
+      Defn.Type(Nil, Type.Name("A"), Nil, Type.Name("Product"), Type.Bounds(None, None))
+    )
+  }
+
+  test("with-followed-by-brace-indent") {
+    runTestAssert[Stat](
+      """|type AA = String with Int with
+         |    type T>: Null
+         |      {
+         |        type T>: Int
+         |      }
+         |""".stripMargin,
+      assertLayout = Some("type AA = String with Int { type T >: Null { type T >: Int } }")
+    )(
+      Defn.Type(
+        Nil,
+        Type.Name("AA"),
+        Nil,
+        Type.Refine(
+          Some(Type.With(Type.Name("String"), Type.Name("Int"))),
+          List(
+            Decl.Type(
+              Nil,
+              Type.Name("T"),
+              Nil,
+              Type.Bounds(
+                Some(
+                  Type.Refine(
+                    Some(Type.Name("Null")),
+                    List(
+                      Decl.Type(Nil, Type.Name("T"), Nil, Type.Bounds(Some(Type.Name("Int")), None))
+                    )
+                  )
+                ),
+                None
+              )
+            )
+          )
+        ),
+        Type.Bounds(None, None)
+      )
+    )
+  }
+
+  test("with-followed-by-brace") {
+    runTestError[Stat](
+      """|type AA = String with Int with
+         |    type T>: Null
+         |{
+         |  type T>: Int
+         |}
+         |""".stripMargin,
+      "; expected but { found"
+    )
+  }
+
   test("T") {
     val TypeName("T") = tpe("T")
   }