Repository: scalameta/scalameta (scala)

Issue:
support SIP 53 in parser
https://docs.scala-lang.org/sips/quote-pattern-type-variable-syntax.html

i.e. some code like this
```scala
import scala.quoted.*

def toTuple(list: List[Type[?]])(using Quotes): Type[?] = list match
  case '[t] :: ts => toTuple(ts) match
    case '[type ts <: Tuple; ts] => Type.of[t *: ts]
  case _ => Type.of[EmptyTuple]
```

im trying this out in 3.4.0 which compiles, but metals complains with `identifier expected but type found (scalameta)`

Pull request description:
Fixes #3610.

Patch:
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 854c6fcd6f..a439938212 100644
--- a/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
+++ b/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
@@ -110,6 +110,8 @@ final class Dialect private[meta] (
     val allowInfixMods: Boolean,
     // Scala 3 splices/quotes
     val allowSpliceAndQuote: Boolean,
+    // https://docs.scala-lang.org/sips/quote-pattern-type-variable-syntax.html
+    val allowQuotedTypeVariables: Boolean,
     // Scala 3 disallowed symbol literals
     val allowSymbolLiterals: Boolean,
     // Scala 3 disallowed symbol literals
@@ -235,6 +237,7 @@ final class Dialect private[meta] (
       allowTypeMatch = false,
       allowInfixMods = false,
       allowSpliceAndQuote = false,
+      allowQuotedTypeVariables = false,
       allowSymbolLiterals = true,
       allowDependentFunctionTypes = false,
       allowPostfixStarVarargSplices = false,
@@ -409,6 +412,9 @@ final class Dialect private[meta] (
   def withAllowSpliceAndQuote(newValue: Boolean): Dialect = {
     privateCopy(allowSpliceAndQuote = newValue)
   }
+  def withAllowQuotedTypeVariables(newValue: Boolean): Dialect = {
+    privateCopy(allowQuotedTypeVariables = newValue)
+  }
   def withAllowSymbolLiterals(newValue: Boolean): Dialect = {
     privateCopy(allowSymbolLiterals = newValue)
   }
@@ -524,6 +530,7 @@ final class Dialect private[meta] (
       allowTypeMatch: Boolean = this.allowTypeMatch,
       allowInfixMods: Boolean = this.allowInfixMods,
       allowSpliceAndQuote: Boolean = this.allowSpliceAndQuote,
+      allowQuotedTypeVariables: Boolean = this.allowQuotedTypeVariables,
       allowSymbolLiterals: Boolean = this.allowSymbolLiterals,
       allowDependentFunctionTypes: Boolean = this.allowDependentFunctionTypes,
       allowPostfixStarVarargSplices: Boolean = this.allowPostfixStarVarargSplices,
@@ -585,6 +592,7 @@ final class Dialect private[meta] (
       allowTypeMatch = allowTypeMatch,
       allowInfixMods = allowInfixMods,
       allowSpliceAndQuote = allowSpliceAndQuote,
+      allowQuotedTypeVariables = allowQuotedTypeVariables,
       allowSymbolLiterals = allowSymbolLiterals,
       allowDependentFunctionTypes = allowDependentFunctionTypes,
       allowPostfixStarVarargSplices = allowPostfixStarVarargSplices,
@@ -648,6 +656,7 @@ final class Dialect private[meta] (
       allowTypeMatch = allowTypeMatch,
       allowInfixMods = allowInfixMods,
       allowSpliceAndQuote = allowSpliceAndQuote,
+      allowQuotedTypeVariables = allowQuotedTypeVariables,
       allowSymbolLiterals = allowSymbolLiterals,
       allowDependentFunctionTypes = allowDependentFunctionTypes,
       allowPostfixStarVarargSplices = allowPostfixStarVarargSplices,
@@ -736,6 +745,7 @@ final class Dialect private[meta] (
       allowTypeMatch: Boolean,
       allowInfixMods: Boolean,
       allowSpliceAndQuote: Boolean,
+      allowQuotedTypeVariables: Boolean,
       allowSymbolLiterals: Boolean,
       allowDependentFunctionTypes: Boolean,
       allowPostfixStarVarargSplices: Boolean,
@@ -795,6 +805,7 @@ final class Dialect private[meta] (
       && this.allowTypeMatch == allowTypeMatch
       && this.allowInfixMods == allowInfixMods
       && this.allowSpliceAndQuote == allowSpliceAndQuote
+      && this.allowQuotedTypeVariables == allowQuotedTypeVariables
       && this.allowSymbolLiterals == allowSymbolLiterals
       && this.allowDependentFunctionTypes == allowDependentFunctionTypes
       && this.allowPostfixStarVarargSplices == allowPostfixStarVarargSplices
@@ -857,6 +868,7 @@ final class Dialect private[meta] (
       allowTypeMatch = that.allowTypeMatch,
       allowInfixMods = that.allowInfixMods,
       allowSpliceAndQuote = that.allowSpliceAndQuote,
+      allowQuotedTypeVariables = that.allowQuotedTypeVariables,
       allowSymbolLiterals = that.allowSymbolLiterals,
       allowDependentFunctionTypes = that.allowDependentFunctionTypes,
       allowPostfixStarVarargSplices = that.allowPostfixStarVarargSplices,
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 6418b7bc98..b9c3025962 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
@@ -153,6 +153,7 @@ package object dialects {
     .withAllowParamClauseInterleaving(true)
 
   implicit val Scala34: Dialect = Scala33
+    .withAllowQuotedTypeVariables(true)
 
   implicit val Scala3: Dialect = Scala34
 
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 d9ed4b115d..81fee45e10 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
@@ -849,6 +849,23 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
       }).getOrElse(t)
     }
 
+    def typeBlock(): Type =
+      // TypeBlock, https://dotty.epfl.ch/docs/internals/syntax.html#expressions-3
+      if (dialect.allowQuotedTypeVariables && token.is[KwType]) autoPos {
+        val typeDefs = listBy[Stat.TypeDef] { buf =>
+          @tailrec def iter(): Unit = {
+            buf += typeDefOrDcl(Nil)
+            if (StatSep(token)) {
+              next()
+              if (token.is[KwType]) iter()
+            }
+          }
+          iter()
+        }
+        Type.Block(typeDefs, typ())
+      }
+      else typ()
+
     private def typeFuncOnArrow(
         paramPos: Int,
         params: List[Type],
@@ -2297,7 +2314,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     next()
     token match {
       case _: LeftBrace => Term.QuotedMacroExpr(autoPos(inBracesOnOpen(blockWithinDelims())))
-      case _: LeftBracket => Term.QuotedMacroType(inBracketsOnOpen(typ()))
+      case _: LeftBracket => Term.QuotedMacroType(inBracketsOnOpen(typeBlock()))
       case t => syntaxError("Quotation only works for expressions and types", at = t)
     }
   })
@@ -2982,6 +2999,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
    * initiated from non-pattern context.
    */
   def typ() = outPattern.typ()
+  private def typeBlock() = outPattern.typeBlock()
   def typeIndentedOpt() = outPattern.typeIndentedOpt()
   def quasiquoteType() = outPattern.quasiquoteType()
   def entrypointType() = outPattern.entrypointType()
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 bd9e1013fc..42c836f1bd 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
@@ -424,6 +424,9 @@ object Type {
   @ast class ParamClause(values: List[Param]) extends Member.ParamClause
 
   @ast class Match(tpe: Type, cases: List[TypeCase] @nonEmpty) extends Type with Tree.WithCases
+
+  @ast class Block(typeDefs: List[Stat.TypeDef], tpe: Type) extends Type
+
   def fresh(): Type.Name = fresh("fresh")
   def fresh(prefix: String): Type.Name = Type.Name(prefix + Fresh.nextId())
 }
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 0cd1399926..a533237411 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
@@ -705,6 +705,7 @@ object TreeSyntax {
         }
         val cbounds = r(t.cbounds.map { s(kw(":"), " ", _) })
         s(w(mods, " "), variance, t.name, t.tparamClause, tbounds, vbounds, cbounds)
+      case t: Type.Block => s(w(r(t.typeDefs, "; "), "; "), t.tpe)
 
       // Pat
       case t: Pat.Var =>


Test patch:
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 6777f437c8..f70143c101 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
@@ -469,6 +469,7 @@ class SurfaceSuite extends FunSuite {
       |scala.meta.Type.Apply
       |scala.meta.Type.ApplyInfix
       |scala.meta.Type.ArgClause
+      |scala.meta.Type.Block
       |scala.meta.Type.Bounds
       |scala.meta.Type.ByName
       |scala.meta.Type.ContextFunction
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 717e41595b..c20297fa2e 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), (50, 408))
+    assertEquals((root.allBranches.length, root.allLeafs.length), (50, 410))
   }
 
   test("If") {
@@ -98,6 +98,7 @@ class ReflectionSuite extends FunSuite {
       |List[scala.meta.Mod]
       |List[scala.meta.Pat]
       |List[scala.meta.Source]
+      |List[scala.meta.Stat.TypeDef]
       |List[scala.meta.Stat]
       |List[scala.meta.Term.Name]
       |List[scala.meta.Term.Param]
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MacroSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MacroSuite.scala
index eaeef3c3f5..a9eddb30db 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MacroSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MacroSuite.scala
@@ -440,4 +440,32 @@ class MacroSuite extends BaseDottySuite {
       )
     )
   }
+
+  test("#3610 1") {
+    val code = "'[ List[Int] ]"
+    val tree = Term.QuotedMacroType(Type.Apply(pname("List"), List(pname("Int"))))
+    runTestAssert[Stat](code)(tree)
+  }
+
+  test("#3610 2") {
+    val code = "'[ type t = Int; List[t] ]"
+    val tree = Term.QuotedMacroType(
+      Type.Block(
+        List(Defn.Type(Nil, pname("t"), Nil, pname("Int"), noBounds)),
+        Type.Apply(pname("List"), List(pname("t")))
+      )
+    )
+    runTestAssert[Stat](code)(tree)
+  }
+
+  test("#3610 3") {
+    val code = "'[ type tail <: Tuple; *:[Int, tail] ]"
+    val tree = Term.QuotedMacroType(
+      Type.Block(
+        List(Decl.Type(Nil, pname("tail"), Nil, bounds(hi = "Tuple"))),
+        Type.Apply(pname("*:"), List(pname("Int"), pname("tail")))
+      )
+    )
+    runTestAssert[Stat](code)(tree)
+  }
 }
diff --git a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/PublicSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/PublicSuite.scala
index c8587f4239..77a41aa060 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/PublicSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/PublicSuite.scala
@@ -109,7 +109,7 @@ class PublicSuite extends TreeSuiteBase {
   }
 
   test("scala.meta.dialects.Scala33.toString") {
-    assertNoDiff(scala.meta.dialects.Scala33.toString, "Scala34")
+    assertNoDiff(scala.meta.dialects.Scala33.toString, "Scala33")
   }
 
   test("scala.meta.dialects.Scala34.toString") {
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 13e434ce9e..7107f6ce56 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
@@ -2064,4 +2064,19 @@ class SyntacticSuite extends scala.meta.tests.parsers.ParseSuite {
     )
   }
 
+  test("#3610 Type.Block 1") {
+    val tree = Type.Block(Nil, pname("Int"))
+    assertEquals(tree.syntax, "Int")
+  }
+
+  test("#3610 Type.Block 2") {
+    val tree = Type.Block(List(Decl.Type(Nil, pname("t"), Nil, noBounds)), pname("t"))
+    assertEquals(tree.syntax, "type t; t")
+  }
+
+  test("#3610 Type.Block 3") {
+    val tree = Type.Block(List(Defn.Type(Nil, pname("t"), Nil, pname("Int"))), pname("t"))
+    assertEquals(tree.syntax, "type t = Int; t")
+  }
+
 }