Repository: scalameta/scalameta
Pull request #3474 (2024-01-06 08:39:31)

Token: dialect consistency check from TokenSyntax


Patch:
diff --git a/docs/trees/guide.md b/docs/trees/guide.md
index 2a538bc3c..871f16920 100644
--- a/docs/trees/guide.md
+++ b/docs/trees/guide.md
@@ -296,8 +296,10 @@ nodes . Imagine we have a list of type arguments that happens to be empty
 val typeArguments = List.empty[Type]
 ```
 
-If we directly splice the lists into a type application we get a cryptic error
-message "invariant failed"
+If we directly splice the lists into a type application we get an error message
+"invariant failed (targClause should be non-empty)" referring to the `targClause`
+field of `Type.ApplyType` (with additional cryptic information showing specific
+checks performed):
 
 ```scala mdoc:crash
 q"function[..$typeArguments]()"
diff --git a/scalameta/common/shared/src/main/scala/org/scalameta/data/Macros.scala b/scalameta/common/shared/src/main/scala/org/scalameta/data/Macros.scala
index dd8c01fcf..d18c15d0a 100644
--- a/scalameta/common/shared/src/main/scala/org/scalameta/data/Macros.scala
+++ b/scalameta/common/shared/src/main/scala/org/scalameta/data/Macros.scala
@@ -19,16 +19,25 @@ class DataTyperMacrosBundle(val c: Context) extends AdtReflection with MacroHelp
   import definitions._
 
   def nullCheck(x: c.Tree): c.Tree = {
-    if (x.tpe.baseClasses.contains(ObjectClass)) q"$InvariantsRequireMethod($x != null)"
-    else q"()"
+    if (!x.tpe.baseClasses.contains(ObjectClass)) q"()"
+    else {
+      val clue = s"${showCode(x)} should be non-null"
+      q"""$InvariantsRequireMethod($x != null, $clue)"""
+    }
   }
 
   def emptyCheck(x: c.Tree): c.Tree = {
     val emptyCheckRequested =
       try x.symbol.asTerm.accessed.nonEmpty
       catch { case _: AssertionError => x.symbol.nonEmpty }
-    if (emptyCheckRequested)
-      q"$InvariantsRequireMethod($x != null && ($x.isInstanceOf[$QuasiClass] || $x.nonEmpty))"
-    else q"()"
+    if (emptyCheckRequested) {
+      val clue = s"${showCode(x)} should be non-empty"
+      q"""
+        $InvariantsRequireMethod(
+          $x != null && ($x.isInstanceOf[$QuasiClass] || $x.nonEmpty),
+          $clue
+        )
+      """
+    } else q"()"
   }
 }
diff --git a/scalameta/common/shared/src/main/scala/org/scalameta/invariants/Exceptions.scala b/scalameta/common/shared/src/main/scala/org/scalameta/invariants/Exceptions.scala
index 81eede06c..452918d12 100644
--- a/scalameta/common/shared/src/main/scala/org/scalameta/invariants/Exceptions.scala
+++ b/scalameta/common/shared/src/main/scala/org/scalameta/invariants/Exceptions.scala
@@ -5,8 +5,17 @@ import scala.compat.Platform.EOL
 class InvariantFailedException(message: String) extends Exception(message)
 object InvariantFailedException {
   def raise(invariant: String, failures: List[String], debuggees: Map[String, Any]): Nothing = {
+    raise(invariant, null, failures, debuggees)
+  }
+  def raise(
+      invariant: String,
+      clue: String,
+      failures: List[String],
+      debuggees: Map[String, Any]
+  ): Nothing = {
+    val clueStr = if (clue eq null) "" else s" ($clue)"
     val mandatory = s"""
-    |invariant failed:
+    |invariant failed$clueStr:
     |when verifying $invariant
     |found that ${failures.head}
     """.trim.stripMargin.split('\n').mkString(EOL)
diff --git a/scalameta/common/shared/src/main/scala/org/scalameta/invariants/package.scala b/scalameta/common/shared/src/main/scala/org/scalameta/invariants/package.scala
index 50d24351f..473c83256 100644
--- a/scalameta/common/shared/src/main/scala/org/scalameta/invariants/package.scala
+++ b/scalameta/common/shared/src/main/scala/org/scalameta/invariants/package.scala
@@ -14,6 +14,7 @@ package object invariants {
   // 1) Values of local variables.
   // 2) Calls to `org.scalameta.debug` as explained in documentation to that method.
   def require(requirement: Boolean): Unit = macro Macros.require
+  def require(requirement: Boolean, clue: String): Unit = macro Macros.requireWithClue
 
   implicit class XtensionRequireCast[T](private val x: T) extends AnyVal {
     // Equivalent to requiring that `x.getClass` is assignable from `U`.
@@ -35,22 +36,29 @@ package invariants {
     import c.universe._
 
     def require(requirement: Tree): Tree = {
+      requireWithClue(requirement, q"null")
+    }
+
+    def requireWithClue(requirement: Tree, clue: Tree): Tree = {
       val failures = c.freshName(TermName("failures"))
       val allDebuggees = freeLocals(requirement) ++ debuggees(requirement)
       q"""
-        ${instrument(requirement)} match {
-          case (true, _) => ()
-          case (false, $failures) => $InvariantFailedRaiseMethod(${showCode(requirement)}, $failures, $allDebuggees)
-        }
+        val $failures = ${instrument(requirement)}
+        if (!$failures.isEmpty)
+          $InvariantFailedRaiseMethod(${showCode(requirement)}, $clue, $failures, $allDebuggees)
       """
     }
 
     def requireCast[U](ev: c.Tree)(U: c.WeakTypeTag[U]): c.Tree = {
       val q"$_($x)" = c.prefix.tree
+      val clue = s"${showCode(x)} can not be cast to type ${showDecl(U.tpe.typeSymbol)}"
       q"""
         val temp = ${c.untypecheck(x)}
         val tempClass = if (temp != null) temp.getClass else null
-        $InvariantsRequireMethod(tempClass != null && _root_.scala.reflect.classTag[$U].unapply(temp).isDefined)
+        $InvariantsRequireMethod(
+          tempClass != null && _root_.scala.reflect.classTag[$U].unapply(temp).isDefined,
+          $clue
+        )
         temp.asInstanceOf[$U]
       """
     }
@@ -68,13 +76,13 @@ package invariants {
         override def emit = {
           q"""
             val $result = $tree
-            if ($result) (true, _root_.scala.collection.immutable.Nil)
-            else (false, _root_.scala.collection.immutable.List($diagnostic))
+            if ($result) _root_.scala.collection.immutable.Nil
+            else _root_.scala.collection.immutable.List($diagnostic)
           """
         }
       }
       case class Debug() extends Prop {
-        override def emit = q"(true, _root_.scala.collection.immutable.Nil)"
+        override def emit = q"_root_.scala.collection.immutable.Nil"
       }
       case class Atom(tree: Tree) extends Prop with Simple {
         override def diagnostic = showCode(tree) + " is false"
@@ -92,8 +100,8 @@ package invariants {
               prop.emit
             case prop :: rest =>
               q"""
-                val ($result, $failures) = ${prop.emit}
-                if (!$result) (false, $failures)
+                val $failures = ${prop.emit}
+                if (!$failures.isEmpty) $failures
                 else ${loop(rest)}
               """
           }
@@ -111,12 +119,12 @@ package invariants {
               val restResult = c.freshName(TermName("restResult"))
               val restFailures = c.freshName(TermName("restFailures"))
               q"""
-                val ($result, $failures) = ${prop.emit}
-                if ($result) (true, _root_.scala.collection.immutable.Nil)
+                val $failures = ${prop.emit}
+                if ($failures.isEmpty) _root_.scala.collection.immutable.Nil
                 else {
-                  val ($restResult, $restFailures) = ${loop(rest)}
-                  if ($restResult) (true, _root_.scala.collection.immutable.Nil)
-                  else (false, $failures ++ $restFailures)
+                  val $restFailures = ${loop(rest)}
+                  if ($restFailures.isEmpty) _root_.scala.collection.immutable.Nil
+                  else $failures ++ $restFailures
                 }
               """
           }
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 1ab5d054d..2682e7bd7 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
@@ -4,28 +4,31 @@ 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 = "\"" }
-case object TripleQuotes extends QuoteStyle { override def toString = "\"\"\"" }
+sealed abstract class QuoteStyle(styleStr: String) {
+  private final val styleEscaped: String = {
+    val sb = new java.lang.StringBuilder(2 * styleStr.length)
+    styleStr.foreach { ch => sb.append("\\").append(ch) }
+    sb.toString
+  }
+
+  override def toString: String = styleStr
 
-object enquote {
-  def apply(s: String, style: QuoteStyle): String = {
-    val styleStr = style.toString
+  def apply(s: String): String = {
     val sb = new java.lang.StringBuilder(styleStr)
-    if (style == TripleQuotes) {
-      val styleLen = styleStr.length
+    val styleLen = styleStr.length
+    if (styleLen > 1) {
       @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("\\\"\\\"\\\"")
+          sb.append(styleEscaped)
           iter(newoff + styleLen)
         }
       }
       iter(0)
     } else {
+      val styleCh = styleStr(0)
       s.foreach {
         case '\t' => sb.append("\\t")
         case '\b' => sb.append("\\b")
@@ -33,10 +36,7 @@ object enquote {
         case '\r' => sb.append("\\r")
         case '\f' => sb.append("\\f")
         case '\\' => sb.append("\\\\")
-        case '"' if style eq DoubleQuotes =>
-          sb.append("\\\"")
-        case '\'' if style eq SingleQuotes =>
-          sb.append("\\\'")
+        case `styleCh` => sb.append(styleEscaped)
         case c =>
           val isNonReadableAscii = c < ' ' || c > '~'
           if (isNonReadableAscii && !Character.isLetter(c)) sb.append("\\u%04x".format(c.toInt))
@@ -47,3 +47,11 @@ object enquote {
     sb.toString
   }
 }
+
+case object SingleQuotes extends QuoteStyle("'") {
+  def apply(ch: Char): String = super.apply(ch.toString)
+}
+case object DoubleQuotes extends QuoteStyle("\"") {
+  def orTriple(str: String): String = if (str.contains('\n')) TripleQuotes(str) else apply(str)
+}
+case object TripleQuotes extends QuoteStyle("\"\"\"")
diff --git a/scalameta/tokens/shared/src/main/scala/scala/meta/internal/prettyprinters/TokenSyntax.scala b/scalameta/tokens/shared/src/main/scala/scala/meta/internal/prettyprinters/TokenSyntax.scala
index 493d7e7c2..ab43dfbeb 100644
--- a/scalameta/tokens/shared/src/main/scala/scala/meta/internal/prettyprinters/TokenSyntax.scala
+++ b/scalameta/tokens/shared/src/main/scala/scala/meta/internal/prettyprinters/TokenSyntax.scala
@@ -9,23 +9,6 @@ import scala.meta.tokens.Token._
 
 object TokenSyntax {
   def apply[T <: Token](dialect: Dialect): Syntax[T] = Syntax { x =>
-    def failXml() =
-      throw new UnsupportedOperationException(s"$dialect doesn't support xml literals")
-    def failQuasiquote() =
-      throw new UnsupportedOperationException(s"$dialect doesn't support unquoting")
-    def failViewBound() =
-      throw new UnsupportedOperationException(s"$dialect doesn't support view bounds")
-    x match {
-      case Xml.Start() if !dialect.allowXmlLiterals => failXml()
-      case Xml.Part(_) if !dialect.allowXmlLiterals => failXml()
-      case Xml.SpliceStart() if !dialect.allowXmlLiterals => failXml()
-      case Xml.SpliceEnd() if !dialect.allowXmlLiterals => failXml()
-      case Xml.End() if !dialect.allowXmlLiterals => failXml()
-      case Unquote() if !dialect.allowUnquotes => failQuasiquote()
-      case Ellipsis(_) if !dialect.allowUnquotes => failQuasiquote()
-      case Viewbound() if !dialect.allowViewBounds => failViewBound()
-      case _ => // do nothing, check passed
-    }
     s(x.text)
   }
 }
diff --git a/scalameta/tokens/shared/src/main/scala/scala/meta/tokens/Token.scala b/scalameta/tokens/shared/src/main/scala/scala/meta/tokens/Token.scala
index c21ed4022..5272a1c7b 100644
--- a/scalameta/tokens/shared/src/main/scala/scala/meta/tokens/Token.scala
+++ b/scalameta/tokens/shared/src/main/scala/scala/meta/tokens/Token.scala
@@ -99,7 +99,9 @@ object Token {
   // Symbolic keywords
   @fixed("#") class Hash extends SymbolicKeyword
   @fixed(":") class Colon extends SymbolicKeyword
-  @fixed("<%") class Viewbound extends SymbolicKeyword
+  @fixed("<%") class Viewbound extends SymbolicKeyword {
+    require(dialect.allowViewBounds, s"$dialect doesn't support view bounds")
+  }
   @freeform("<-") class LeftArrow extends SymbolicKeyword
   @fixed("<:") class Subtype extends SymbolicKeyword
   @fixed("=") class Equals extends SymbolicKeyword
@@ -144,11 +146,21 @@ object Token {
     @freeform("interpolation end") class End extends Token
   }
   object Xml {
-    @freeform("xml start") class Start extends Token
-    @freeform("xml part") class Part(value: String) extends Token
-    @freeform("xml splice start") class SpliceStart extends Token
-    @freeform("xml splice end") class SpliceEnd extends Token
-    @freeform("xml end") class End extends Token
+    @freeform("xml start") class Start extends Token {
+      require(dialect.allowXmlLiterals, s"$dialect doesn't support xml literals")
+    }
+    @freeform("xml part") class Part(value: String) extends Token {
+      require(dialect.allowXmlLiterals, s"$dialect doesn't support xml literals")
+    }
+    @freeform("xml splice start") class SpliceStart extends Token {
+      require(dialect.allowXmlLiterals, s"$dialect doesn't support xml literals")
+    }
+    @freeform("xml splice end") class SpliceEnd extends Token {
+      require(dialect.allowXmlLiterals, s"$dialect doesn't support xml literals")
+    }
+    @freeform("xml end") class End extends Token {
+      require(dialect.allowXmlLiterals, s"$dialect doesn't support xml literals")
+    }
   }
 
   @branch trait Indentation extends Whitespace
@@ -177,8 +189,12 @@ object Token {
 
   // NOTE: in order to maintain conceptual compatibility with scala.reflect's implementation,
   // Ellipsis.rank = 1 means .., Ellipsis.rank = 2 means ..., etc
-  @freeform("ellipsis") private[meta] class Ellipsis(rank: Int) extends Token
-  @freeform("unquote") private[meta] class Unquote extends Token
+  @freeform("ellipsis") private[meta] class Ellipsis(rank: Int) extends Token {
+    require(dialect.allowUnquotes, s"$dialect doesn't support unquoting")
+  }
+  @freeform("unquote") private[meta] class Unquote extends Token {
+    require(dialect.allowUnquotes, s"$dialect doesn't support unquoting")
+  }
 
   implicit def classifiable[T <: Token]: Classifiable[T] = null
   implicit def showStructure[T <: Token]: Structure[T] = TokenStructure.apply[T]
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreePositions.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreePositions.scala
index 5d8c7e075..6b3b5539d 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreePositions.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreePositions.scala
@@ -42,7 +42,7 @@ object Positions {
           if (sliceStyle == SliceStyle.Show) (s + "<" + slice + ">") else s
       }
       def loopField(x: Any, color: String): Show.Result = x match {
-        case el: String => s(enquote(el, DoubleQuotes).colored(color))
+        case el: String => s(DoubleQuotes(el).colored(color))
         case el: Tree => loopTree(el)
         case el: Nil.type => s("Nil".colored(color))
         case el @ List(List()) => s("List(List())".colored(color))
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeStructure.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeStructure.scala
index 1cfbc0196..11ff9336b 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeStructure.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeStructure.scala
@@ -18,14 +18,14 @@ object TreeStructure {
       case _: Name.Placeholder =>
         s(s"""Name.Placeholder()""")
       case Name.Indeterminate(value) =>
-        s("Name(", enquote(value, DoubleQuotes), ")")
+        s("Name(", DoubleQuotes(value), ")")
       case x =>
         s(
           x.productPrefix,
           "(", {
             def default = {
               def anyStructure(x: Any): String = x match {
-                case el: String => enquote(el, DoubleQuotes)
+                case el: String => DoubleQuotes(el)
                 case el: Tree => el.structure
                 case None => "None"
                 case Some(el) => "Some(" + anyStructure(el) + ")"
@@ -42,7 +42,7 @@ object TreeStructure {
               case _: Quasi =>
                 default
               case Lit(value: String) =>
-                s(enquote(value, DoubleQuotes))
+                s(DoubleQuotes(value))
               case _: Lit.Unit | _: Lit.Null =>
                 s()
               case x: Lit.Double =>
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 3e6c8e1e0..95145fbb0 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
@@ -804,14 +804,13 @@ object TreeSyntax {
         }
       case t @ Lit.Char(value) =>
         val syntax = t.pos match {
-          case Position.None => enquote(value.toString, SingleQuotes)
+          case Position.None => SingleQuotes(value)
           case pos => pos.text
         }
         m(Literal, s(syntax))
       case t @ Lit.String(value) =>
         val syntax = t.pos match {
-          case Position.None =>
-            enquote(value, if (value.contains('\n')) TripleQuotes else DoubleQuotes)
+          case Position.None => DoubleQuotes.orTriple(value)
           case pos => pos.text
         }
         m(Literal, s(syntax))
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeToString.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeToString.scala
index 2f9136199..8d5c257b8 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeToString.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeToString.scala
@@ -2,22 +2,21 @@ package scala.meta
 package internal
 package prettyprinters
 
-import scala.meta.dialects.Scala211
+import scala.meta.dialects
 import scala.meta.internal.trees.{Quasi, Origin}
 
 object TreeToString {
+  // this dialect is as good as any as a default
+  private val defaultDialect = dialects.Scala213
+
   def apply(tree: Tree) = {
+    val isQuasi = tree.isInstanceOf[Quasi]
     val dialect = tree.origin match {
       case x: Origin.Parsed => x.dialect
-      case Origin.None if tree.isInstanceOf[Quasi] => Scala211.unquoteTerm(multiline = true)
-      case Origin.None => Scala211 // this dialect is as good as any as a default
+      case _ => if (isQuasi) defaultDialect.unquoteTerm(multiline = true) else defaultDialect
     }
     val prettyprinter = TreeSyntax[Tree](dialect)
     val code = prettyprinter(tree).toString
-    tree match {
-      case _: Quasi => code
-      case Ctor.Primary(_, name, _) => s"def this$code"
-      case _ => code
-    }
+    if (!isQuasi && tree.isInstanceOf[Ctor.Primary]) s"def this$code" else code
   }
 }


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/AndOrTypesSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/AndOrTypesSuite.scala
index e520b047e..b8160a6cc 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/AndOrTypesSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/AndOrTypesSuite.scala
@@ -10,7 +10,9 @@ class AndOrTypesSuite extends BaseDottySuite {
    *   - [[https://dotty.epfl.ch/docs/reference/new-types/union-types.html]]
    */
   test("view bounds not allowed") {
-    intercept[ParseException] {
+    interceptMessage[IllegalArgumentException](
+      "requirement failed: Scala33 doesn't support view bounds"
+    ) {
       dialects.Scala3("{ def foo[T <% Int](t: T) = ??? }").parse[Term].get
     }
   }
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 792cc8b38..6a847cf1e 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
@@ -296,7 +296,7 @@ class SuccessSuite extends TreeSuiteBase {
     val foo = q"foo"
     val types = List.empty[Type]
     val error =
-      """|invariant failed:
+      """|invariant failed (targClause should be non-empty):
          |when verifying targClause.!=(null).&&(targClause.isInstanceOf[scala.meta.internal.trees.Quasi].||(targClause.nonEmpty))
          |found that targClause.isInstanceOf[scala.meta.internal.trees.Quasi] is false
          |and also targClause.nonEmpty is false