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 => 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") + } + }