Repository: scalameta/scalameta
Pull request #2849 (2022-09-12 20:06:26)

ScalametaParser: param ellipses in extension group
Currently, quasiquote ellipses are not supported when parsing parameters of extension groups.

Instead, let's move the rank-1 ellipsis parsing into the param() method and add the rank-2 parsing into the extension group invocation.


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 86fffdc4f..e12c44e80 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
@@ -2835,11 +2835,9 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
           case soft.KwUsing() => modUsing = Some(atCurPosNext(Mod.Using()))
           case _ =>
         }
-        commaSeparated(token match {
-          case t: Ellipsis => ellipsis[Term.Param](t)
-          case _ =>
-            param(ownerIsCase && first, ownerIsType, modImplicit = modImplicit, modUsing = modUsing)
-        })
+        commaSeparated(
+          param(ownerIsCase && first, ownerIsType, modImplicit = modImplicit, modUsing = modUsing)
+        )
     })(Nil)
     if (!isAfterOptNewLine[LeftParen]) Nil
     else
@@ -2911,10 +2909,15 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
       mods += atCurPosNext(Mod.VarParam())
     }
     def endParamQuasi = token.is[RightParen] || token.is[Comma]
-    mods.headOption match {
-      case Some(q: Mod.Quasi) if endParamQuasi =>
-        q.become[Term.Param]
-      case _ =>
+    mods.headOption
+      .collect {
+        case q: Mod.Quasi if endParamQuasi => q.become[Term.Param]
+      }
+      .orElse(token match {
+        case t: Ellipsis => Some(ellipsis[Term.Param](t, 1))
+        case _ => None
+      })
+      .getOrElse {
         var anonymousUsing = false
         val name = if (modUsing.isDefined && ahead(!token.is[Colon])) { // anonymous using
           anonymousUsing = true
@@ -2955,7 +2958,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
             val default = if (acceptOpt[Equals]) Some(expr()) else None
             Term.Param(mods.toList, name, tpt, default)
         }
-    }
+      }
   }
 
   def quasiquoteTermParam(): Term.Param = entrypointTermParam()
@@ -3368,7 +3371,10 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     collectUparams()
 
     paramss += inParens(
-      List(param(ownerIsCase = false, ownerIsType = false))
+      List(token match {
+        case t @ Ellipsis(2) => ellipsis[Term.Param](t)
+        case _ => param(ownerIsCase = false, ownerIsType = false)
+      })
     )
 
     collectUparams()


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/TreeSuiteBase.scala b/tests/shared/src/test/scala/scala/meta/tests/TreeSuiteBase.scala
index 747d5431a..7adc98641 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/TreeSuiteBase.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/TreeSuiteBase.scala
@@ -9,6 +9,13 @@ abstract class TreeSuiteBase extends FunSuite {
   protected def assertTree(obtained: Tree)(expected: Tree)(implicit loc: munit.Location): Unit =
     assertNoDiff(obtained.structure, expected.structure)
 
+  protected def assertTrees(
+      obtained: Tree*
+  )(expected: Tree*)(implicit loc: munit.Location): Unit = {
+    assertEquals(obtained.length, expected.length)
+    obtained.zip(expected).foreach { case (o, e) => assertTree(o)(e) }
+  }
+
   protected def assertTree(obtained: Option[Tree])(expected: Option[Tree])(
       implicit loc: munit.Location
   ): Unit =
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ExtensionMethodsSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ExtensionMethodsSuite.scala
index 45e4b30d4..30d0d1aa6 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ExtensionMethodsSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ExtensionMethodsSuite.scala
@@ -7,6 +7,9 @@ class ExtensionMethodsSuite extends BaseDottySuite {
 
   implicit val parseBlock: String => Stat = code => blockStat(code)(dialects.Scala3)
 
+  private final val cparam = tparam("c", "Circle")
+  private final val cparamss = List(List(cparam))
+
   /**
    * For checking examples in repl declare:
    * {{{
@@ -21,7 +24,7 @@ class ExtensionMethodsSuite extends BaseDottySuite {
     runTestAssert[Stat]("extension (c: Circle) def crc: Int = 2")(
       Defn.ExtensionGroup(
         Nil,
-        cparam,
+        cparamss,
         Defn.Def(Nil, tname("crc"), Nil, Nil, Some(pname("Int")), int(2))
       )
     )
@@ -31,7 +34,7 @@ class ExtensionMethodsSuite extends BaseDottySuite {
     runTestAssert[Stat]("extension (c: Circle) private def crc: Int = 2")(
       Defn.ExtensionGroup(
         Nil,
-        cparam,
+        cparamss,
         Defn.Def(
           List(Mod.Private(Name.Anonymous())),
           tname("crc"),
@@ -51,7 +54,7 @@ class ExtensionMethodsSuite extends BaseDottySuite {
     runTestAssert[Stat](code, assertLayout = Some("extension (c: Circle) def crc: Int = 2"))(
       Defn.ExtensionGroup(
         Nil,
-        cparam,
+        cparamss,
         Defn.Def(Nil, tname("crc"), Nil, Nil, Some(pname("Int")), int(2))
       )
     )
@@ -433,7 +436,128 @@ class ExtensionMethodsSuite extends BaseDottySuite {
     )
   }
 
-  final val defcrc = Defn.Def(Nil, tname("crc"), Nil, Nil, Some(pname("Int")), int(2))
+  test("extension quasiquotes: triple, no tparams") {
+    import dialects.Scala3
+
+    val q"extension [..$tparams](...$paramss) { ..$stats }" =
+      q"""extension (c: Circle)(using Context, x: Int)(using y: String, File) {
+                def crc: Int = 2
+              }"""
+
+    assertEquals(tparams.length, 0)
+
+    assertEquals(paramss.length, 3)
+    assertTrees(paramss(0): _*)(cparam)
+    assertTrees(paramss(1): _*)(
+      Term.Param(List(Mod.Using()), Name(""), Some(Type.Name("Context")), None),
+      Term.Param(List(Mod.Using()), Term.Name("x"), Some(Type.Name("Int")), None)
+    )
+    assertTrees(paramss(2): _*)(
+      Term.Param(List(Mod.Using()), Term.Name("y"), Some(Type.Name("String")), None),
+      Term.Param(List(Mod.Using()), Name(""), Some(Type.Name("File")), None)
+    )
+
+    assertTrees(stats: _*)(
+      Defn.Def(Nil, Term.Name("crc"), Nil, Nil, Some(Type.Name("Int")), Lit.Int(2))
+    )
+  }
+
+  test("extension quasiquotes: triple, with tparams") {
+    import dialects.Scala3
+
+    val q"extension [..$tparams](...$paramss) { ..$stats }" =
+      q"""extension [A, B](c: Circle)(using Context, x: Int)(using y: String, File) {
+                def crc: Int = 2
+              }"""
+
+    assertTrees(tparams: _*)(pparam("A"), pparam("B"))
+
+    assertEquals(paramss.length, 3)
+    assertTrees(paramss(0): _*)(cparam)
+    assertTrees(paramss(1): _*)(
+      Term.Param(List(Mod.Using()), Name(""), Some(Type.Name("Context")), None),
+      Term.Param(List(Mod.Using()), Term.Name("x"), Some(Type.Name("Int")), None)
+    )
+    assertTrees(paramss(2): _*)(
+      Term.Param(List(Mod.Using()), Term.Name("y"), Some(Type.Name("String")), None),
+      Term.Param(List(Mod.Using()), Name(""), Some(Type.Name("File")), None)
+    )
+
+    assertTrees(stats: _*)(
+      Defn.Def(Nil, Term.Name("crc"), Nil, Nil, Some(Type.Name("Int")), Lit.Int(2))
+    )
+  }
+
+  test("extension quasiquotes: double, no tparams") {
+    import dialects.Scala3
+
+    val q"extension [..$tparams](..$params) { ..$stats }" =
+      q"""extension (c: Circle) {
+              def crc: Int = 2
+            }"""
+
+    assertEquals(tparams.length, 0)
+
+    assertTrees(params: _*)(cparam)
+
+    assertTrees(stats: _*)(
+      Defn.Def(Nil, Term.Name("crc"), Nil, Nil, Some(Type.Name("Int")), Lit.Int(2))
+    )
+  }
+
+  test("extension quasiquotes: double, with tparams") {
+    import dialects.Scala3
+
+    val q"extension [..$tparams](..$params) { ..$stats }" =
+      q"""extension [A](c: Circle) {
+              def crc: Int = 2
+            }"""
+
+    assertTrees(tparams: _*)(pparam("A"))
+
+    assertTrees(params: _*)(cparam)
+
+    assertTrees(stats: _*)(
+      Defn.Def(Nil, Term.Name("crc"), Nil, Nil, Some(Type.Name("Int")), Lit.Int(2))
+    )
+  }
+
+  test("extension quasiquotes: single, no tparams") {
+    import dialects.Scala3
+
+    val q"extension [..$tparams]($param) { ..$stats }" =
+      q"""extension (c: Circle) {
+              def crb: Int = 1
+              def crc: Int = 2
+            }"""
+
+    assertEquals(tparams.length, 0)
+
+    assertTree(param)(cparam)
+
+    assertTrees(stats: _*)(
+      Defn.Def(Nil, Term.Name("crb"), Nil, Nil, Some(Type.Name("Int")), Lit.Int(1)),
+      Defn.Def(Nil, Term.Name("crc"), Nil, Nil, Some(Type.Name("Int")), Lit.Int(2))
+    )
+  }
+
+  test("extension quasiquotes: single, with tparams") {
+    import dialects.Scala3
+
+    val q"extension [..$tparams]($param) { ..$stats }" =
+      q"""extension [A, B, C](c: Circle) {
+              def crb: Int = 1
+              def crc: Int = 2
+            }"""
+
+    assertTrees(tparams: _*)(pparam("A"), pparam("B"), pparam("C"))
+
+    assertTree(param)(cparam)
+
+    assertTrees(stats: _*)(
+      Defn.Def(Nil, Term.Name("crb"), Nil, Nil, Some(Type.Name("Int")), Lit.Int(1)),
+      Defn.Def(Nil, Term.Name("crc"), Nil, Nil, Some(Type.Name("Int")), Lit.Int(2))
+    )
+  }
 
-  final val cparam = List(List(tparam("c", "Circle")))
 }