Repository: scalameta/scalameta
Pull request #3529 (2024-02-02 05:00:03)

ScalametaParser: parse colon-space arg as lambda
Previously, we would simply parse it as a regular expression and then, if we encounter a `=>`, convert it to a `Term.Function`.

However, that approach fails when modifiers are used (such as `erased`), so we need to implement a custom parser.

Helps with scalameta/scalafmt#3763.


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 716b588a4..922caff65 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
@@ -509,14 +509,16 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
 
   def summonClassifierFunc[A, B](implicit v: Classifier[A, B]): A => Boolean = v.apply
 
-  def onlyAcceptMod[M <: Mod: ClassTag, T: TokenClassifier](
+  def onlyAcceptMod[M <: Mod: ClassTag, T](
       mods: List[Mod],
       errorMsg: String
-  )(implicit classifier: Classifier[Mod, M]) = {
-    if (token.isNot[T]) {
+  )(implicit classifier: TokenClassifier[T]) =
+    onlyAcceptMod[M](classifier.apply)(mods)(errorMsg)
+
+  def onlyAcceptMod[M <: Mod: ClassTag](ok: Token => Boolean)(mods: List[Mod])(errorMsg: String) =
+    if (!ok(token)) {
       mods.first[M].foreach(m => syntaxError(errorMsg, at = m))
     }
-  }
 
   class InvalidModCombination[M1 <: Mod, M2 <: Mod](m1: M1, m2: M2) {
     def errorMessage: String = Messages.IllegalCombinationModifiers(m1, m2)
@@ -2405,11 +2407,58 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
      * ```
      * Without manual handling here, filter would be included for `(a+1).filter`
      */
-    val param = simpleExpr(allowRepeated = false)
-    val contextFunction = token.is[ContextArrow]
-    if ((contextFunction || token.is[RightArrow]) && isIndentAfter())
-      convertToParamClause(param)(true, true).map { pc =>
-        val params = autoEndPos(paramPos)(pc)
+    @tailrec
+    def getParamClause(pt: Option[Mod.ParamsType], mods: List[Mod]): Term.ParamClause =
+      token match {
+        case _: KwImplicit if pt.isEmpty =>
+          val mod = atCurPosNext(Mod.Implicit())
+          getParamClause(Some(mod), mod :: mods)
+        case _ =>
+          commaSeparated(getParamWithPos(mods, fullTypeOK = true)).reduceWith(toParamClause(pt))
+      }
+    def getParamAsClause(
+        pt: Option[Mod.ParamsType],
+        mods: List[Mod]
+    ): Option[Term.ParamClause] = {
+      val param = getParamWithPos(mods, fullTypeOK = false)
+      Some(copyPos(param)(reduceAs(param :: Nil, toParamClause(pt))))
+    }
+    def getParamWithPos(mods: List[Mod], fullTypeOK: Boolean) =
+      autoPos(getParam(mods, fullTypeOK))
+    @tailrec
+    def getParam(mods: List[Mod], fullTypeOK: Boolean): Term.Param = {
+      def afterName(name: Name) =
+        Term.Param(mods, name, getDeclTpeOpt(fullTypeOK = fullTypeOK), None)
+      token match {
+        case t: Ellipsis => ellipsis[Term.Param](t, 1)
+        case t: Ident =>
+          val mod = if (peekToken.is[Ident]) {
+            t.text match {
+              case soft.KwErased() => atCurPosNext(Mod.Erased())
+              case soft.KwUsing() => atCurPosNext(Mod.Using())
+              case _ => null
+            }
+          } else null
+          if (mod eq null) afterName(termName(t)) else getParam(mod :: mods, fullTypeOK)
+        case _: Underscore => afterName(namePlaceholder())
+        case _ => syntaxErrorExpected[Ident]
+      }
+    }
+
+    val paramClauseOpt = token match {
+      case _: LeftParen =>
+        Some(autoPos(inParensOnOpenOr(getParamClause(None, Nil))(Term.ParamClause(Nil))))
+      case t: Ellipsis if t.rank == 2 => Some(ellipsis[Term.ParamClause](t))
+      case _: Ident | _: Underscore | _: Ellipsis => getParamAsClause(None, Nil)
+      case _: KwImplicit =>
+        val mod = atCurPosNext(Mod.Implicit())
+        getParamAsClause(Some(mod), mod :: Nil)
+      case _ => None
+    }
+
+    paramClauseOpt.flatMap { params =>
+      val contextFunction = token.is[ContextArrow]
+      if ((contextFunction || token.is[RightArrow]) && isIndentAfter()) Some {
         next()
         val trm = blockExprOnIndent()
         autoEndPos(paramPos) {
@@ -2419,7 +2468,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
             Term.Function(params, trm)
         }
       }
-    else None
+      else None
+    }
   }.getOrElse(None)
 
   private def getFewerBracesApplyOnColon(fun: Term, startPos: Int): Option[Term] = {
@@ -3569,7 +3619,9 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
       enumCaseAllowed: Boolean = false,
       secondaryConstructorAllowed: Boolean = false
   ): Stat = {
-    onlyAcceptMod[Mod.Lazy, KwVal](mods, "lazy not allowed here. Only vals can be lazy")
+    onlyAcceptMod[Mod.Lazy](_.isAny[KwVal, KwGiven])(mods)(
+      "lazy not allowed here. Only val/given can be lazy."
+    )
     onlyAcceptMod[Mod.Opaque, KwType](mods, "opaque not allowed here. Only types can be opaque.")
     token match {
       case KwVal() | KwVar() =>


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/FewerBracesSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/FewerBracesSuite.scala
index c4712f467..03a47fd70 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/FewerBracesSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/FewerBracesSuite.scala
@@ -1618,11 +1618,24 @@ class FewerBracesSuite extends BaseDottySuite {
         |   foo: implicit bar =>
         |      baz
         |""".stripMargin
-    val error =
-      """|<input>:3: error: identifier expected but implicit found
-         |   foo: implicit bar =>
-         |        ^""".stripMargin
-    runTestError[Stat](code, error)
+    val layout = "def a = foo { implicit bar => baz }"
+    val tree = Defn.Def(
+      Nil,
+      tname("a"),
+      Nil,
+      None,
+      Term.Apply(
+        tname("foo"),
+        Term.Function(
+          Term.ParamClause(
+            List(tparam(List(Mod.Implicit()), "bar")),
+            Some(Mod.Implicit())
+          ),
+          tname("baz")
+        ) :: Nil
+      )
+    )
+    runTestAssert[Stat](code, layout)(tree)
   }
 
   test("scalafmt #3763 implicit after space, with type") {
@@ -1632,11 +1645,24 @@ class FewerBracesSuite extends BaseDottySuite {
         |   foo: implicit bar: Int =>
         |      baz
         |""".stripMargin
-    val error =
-      """|<input>:3: error: identifier expected but implicit found
-         |   foo: implicit bar: Int =>
-         |        ^""".stripMargin
-    runTestError[Stat](code, error)
+    val layout = "def a = foo { implicit bar: Int => baz }"
+    val tree = Defn.Def(
+      Nil,
+      tname("a"),
+      Nil,
+      None,
+      Term.Apply(
+        tname("foo"),
+        Term.Function(
+          Term.ParamClause(
+            List(tparam(List(Mod.Implicit()), "bar", "Int")),
+            Some(Mod.Implicit())
+          ),
+          tname("baz")
+        ) :: Nil
+      )
+    )
+    runTestAssert[Stat](code, layout)(tree)
   }
 
   test("scalafmt #3763 implicit after newline") {
@@ -1741,11 +1767,24 @@ class FewerBracesSuite extends BaseDottySuite {
         |   foo: using bar =>
         |      baz
         |""".stripMargin
-    val error =
-      """|<input>:3: error: identifier expected but => found
-         |   foo: using bar =>
-         |                  ^""".stripMargin
-    runTestError[Stat](code, error)
+    val layout = "def a = foo { (using bar) => baz }"
+    val tree = Defn.Def(
+      Nil,
+      tname("a"),
+      Nil,
+      None,
+      Term.Apply(
+        tname("foo"),
+        Term.Function(
+          Term.ParamClause(
+            List(tparam(List(Mod.Using()), "bar")),
+            Some(Mod.Using())
+          ),
+          tname("baz")
+        ) :: Nil
+      )
+    )
+    runTestAssert[Stat](code, layout)(tree)
   }
 
   test("scalafmt #3763 using after space, with type") {
@@ -1755,11 +1794,24 @@ class FewerBracesSuite extends BaseDottySuite {
         |   foo: using bar: Int =>
         |      baz
         |""".stripMargin
-    val error =
-      """|<input>:3: error: identifier expected but : found
-         |   foo: using bar: Int =>
-         |                 ^""".stripMargin
-    runTestError[Stat](code, error)
+    val layout = "def a = foo { (using bar: Int) => baz }"
+    val tree = Defn.Def(
+      Nil,
+      tname("a"),
+      Nil,
+      None,
+      Term.Apply(
+        tname("foo"),
+        Term.Function(
+          Term.ParamClause(
+            List(tparam(List(Mod.Using()), "bar", "Int")),
+            Some(Mod.Using())
+          ),
+          tname("baz")
+        ) :: Nil
+      )
+    )
+    runTestAssert[Stat](code, layout)(tree)
   }
 
   test("scalafmt #3763 using after newline") {
@@ -2083,11 +2135,18 @@ class FewerBracesSuite extends BaseDottySuite {
         |   foo: erased bar =>
         |      baz
         |""".stripMargin
-    val error =
-      """|<input>:3: error: identifier expected but => found
-         |   foo: erased bar =>
-         |                   ^""".stripMargin
-    runTestError[Stat](code, error)
+    val layout = "def a = foo(erased bar => baz)"
+    val tree = Defn.Def(
+      Nil,
+      tname("a"),
+      Nil,
+      None,
+      Term.Apply(
+        tname("foo"),
+        Term.Function(List(tparam(List(Mod.Erased()), "bar")), tname("baz")) :: Nil
+      )
+    )
+    parseAndCheckTree[Stat](code, layout)(tree)
   }
 
   test("scalafmt #3763 erased after space, with type") {
@@ -2097,11 +2156,21 @@ class FewerBracesSuite extends BaseDottySuite {
         |   foo: erased bar: Int =>
         |      baz
         |""".stripMargin
-    val error =
-      """|<input>:3: error: identifier expected but : found
-         |   foo: erased bar: Int =>
-         |                  ^""".stripMargin
-    runTestError[Stat](code, error)
+    val layout = "def a = foo((erased bar: Int) => baz)"
+    val tree = Defn.Def(
+      Nil,
+      tname("a"),
+      Nil,
+      None,
+      Term.Apply(
+        tname("foo"),
+        Term.Function(
+          List(tparam(List(Mod.Erased()), "bar", "Int")),
+          tname("baz")
+        ) :: Nil
+      )
+    )
+    runTestAssert[Stat](code, layout)(tree)
   }
 
   test("scalafmt #3763 erased after newline") {
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/GivenUsingSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/GivenUsingSuite.scala
index 275841ccc..008791e99 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/GivenUsingSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/GivenUsingSuite.scala
@@ -1152,4 +1152,11 @@ class GivenUsingSuite extends BaseDottySuite {
       )
     )
   }
+
+  test("lazy given") {
+    val code = "lazy given foo: Foo = ???"
+    val tree = Defn.GivenAlias(List(Mod.Lazy()), tname("foo"), None, pname("Foo"), tname("???"))
+    runTestAssert[Stat](code)(tree)
+  }
+
 }
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 97ef11f32..e13b823d7 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
@@ -553,7 +553,7 @@ class MinorDottySuite extends BaseDottySuite {
     )
     runTestError[Stat](
       "trait Foo { protected[this] lazy var from: Int }",
-      "lazy not allowed here. Only vals can be lazy"
+      "lazy not allowed here. Only val/given can be lazy"
     )
   }