Repository: scalameta/scalameta (scala)

Issue:
Support erased definitions syntax
Scalameta should be able to parse `erased` definitions correctly.

e.g.
```scala
class Y(erased x: Int)
```

Links:
- docs: https://dotty.epfl.ch/docs/reference/experimental/erased-defs.html#
- more docs: https://dotty.epfl.ch/docs/reference/experimental/erased-defs-spec.html

Pull request description:
Fixes #3999.

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..61d56e637a 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
@@ -2866,7 +2866,6 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
       isLocal: Boolean = false,
       isParams: Boolean = false
   ): Unit = {
-    val mods = buf.view.drop(buf.length)
     def appendMod: Unit = {
       val mod = modifier(isLocal)
       buf += mod
@@ -2880,12 +2879,15 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
     }
     @tailrec
     def loop: Unit = currToken match {
-      case NonParamsModifier() if isParams =>
       case _: Unquote if continueLoop =>
       case _: AtEOL if !isLocal => next(); loop
       case _: Unquote | _: Ellipsis => appendMod; loop
       case _: KwCase if peekToken.isAny[KwObject, KwClass] => buf += atCurPosNext(Mod.Case())
-      case _ if isModifier(currIndex) => appendMod; loop
+      case _: ModifierKeyword => appendMod; loop
+      case t: Ident if {
+            if (isParams) !peek[Colon] && ParamsModifier.matches(t.value)
+            else isSoftModifier(currIndex)
+          } => appendMod; loop
       case _ =>
     }
     loop
@@ -3020,7 +3022,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
       otherMods()
     }
     val hasExplicitMods = mods.view.drop(numAnnots).exists {
-      case _: Mod.Quasi => false
+      case _: Mod.Quasi | _: Mod.Erased => false
       case m: Mod.Private => m.within.is[Name.Anonymous]
       case m: Mod.Protected => m.within.is[Name.Anonymous]
       case _ => true
@@ -3546,7 +3548,6 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
     next()
 
     val className = typeName()
-    def culprit = Some(s"class $className")
     val typeParams =
       typeParamClauseOpt(ownerIsType = true, allowUnderscore = dialect.allowTypeParamUnderscore)
     val ctor = primaryCtor(if (mods.has[Mod.Case]) OwnedByCaseClass else OwnedByClass)
@@ -3597,7 +3598,6 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
   def objectDef(mods: List[Mod]): Defn.Object = {
     next()
     val objectName = termName()
-    val culprit = s"object $objectName"
     Defn.Object(mods, objectName, templateOpt(OwnedByObject))
   }
 
@@ -3610,7 +3610,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
         ctorModifiers(buf)
       }
       val name = anonNameEmpty()
-      val paramss = termParamClauses(ownerIsType = true, owner == OwnedByCaseClass)
+      val paramss = termParamClauses(ownerIsType = true, ownerIsCase = owner == OwnedByCaseClass)
       Ctor.Primary(mods, name, paramss)
     } else Ctor.Primary(Nil, anonNameEmpty(), Seq.empty[Term.ParamClause])
   }
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala
index 98552e62b9..6f0c3d5ee4 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala
@@ -174,9 +174,10 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
 
   def isModifier(index: Int): Boolean = tokens(index).is[ModifierKeyword] || isSoftModifier(index)
 
-  object NonParamsModifier {
-    def unapply(token: Token): Boolean = token.text match {
-      case soft.KwOpen() | soft.KwOpaque() | soft.KwTransparent() | soft.KwInfix() => true
+  object ParamsModifier {
+    def unapply(token: Token): Boolean = matches(token.text)
+    def matches(text: String): Boolean = text match {
+      case soft.KwInline() | soft.KwErased() | soft.KwTracked() => true
       case _ => false
     }
   }


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ErasedDefsSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ErasedDefsSuite.scala
index 38ff61b9fb..ccebe5aa70 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ErasedDefsSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ErasedDefsSuite.scala
@@ -7,8 +7,22 @@ class ErasedDefsSuite extends BaseDottySuite {
   /**
    * All examples based on dotty documentation:
    *   - [[https://dotty.epfl.ch/docs/reference/experimental/erased-defs.html]]
+   *   - [[https://dotty.epfl.ch/docs/reference/experimental/erased-defs-spec.html]]
    */
 
+  test("class with erased params") {
+    val code = "class ClassWithErasedEv(erased ev: Ev, x: Int) {}"
+    val layout = "class ClassWithErasedEv(erased ev: Ev, x: Int)"
+    val tree = Defn.Class(
+      Nil,
+      pname("ClassWithErasedEv"),
+      Nil,
+      ctorp(tparam(List(Mod.Erased()), "ev", "Ev"), tparam("x", "Int")),
+      tplNoBody()
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
   test("def with erased params") {
     val code = "def methodWithErasedEv(erased ev: Ev, x: Int): Int = x + 2"
     runTestAssert[Stat](code)(Defn.Def(