Repository: scalameta/scalameta
Pull request #3637 (2024-03-12 07:33:21)

Keywords: match for allowStarAsTypePlaceholder
Helps with #3631.


Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/Keywords.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/Keywords.scala
index dd2789090..0502703cc 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/Keywords.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/Keywords.scala
@@ -7,6 +7,18 @@ object Keywords {
 
   private val identClass = classTag[Token.Ident].runtimeClass
 
+  abstract class AsWithFunc[A](isEnabled: Boolean, func: String => Option[A]) {
+    private def getEnabled(value: => String): Option[A] = func(value)
+    private def getDisabled(value: => String): Option[A] = None
+    private val get: (=> String) => Option[A] = if (isEnabled) getEnabled else getDisabled
+    @inline final def unapply(value: String): Option[A] = get(value)
+    @inline final def unapply(token: Token.Ident): Option[A] = get(token.text)
+    @inline final def unapply(token: Token): Option[A] =
+      if (identClass.isInstance(token)) get(token.text) else None
+    @inline final def apply(token: Token.Ident): Option[A] = unapply(token)
+    @inline final def apply(token: Token): Option[A] = unapply(token)
+  }
+
   abstract class IsWithPred(isEnabled: Boolean, pred: String => Boolean) {
     private def checkEnabled(value: => String): Boolean = pred(value)
     private val check: (=> String) => Boolean = if (isEnabled) checkEnabled else _ => false
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 83e5933df..8ebc59573 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
@@ -556,6 +556,18 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     }
   }
 
+  private object VarArgTypeParam {
+    // we assume that this is a type specification for a vararg parameter
+    private def check(tok: Token): Boolean = peekToken match {
+      case _: RightParen | _: Comma | _: Equals | _: RightBrace | _: EOF => tok.text == "*"
+      case _ => false
+    }
+    @inline def unapply(tok: Token.Ident): Boolean = check(tok)
+    object Non {
+      def unapply(tok: Token.Ident): Boolean = !check(tok)
+    }
+  }
+
   /* ---------- TREE CONSTRUCTION ------------------------------------------- */
 
   private def listBy[T](f: ListBuffer[T] => Unit): List[T] = {
@@ -923,40 +935,35 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
         f: Type => Type,
         inMatchType: Boolean = false
     ): Type = {
-      @inline def verifyLeftAssoc(at: Tree, leftAssoc: Boolean = true) =
-        if (mode != InfixMode.FirstOp) checkAssoc(at, leftAssoc, mode == InfixMode.LeftOp)
-      token match {
-        case Ident("*") if (peekToken match {
-              case _: RightParen | _: Comma | _: Equals | _: RightBrace | _: EOF => true
-              case _ => false
-            }) => // we assume that this is a type specification for a vararg parameter
-          f(t)
-        case _: Ident | _: Unquote =>
-          val op = typeName()
-          val leftAssoc = op.isLeftAssoc
-          verifyLeftAssoc(op, leftAssoc)
-          newLineOptWhenFollowedBy(TypeIntro(_))
-          val typ = compoundType(inMatchType = inMatchType)
-          def mkOp(t1: Type) = atPos(startPos, t1)(Type.ApplyInfix(t, op, t1))
-          if (leftAssoc)
-            infixTypeRestWithMode(
-              mkOp(typ),
-              InfixMode.LeftOp,
-              startPos,
-              f,
-              inMatchType = inMatchType
-            )
-          else
-            infixTypeRestWithMode(
-              typ,
-              InfixMode.RightOp,
-              typ.startTokenPos,
-              f.compose(mkOp),
-              inMatchType = inMatchType
-            )
-        case _ =>
-          f(t)
+      val ok = token match {
+        case _: Unquote | VarArgTypeParam.Non() => true
+        case _ => false
       }
+      if (ok) {
+        val op = typeName()
+        val leftAssoc = op.isLeftAssoc
+        if (mode != InfixMode.FirstOp) checkAssoc(op, leftAssoc, mode == InfixMode.LeftOp)
+        newLineOptWhenFollowedBy(TypeIntro(_))
+        val typ = compoundType(inMatchType = inMatchType)
+        def mkOp(t1: Type) = atPos(startPos, t1)(Type.ApplyInfix(t, op, t1))
+        if (leftAssoc)
+          infixTypeRestWithMode(
+            mkOp(typ),
+            InfixMode.LeftOp,
+            startPos,
+            f,
+            inMatchType = inMatchType
+          )
+        else
+          infixTypeRestWithMode(
+            typ,
+            InfixMode.RightOp,
+            typ.startTokenPos,
+            f.compose(mkOp),
+            inMatchType = inMatchType
+          )
+      } else
+        f(t)
     }
 
     private final def infixTypeRestWithPrecedence(t: Type, inMatchType: Boolean = false): Type = {
@@ -971,10 +978,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
       }
       @tailrec
       def loop(rhs: ctx.Typ): ctx.Typ = token match {
-        case Ident("*") if (peekToken match {
-              case _: RightParen | _: Comma | _: Equals | _: RightBrace | _: EOF => true
-              case _ => false
-            }) => // we assume that this is a type specification for a vararg parameter
+        case VarArgTypeParam() =>
           reduce(rhs, None)
         case _: Ident | _: Unquote =>
           loop(getNextRhs(typeName(), rhs))
@@ -1035,8 +1039,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
         Type.Wildcard(typeBounds())
       }
       def anonymousParamWithVariant(value: String): Type = {
-        val variant = if (value(0) == '+') Mod.Covariant() else Mod.Contravariant()
-        Type.AnonymousParam(Some(atPos(startPos)(variant)))
+        val variant = tparamVariant(value).map(v => atPos(startPos)(v))
+        Type.AnonymousParam(variant)
       }
       def pathSimpleType(): Type = {
         val ref = path()
@@ -1080,9 +1084,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
           wildcardType()
         case Ident("?") if !inMatchType && dialect.allowQuestionMarkAsTypeWildcard =>
           wildcardType()
-        case Ident("*") if !inMatchType && dialect.allowStarAsTypePlaceholder =>
-          next(); Type.AnonymousParam(None)
-        case Ident(value @ ("-*" | "+*")) if !inMatchType && dialect.allowStarAsTypePlaceholder =>
+        case soft.StarAsTypePlaceholder(value) if !inMatchType =>
           next(); anonymousParamWithVariant(value)
         case Ident(value @ ("+" | "-"))
             if (dialect.allowPlusMinusUnderscoreAsIdent || dialect.allowUnderscoreAsTypePlaceholder) &&
@@ -3124,11 +3126,17 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
   private def tparamModifiers(buf: ListBuffer[Mod]): Unit = token match {
     case t: Unquote => if (peekToken.isAny[Ident, Unquote]) buf += unquote[Mod](t)
     case t: Ellipsis => buf += ellipsis[Mod](t, 1)
-    case Ident("+") => buf += atCurPosNext(Mod.Covariant())
-    case Ident("-") => buf += atCurPosNext(Mod.Contravariant())
+    case t: Ident => tparamVariant(t.text).foreach(buf += atCurPosNext(_))
     case _ =>
   }
 
+  private def tparamVariant(ident: String): Option[Mod.Variant] =
+    ident match {
+      case "+" => Some(Mod.Covariant())
+      case "-" => Some(Mod.Contravariant())
+      case _ => None
+    }
+
   def modifiersBuf(
       buf: ListBuffer[Mod],
       isLocal: Boolean = false,
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/SoftKeywords.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/SoftKeywords.scala
index 71f87d2c8..b45d385b6 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/SoftKeywords.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/SoftKeywords.scala
@@ -18,4 +18,13 @@ class SoftKeywords(dialect: Dialect) {
   object KwExtension extends IsWithName(dialect.allowExtensionMethods, "extension")
   object KwErased extends IsWithName(dialect.allowErasedDefs, "erased")
 
+  object StarAsTypePlaceholder
+      extends AsWithFunc(
+        dialect.allowStarAsTypePlaceholder,
+        { (x: String) =>
+          val last = x.length - 1
+          if (last >= 0 && x.charAt(last) == '*') Some(x.substring(0, last)) else None
+        }
+      )
+
 }
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 18ad65ada..f2646173c 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
@@ -102,6 +102,11 @@ object TreeSyntax {
           Seq(classTag[Term.Apply], classTag[Term.ApplyUsing], classTag[Term.ApplyInfix])
 
       if (dialect.allowStarWildcardImport) seq += "*" -> Seq(classTag[Importee])
+      if (dialect.allowStarAsTypePlaceholder) {
+        seq += "*" -> Seq(classTag[Type.ArgClause])
+        seq += "-*" -> Seq(classTag[Type.ArgClause])
+        seq += "+*" -> Seq(classTag[Type.ArgClause])
+      }
 
       seq.result().groupBy(_._1).map { case (k, vs) =>
         k -> vs.flatMap(_._2).distinct.map(_.runtimeClass)


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/ModSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/ModSuite.scala
index 4e988edbc..971d8e2df 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/ModSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/ModSuite.scala
@@ -539,15 +539,11 @@ class ModSuite extends ParseSuite {
   }
 
   test("covariant-like in type") {
-    runTestAssert[Stat]("type A[`+`T] = B[T]", "type A[+T] = B[T]") {
-      Defn.Type(
-        Nil,
-        pname("A"),
-        pparam(List(Mod.Covariant()), "T") :: Nil,
-        Type.Apply(pname("B"), List(pname("T"))),
-        noBounds
-      )
-    }
+    val error =
+      """|<input>:1: error: ] expected but identifier found
+         |type A[`+`T] = B[T]
+         |          ^""".stripMargin
+    runTestError[Stat]("type A[`+`T] = B[T]", error)
   }
 
   test("covariant in def") {
@@ -589,19 +585,11 @@ class ModSuite extends ParseSuite {
   }
 
   test("contravariant-like in class") {
-    runTestAssert[Stat]("class A[`-`T](t: T)", "class A[-T](t: T)") {
-      Defn.Class(
-        Nil,
-        pname("A"),
-        pparam(List(Mod.Contravariant()), "T") :: Nil,
-        Ctor.Primary(
-          Nil,
-          anon,
-          List(tparam("t", "T")) :: Nil
-        ),
-        EmptyTemplate()
-      )
-    }
+    val error =
+      """|<input>:1: error: ] expected but identifier found
+         |class A[`-`T](t: T)
+         |           ^""".stripMargin
+    runTestError[Stat]("class A[`-`T](t: T)", error)
   }
 
   test("contravariant in type") {
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/VarargParameterSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/VarargParameterSuite.scala
index 269c825fb..e58ac7b0a 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/VarargParameterSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/VarargParameterSuite.scala
@@ -106,20 +106,13 @@ class VarargParameterSuite extends ParseSuite {
   }
 
   test("vararg-like parameters") {
-    check(
+    checkError(
       "def obj(fa: Int, fb: Int`*`) = true",
-      Defn.Def(
-        Nil,
-        tname("obj"),
-        Nil,
-        List(List(tparam("fa", "Int"), tparam("fb", Type.Repeated(pname("Int"))))),
-        None,
-        lit(true)
-      )
+      "error: identifier expected but ) found"
     )
     checkError(
       "def obj(fa: Int`*`, fb: Int) = true",
-      "error: *-parameter must come last"
+      "error: identifier expected but , found"
     )
   }
 
@@ -131,7 +124,8 @@ class VarargParameterSuite extends ParseSuite {
     val error = intercept[parsers.ParseException] {
       templStat(definition)
     }
-    assert(error.getMessage.contains(expected))
+    val obtained = error.getMessage
+    assert(obtained.contains(expected), s"got: [$obtained]")
   }
 
 }
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MinorDottySuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MinorDottySuite.scala
index 77c1bbbd7..33626f314 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MinorDottySuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MinorDottySuite.scala
@@ -492,15 +492,11 @@ class MinorDottySuite extends BaseDottySuite {
   }
 
   test("repeated-like-class-parameter") {
-    runTestAssert[Stat]("class Foo(bars: Int`*`)", "class Foo(bars: Int*)")(
-      Defn.Class(
-        Nil,
-        pname("Foo"),
-        Nil,
-        ctorp(tparam("bars", Type.Repeated(pname("Int")))),
-        EmptyTemplate()
-      )
-    )
+    val error =
+      """|<input>:1: error: identifier expected but ) found
+         |class Foo(bars: Int`*`)
+         |                      ^""".stripMargin
+    runTestError[Stat]("class Foo(bars: Int`*`)", error)
   }
 
   test("lazy-abstract-class-value") {
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 97dc57af1..bc4bb8c70 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
@@ -456,14 +456,14 @@ class TypeSuite extends BaseDottySuite {
   test("F[`*`]") {
     // will be deprecated in later versions
     implicit val dialect: Dialect = dialects.Scala31
-    runTestAssert[Type]("F[`*`]", "F[*]") {
-      AnonymousLambda(Apply(TypeName("F"), List(AnonymousParam(None))))
+    runTestAssert[Type]("F[`*`]") {
+      Apply(pname("F"), List(pname("*")))
     }
-    runTestAssert[Type]("F[`+*`]", "F[+*]") {
-      AnonymousLambda(Apply(TypeName("F"), List(AnonymousParam(Some(Mod.Covariant())))))
+    runTestAssert[Type]("F[`+*`]") {
+      Apply(pname("F"), List(pname("+*")))
     }
-    runTestAssert[Type]("F[`-*`]", "F[-*]") {
-      AnonymousLambda(Apply(TypeName("F"), List(AnonymousParam(Some(Mod.Contravariant())))))
+    runTestAssert[Type]("F[`-*`]") {
+      Apply(pname("F"), List(pname("-*")))
     }
   }