Repository: scalameta/scalameta
Pull request #3070 (2023-02-06 18:13:39)

ast: move Version into ReplacedField, sort
Helps with #3067.


Patch:
diff --git a/project/Mima.scala b/project/Mima.scala
index 4e2344e41..9a45fec39 100644
--- a/project/Mima.scala
+++ b/project/Mima.scala
@@ -20,7 +20,7 @@ object Mima {
     val public = ref.isPublic && notScopedPrivate
     def include = fullName.startsWith("scala.meta.")
     def exclude = fullName.contains(".internal.") || fullName.contains(".contrib.") ||
-      fullName.contains("#internal#")
+      fullName.contains("$internal$")
     public && include && !exclude
   }
 
diff --git a/scalameta/common/shared/src/main/scala/org/scalameta/adt/Liftables.scala b/scalameta/common/shared/src/main/scala/org/scalameta/adt/Liftables.scala
index e916705b8..ae82b169f 100644
--- a/scalameta/common/shared/src/main/scala/org/scalameta/adt/Liftables.scala
+++ b/scalameta/common/shared/src/main/scala/org/scalameta/adt/Liftables.scala
@@ -73,7 +73,7 @@ class LiftableMacros(val c: Context) extends AdtReflection {
           // in the current scope. The resulting type inference error (see above) can be fixed by providing an explicit type in the local definition for stats.
           // q"$u.AssignOrNamedArg($fieldName, $fieldValue)"
           }
-          val namePath = getNamePath(nameParts.toSeq ++ Seq("internal", "Impl"))
+          val namePath = getNamePath(nameParts.toSeq ++ Seq("internal", "Latest"))
           q"""$u.Apply($namePath, $args)"""
         } else getNamePath(nameParts)
         q"def $defName($localName: ${adt.tpe}): $u.Tree = $body"
diff --git a/scalameta/common/shared/src/main/scala/scala/meta/internal/prettyprinters/Enquote.scala b/scalameta/common/shared/src/main/scala/scala/meta/internal/prettyprinters/Enquote.scala
index 65cf26491..1ab5d054d 100644
--- a/scalameta/common/shared/src/main/scala/scala/meta/internal/prettyprinters/Enquote.scala
+++ b/scalameta/common/shared/src/main/scala/scala/meta/internal/prettyprinters/Enquote.scala
@@ -2,6 +2,8 @@ package scala.meta
 package internal
 package prettyprinters
 
+import scala.annotation.tailrec
+
 sealed trait QuoteStyle
 case object SingleQuotes extends QuoteStyle { override def toString = "'" }
 case object DoubleQuotes extends QuoteStyle { override def toString = "\"" }
@@ -9,10 +11,21 @@ case object TripleQuotes extends QuoteStyle { override def toString = "\"\"\"" }
 
 object enquote {
   def apply(s: String, style: QuoteStyle): String = {
-    val sb = new StringBuilder(style.toString)
-    if (style == TripleQuotes)
-      sb.append(s)
-    else {
+    val styleStr = style.toString
+    val sb = new java.lang.StringBuilder(styleStr)
+    if (style == TripleQuotes) {
+      val styleLen = styleStr.length
+      @tailrec def iter(off: Int): Unit = {
+        val newoff = s.indexOf(styleStr, off)
+        if (newoff < 0) sb.append(s, off, s.length)
+        else {
+          sb.append(s, off, newoff)
+          sb.append("\\\"\\\"\\\"")
+          iter(newoff + styleLen)
+        }
+      }
+      iter(0)
+    } else {
       s.foreach {
         case '\t' => sb.append("\\t")
         case '\b' => sb.append("\\b")
@@ -30,7 +43,7 @@ object enquote {
           else sb.append(c)
       }
     }
-    sb.append(style.toString)
+    sb.append(styleStr)
     sb.toString
   }
 }
diff --git a/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/ast.scala b/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/ast.scala
index 420b24b90..8bb31b28e 100644
--- a/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/ast.scala
+++ b/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/ast.scala
@@ -54,7 +54,7 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
         val iparents1 = ListBuffer[Tree]() ++ iparents
         def parents1 = List(tq"$iname")
         val mstats1 = ListBuffer[Tree]() ++ mstats
-        val mstats2 = ListBuffer[Tree]()
+        val mstatsLatest = ListBuffer[Tree]()
         val manns1 = ListBuffer[Tree]() ++ mmods.annotations
         def mmods1 = mmods.mapAnnotations(_ => manns1.toList)
         val quasiCopyExtraParamss = ListBuffer[List[ValDef]]()
@@ -76,8 +76,8 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
         // step 1a: identify modified fields of the class
         val versionedParams = if (isQuasi) Nil else getVersionedParams(params, stats)
         val paramsVersions = versionedParams.flatMap(_.getVersions).distinct.sorted(versionOrdering)
-        val replacedFields = versionedParams.flatMap(_.replaced.flatMap { case (version, field) =>
-          field.oldDefs.map { case (oldDef, _) => version -> oldDef }
+        val replacedFields = versionedParams.flatMap(_.replaced.flatMap { field =>
+          field.oldDefs.map { case (oldDef, _) => field.version -> oldDef }
         })
         def paramsForVersion(v: Version): List[ValDef] =
           positionVersionedParams(versionedParams.flatMap(_.getApplyDeclDefnBefore(v)._1))
@@ -343,7 +343,7 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
             ..$internalBody
           }
         """
-        mstats2 += q"""
+        mstatsLatest += q"""
           @$InlineAnnotation def apply(..$applyParams): $iname = $mname.apply(..$internalArgs)
         """
 
@@ -353,21 +353,16 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
         // apply(A, B, C), apply(A, B, C, D), apply(A, B, C, D, E)
         paramsVersions.foreach { v =>
           val applyParamsBuilder = List.newBuilder[(ValDef, Int)]
-          val applyCastBuilder = List.newBuilder[ValDef]
+          val applyBodyBuilder = List.newBuilder[Tree]
           versionedParams.foreach { vp =>
             val (decl, defn) = vp.getApplyDeclDefnBefore(v)
             decl.foreach(applyParamsBuilder += _)
-            defn.foreach(applyCastBuilder += _)
+            defn.foreach(applyBodyBuilder += _)
           }
           val params = positionVersionedParams(applyParamsBuilder.result())
-          val castFields = applyCastBuilder.result()
-          val anno = getDeprecatedAnno(v)
-          mstats1 += q"""
-            @$anno def apply(..$params): $iname = {
-              ..$castFields
-              $mname.apply(..$internalArgs)
-            }
-          """
+          applyBodyBuilder += q"$mname.apply(..$internalArgs)"
+          val applyBody = applyBodyBuilder.result()
+          mstats1 += q"@${getDeprecatedAnno(v)} def apply(..$params): $iname = { ..$applyBody }"
         }
 
         // step 14: generate Companion.unapply
@@ -385,15 +380,17 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
             """
           }
           if (params.nonEmpty) {
+            val latestTree = getUnapply(params)
             mstats1 += paramsVersions.headOption.fold {
-              getUnapply(params)
+              latestTree
             } { ver =>
               getUnapply(paramsForVersion(ver), getDeprecatedAnno(ver))
             }
-            mstats2 += getUnapply(params)
+            mstatsLatest += latestTree
           } else {
-            mstats1 += q"@$InlineAnnotation final def unapply(x: $iname): $BooleanClass = true"
-            mstats2 += q"@$InlineAnnotation final def unapply(x: $iname): $BooleanClass = true"
+            val tree = q"@$InlineAnnotation final def unapply(x: $iname): $BooleanClass = true"
+            mstats1 += tree
+            mstatsLatest += tree
           }
         }
 
@@ -426,8 +423,8 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
 
         mstats1 += q"""
           object internal { // to be ignored by Mima
-            object Impl {
-              ..$mstats2
+            object Latest {
+              ..$mstatsLatest
             }
           }
         """
@@ -501,38 +498,36 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
   private class VersionedParam(
       val param: ValDef,
       val appended: Option[Version],
-      val replaced: Map[Version, ReplacedField]
+      val replaced: Seq[ReplacedField]
   ) {
     appended.foreach { aver =>
-      replaced.foreach { case (rver, rfield) =>
-        if (versionOrdering.lteq(rver, aver)) {
+      replaced.headOption.foreach { rfield =>
+        if (versionOrdering.lteq(rfield.version, aver)) {
           val oldDef = rfield.oldDefs.head._1
           c.abort(
             param.pos,
             s"${versionToString(aver)} [@newField for ${param.name}] must must precede " +
-              s"${versionToString(rver)} [@replacedField for ${oldDef.name}]"
+              s"${versionToString(rfield.version)} [@replacedField for ${oldDef.name}]"
           )
         }
       }
     }
 
-    def getVersions: Iterable[Version] = appended ++ replaced.keys
+    def getVersions: Iterable[Version] = appended ++ replaced.map(_.version)
     def getApplyDeclDefnBefore(version: Version): (List[(ValDef, Int)], Option[ValDef]) = {
       def checkVersion(ver: Version): Boolean = versionOrdering.lteq(version, ver)
       if (appended.exists(checkVersion)) (Nil, Some(asValDefn(param)))
-      else {
-        val records = replaced.iterator.filter(x => checkVersion(x._1))
-        if (records.isEmpty) (asValDecl(param) -> -1 :: Nil, None)
-        else {
-          val rfield = records.minBy(_._1)(versionOrdering)._2
-          val decls = rfield.oldDefs.map { case (oldDef, pos) => asValDecl(oldDef) -> pos }
-          (decls, Some(rfield.newValDefn))
-        }
-      }
+      else
+        replaced
+          .find(x => checkVersion(x.version))
+          .map { rfield =>
+            val decls = rfield.oldDefs.map { case (oldDef, pos) => asValDecl(oldDef) -> pos }
+            (decls, Some(rfield.newValDefn))
+          }
+          .getOrElse((asValDecl(param) -> -1 :: Nil, None))
     }
     def getDefaultCopyDef(): List[(ValOrDefDef, Int)] = {
-      if (replaced.isEmpty) (param, -1) :: Nil
-      else replaced.minBy(_._1)(versionOrdering)._2.oldDefs
+      replaced.headOption.map(_.oldDefs).getOrElse((param, -1) :: Nil)
     }
   }
 
@@ -559,11 +554,10 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
       stats: List[Tree]
   ): List[VersionedParam] = {
     val appendedFields: Map[String, Version] = getNewFieldVersions(params)
-    val replacedFields: Map[String, Map[Version, ReplacedField]] =
-      ReplacedField.getMap(params, stats)
+    val replacedFields: Map[String, Seq[ReplacedField]] = ReplacedField.getMap(params, stats)
     params.map { p =>
       val pname = p.name.toString
-      new VersionedParam(p, appendedFields.get(pname), replacedFields.getOrElse(pname, Map.empty))
+      new VersionedParam(p, appendedFields.get(pname), replacedFields.getOrElse(pname, Seq.empty))
     }
   }
 
@@ -611,6 +605,7 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
   }
 
   private class ReplacedField(
+      val version: Version,
       val newVal: ValDef,
       ctor: Tree,
       val oldDefs: List[(ValOrDefDef, Int)]
@@ -651,7 +646,7 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
     def getMap(
         params: List[ValDef],
         stats: List[Tree]
-    ): Map[String, Map[Version, ReplacedField]] = {
+    ): Map[String, Seq[ReplacedField]] = {
       val fields: Map[String, (ValDef, Map[Version, Tree])] = params.map { p =>
         val ctorsByVersion = p.mods.annotations.collect {
           case q"new replacesFields($since, $ctor)" =>
@@ -681,11 +676,12 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
           k,
           c.abort(v.head._2._1.pos, s"@replacedField: field `$k` is undefined)")
         )
-        k -> v.map(_._2).groupBy(_._2).map { case (ver, oldFields) =>
+        val replacements = v.map(_._2).groupBy(_._2).toSeq.map { case (ver, oldFields) =>
           val ctor = ctorsByVersion.get(ver).orNull
           val oldDefs = oldFields.map { case (oldField, _, pos) => (oldField, pos) }
-          ver -> new ReplacedField(newVal, ctor, oldDefs)
+          new ReplacedField(ver, newVal, ctor, oldDefs)
         }
+        k -> replacements.sortBy(_.version)(versionOrdering)
       }
     }
 
diff --git a/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala b/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
index 9fc09dfd3..c08ec1754 100644
--- a/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
+++ b/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
@@ -153,6 +153,8 @@ final class Dialect private (
     val useInfixTypePrecedence: Boolean,
     // Scala213Source3 and Scala3 allow infix operator being placed after nl
     val allowInfixOperatorAfterNL: Boolean,
+    // Scala 3 allows `def f[X](x: X)[Y](y: Y)`
+    val allowParamClauseInterleaving: Boolean,
     /* Scala 3 allows dropping braces for block arguments such as `list.map: a =>`
      * It wasn't available in Scala 3.0 and got introduced later.
      */
@@ -248,6 +250,7 @@ final class Dialect private (
       allowGivenImports = false,
       useInfixTypePrecedence = false,
       allowInfixOperatorAfterNL = false,
+      allowParamClauseInterleaving = false,
       allowFewerBraces = false
       // NOTE(olafur): declare the default value for new fields above this comment.
     )
@@ -462,6 +465,10 @@ final class Dialect private (
     privateCopy(allowInfixOperatorAfterNL = newValue)
   }
 
+  def withAllowParamClauseInterleaving(newValue: Boolean): Dialect = {
+    privateCopy(allowParamClauseInterleaving = newValue)
+  }
+
   def withAllowFewerBraces(newValue: Boolean): Dialect = {
     privateCopy(allowFewerBraces = newValue)
   }
@@ -531,6 +538,7 @@ final class Dialect private (
       allowGivenImports: Boolean = this.allowGivenImports,
       useInfixTypePrecedence: Boolean = this.useInfixTypePrecedence,
       allowInfixOperatorAfterNL: Boolean = this.allowInfixOperatorAfterNL,
+      allowParamClauseInterleaving: Boolean = this.allowParamClauseInterleaving,
       allowFewerBraces: Boolean = this.allowFewerBraces
       // NOTE(olafur): add the next parameter above this comment.
   ): Dialect = {
@@ -594,6 +602,7 @@ final class Dialect private (
       allowGivenImports,
       useInfixTypePrecedence,
       allowInfixOperatorAfterNL,
+      allowParamClauseInterleaving,
       allowFewerBraces
       // NOTE(olafur): add the next argument above this comment.
     )
@@ -677,6 +686,8 @@ final class Dialect private (
       && this.allowGivenImports == that.allowGivenImports
       && this.useInfixTypePrecedence == that.useInfixTypePrecedence
       && this.allowInfixOperatorAfterNL == that.allowInfixOperatorAfterNL
+      && this.allowParamClauseInterleaving == that.allowParamClauseInterleaving
+      && this.allowFewerBraces == that.allowFewerBraces
   )
 
   @deprecated("Use withX method instead", "4.3.11")
diff --git a/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala b/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala
index 404d25cc2..37d70408a 100644
--- a/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala
+++ b/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala
@@ -150,7 +150,9 @@ package object dialects {
 
   implicit val Scala32 = Scala31
 
-  implicit val Scala33 = Scala32.withAllowFewerBraces(true)
+  implicit val Scala33 = Scala32
+    .withAllowFewerBraces(true)
+    .withAllowParamClauseInterleaving(true)
 
   implicit val Scala3 = Scala33
 
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 319545fca..f511165b7 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
@@ -2974,6 +2974,41 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     case _ =>
   }
 
+  def memberParamClauseGroups(
+      ownerIsType: Boolean,
+      ctxBoundsAllowed: Boolean
+  ): List[Member.ParamClauseGroup] =
+    listBy[Member.ParamClauseGroup] { groups =>
+      @tailrec def iter(): Unit =
+        getAfterOptNewLine(token match {
+          case _: LeftParen =>
+            val tparams = emptyTypeParams
+            Some(autoPos {
+              termParamClausesOnParen(ownerIsType, ellipsisMaxRank = 3) match {
+                case (x: Quasi) :: Nil if x.rank == 2 => reellipsis[Member.ParamClauseGroup](x, 1)
+                case x => Member.ParamClauseGroup(tparams, x)
+              }
+            })
+          case _: LeftBracket =>
+            Some(autoPos {
+              val tparamClause = typeParamClauseOnBracket(ownerIsType, ctxBoundsAllowed)
+              val paramClauses = termParamClauses(ownerIsType)
+              Member.ParamClauseGroup(tparamClause, paramClauses)
+            })
+          case _ => None
+        }) match {
+          case Some(group) =>
+            groups += group
+            // can't have consecutive type clauses (so params must be present)
+            // also, only the very last param may contain implicit
+            val ok = group.is[Quasi] || group.paramClauses.lastOption
+              .exists { x => x.is[Quasi] || !x.mod.exists(_.is[Mod.Implicit]) }
+            if (ok) iter()
+          case _ =>
+        }
+      iter()
+    }
+
   def termParamClauses(
       ownerIsType: Boolean,
       ownerIsCase: Boolean = false
@@ -2984,7 +3019,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
 
   private def termParamClausesOnParen(
       ownerIsType: Boolean,
-      ownerIsCase: Boolean = false
+      ownerIsCase: Boolean = false,
+      ellipsisMaxRank: Int = 2
   ): List[Term.ParamClause] = {
     var hadModImplicit = false
     def paramClause(first: Boolean) = autoPos(inParensOnOpenOr {
@@ -2996,7 +3032,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
       def parseParams(mod: Option[Mod.ParamsType] = None) =
         reduceParams(commaSeparated(termParam(ownerIsCase && first, ownerIsType, mod = mod)), mod)
       token match {
-        case t @ Ellipsis(2) =>
+        case t @ Ellipsis(rank) if rank >= 2 && rank <= ellipsisMaxRank =>
           reduceParams(List(ellipsis[Term.Param](t)))
         case _: KwImplicit =>
           hadModImplicit = true
@@ -3594,11 +3630,15 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
         syntaxError(s"Procedure syntax is not supported. $hint", at = name)
     }
 
-    val paramClauses = {
+    def nonInterleavedParamClauses = {
       val tparams = typeParamClauseOpt(ownerIsType = false, ctxBoundsAllowed = true)
       val paramss = termParamClauses(ownerIsType = false)
       getParamClauseGroup(tparams, paramss)
     }
+    val paramClauses: List[Member.ParamClauseGroup] =
+      if (dialect.allowParamClauseInterleaving)
+        memberParamClauseGroups(ownerIsType = false, ctxBoundsAllowed = true)
+      else nonInterleavedParamClauses.toList
 
     val restype = ReturnTypeContext.within(typedOpt())
     if (restype.isEmpty && isAfterOptNewLine[LeftBrace]) {
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala b/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
index 17cbab64b..60619d6c0 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
@@ -43,6 +43,9 @@ object Tree extends InternalTreeXtensions {
     final def paramClauses: Seq[Term.ParamClause] =
       paramClauseGroup.fold(Seq.empty[Term.ParamClause])(_.paramClauses)
   }
+  @branch trait WithParamClauseGroups extends Tree {
+    def paramClauseGroups: List[Member.ParamClauseGroup]
+  }
 
 }
 
@@ -475,11 +478,19 @@ object Member {
         paramClauseGroup: Option[ParamClauseGroup]
     ): List[sm.Type.Param] =
       paramClauseGroup.fold(List.empty[sm.Type.Param])(_.tparamClause.values)
+    private[meta] def toTparams(
+        paramClauseGroups: List[ParamClauseGroup]
+    ): List[sm.Type.Param] =
+      toTparams(paramClauseGroups.headOption)
 
     private[meta] def toParamss(
         paramClauseGroup: Option[ParamClauseGroup]
     ): List[List[sm.Term.Param]] =
       paramClauseGroup.fold(List.empty[List[sm.Term.Param]])(_.paramClauses.map(_.values))
+    private[meta] def toParamss(
+        paramClauseGroups: List[ParamClauseGroup]
+    ): List[List[sm.Term.Param]] =
+      toParamss(paramClauseGroups.headOption)
   }
 
   private[meta] object ParamClauseGroupCtor {
@@ -500,6 +511,18 @@ object Member {
       ParamClauseGroupCtor(tparams, sparams)
   }
 
+  private[meta] object ParamClauseGroupsCtor {
+    def apply(
+        tparams: List[sm.Type.Param],
+        paramss: List[List[sm.Term.Param]]
+    ): List[ParamClauseGroup] =
+      if (tparams.isEmpty && paramss.isEmpty) Nil
+      else ParamClauseGroup(tparamClause = tparams, paramClauses = paramss) :: Nil
+
+    def apply(paramClauseGroup: Option[ParamClauseGroup]): List[ParamClauseGroup] =
+      paramClauseGroup.toList
+  }
+
   @branch trait ArgClause extends SyntaxValuesClause {
     def values: List[Tree]
   }
@@ -532,18 +555,23 @@ object Decl {
   @ast class Def(
       mods: List[Mod],
       name: Term.Name,
-      @replacesFields("4.6.0", Member.ParamClauseGroupCtor)
-      paramClauseGroup: Option[Member.ParamClauseGroup],
+      @replacesFields("4.6.0", Member.ParamClauseGroupsCtor)
+      @replacesFields("4.7.3", Member.ParamClauseGroupsCtor)
+      paramClauseGroups: List[Member.ParamClauseGroup],
       decltpe: sm.Type
   ) extends Decl
       with Member.Term
       with Stat.WithMods
       with Tree.WithParamClauseGroup
+      with Tree.WithParamClauseGroups
       with Tree.WithDeclTpe {
     @replacedField("4.6.0", pos = 2) final def tparams: List[sm.Type.Param] =
-      Member.ParamClauseGroup.toTparams(paramClauseGroup)
+      Member.ParamClauseGroup.toTparams(paramClauseGroups)
     @replacedField("4.6.0", pos = 3) final def paramss: List[List[Term.Param]] =
-      Member.ParamClauseGroup.toParamss(paramClauseGroup)
+      Member.ParamClauseGroup.toParamss(paramClauseGroups)
+
+    @replacedField("4.7.3") final def paramClauseGroup: Option[Member.ParamClauseGroup] =
+      paramClauseGroups.headOption
   }
 
   @ast class Type(
@@ -675,39 +703,49 @@ object Defn {
   @ast class Def(
       mods: List[Mod],
       name: Term.Name,
-      @replacesFields("4.6.0", Member.ParamClauseGroupCtor)
-      paramClauseGroup: Option[Member.ParamClauseGroup],
+      @replacesFields("4.6.0", Member.ParamClauseGroupsCtor)
+      @replacesFields("4.7.3", Member.ParamClauseGroupsCtor)
+      paramClauseGroups: List[Member.ParamClauseGroup],
       decltpe: Option[sm.Type],
       body: Term
   ) extends Defn
       with Member.Term
       with Stat.WithMods
       with Tree.WithParamClauseGroup
+      with Tree.WithParamClauseGroups
       with Tree.WithDeclTpeOpt
       with Tree.WithBody {
     @replacedField("4.6.0", pos = 2) final def tparams: List[sm.Type.Param] =
-      Member.ParamClauseGroup.toTparams(paramClauseGroup)
+      Member.ParamClauseGroup.toTparams(paramClauseGroups)
     @replacedField("4.6.0", pos = 3) final def paramss: List[List[Term.Param]] =
-      Member.ParamClauseGroup.toParamss(paramClauseGroup)
+      Member.ParamClauseGroup.toParamss(paramClauseGroups)
+
+    @replacedField("4.7.3") final def paramClauseGroup: Option[Member.ParamClauseGroup] =
+      paramClauseGroups.headOption
   }
 
   @ast class Macro(
       mods: List[Mod],
       name: Term.Name,
-      @replacesFields("4.6.0", Member.ParamClauseGroupCtor)
-      paramClauseGroup: Option[Member.ParamClauseGroup],
+      @replacesFields("4.6.0", Member.ParamClauseGroupsCtor)
+      @replacesFields("4.7.3", Member.ParamClauseGroupsCtor)
+      paramClauseGroups: List[Member.ParamClauseGroup],
       decltpe: Option[sm.Type],
       body: Term
   ) extends Defn
       with Member.Term
       with Stat.WithMods
       with Tree.WithParamClauseGroup
+      with Tree.WithParamClauseGroups
       with Tree.WithDeclTpeOpt
       with Tree.WithBody {
     @replacedField("4.6.0", pos = 2) final def tparams: List[sm.Type.Param] =
-      Member.ParamClauseGroup.toTparams(paramClauseGroup)
+      Member.ParamClauseGroup.toTparams(paramClauseGroups)
     @replacedField("4.6.0", pos = 3) final def paramss: List[List[Term.Param]] =
-      Member.ParamClauseGroup.toParamss(paramClauseGroup)
+      Member.ParamClauseGroup.toParamss(paramClauseGroups)
+
+    @replacedField("4.7.3") final def paramClauseGroup: Option[Member.ParamClauseGroup] =
+      paramClauseGroups.headOption
   }
 
   @ast class Type(
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 2bb1e431d..e97b9c74c 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
@@ -792,7 +792,7 @@ object TreeSyntax {
         s(w(t.mods, " "), kw("var"), " ", r(t.pats, ", "), kw(":"), " ", t.decltpe)
       case t: Decl.Type => s(w(t.mods, " "), kw("type"), " ", t.name, t.tparamClause, t.bounds)
       case t: Decl.Def =>
-        s(w(t.mods, " "), kw("def "), t.name, o(t.paramClauseGroup), kw(": "), t.decltpe)
+        s(w(t.mods, " "), kw("def "), t.name, t.paramClauseGroups, kw(": "), t.decltpe)
       case t: Decl.Given =>
         s(w(t.mods, " "), kw("given "), t.name, o(t.paramClauseGroup), kw(": "), t.decltpe)
       case t: Defn.Val =>
@@ -865,9 +865,9 @@ object TreeSyntax {
         s(kw("extension"), " ", o(t.paramClauseGroup), m)
       case t: Defn.Object => r(" ")(t.mods, kw("object"), t.name, t.templ)
       case t: Defn.Def =>
-        s(w(t.mods, " "), kw("def "), t.name, o(t.paramClauseGroup), t.decltpe, " = ", t.body)
+        s(w(t.mods, " "), kw("def "), t.name, t.paramClauseGroups, t.decltpe, " = ", t.body)
       case t: Defn.Macro =>
-        s(w(t.mods, " "), kw("def "), t.name, o(t.paramClauseGroup), t.decltpe, " = macro ", t.body)
+        s(w(t.mods, " "), kw("def "), t.name, t.paramClauseGroups, t.decltpe, " = macro ", t.body)
       case t: Pkg =>
         if (guessHasBraces(t)) s(kw("package"), " ", t.ref, " {", r(t.stats.map(i(_)), ""), n("}"))
         else s(kw("package"), " ", t.ref, r(t.stats.map(n(_))))
@@ -1044,6 +1044,7 @@ object TreeSyntax {
         case _ => s(args)
       }
 
+    implicit def syntaxParamClauseGroups: Syntax[Seq[Member.ParamClauseGroup]] = Syntax { r(_) }
     implicit def syntaxArgss: Syntax[Seq[Term.ArgClause]] = Syntax { r(_) }
     implicit def syntaxMods: Syntax[Seq[Mod]] = Syntax { r(_, " ") }
     private def isUsingOrImplicit(m: Mod): Boolean = m.is[Mod.ParamsType]


Test patch:
diff --git a/tests/js/src/test/scala/scala/meta/tests/parsers/JSFacadeSuite.scala b/tests/js/src/test/scala/scala/meta/tests/parsers/JSFacadeSuite.scala
index 18168be01..697b5803d 100644
--- a/tests/js/src/test/scala/scala/meta/tests/parsers/JSFacadeSuite.scala
+++ b/tests/js/src/test/scala/scala/meta/tests/parsers/JSFacadeSuite.scala
@@ -63,7 +63,7 @@ class JSFacadeSuite extends FunSuite {
                   "pos" -> pos(20, 24),
                   "value" -> "main"
                 ),
-                "paramClauseGroup" -> {
+                "paramClauseGroups" -> a(
                   d(
                     "type" -> "Member.ParamClauseGroup",
                     "pos" -> pos(24, 45),
@@ -111,7 +111,7 @@ class JSFacadeSuite extends FunSuite {
                       )
                     )
                   )
-                },
+                ),
                 "decltpe" -> d(
                   "type" -> "Type.Name",
                   "pos" -> pos(47, 51),
diff --git a/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala b/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
index 90c895756..50d485fe1 100644
--- a/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
+++ b/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
@@ -419,6 +419,7 @@ class SurfaceSuite extends FunSuite {
       |scala.meta.Tree.WithDeclTpeOpt
       |scala.meta.Tree.WithEnums
       |scala.meta.Tree.WithParamClauseGroup
+      |scala.meta.Tree.WithParamClauseGroups
       |scala.meta.Tree.WithParamClauses
       |scala.meta.Tree.WithTParamClause
       |scala.meta.Type
diff --git a/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala b/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala
index 1d9f4386a..044fd041e 100644
--- a/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala
+++ b/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala
@@ -24,7 +24,7 @@ class ReflectionSuite extends FunSuite {
     val sym = symbolOf[scala.meta.Tree]
     assert(sym.isRoot)
     val root = sym.asRoot
-    assertEquals((root.allBranches.length, root.allLeafs.length), (46, 398))
+    assertEquals((root.allBranches.length, root.allLeafs.length), (47, 399))
   }
 
   test("If") {
@@ -83,6 +83,7 @@ class ReflectionSuite extends FunSuite {
       |List[scala.meta.Importer]
       |List[scala.meta.Init]
       |List[scala.meta.Lit]
+      |List[scala.meta.Member.ParamClauseGroup]
       |List[scala.meta.Mod.Annot]
       |List[scala.meta.Mod]
       |List[scala.meta.Pat]
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InterleavedDeclSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InterleavedDeclSuite.scala
new file mode 100644
index 000000000..12da35161
--- /dev/null
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InterleavedDeclSuite.scala
@@ -0,0 +1,257 @@
+package scala.meta.tests.parsers.dotty
+
+import scala.meta._
+
+class InterleavedDeclSuite extends BaseDottySuite {
+
+  import dialects.Scala3Future
+
+  protected override val dialect: Dialect = Scala3Future
+
+  test("def f: Unit") {
+    checkTree(templStat("def f: Unit")) {
+      Decl.Def(Nil, Term.Name("f"), Nil, Type.Name("Unit"))
+    }
+  }
+
+  test("def f(x: Int): Unit") {
+    checkTree(templStat("def f(x: Int): Unit")) {
+      Decl.Def(
+        Nil,
+        Term.Name("f"),
+        Nil,
+        List(
+          Term.Param(Nil, Term.Name("x"), Some(Type.Name("Int")), None) :: Nil
+        ),
+        Type.Name("Unit")
+      )
+    }
+  }
+
+  test("def f(x: Int*): Unit") {
+    checkTree(templStat("def f(x: Int*): Unit")) {
+      Decl.Def(
+        Nil,
+        Term.Name("f"),
+        Nil,
+        List(
+          Term.Param(Nil, Term.Name("x"), Some(Type.Repeated(Type.Name("Int"))), None) :: Nil
+        ),
+        Type.Name("Unit")
+      )
+    }
+  }
+
+  test("def f(x: => Int): Unit") {
+    checkTree(templStat("def f(x: => Int): Unit")) {
+      Decl.Def(
+        Nil,
+        Term.Name("f"),
+        Nil,
+        List(
+          Term.Param(Nil, Term.Name("x"), Some(Type.ByName(Type.Name("Int"))), None) :: Nil
+        ),
+        Type.Name("Unit")
+      )
+    }
+  }
+
+  test("def f(implicit x: Int): Unit") {
+    checkTree(templStat("def f(implicit x: Int): Unit")) {
+      Decl.Def(
+        Nil,
+        Term.Name("f"),
+        Nil,
+        List(
+          Term.Param(Mod.Implicit() :: Nil, Term.Name("x"), Some(Type.Name("Int")), None) :: Nil
+        ),
+        Type.Name("Unit")
+      )
+    }
+  }
+
+  test("def f: X") {
+    checkTree(templStat("def f: X")) {
+      Decl.Def(Nil, Term.Name("f"), Nil, Type.Name("X"))
+    }
+  }
+
+  test("def f[T]: T") {
+    checkTree(templStat("def f[T]: T")) {
+      Decl.Def(
+        Nil,
+        Term.Name("f"),
+        Type.Param(Nil, Type.Name("T"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil,
+        Nil,
+        Type.Name("T")
+      )
+    }
+  }
+
+  test("def f[A][B]: A") {
+    runTestError[Stat](
+      "def f[A][B]: A",
+      """|error: = expected but [ found
+         |def f[A][B]: A
+         |        ^""".stripMargin
+    )
+  }
+
+  test("def f[A](a: A, as: A*)[B]: B") {
+    runTestAssert[Stat]("def f[A](a: A, as: A*)[B]: B") {
+      Decl.Def(
+        Nil,
+        Term.Name("f"),
+        Member.ParamClauseGroup(
+          Type.ParamClause(
+            Type.Param(Nil, Type.Name("A"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+          ),
+          Term.ParamClause(
+            List(
+              Term.Param(Nil, Term.Name("a"), Some(Type.Name("A")), None),
+              Term.Param(Nil, Term.Name("as"), Some(Type.Repeated(Type.Name("A"))), None)
+            ),
+            None
+          ) :: Nil
+        ) :: Member.ParamClauseGroup(
+          Type.ParamClause(
+            Type.Param(Nil, Type.Name("B"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+          ),
+          Nil
+        ) :: Nil,
+        Type.Name("B")
+      )
+    }
+  }
+
+  test("def f[A](implicit a: A)[B](implicit b: B): B") {
+    runTestError[Stat](
+      "def f[A](implicit a: A)[B](implicit b: B): B",
+      """|error: = expected but [ found
+         |def f[A](implicit a: A)[B](implicit b: B): B
+         |                       ^""".stripMargin
+    )
+  }
+
+  test("def f[A](a: A, as: A*)[B](b: B, bs: B*)[C](implicit c: C): B") {
+    runTestAssert[Stat]("def f[A](a: A, as: A*)[B](b: B, bs: B*)[C](implicit c: C): B") {
+      Decl.Def(
+        Nil,
+        Term.Name("f"),
+        Member.ParamClauseGroup(
+          Type.ParamClause(
+            Type.Param(Nil, Type.Name("A"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+          ),
+          Term.ParamClause(
+            List(
+              Term.Param(Nil, Term.Name("a"), Some(Type.Name("A")), None),
+              Term.Param(Nil, Term.Name("as"), Some(Type.Repeated(Type.Name("A"))), None)
+            ),
+            None
+          ) :: Nil
+        ) :: Member.ParamClauseGroup(
+          Type.ParamClause(
+            Type.Param(Nil, Type.Name("B"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+          ),
+          Term.ParamClause(
+            List(
+              Term.Param(Nil, Term.Name("b"), Some(Type.Name("B")), None),
+              Term.Param(Nil, Term.Name("bs"), Some(Type.Repeated(Type.Name("B"))), None)
+            ),
+            None
+          ) :: Nil
+        ) :: Member.ParamClauseGroup(
+          Type.ParamClause(
+            Type.Param(Nil, Type.Name("C"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+          ),
+          Term.ParamClause(
+            List(Term.Param(List(Mod.Implicit()), Term.Name("c"), Some(Type.Name("C")), None)),
+            Some(Mod.Implicit())
+          ) :: Nil
+        ) :: Nil,
+        Type.Name("B")
+      )
+    }
+  }
+
+  test("single ..., without tparams") {
+    val q"..$mods def $name(...$paramss): $tpe" = q"def f(x: Int)(y: Int): Unit"
+    val paramsExpected =
+      Term.ParamClause(
+        Term.Param(Nil, Term.Name("x"), Some(Type.Name("Int")), None) :: Nil,
+        None
+      ) :: Term.ParamClause(
+        Term.Param(Nil, Term.Name("y"), Some(Type.Name("Int")), None) :: Nil,
+        None
+      ) :: Nil
+
+    checkTreesWithSyntax(paramss: _*)("(x: Int)", "(y: Int)")(paramsExpected: _*)
+    assertTree(q"..$mods def $name(...$paramss): $tpe")(
+      Decl.Def(
+        Nil,
+        Term.Name("f"),
+        Member.ParamClauseGroup(Nil, paramsExpected) :: Nil,
+        Type.Name("Unit")
+      )
+    )
+  }
+
+  test("single ..., with tparams") {
+    val q"..$mods def $name[..$tparams](...$paramss): $tpe" = q"def f[A, B](x: Int)(y: Int): Unit"
+    val tparamsExpected = Type.ParamClause(
+      List(
+        Type.Param(Nil, Type.Name("A"), Type.ParamClause(Nil), Type.Bounds(None, None), Nil, Nil),
+        Type.Param(Nil, Type.Name("B"), Type.ParamClause(Nil), Type.Bounds(None, None), Nil, Nil)
+      )
+    )
+    val paramsExpected =
+      Term.ParamClause(
+        Term.Param(Nil, Term.Name("x"), Some(Type.Name("Int")), None) :: Nil,
+        None
+      ) :: Term.ParamClause(
+        Term.Param(Nil, Term.Name("y"), Some(Type.Name("Int")), None) :: Nil,
+        None
+      ) :: Nil
+
+    checkTree(tparams, "[A, B]")(tparamsExpected)
+    checkTreesWithSyntax(paramss: _*)("(x: Int)", "(y: Int)")(paramsExpected: _*)
+    assertTree(q"..$mods def $name[..$tparams](...$paramss): $tpe")(
+      Decl.Def(
+        Nil,
+        Term.Name("f"),
+        Member.ParamClauseGroup(tparamsExpected, paramsExpected) :: Nil,
+        Type.Name("Unit")
+      )
+    )
+  }
+
+  test("single ...., with tparams") {
+    val q"..$mods def $name(....$paramss): $tpe" = q"def f(x: Int)[A](y: Int)[B]: Unit"
+    val pcGroups = Member.ParamClauseGroup(
+      Nil,
+      Term.ParamClause(
+        List(Term.Param(Nil, Term.Name("x"), Some(Type.Name("Int")), None)),
+        None
+      ) :: Nil
+    ) :: Member.ParamClauseGroup(
+      Type.ParamClause(
+        Type.Param(Nil, Type.Name("A"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+      ),
+      Term.ParamClause(
+        List(Term.Param(Nil, Term.Name("y"), Some(Type.Name("Int")), None)),
+        None
+      ) :: Nil
+    ) :: Member.ParamClauseGroup(
+      Type.ParamClause(
+        Type.Param(Nil, Type.Name("B"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+      ),
+      Nil
+    ) :: Nil
+
+    checkTreesWithSyntax(paramss: _*)("(x: Int)", "[A](y: Int)", "[B]")(pcGroups: _*)
+    assertTree(q"..$mods def $name(....$paramss): $tpe")(
+      Decl.Def(Nil, Term.Name("f"), pcGroups, Type.Name("Unit"))
+    )
+  }
+
+}
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InterleavedDefnSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InterleavedDefnSuite.scala
new file mode 100644
index 000000000..ce4e65e0a
--- /dev/null
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InterleavedDefnSuite.scala
@@ -0,0 +1,246 @@
+package scala.meta.tests.parsers.dotty
+
+import scala.meta._
+
+class InterleavedDefnSuite extends BaseDottySuite {
+
+  import dialects.Scala3Future
+
+  protected override val dialect: Dialect = Scala3Future
+
+  test("def x = 2") {
+    checkTree(templStat("def x = 2")) {
+      Defn.Def(Nil, Term.Name("x"), Nil, None, Lit.Int(2))
+    }
+  }
+
+  test("def x[A <: B] = 2") {
+    checkTree(templStat("def x[A <: B] = 2")) {
+      Defn.Def(
+        Nil,
+        Term.Name("x"),
+        Type.Param(
+          Nil,
+          Type.Name("A"),
+          Type.ParamClause(Nil),
+          Type.Bounds(None, Some(Type.Name("B"))),
+          Nil,
+          Nil
+        ) :: Nil,
+        Nil,
+        None,
+        Lit.Int(2)
+      )
+    }
+  }
+
+  test("def x[A: B] = 2") {
+    checkTree(templStat("def x[A: B] = 2")) {
+      Defn.Def(
+        Nil,
+        Term.Name("x"),
+        Type.Param(
+          Nil,
+          Type.Name("A"),
+          Type.ParamClause(Nil),
+          Type.Bounds(None, None),
+          Nil,
+          Type.Name("B") :: Nil
+        ) :: Nil,
+        Nil,
+        None,
+        Lit.Int(2)
+      )
+    }
+  }
+
+  test("def f(a: Int)(implicit b: Int) = a + b") {
+    checkTree(templStat("def f(a: Int)(implicit b: Int) = a + b")) {
+      Defn.Def(
+        Nil,
+        Term.Name("f"),
+        Nil,
+        List(
+          Term.Param(Nil, Term.Name("a"), Some(Type.Name("Int")), None) :: Nil,
+          Term.Param(Mod.Implicit() :: Nil, Term.Name("b"), Some(Type.Name("Int")), None) :: Nil
+        ),
+        None,
+        Term.ApplyInfix(Term.Name("a"), Term.Name("+"), Nil, Term.Name("b") :: Nil)
+      )
+    }
+  }
+
+  test("def f(x: Int) = macro impl") {
+    checkTree(templStat("def f(x: Int) = macro impl")) {
+      Defn.Macro(
+        Nil,
+        Term.Name("f"),
+        Nil,
+        List(Term.Param(List(), Term.Name("x"), Some(Type.Name("Int")), None) :: Nil),
+        None,
+        Term.Name("impl")
+      )
+    }
+  }
+
+  test("def f(x: Int): Int = macro impl") {
+    checkTree(templStat("def f(x: Int): Int = macro impl")) {
+      Defn.Macro(
+        Nil,
+        Term.Name("f"),
+        Nil,
+        List(Term.Param(List(), Term.Name("x"), Some(Type.Name("Int")), None) :: Nil),
+        Some(Type.Name("Int")),
+        Term.Name("impl")
+      )
+    }
+  }
+
+  test("braces-in-functions") {
+    val defn = templStat(
+      """|def f = { (n: Int) =>
+         |  {
+         |    for {
+         |      _ <- scala.util.Success(123)
+         |    } yield 42
+         |  }.recover(???)
+         |}""".stripMargin
+    )
+    checkTree(defn)(
+      Defn.Def(
+        Nil,
+        Term.Name("f"),
+        Nil,
+        None,
+        Term.Block(
+          List(
+            Term.Function(
+              List(Term.Param(Nil, Term.Name("n"), Some(Type.Name("Int")), None)),
+              Term.Apply(
+                Term.Select(
+                  Term.Block(
+                    List(
+                      Term.ForYield(
+                        List(
+                          Enumerator.Generator(
+                            Pat.Wildcard(),
+                            Term.Apply(
+                              Term.Select(
+                                Term.Select(Term.Name("scala"), Term.Name("util")),
+                                Term.Name("Success")
+                              ),
+                              List(Lit.Int(123))
+                            )
+                          )
+                        ),
+                        Lit.Int(42)
+                      )
+                    )
+                  ),
+                  Term.Name("recover")
+                ),
+                List(Term.Name("???"))
+              )
+            )
+          )
+        )
+      )
+    )
+  }
+
+  test("def f[A][B]: A = ???") {
+    runTestError[Stat](
+      "def f[A][B]: A = ???",
+      """|error: = expected but [ found
+         |def f[A][B]: A = ???
+         |        ^""".stripMargin
+    )
+  }
+
+  test("def f[A](a: A, as: A*)[B]: B = ???") {
+    runTestAssert[Stat]("def f[A](a: A, as: A*)[B]: B = ???") {
+      Defn.Def(
+        Nil,
+        Term.Name("f"),
+        List(
+          Member.ParamClauseGroup(
+            Type.ParamClause(
+              Type.Param(Nil, Type.Name("A"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+            ),
+            Term.ParamClause(
+              List(
+                Term.Param(Nil, Term.Name("a"), Some(Type.Name("A")), None),
+                Term.Param(Nil, Term.Name("as"), Some(Type.Repeated(Type.Name("A"))), None)
+              ),
+              None
+            ) :: Nil
+          ),
+          Member.ParamClauseGroup(
+            Type.ParamClause(
+              Type.Param(Nil, Type.Name("B"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+            ),
+            Nil
+          )
+        ),
+        Some(Type.Name("B")),
+        Term.Name("???")
+      )
+    }
+  }
+
+  test("def f[A](implicit a: A)[B](implicit b: B): B = ???") {
+    runTestError[Stat](
+      "def f[A](implicit a: A)[B](implicit b: B): B = ???",
+      """|error: = expected but [ found
+         |def f[A](implicit a: A)[B](implicit b: B): B = ???
+         |                       ^""".stripMargin
+    )
+  }
+
+  test("def f[A](a: A, as: A*)[B](b: B, bs: B*)[C](implicit c: C): B = ???") {
+    runTestAssert[Stat]("def f[A](a: A, as: A*)[B](b: B, bs: B*)[C](implicit c: C): B = ???") {
+      Defn.Def(
+        Nil,
+        Term.Name("f"),
+        List(
+          Member.ParamClauseGroup(
+            Type.ParamClause(
+              Type.Param(Nil, Type.Name("A"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+            ),
+            Term.ParamClause(
+              List(
+                Term.Param(Nil, Term.Name("a"), Some(Type.Name("A")), None),
+                Term.Param(Nil, Term.Name("as"), Some(Type.Repeated(Type.Name("A"))), None)
+              ),
+              None
+            ) :: Nil
+          ),
+          Member.ParamClauseGroup(
+            Type.ParamClause(
+              Type.Param(Nil, Type.Name("B"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+            ),
+            Term.ParamClause(
+              List(
+                Term.Param(Nil, Term.Name("b"), Some(Type.Name("B")), None),
+                Term.Param(Nil, Term.Name("bs"), Some(Type.Repeated(Type.Name("B"))), None)
+              ),
+              None
+            ) :: Nil
+          ),
+          Member.ParamClauseGroup(
+            Type.ParamClause(
+              Type.Param(Nil, Type.Name("C"), Nil, Type.Bounds(None, None), Nil, Nil) :: Nil
+            ),
+            Term.ParamClause(
+              List(Term.Param(List(Mod.Implicit()), Term.Name("c"), Some(Type.Name("C")), None)),
+              Some(Mod.Implicit())
+            ) :: Nil
+          )
+        ),
+        Some(Type.Name("B")),
+        Term.Name("???")
+      )
+    }
+  }
+
+}
diff --git a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/SyntacticSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/SyntacticSuite.scala
index 3fb389872..efbb1c624 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/SyntacticSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/SyntacticSuite.scala
@@ -1926,4 +1926,9 @@ class SyntacticSuite extends scala.meta.tests.parsers.ParseSuite {
     }
   }
 
+  test("#3065 embedded triple quotes") {
+    val tree = Lit.String("\n\"\"\"")
+    assertSyntax(tree, "\"\"\"\n\\\"\\\"\\\"\"\"\"")(tree)
+  }
+
 }