Repository: scalameta/scalameta
Pull request #2854 (2022-09-14 16:53:56)

bugfix: Allow refinement after with in type
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/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a587858c2..512aeddc0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,9 +19,8 @@ jobs:
           - "++2.12.13 testsJVM/test"
           - "++2.12.14 testsJVM/test"
           - "++2.12.16 testsJVM/test"
-          - "++2.12.17 testsJVM/test"
-          - "++2.12.17 testsJS/test"
-          - "++2.12.17 testsNative/test"
+          - "++2.12.16 testsJS/test"
+          - "++2.12.16 testsNative/test"
           - "++2.13.1 testsJVM/test"
           - "++2.13.2 testsJVM/test"
           - "++2.13.3 testsJVM/test"
diff --git a/RELEASING.md b/RELEASING.md
index 710ad894e..2242874db 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -77,7 +77,7 @@ You can also do it manually. The local flow is:
 
 - run `set every version := "4.5.9"` - replace `4.5.9` with the specific version you are releasing for
 
-- run `++2.12.17` or anothe Scala version you need
+- run `++2.12.16` or anothe Scala version you need
 
 - run `semanticdbScalacCore/publishSigned`, `semanticdbScalacPlugin/publishSigned` and `metac/publishSigned`
 
diff --git a/build.sbt b/build.sbt
index ffc1c24e1..c2843a674 100644
--- a/build.sbt
+++ b/build.sbt
@@ -362,12 +362,6 @@ lazy val tests = crossProject(JSPlatform, JVMPlatform, NativePlatform)
     libraryDependencies ++= List(
       "io.get-coursier" %% "coursier" % "2.0.0-RC5-6"
     ),
-    dependencyOverrides += {
-      if (isScala211.value)
-        "org.scala-lang.modules" %%% "scala-xml" % "1.2.0"
-      else
-        "org.scala-lang.modules" %%% "scala-xml" % "2.1.0"
-    },
     // Needed because some tests rely on the --usejavacp option
     Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat
   )
diff --git a/project/Versions.scala b/project/Versions.scala
index dd3c1d0b3..dbead9a80 100644
--- a/project/Versions.scala
+++ b/project/Versions.scala
@@ -3,10 +3,11 @@ package build
 
 object Versions {
   val LatestScala211 = "2.11.12"
-  val LatestScala212 = "2.12.17"
+  val LatestScala212 = "2.12.16"
   val LatestScala213 = "2.13.8"
   val LegacyScalaVersions =
     List(
+      "2.12.8",
       "2.12.9",
       "2.12.10",
       "2.12.11",
@@ -14,7 +15,6 @@ object Versions {
       "2.12.13",
       "2.12.14",
       "2.12.15",
-      "2.12.16",
       "2.13.0",
       "2.13.1",
       "2.13.2",
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 3f410211f..cb2911c7a 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 691a1c0e2..3f5c3554d 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 5c9da75d9..36f33b663 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
diff --git a/semanticdb/scalac/library/src/main/scala-2.12.17/scala/meta/internal/semanticdb/scalac/SemanticdbReporter.scala b/semanticdb/scalac/library/src/main/scala-2.12.17/scala/meta/internal/semanticdb/scalac/SemanticdbReporter.scala
deleted file mode 100644
index cc47c10e1..000000000
--- a/semanticdb/scalac/library/src/main/scala-2.12.17/scala/meta/internal/semanticdb/scalac/SemanticdbReporter.scala
+++ /dev/null
@@ -1,37 +0,0 @@
-package scala.meta.internal.semanticdb.scalac
-
-import scala.reflect.internal.util.Position
-import scala.tools.nsc.Settings
-import scala.tools.nsc.reporters.{Reporter, StoreReporter}
-
-class SemanticdbReporter(underlying: Reporter)
-    extends StoreReporter(SemanticdbReporter.defaultSettings()) {
-  override protected def info0(
-      pos: Position,
-      msg: String,
-      severity: Severity,
-      force: Boolean
-  ): Unit = {
-    super.info0(pos, msg, severity, force)
-    severity.id match {
-      case 0 => underlying.info(pos, msg, force)
-      case 1 => underlying.warning(pos, msg)
-      case 2 => underlying.error(pos, msg)
-      case _ =>
-    }
-
-  }
-
-  override def hasErrors: Boolean = underlying.hasErrors
-
-  override def hasWarnings: Boolean = underlying.hasWarnings
-
-}
-
-object SemanticdbReporter {
-  def defaultSettings(): Settings = {
-    val s = new Settings()
-    s.processArguments(List("-Xmaxwarns", "-1", "-Xmaxerrs", "-1"), true)
-    s
-  }
-}
diff --git a/semanticdb/scalac/library/src/main/scala-2.12.17/scala/meta/internal/semanticdb/scalac/VersionCompilerOps.scala b/semanticdb/scalac/library/src/main/scala-2.12.17/scala/meta/internal/semanticdb/scalac/VersionCompilerOps.scala
deleted file mode 100644
index 280c292e1..000000000
--- a/semanticdb/scalac/library/src/main/scala-2.12.17/scala/meta/internal/semanticdb/scalac/VersionCompilerOps.scala
+++ /dev/null
@@ -1,9 +0,0 @@
-package scala.meta.internal.semanticdb.scalac
-
-import scala.tools.nsc.interactive.Global
-
-trait VersionCompilerOps {
-
-  def forceWarnings(global: Global) = {}
-
-}


Test patch:
diff --git a/bin/test-release.sh b/bin/test-release.sh
index bb61b62a6..f4c283ba9 100755
--- a/bin/test-release.sh
+++ b/bin/test-release.sh
@@ -10,7 +10,7 @@ coursier resolve \
   org.scalameta:scalameta_sjs1_2.13:$version \
   org.scalameta:scalameta_sjs1_2.12:$version \
   org.scalameta:metac_2.13.8:$version \
-  org.scalameta:metac_2.12.17:$version \
+  org.scalameta:metac_2.12.16:$version \
   org.scalameta:metac_2.11.12:$version \
   org.scalameta:semanticdb-scalac-core_2.13.8:$version \
   org.scalameta:semanticdb-scalac-core_2.13.7:$version \
@@ -28,7 +28,6 @@ coursier resolve \
   org.scalameta:semanticdb-scalac-core_2.12.14:$version \
   org.scalameta:semanticdb-scalac-core_2.12.15:$version \
   org.scalameta:semanticdb-scalac-core_2.12.16:$version \
-  org.scalameta:semanticdb-scalac-core_2.12.17:$version \
   org.scalameta:semanticdb-scalac-core_2.11.12:$version \
   org.scalameta:semanticdb-scalac_2.13.8:$version \
   org.scalameta:semanticdb-scalac_2.13.7:$version \
@@ -47,6 +46,5 @@ coursier resolve \
   org.scalameta:semanticdb-scalac_2.12.14:$version \
   org.scalameta:semanticdb-scalac_2.12.15:$version \
   org.scalameta:semanticdb-scalac_2.12.16:$version \
-  org.scalameta:semanticdb-scalac_2.12.17:$version \
   org.scalameta:semanticdb-scalac_2.11.12:$version \
   -r sonatype:staging
diff --git a/tests/jvm/src/test/scala/scala/meta/tests/semanticdb/Source3Suite.scala b/tests/jvm/src/test/scala/scala/meta/tests/semanticdb/Source3Suite.scala
index bf6f16665..f6ddb349f 100644
--- a/tests/jvm/src/test/scala/scala/meta/tests/semanticdb/Source3Suite.scala
+++ b/tests/jvm/src/test/scala/scala/meta/tests/semanticdb/Source3Suite.scala
@@ -88,8 +88,7 @@ class Source3Suite extends FunSuite {
     compat = List(
       "2.12.14" -> expected.replace("scala/package.List.", "scala/collection/immutable/List."),
       "2.12.15" -> expected.replace("scala/package.List.", "scala/collection/immutable/List."),
-      "2.12.16" -> expected.replace("scala/package.List.", "scala/collection/immutable/List."),
-      "2.12.17" -> expected.replace("scala/package.List.", "scala/collection/immutable/List.")
+      "2.12.16" -> expected.replace("scala/package.List.", "scala/collection/immutable/List.")
     )
   )
 
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 e5ac43931..87486b962 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")
   }