Repository: scalameta/scalameta (scala)

Issue:
return `Type.AnonymousParam(None)` if type name endsWith `*`
`Main.scala`

```scala
import scala.meta._

object Main {
  def main(args: Array[String]): Unit = {
    val src = Input.String("type A = ***")
    val p = implicitly[parsers.Parse[Stat]]
    println(p.apply(src, dialects.Scala3).get.structure)
  }
}
```

`build.sbt`

```scala
libraryDependencies += "org.scalameta" %% "scalameta" % "4.9.3"

scalaVersion := "2.13.13"
```

scalameta 4.9.3 and 4.9.2

```scala
Defn.Type(Nil, Type.Name("A"), Type.ParamClause(Nil), Type.AnonymousParam(None), Type.Bounds(None, None))
```

scalameta 4.9.1

```scala
Defn.Type(Nil, Type.Name("A"), Type.ParamClause(Nil), Type.Name("***"), Type.Bounds(None, None))
```

Pull request:
Fixes #3672.

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 3cf0da0d36..d28fff7833 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
@@ -1029,9 +1029,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
         next()
         Type.Wildcard(typeBounds())
       }
-      def anonymousParamWithVariant(value: String): Type = {
-        val variant = tparamVariant(value).map(v => atPos(startPos)(v))
-        Type.AnonymousParam(variant)
+      def anonymousParamWithVariant(modOpt: Option[Mod.Variant]): Type = {
+        Type.AnonymousParam(modOpt.map(v => atPos(startPos)(v)))
       }
       def pathSimpleType(): Type = {
         val ref = path()
@@ -1082,11 +1081,11 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
           t.text match {
             case soft.QuestionMarkAsTypeWildcard() => wildcardType()
             case soft.StarAsTypePlaceholder(value) => next(); anonymousParamWithVariant(value)
-            case value @ ("+" | "-")
+            case value @ TParamVariantStr(mod)
                 if (dialect.allowPlusMinusUnderscoreAsIdent || dialect.allowUnderscoreAsTypePlaceholder) &&
                   tryAhead[Underscore] =>
               next() // Ident and Underscore
-              if (dialect.allowUnderscoreAsTypePlaceholder) anonymousParamWithVariant(value)
+              if (dialect.allowUnderscoreAsTypePlaceholder) anonymousParamWithVariant(Some(mod))
               else Type.Name(s"${value}_")
             case _ => pathSimpleType()
           }
@@ -3114,17 +3113,10 @@ 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 t: Ident => tparamVariant(t.text).foreach(buf += atCurPosNext(_))
+    case TParamVariant(mod) => buf += atCurPosNext(mod)
     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,
@@ -4886,6 +4878,18 @@ object ScalametaParser {
   private def syntaxNotExpectedMessage[T <: Token: ClassTag]: String =
     s"not expected `${getTokenName[T]}`"
 
+  private object TParamVariantStr {
+    def unapply(ident: String): Option[Mod.Variant] = ident match {
+      case "+" => Some(Mod.Covariant())
+      case "-" => Some(Mod.Contravariant())
+      case _ => None
+    }
+  }
+
+  private object TParamVariant {
+    def unapply(ident: Token.Ident): Option[Mod.Variant] = TParamVariantStr.unapply(ident.text)
+  }
+
 }
 
 class Location private (
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 eb281317c4..116ed5de49 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
@@ -1,6 +1,7 @@
 package scala.meta.internal.parsers
 
 import scala.meta.Dialect
+import scala.meta.Mod
 
 class SoftKeywords(dialect: Dialect) {
 
@@ -22,9 +23,11 @@ class SoftKeywords(dialect: Dialect) {
   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
+        {
+          case "*" => Some(None)
+          case "+*" => Some(Some(Mod.Covariant()))
+          case "-*" => Some(Some(Mod.Contravariant()))
+          case _ => None
         }
       )
   object QuestionMarkAsTypeWildcard extends IsWithName(dialect.allowQuestionMarkAsTypeWildcard, "?")


Test patch:
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 2ba65d0efe..2b126cb786 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
@@ -679,7 +679,7 @@ class TypeSuite extends BaseDottySuite {
   }
 
   test("#3672 [scala3] ***") {
-    parseAndCheckTree[Type]("***", "*")(Type.AnonymousParam(None))
+    runTestAssert[Type]("***")(pname("***"))
   }
 
 }