Repository: scalameta/scalameta (scala)

Issue:
Support named type arguments syntax
Scalameta should support named type arguments syntax.
This syntax allows for specifying type arguments by name in a type parameter clause.

Example:
```scala
def construct[Elem, Coll[_]](xs: Elem*): Coll[Elem] = ???

val xs1 = construct[Coll = List, Elem = Int](1, 2, 3)
val xs2 = construct[Coll = List](1, 2, 3)
```

Syntax changes:
```
SimpleExpr1       ::=  ...
                    |  SimpleExpr (TypeArgs | NamedTypeArgs)
NamedTypeArgs     ::=  ‘[’ NamedTypeArg {‘,’ NamedTypeArg} ‘]’
NamedTypeArg      ::=  id ‘=’ Type
```

Links:
- docs: https://dotty.epfl.ch/docs/reference/experimental/named-typeargs.html#
- more docs: https://dotty.epfl.ch/docs/reference/experimental/named-typeargs-spec.html

Pull request:
Fixes #3998.

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 d5c0c4147f..2776146653 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
@@ -836,8 +836,17 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
 
     def entrypointType(): Type = paramType()
 
-    def typeArgs(): Type.ArgClause = TypeBracketsContext
-      .within(autoPos(inBrackets(types()).reduceWith(Type.ArgClause.apply)))
+    private def typeArg(): Type = currToken match {
+      case t: Ident if peek[Equals] =>
+        val startPos = currIndex
+        nextTwice() // skip ident and equals
+        val name = atPos(startPos)(Type.Name(t.value))
+        autoEndPos(startPos)(Type.Assign(name, typ()))
+      case _ => typ()
+    }
+
+    def typeArgsInBrackets(): Type.ArgClause = TypeBracketsContext
+      .within(autoPos(inBrackets(commaSeparated(typeArg())).reduceWith(Type.ArgClause.apply)))
 
     def infixTypeOrTuple(inMatchType: Boolean = false): Type =
       if (at[LeftParen]) tupleInfixType(allowFunctionType = !inMatchType)
@@ -972,7 +981,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
       }
       val res = currToken match {
         case _: Ident if peek[Dot] => pathSimpleType()
-        case _: LeftParen => makeTupleType(startPos, inParensOnOpen(types()))
+        case _: LeftParen => makeTupleType(startPos, typesInParens())
         case MacroSplicedIdent(ident) => Type.Macro(macroSplicedIdent(ident))
         case _: MacroSplice => Type.Macro(macroSplice())
         case _: Underscore if inMatchType => next(); Type.PatWildcard()
@@ -1010,12 +1019,12 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
         next()
         simpleTypeRest(autoEndPos(startPos)(Type.Project(t, typeName())), startPos)
       case _: LeftBracket =>
-        simpleTypeRest(autoEndPos(startPos)(Type.Apply(t, typeArgs())), startPos)
+        simpleTypeRest(autoEndPos(startPos)(Type.Apply(t, typeArgsInBrackets())), startPos)
       case _ => t
     }
 
-    def types(): List[Type] =
-      commaSeparated(namedTypeOpt(Nil, allowFunctionType = false).getOrElse(typ()))
+    def typesInParens(): List[Type] =
+      inParensOnOpen(commaSeparated(namedTypeOpt(Nil, allowFunctionType = false).getOrElse(typ())))
 
     def patternTyp(allowInfix: Boolean, allowImmediateTypevars: Boolean): Type = {
       def setPos(outerTpe: Type)(innerTpe: Type): Type =
@@ -2741,7 +2750,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
   def entrypointType() = outPattern.entrypointType()
   def startInfixType() = outPattern.infixType()
   def startModType() = outPattern.annotType()
-  def exprTypeArgs() = outPattern.typeArgs()
+  def exprTypeArgs() = outPattern.typeArgsInBrackets()
   def exprSimpleType() = outPattern.simpleType()
 
   /** Default entry points into some pattern contexts. */
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala b/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
index 8f332a181d..1dc6ea5489 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
@@ -675,6 +675,11 @@ object Type {
     override def nameOpt: Option[Name] = None
   }
 
+  @ast
+  class Assign(name: Name, rhs: Type) extends Type with Tree.WithBody {
+    override def body: Tree = rhs
+  }
+
   @ast
   class Param(
       mods: List[Mod],
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
index 78e97a22cf..dd220de1b3 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
@@ -688,6 +688,7 @@ object TreeSyntax {
       case t: Type.Var => m(SimpleTyp, s(t.name.value))
       case t: Type.FunctionArg => m(ParamTyp, w(t.mods, " "), p(Typ, t.tpe))
       case t: Type.TypedParam => m(SimpleTyp, w(t.mods, " "), s(t.name.value), ": ", p(Typ, t.typ))
+      case t: Type.Assign => m(SimpleTyp, s(t.name.value), " ", kw("="), " ", p(Typ, t.rhs))
       case t: Type.ParamClause => r(t.values, "[", ", ", "]")
       case t: Type.BoundsAlias => m(SimpleTyp, s(t.bounds, " ", "as", " ", t.name))
       case t: Type.Param =>


Test patch:
diff --git a/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala b/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
index 366ec92729..bee01613c8 100644
--- a/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
+++ b/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
@@ -488,6 +488,7 @@ class SurfaceSuite extends TreeSuiteBase {
          |scala.meta.Type.Apply
          |scala.meta.Type.ApplyInfix
          |scala.meta.Type.ArgClause
+         |scala.meta.Type.Assign
          |scala.meta.Type.Block
          |scala.meta.Type.Bounds
          |scala.meta.Type.BoundsAlias
diff --git a/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala b/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala
index 42ff054752..d434ecc2e1 100644
--- a/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala
+++ b/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala
@@ -24,7 +24,7 @@ class ReflectionSuite extends TreeSuiteBase {
     val sym = symbolOf[scala.meta.Tree]
     assert(sym.isRoot)
     val root = sym.asRoot
-    assertEquals((root.allBranches.length, root.allLeafs.length), (63, 443))
+    assertEquals((root.allBranches.length, root.allLeafs.length), (63, 445))
   }
 
   test("If") {
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 1f2c8730a4..923770d386 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
@@ -574,4 +574,21 @@ class TypeSuite extends BaseDottySuite {
 
   test("#3672 [scala3] ***")(runTestAssert[Type]("***")(pname("***")))
 
+  test("#3998") {
+    val code = "val xs1 = construct[Coll = List, Elem = Int](1, 2, 3)"
+    val tree = Defn.Val(
+      Nil,
+      List(Pat.Var(tname("xs1"))),
+      None,
+      Term.Apply(
+        Term.ApplyType(
+          tname("construct"),
+          List(Type.Assign(pname("Coll"), pname("List")), Type.Assign(pname("Elem"), pname("Int")))
+        ),
+        List(lit(1), lit(2), lit(3))
+      )
+    )
+    runTestAssert[Stat](code)(tree)
+  }
+
 }