Repository: scalameta/scalameta (scala)

Issue:
Named tuples parser error on latest metals snapshot
Metals server version: `1.4.0+6-73dc6db8-SNAPSHOT` which already includes `Scalameta v4.11.0` that added support for named tuples.

Code:
```scala
val y = summon[Tuple2[Int, String] <:< (x: Int, y: String)]
```

Error:
```
`)` expected but `:` found
```

Pull request:
Fixes #4007. Alternative to #4009.

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 cb9d9e52ed..d5c0c4147f 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
@@ -668,28 +668,29 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
       // We need to accept `(T, U) => W`, `(x: T): x.U` and also support unquoting.
       var hasImplicits = false
 
-      @tailrec
-      def paramOrType(modsBuf: mutable.Builder[Mod, List[Mod]]): Type = currToken match {
+      def modOrType(modsBuf: mutable.Builder[Mod, List[Mod]]): Type = currToken match {
         case _: KwImplicit if !hasImplicits && allowFunctionType =>
           next()
           hasImplicits = true
-          paramOrType(modsBuf)
-        case t: Ident if tryAhead[Colon] =>
-          val startPos = prevIndex
-          next() // skip colon
-          val mods = modsBuf.result()
-          val modPos = if (mods.isEmpty) startPos else mods.head.begIndex
-          val name = atPos(startPos)(Type.Name(t.value))
-          val tpe = maybeParamType(allowFunctionType)
-          autoEndPos(modPos)(Type.TypedParam(name, tpe, mods))
+          null
         case soft.KwErased() if allowFunctionType =>
-          paramOrType(modsBuf += atCurPosNext(Mod.Erased()))
+          modsBuf += atCurPosNext(Mod.Erased())
+          null
         case _ =>
           val mods = modsBuf.result()
           val tpe = maybeParamType(allowFunctionType)
           if (mods.isEmpty) tpe else autoEndPos(mods.head)(Type.FunctionArg(mods, tpe))
       }
 
+      @tailrec
+      def paramOrType(modsBuf: mutable.Builder[Mod, List[Mod]]): Type =
+        namedTypeOpt(modsBuf.result(), allowFunctionType = allowFunctionType) match {
+          case Some(x) => x
+          case None =>
+            val x = modOrType(modsBuf)
+            if (x ne null) x else paramOrType(modsBuf)
+        }
+
       val openParenPos = currIndex
       val ts = inParensOr(commaSeparated(paramOrType(List.newBuilder[Mod])))(Nil)
       // NOTE: can't have this, because otherwise we run into #312
@@ -780,6 +781,21 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
     private def maybeParamType(allowFunctionType: Boolean): Type =
       if (allowFunctionType) paramType() else typ()
 
+    private def namedTypeOpt(
+        fmods: => List[Mod],
+        allowFunctionType: Boolean
+    ): Option[Type.TypedParam] = currToken match {
+      case t: Ident if peek[Colon] =>
+        val startPos = currIndex
+        nextTwice() // skip ident and colon
+        val mods = fmods
+        val modPos = if (mods.isEmpty) startPos else mods.head.begIndex
+        val name = atPos(startPos)(Type.Name(t.value))
+        val tpe = maybeParamType(allowFunctionType)
+        Some(autoEndPos(modPos)(Type.TypedParam(name, tpe, mods)))
+      case _ => None
+    }
+
     def typeBlock(): Type =
       // TypeBlock, https://dotty.epfl.ch/docs/internals/syntax.html#expressions-3
       if (dialect.allowQuotedTypeVariables && at[KwType]) autoPos {
@@ -998,7 +1014,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
       case _ => t
     }
 
-    def types(): List[Type] = commaSeparated(typ())
+    def types(): List[Type] =
+      commaSeparated(namedTypeOpt(Nil, allowFunctionType = false).getOrElse(typ()))
 
     def patternTyp(allowInfix: Boolean, allowImmediateTypevars: Boolean): Type = {
       def setPos(outerTpe: Type)(innerTpe: Type): Type =


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/NamedTuplesSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/NamedTuplesSuite.scala
index 7b05d98b0f..bb3bb16a96 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/NamedTuplesSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/NamedTuplesSuite.scala
@@ -29,6 +29,40 @@ class NamedTuplesSuite extends BaseDottySuite {
 
   }
 
+  test("simple-named-type-assign") {
+    runTestAssert[Stat]("type T = (a: Int, b: String)")(Defn.Type(
+      Nil,
+      pname("T"),
+      Nil,
+      Type.Tuple(List(
+        Type.TypedParam(pname("a"), pname("Int"), Nil),
+        Type.TypedParam(pname("b"), pname("String"), Nil)
+      )),
+      noBounds
+    ))
+  }
+
+  test("named-tuple-summon") {
+    val code = """|val y = summon[Tuple2[Int, String] <:< (x: Int, y: String)]
+                  |""".stripMargin
+    runTestAssert[Stat](code)(Defn.Val(
+      Nil,
+      List(Pat.Var(tname("y"))),
+      None,
+      Term.ApplyType(
+        tname("summon"),
+        List(Type.ApplyInfix(
+          Type.Apply(pname("Tuple2"), List(pname("Int"), pname("String"))),
+          pname("<:<"),
+          Type.Tuple(List(
+            Type.TypedParam(pname("x"), pname("Int"), Nil),
+            Type.TypedParam(pname("y"), pname("String"), Nil)
+          ))
+        ))
+      )
+    ))
+  }
+
   test("complex-named-type") {
     runTestAssert[Type]("(a: Int, b: String, c: (a: Int, d: Double))")(Type.Tuple(List(
       Type.TypedParam(pname("a"), pname("Int"), Nil),