Repository: scalameta/scalameta
Pull request #3475 (2024-01-06 16:19:09)

Invariants: provide human-readable clue of failure


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
                 }
               """
           }


Test patch:
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