Repository: scalameta/scalameta
Pull request #3087 (2023-02-21 15:06:53)

ScalametaParser: validate parameter modifiers
1. check that val must follow explicit param modifiers

2. check there's no repeated implicit modifier on the first parameter; the existing code already checks for repeated modifiers except for the implicit/using modifier on the first parameter because the clause modifier is parsed outside of the `termParam` method.



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 59acd2048..840a1682b 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
@@ -3087,12 +3087,10 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     val mods = new ListBuffer[Mod]
     annotsBuf(mods, skipNewLines = false)
     rejectMod[Mod.Open](mods, "Open modifier only applied to classes")
+    val numAnnots = mods.length
     if (ownerIsType) {
       modifiersBuf(mods, isParams = true)
-      rejectMod[Mod.Lazy](
-        mods,
-        "lazy modifier not allowed here. Use call-by-name parameters instead."
-      )
+      rejectMod[Mod.Lazy](mods, "`lazy' modifier not allowed here, use call-by-name")
       rejectMod[Mod.Sealed](mods, "`sealed' modifier can be used only for classes")
       if (!mods.has[Mod.Override])
         rejectMod[Mod.Abstract](mods, Messages.InvalidAbstract)
@@ -3101,16 +3099,21 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
         mods += autoEndPos(prevTokenPos)(Mod.Inline())
       }
     }
+    val hasExplicitMods = mods.view.drop(numAnnots).exists(!_.is[Mod.Quasi])
+
     mod.foreach { mod =>
       val clazz = mod.getClass
-      if (!mods.exists(_.getClass eq clazz)) mods += mod
+      mods.find(_.getClass eq clazz) match {
+        case None => mods += mod
+        case Some(x) => if (paramIdx == 0) syntaxError("repeated modifier", at = x)
+      }
     }
 
     val varOrVarParamMod = token match {
       case _ if !ownerIsType => None
       case _: KwVal => Some(atCurPosNext(Mod.ValParam()))
       case _: KwVar => Some(atCurPosNext(Mod.VarParam()))
-      case _ => None
+      case _ => if (hasExplicitMods) syntaxErrorExpected[KwVal]; None
     }
     varOrVarParamMod.foreach(mods += _)
 
@@ -3124,13 +3127,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
         case _ => None
       })
       .getOrElse {
-        var anonymousUsing = false
-        val name = if (mod.exists(_.is[Mod.Using]) && !peekToken.is[Colon]) { // anonymous using
-          anonymousUsing = true
-          autoPos(Name.Anonymous())
-        } else {
-          termName().become[Name]
-        }
+        val anonymousUsing = mod.exists(_.is[Mod.Using]) && !peekToken.is[Colon]
+        val name = if (anonymousUsing) autoPos(Name.Anonymous()) else termName().become[Name]
         name match {
           case q: Quasi if endParamQuasi =>
             q.become[Term.Param]
@@ -3143,9 +3141,9 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
                 val tpt = paramType()
                 if (tpt.is[Type.ByName]) {
                   def mayNotBeByName(mod: Mod) =
-                    syntaxError(s"$mod parameters may not be call-by-name", at = name)
+                    syntaxError(s"`$mod' parameters may not be call-by-name", at = name)
                   val isLocalToThis: Boolean =
-                    ownerIsCase || varOrVarParamMod.isEmpty || mods.exists {
+                    (!ownerIsCase && varOrVarParamMod.isEmpty) || mods.exists {
                       case Mod.Private(_: Term.This) => true; case _ => false
                     }
                   val badMod =


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 98f94fc9f..3b9b532ae 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
@@ -778,26 +778,64 @@ class ModSuite extends ParseSuite {
   }
 
   test("missing val after parameter modifier") {
-    assertNoDiff(
-      templStat("class A(implicit b: B, implicit c: C)").syntax,
-      "class A(implicit b: B, implicit c: C)"
-    )
+    val actual = interceptParseError("class A(implicit b: B, implicit c: C)")
+    val expected =
+      s"""|error: val expected but identifier found
+          |class A(implicit b: B, implicit c: C)
+          |                                ^""".stripMargin
+    assert(actual.contains(expected), actual)
   }
 
-  test("repeated parameter modifier") {
-    assertNoDiff(
-      templStat("class A(implicit implicit b: B)").syntax,
-      "class A(implicit implicit b: B)"
-    )
+  test("repeated parameter modifier on first parameter") {
+    val actual = interceptParseError("class A(implicit implicit val b: B)")
+    val expected =
+      s"""|error: repeated modifier
+          |class A(implicit implicit val b: B)
+          |                 ^""".stripMargin
+    assert(actual.contains(expected), actual)
   }
 
   test("repeated parameter modifier on second parameter") {
-    val actual = interceptParseError("class A(implicit b: B, implicit implicit c: C)")
+    val actual = interceptParseError("class A(implicit b: B, implicit implicit val c: C)")
     val expected =
       s"""|error: repeated modifier
-          |class A(implicit b: B, implicit implicit c: C)
+          |class A(implicit b: B, implicit implicit val c: C)
           |                                ^""".stripMargin
     assert(actual.contains(expected), actual)
   }
 
+  test("by-name parameter: class with val") {
+    val actual = interceptParseError("class A(val b: => B)")
+    val expected =
+      s"""|error: `val' parameters may not be call-by-name
+          |class A(val b: => B)
+          |            ^""".stripMargin
+    assert(actual.contains(expected), actual)
+  }
+
+  test("by-name parameter: class with private[this] val") {
+    assertNoDiff(
+      templStat("class A(private[this] val b: => B)").syntax,
+      "class A(private[this] val b: => B)"
+    )
+  }
+
+  test("by-name parameter: case class with val") {
+    val actual = interceptParseError("case class A(val b: => B)")
+    val expected =
+      s"""|error: `val' parameters may not be call-by-name
+          |case class A(val b: => B)
+          |                 ^""".stripMargin
+    assert(actual.contains(expected), actual)
+  }
+
+  test("by-name parameter: class with implicit val") {
+    val actual = interceptParseError("class A(implicit val b: => B)")
+    val expected =
+      s"""|error: `val' parameters may not be call-by-name
+          |class A(implicit val b: => B)
+          |                     ^""".stripMargin
+    assert(actual.contains(expected), actual)
+  }
+
 }
diff --git a/tests/shared/src/test/scala/scala/meta/tests/quasiquotes/SuccessSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/quasiquotes/SuccessSuite.scala
index 3c6fe233e..79f7cb096 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/quasiquotes/SuccessSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/quasiquotes/SuccessSuite.scala
@@ -2127,9 +2127,8 @@ class SuccessSuite extends TreeSuiteBase {
   }
 
   test("1 param\"..mods paramname: tpeopt = expropt\"") {
-    val param"..$mods $paramname: $tpeopt = $expropt" = param"private final x: X = 42"
-    assertEquals(mods.toString, "List(private, final)")
-    assertTrees(mods: _*)(Mod.Private(Name("")), Mod.Final())
+    val param"..$mods $paramname: $tpeopt = $expropt" = param"private final val x: X = 42"
+    assertTrees(mods: _*)(Mod.Private(Name("")), Mod.Final(), Mod.ValParam())
     assertTree(paramname)(Term.Name("x"))
     assertTree(tpeopt)(Some(Type.Name("X")))
     assertTree(expropt)(Some(Lit.Int(42)))