├── project ├── build.properties ├── plugins.sbt └── Boilerplate.scala ├── version.sbt ├── .gitignore ├── .scalafmt.conf ├── core └── src │ ├── test │ └── scala │ │ └── org │ │ └── typelevel │ │ └── claimant │ │ ├── Test.scala │ │ ├── AnonymousFnTest.scala │ │ ├── RichPrimitiveTest.scala │ │ ├── RenderTest.scala │ │ └── ClaimTest.scala │ └── main │ └── scala │ └── org │ └── typelevel │ └── claimant │ ├── Scribe.scala │ ├── Tinker.scala │ ├── tinker │ ├── ForBooleanOps.scala │ ├── ForTypeClasses.scala │ └── ForAdHoc.scala │ ├── scribe │ ├── ForCollections.scala │ ├── ForRichWrappers.scala │ └── ForComparators.scala │ ├── render │ └── CaseClass.scala │ ├── Format.scala │ ├── System.scala │ ├── Claim.scala │ └── Render.scala ├── .github └── workflows │ └── scala.yml ├── mc └── src │ └── main │ └── scala │ └── org │ └── typelevel │ └── claimant │ └── mc │ └── Macros.scala └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.5 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "0.2.1-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | project/boot 2 | target 3 | .ensime 4 | .ensime_lucene 5 | .ensime_cache 6 | TAGS 7 | \#*# 8 | *~ 9 | .#* 10 | .lib 11 | .history 12 | .*.swp 13 | .idea 14 | .idea/* 15 | .idea_modules 16 | .DS_Store 17 | .sbtrc 18 | *.sublime-project 19 | *.sublime-workspace 20 | tests.iml 21 | .bsp 22 | # Auto-copied by sbt-microsites 23 | docs/src/main/tut/contributing.md 24 | docs/src/main/tut/index.md 25 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version=3.8.1 2 | 3 | runner.dialect = scala213 4 | 5 | maxColumn = 120 6 | docstrings.style = Asterisk 7 | 8 | align.openParenCallSite = true 9 | align.openParenDefnSite = true 10 | 11 | continuationIndent.defnSite = 2 12 | assumeStandardLibraryStripMargin = true 13 | danglingParentheses.preset = true 14 | 15 | rewrite.rules = [ 16 | AvoidInfix, 17 | SortImports, 18 | RedundantBraces, 19 | RedundantParens, 20 | SortModifiers 21 | ] 22 | -------------------------------------------------------------------------------- /core/src/test/scala/org/typelevel/claimant/Test.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import org.scalacheck.{Gen, Prop} 4 | 5 | object Test { 6 | def run(p: Prop): Either[Set[String], Set[String]] = { 7 | val r = p.apply(Gen.Parameters.default) 8 | val passed = r.status == Prop.True || r.status == Prop.Proof 9 | if (passed) Right(r.labels) else Left(r.labels) 10 | } 11 | 12 | def test(p: Prop, msg: String): Prop = 13 | Claim(run(p) == Left(Set(msg))) 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | scala: [2.12.19, 2.13.14] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: olafurpg/setup-scala@v13 17 | with: 18 | java-version: openjdk@1.11.0 19 | - name: Scalafmt 20 | if: startsWith(matrix.scala, '2.13') 21 | run: sbt scalafmtSbtCheck scalafmtCheckAll 22 | - name: Run tests 23 | run: sbt ++${{matrix.scala}} test 24 | -------------------------------------------------------------------------------- /mc/src/main/scala/org/typelevel/claimant/mc/Macros.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | package mc 3 | 4 | import scala.reflect.macros.blackbox.Context 5 | import scala.util.Properties 6 | 7 | object Macros { 8 | 9 | private val Scala211 = """2\.11\..*""".r 10 | 11 | def forVersion[A](curr: A)(for211: A): A = 12 | macro forVersionMacro[A] 13 | 14 | def forVersionMacro[A](c: Context)(curr: c.Expr[A])(for211: c.Expr[A]): c.Expr[A] = 15 | Properties.versionNumberString match { 16 | case Scala211() => for211 17 | case _ => curr 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0") 2 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") 3 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") 4 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") 5 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.1") 6 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") 7 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0") 8 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") 9 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.12") 10 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/Scribe.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import scala.reflect.macros.blackbox.Context 4 | 5 | /** 6 | * Scribe represents a set of strategies for annotating expressions to produce more interesting String representations. 7 | * 8 | * The annotate method should return None in cases where the expression shape is not recognized. It is given a reference 9 | * to the System because annotation might be recursive. (Currently recursive annotations are not used due to the 10 | * complexity of displaying that information.) 11 | * 12 | * The trees that result from annotate must be String expressions. 13 | */ 14 | trait Scribe { 15 | def annotate(c: Context)(input: c.Tree, sys: System): Option[c.Tree] 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/Tinker.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import scala.reflect.macros.blackbox.Context 4 | 5 | /** 6 | * Tinker represents a set of strategies for deconstructing Boolean expressions into Claims (the `deconstruct` method). 7 | * 8 | * The deconstruct method should return None in cases where the expression shape is not recognized. It is given a 9 | * reference to the System because deconstruction is often recursive -- for example, deconstructing (x && y) may involve 10 | * deconstructing x and deconstructing y independently. 11 | * 12 | * Only c.Expr[Boolean] expressions can be deconstructed. A different process (annotation) is used to create labels -- 13 | * see Scribe for more information. 14 | */ 15 | trait Tinker { 16 | def deconstruct(c: Context)(e0: c.Expr[Boolean], sys: System): Option[c.Expr[Claim]] 17 | } 18 | -------------------------------------------------------------------------------- /core/src/test/scala/org/typelevel/claimant/AnonymousFnTest.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import org.scalacheck.{Prop, Properties} 4 | 5 | // the commented-out first property will fail to compile on 2.11, 6 | // per https://github.com/typelevel/claimant/issues/22 7 | 8 | object AnonymousFnTest extends Properties("EnrichmentTest") { 9 | val xs = List(1, 2, 3, 4) 10 | 11 | // property("works on 2.12+, fails to compile on 2.11") = 12 | // Claim(xs.flatMap(x => List(x).filter(_ => false)) == Nil) 13 | 14 | property("this works on 2.11+") = { 15 | val f = (x: Int) => List(x).filter(_ => false) 16 | Claim(xs.flatMap(f) == Nil) 17 | } 18 | 19 | property("also works on 2.11+") = { 20 | val p = (x: Int) => false 21 | Claim(xs.flatMap(x => List(x).filter(p)) == Nil) 22 | } 23 | 24 | property("explicit props are fine on 2.11+") = Prop(xs.flatMap(x => List(x).filter(_ => false)) == Nil) 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/tinker/ForBooleanOps.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | package tinker 3 | 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | object ForBooleanOps extends Tinker { 7 | val binops = Set("$amp$amp", "$amp", "$bar$bar", "$bar", "$up") 8 | 9 | def deconstruct(c: Context)(t: c.Expr[Boolean], sys: System): Option[c.Expr[Claim]] = { 10 | import c.universe._ 11 | t.tree match { 12 | case q"!$x" => 13 | val xx = sys.deconstruct(c)(c.Expr(x)) 14 | Some(c.Expr(q"!$xx")) 15 | case q"$x.$method($y)" if binops(method.toString) => 16 | val xx = sys.deconstruct(c)(c.Expr(x)) 17 | val yy = sys.deconstruct(c)(c.Expr(y)) 18 | Some(c.Expr(method.toString match { 19 | case "$amp$amp" | "$amp" => q"$xx & $yy" 20 | case "$bar$bar" | "$bar" => q"$xx | $yy" 21 | case "$up" => q"$xx ^ $yy" 22 | })) 23 | case _ => 24 | None 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/scribe/ForCollections.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | package scribe 3 | 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | object ForCollections extends Scribe { 7 | def annotate(c: Context)(input: c.Tree, sys: System): Option[c.Tree] = { 8 | import c.universe._ 9 | input match { 10 | case q"($x).size" => 11 | val sx = sys.annotate(c)(x) 12 | Some(Format.str1(c, sys)(sx, "size", Some(input))) 13 | case q"($x).length" => 14 | val sx = sys.annotate(c)(x) 15 | Some(Format.str1(c, sys)(sx, "length", Some(input))) 16 | case q"($x).lengthCompare($y)" => 17 | val sx = sys.annotate(c)(x) 18 | val sy = sys.annotate(c)(y) 19 | Some(Format.str1_1(c, sys)(sx, "lengthCompare", sy, Some(input))) 20 | case q"($x).min[$tpe]($o)" => 21 | val sx = sys.annotate(c)(x) 22 | Some(Format.str1(c, sys)(sx, "min", Some(input))) 23 | case q"($x).max[$tpe]($o)" => 24 | val sx = sys.annotate(c)(x) 25 | Some(Format.str1(c, sys)(sx, "max", Some(input))) 26 | case _ => 27 | None 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/tinker/ForTypeClasses.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | package tinker 3 | 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | object ForTypeClasses extends Tinker { 7 | 8 | val ops: Map[String, Option[String]] = 9 | Map("equiv" -> None, 10 | "eqv" -> Some("==="), 11 | "neqv" -> Some("=!="), 12 | "lt" -> Some("<"), 13 | "lteqv" -> Some("<="), 14 | "gt" -> Some(">"), 15 | "gteqv" -> Some(">=") 16 | ) 17 | 18 | def deconstruct(c: Context)(e0: c.Expr[Boolean], sys: System): Option[c.Expr[Claim]] = { 19 | import c.universe._ 20 | val t = e0.tree 21 | t match { 22 | case q"($o).$method($x, $y)" if ops.contains(method.toString) => 23 | val xx = sys.annotate(c)(x) 24 | val yy = sys.annotate(c)(y) 25 | val label = ops.get(method.toString).flatten match { 26 | case Some(op) => 27 | Format.str2(c, sys)(xx, op, yy, None) 28 | case None => 29 | Format.str1_2(c, sys)(o, method.toString, xx, yy, None) 30 | } 31 | Some(c.Expr(q"_root_.org.typelevel.claimant.Claim($t, $label)")) 32 | 33 | // fall-through 34 | case _ => 35 | None 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/render/CaseClass.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | package render 3 | 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | object CaseClass { 7 | 8 | def impl[A: c.WeakTypeTag](c: Context) = { 9 | import c.universe._ 10 | 11 | val A = weakTypeOf[A] 12 | 13 | def isTuple(sym: Symbol): Boolean = 14 | sym.name.decodedName.toString.startsWith("Tuple") && 15 | sym.owner == typeOf[Any].typeSymbol.owner 16 | 17 | if (!A.typeSymbol.asClass.isCaseClass) 18 | c.abort(c.enclosingPosition, "Not a case class!") 19 | else if (A.baseClasses.exists(isTuple)) 20 | c.abort(c.enclosingPosition, "Not needed for tuples!") 21 | else { 22 | val name = A.typeSymbol.name.toString 23 | val fields = A.decls.collect { case m: MethodSymbol if m.isCaseAccessor => m } 24 | 25 | val evs = fields.zipWithIndex.map { case (m, i) => 26 | val ev = TermName(s"ev$i") 27 | q"private val $ev = _root_.org.typelevel.claimant.Render[${m.returnType}]" 28 | } 29 | 30 | val stmts = fields.zipWithIndex.flatMap { case (m, i) => 31 | val ev = TermName(s"ev$i") 32 | val stmt = q"$ev.renderInto(sb, a.${m.name})" 33 | if (i > 0) q"""sb.append(", ")""" :: stmt :: Nil else stmt :: Nil 34 | } 35 | 36 | c.Expr(q""" 37 | new _root_.org.typelevel.claimant.Render[$A] { 38 | 39 | ..$evs 40 | 41 | def renderInto(sb: _root_.scala.collection.mutable.StringBuilder, a: $A): StringBuilder = { 42 | sb.append($name) 43 | sb.append("(") 44 | ..$stmts 45 | sb.append(")") 46 | } 47 | }""") 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/Format.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import scala.reflect.macros.blackbox.Context 4 | 5 | /** 6 | * Format provides supports building trees of different shapes with quasiquotes. 7 | * 8 | * The trees don't represent actual expressions, but rather represent String expressions involving concatenation of 9 | * other Strings and String expressions. 10 | * 11 | * These methods will also annotate the String with an optional value (representing the result of the expression). 12 | */ 13 | object Format { 14 | 15 | // appends {value} to the string 16 | def addValue(c: Context, sys: System)(s: c.Tree, value: Option[c.Tree]): c.Tree = { 17 | import c.universe._ 18 | value.fold(s) { in => 19 | val sin = sys.render(c)(in) 20 | q"""$s + " {" + $sin + "}"""" 21 | } 22 | } 23 | 24 | // shape: x.method 25 | def str1(c: Context, sys: System)(x: c.Tree, method: String, value: Option[c.Tree]): c.Tree = { 26 | import c.universe._ 27 | addValue(c, sys)(q"""$x + "." + $method""", value) 28 | } 29 | 30 | // shape: x op y 31 | def str2(c: Context, sys: System)(x: c.Tree, op: String, y: c.Tree, value: Option[c.Tree]): c.Tree = { 32 | import c.universe._ 33 | val sx = sys.tostr(c)(x) 34 | val sy = sys.tostr(c)(y) 35 | addValue(c, sys)(q"""$sx + " " + $op + " " + $sy""", value) 36 | } 37 | 38 | // shape: x.method(y) 39 | def str1_1(c: Context, sys: System)(x: c.Tree, method: String, y: c.Tree, value: Option[c.Tree]): c.Tree = { 40 | import c.universe._ 41 | val sx = sys.tostr(c)(x) 42 | val sy = sys.tostr(c)(y) 43 | addValue(c, sys)(q"""$sx + "." + $method + "(" + $sy + ")"""", value) 44 | } 45 | 46 | // shape: o.method(x, y) 47 | def str1_2(c: Context, 48 | sys: System 49 | )(o: c.Tree, method: String, x: c.Tree, y: c.Tree, value: Option[c.Tree]): c.Tree = { 50 | import c.universe._ 51 | val so = sys.tostr(c)(o) 52 | val sx = sys.tostr(c)(x) 53 | val sy = sys.tostr(c)(y) 54 | addValue(c, sys)(q"""$so + "." + $method + "(" + $sx + ", " + $sy + ")"""", value) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/scribe/ForRichWrappers.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | package scribe 3 | 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | abstract class ForRichWrappers extends Scribe { 7 | def wrappers: Set[String] 8 | def unops: Set[String] 9 | def binops: Set[String] 10 | 11 | def annotate(c: Context)(input: c.Tree, sys: System): Option[c.Tree] = { 12 | import c.universe._ 13 | input match { 14 | case q"$w($x).$m($y)" if wrappers(w.toString) && binops(m.toString) => 15 | val sx = sys.annotate(c)(x) 16 | val sy = sys.annotate(c)(y) 17 | Some(Format.str2(c, sys)(sx, m.toString, sy, Some(input))) 18 | case q"$w($x).$m" if wrappers(w.toString) && unops(m.toString) => 19 | val sx = sys.annotate(c)(x) 20 | Some(Format.str1(c, sys)(sx, m.toString, Some(input))) 21 | case _ => 22 | None 23 | } 24 | } 25 | } 26 | 27 | object ForRichWrappers { 28 | 29 | val ints: Set[String] = 30 | Set("scala.Predef.byteWrapper", "scala.Predef.shortWrapper", "scala.Predef.intWrapper", "scala.Predef.longWrapper") 31 | 32 | val ints211: Set[String] = Set("scala.this.Predef.byteWrapper", 33 | "scala.this.Predef.shortWrapper", 34 | "scala.this.Predef.intWrapper", 35 | "scala.this.Predef.longWrapper" 36 | ) 37 | 38 | object ForIntWrapper extends ForRichWrappers { 39 | val wrappers: Set[String] = mc.Macros.forVersion(ints)(ints211) 40 | val unops: Set[String] = Set("signum") 41 | val binops: Set[String] = Set("max", "min") 42 | } 43 | 44 | val floats: Set[String] = Set("scala.Predef.floatWrapper", "scala.Predef.doubleWrapper") 45 | 46 | val floats211: Set[String] = Set("scala.this.Predef.floatWrapper", "scala.this.Predef.doubleWrapper") 47 | 48 | object ForFloatWrapper extends ForRichWrappers { 49 | val wrappers: Set[String] = mc.Macros.forVersion(floats)(floats211) 50 | val unops: Set[String] = Set("abs", "ceil", "floor", "round") 51 | val binops: Set[String] = Set("max", "min") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/scribe/ForComparators.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | package scribe 3 | 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | object ForComparators extends Scribe { 7 | def annotate(c: Context)(input: c.Tree, sys: System): Option[c.Tree] = { 8 | import c.universe._ 9 | 10 | val augmentString: String = { 11 | val prefix = mc.Macros.forVersion("scala")("scala.this") 12 | s"$prefix.Predef.augmentString" 13 | } 14 | 15 | input match { 16 | case q"($o).min($x, $y)" => 17 | val sx = sys.annotate(c)(x) 18 | val sy = sys.annotate(c)(y) 19 | Some(Format.str2(c, sys)(sx, "min", sy, Some(input))) 20 | case q"($o).max($x, $y)" => 21 | val sx = sys.annotate(c)(x) 22 | val sy = sys.annotate(c)(y) 23 | Some(Format.str2(c, sys)(sx, "max", sy, Some(input))) 24 | case q"($o).pmin($x, $y)" => 25 | val sx = sys.annotate(c)(x) 26 | val sy = sys.annotate(c)(y) 27 | Some(Format.str2(c, sys)(sx, "pmin", sy, Some(input))) 28 | case q"($o).pmax($x, $y)" => 29 | val sx = sys.annotate(c)(x) 30 | val sy = sys.annotate(c)(y) 31 | Some(Format.str2(c, sys)(sx, "pmax", sy, Some(input))) 32 | case q"($o).compare($x, $y)" => 33 | val so = sys.annotate(c)(o) 34 | val sx = sys.annotate(c)(x) 35 | val sy = sys.annotate(c)(y) 36 | Some(Format.str1_2(c, sys)(so, "compare", sx, sy, Some(input))) 37 | case q"($o).tryCompare($x, $y)" => 38 | val so = sys.annotate(c)(o) 39 | val sx = sys.annotate(c)(x) 40 | val sy = sys.annotate(c)(y) 41 | Some(Format.str1_2(c, sys)(so, "tryCompare", sx, sy, Some(input))) 42 | case q"($o).partialCompare($x, $y)" => 43 | val so = sys.annotate(c)(o) 44 | val sx = sys.annotate(c)(x) 45 | val sy = sys.annotate(c)(y) 46 | Some(Format.str1_2(c, sys)(so, "partialCompare", sx, sy, Some(input))) 47 | 48 | case q"($x).compare($y)" => 49 | val sx = sys.annotate(c)(x) 50 | val sy = sys.annotate(c)(y) 51 | Some(Format.str1_1(c, sys)(sx, "compare", sy, Some(input))) 52 | case q"($x).compareTo($y)" => 53 | val sx = sys.annotate(c)(x) 54 | val sy = sys.annotate(c)(y) 55 | Some(Format.str1_1(c, sys)(sx, "compareTo", sy, Some(input))) 56 | case q"scala.`package`.Ordering.Implicits.infixOrderingOps[$tpe]($x)($o)" => 57 | Some(sys.annotate(c)(x)) 58 | case q"$meth($s)" if meth.toString == augmentString => 59 | Some(sys.annotate(c)(s)) 60 | 61 | case _ => 62 | None 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/tinker/ForAdHoc.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | package tinker 3 | 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | object ForAdHoc extends Tinker { 7 | 8 | val unops = 9 | Set("isEmpty", "nonEmpty", "isZero", "nonZero") 10 | 11 | val binops = 12 | Set( 13 | "$eq$eq", 14 | "$bang$eq", 15 | "eq", 16 | "ne", 17 | "equals", 18 | "$less", 19 | "$greater", 20 | "$less$eq", 21 | "$greater$eq", 22 | "startsWith", 23 | "endsWith", 24 | "contains", 25 | "containsSlice", 26 | "apply", 27 | "isDefinedAt", 28 | "sameElements", 29 | "subsetOf", 30 | "exists", 31 | "forall", 32 | "min", 33 | "max", 34 | "pmin", 35 | "pmax" 36 | ) 37 | 38 | def deconstruct(c: Context)(e0: c.Expr[Boolean], sys: System): Option[c.Expr[Claim]] = { 39 | import c.universe._ 40 | 41 | val t = e0.tree 42 | 43 | def unop(meth: TermName, x: c.Tree): c.Expr[Claim] = { 44 | val xx = sys.annotate(c)(x) 45 | val label = Format.str1(c, sys)(xx, meth.toString, None) 46 | c.Expr(q"_root_.org.typelevel.claimant.Claim($t, $label)") 47 | } 48 | 49 | def binop(meth: TermName, x: c.Tree, y: c.Tree): c.Expr[Claim] = { 50 | val xx = sys.annotate(c)(x) 51 | val yy = sys.annotate(c)(y) 52 | val label: c.Tree = meth.toString match { 53 | case "$eq$eq" => Format.str2(c, sys)(xx, "==", yy, None) 54 | case "$bang$eq" => Format.str2(c, sys)(xx, "!=", yy, None) 55 | case "eq" => Format.str2(c, sys)(xx, "eq", yy, None) 56 | case "ne" => Format.str2(c, sys)(xx, "ne", yy, None) 57 | 58 | case "$less" => Format.str2(c, sys)(xx, "<", yy, None) 59 | case "$less$eq" => Format.str2(c, sys)(xx, "<=", yy, None) 60 | case "$greater" => Format.str2(c, sys)(xx, ">", yy, None) 61 | case "$greater$eq" => Format.str2(c, sys)(xx, ">=", yy, None) 62 | 63 | case "exists" | "forall" => 64 | Format.str1(c, sys)(xx, meth.toString + "(...)", None) 65 | 66 | case _ => 67 | Format.str1_1(c, sys)(xx, meth.toString, yy, None) 68 | } 69 | 70 | c.Expr(q"_root_.org.typelevel.claimant.Claim($t, $label)") 71 | } 72 | 73 | t match { 74 | 75 | // unops 76 | case q"$x.$method" if unops(method.toString) => 77 | Some(unop(method, x)) 78 | case q"$x.$method()" if unops(method.toString) => 79 | Some(unop(method, x)) 80 | 81 | // binops 82 | case q"$x.$method($y)" if binops(method.toString) => 83 | Some(binop(method, x, y)) 84 | case q"$x.$method[$tpe]($y)" if binops(method.toString) => 85 | Some(binop(method, x, y)) 86 | 87 | // fall-through 88 | case _ => 89 | None 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/System.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import scala.reflect.macros.blackbox.Context 4 | 5 | /** 6 | * System encapsulates the strategies used by Claimant. 7 | * 8 | * Tinkers describe how to decompose Boolean expressions, and scribes describe how to label _any_ expression. Together 9 | * we use them to build labels for labeled Prop values. 10 | */ 11 | abstract class System { sys => 12 | 13 | def tinkers: List[Tinker] 14 | def scribes: List[Scribe] 15 | def render(c: Context)(t: c.Tree): c.Tree 16 | 17 | final def tostr(c: Context)(t: c.Tree): c.Tree = { 18 | import c.universe._ 19 | q"$t.toString" 20 | } 21 | 22 | /** 23 | * System.deconstruct is where the magic happens. 24 | * 25 | * This is the high-level logic of how Claimant works. Basically, we're given a top-level Boolean expression. First we 26 | * try to break that expression into sub-expressions. Each Boolean sub-expression which can't be split into smaller 27 | * ones is represented by a simple claim (Claim.Simple value). For each of these we generate a label. 28 | * 29 | * Then we recombine these claims using the same operations that connected their sub-expressions (e.g. AND, OR, etc.), 30 | * producing a single top-level Claim. 31 | */ 32 | def deconstruct(c: Context)(e0: c.Expr[Boolean]): c.Expr[Claim] = { 33 | import c.universe._ 34 | def loop(lst: List[Tinker]): c.Expr[Claim] = 35 | lst match { 36 | case Nil => 37 | val label = annotate(c)(e0.tree) 38 | c.Expr(q"_root_.org.typelevel.claimant.Claim($e0, $label)") 39 | case tinker :: rest => 40 | tinker.deconstruct(c)(e0, sys) match { 41 | case Some(e1) => e1 42 | case None => loop(rest) 43 | } 44 | } 45 | loop(tinkers) 46 | } 47 | 48 | /** 49 | * Annotate any Tree with a description of its expression. 50 | * 51 | * In many cases this will just stringify the resulting value of an expression. But in other cases it will display 52 | * parts of the expression as well as its result. 53 | */ 54 | def annotate(c: Context)(input: c.Tree): c.Tree = { 55 | def loop(lst: List[Scribe]): c.Tree = 56 | lst match { 57 | case Nil => 58 | render(c)(input) 59 | case scribe :: rest => 60 | scribe.annotate(c)(input, sys) match { 61 | case Some(t) => t 62 | case None => loop(rest) 63 | } 64 | } 65 | loop(scribes) 66 | } 67 | } 68 | 69 | object System { 70 | 71 | /** 72 | * Default system factory method. 73 | * 74 | * Builds using the given scribes and tinkers, using .toString to stringify values. 75 | */ 76 | def apply(tinkers0: List[Tinker], scribes0: List[Scribe]): System = 77 | new System { 78 | def tinkers: List[Tinker] = tinkers0 79 | def scribes: List[Scribe] = scribes0 80 | def render(c: Context)(t: c.Tree): c.Tree = { 81 | import c.universe._ 82 | q"_root_.org.typelevel.claimant.Render.render($t)" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core/src/test/scala/org/typelevel/claimant/RichPrimitiveTest.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import org.scalacheck.Properties 4 | import Test.test 5 | 6 | object RichPrimitiveTest extends Properties("RichPrimitiveTest") { 7 | 8 | // RichByte 9 | 10 | val (b1, b2) = (1.toByte, 2.toByte) 11 | 12 | property("(b1 min b2) = 0") = test(Claim((b1.min(b2)) == 0), s"falsified: $b1 min $b2 {${b1.min(b2)}} == 0") 13 | 14 | property("(b1 max b2) = 0") = test(Claim((b1.max(b2)) == 0), s"falsified: $b1 max $b2 {${b1.max(b2)}} == 0") 15 | 16 | property("b1.signum = 0") = test(Claim(b1.signum == 0), s"falsified: 1.signum {${b1.signum}} == 0") 17 | 18 | // RichShort 19 | 20 | val (s1, s2) = (1.toShort, 2.toShort) 21 | 22 | property("(s1 min s2) = 0") = test(Claim((s1.min(s2)) == 0), s"falsified: $s1 min $s2 {${s1.min(s2)}} == 0") 23 | 24 | property("(s1 max s2) = 0") = test(Claim((s1.max(s2)) == 0), s"falsified: $s1 max $s2 {${s1.max(s2)}} == 0") 25 | 26 | property("s1.signum = 0") = test(Claim(s1.signum == 0), s"falsified: 1.signum {${s1.signum}} == 0") 27 | 28 | // RichInt 29 | 30 | val (i1, i2) = (1, 2) 31 | 32 | property("(i1 min i2) = 0") = test(Claim((i1.min(i2)) == 0), s"falsified: $i1 min $i2 {${i1.min(i2)}} == 0") 33 | 34 | property("(i1 max i2) = 0") = test(Claim((i1.max(i2)) == 0), s"falsified: $i1 max $i2 {${i1.max(i2)}} == 0") 35 | 36 | property("i1.signum = 0") = test(Claim(i1.signum == 0), s"falsified: 1.signum {${i1.signum}} == 0") 37 | 38 | // RichInt 39 | 40 | val (l1, l2) = (1L, 2L) 41 | 42 | property("(l1 min l2) = 0") = test(Claim((l1.min(l2)) == 0), s"falsified: $l1 min $l2 {${l1.min(l2)}} == 0") 43 | 44 | property("(l1 max l2) = 0") = test(Claim((l1.max(l2)) == 0), s"falsified: $l1 max $l2 {${l1.max(l2)}} == 0") 45 | 46 | property("l1.signum = 0") = test(Claim(l1.signum == 0), s"falsified: 1.signum {${l1.signum}} == 0") 47 | 48 | // RichDouble 49 | 50 | val (f1, f2) = (1.0f, 2.0f) 51 | 52 | property("f1.abs = 0") = test(Claim(f1.abs == 0), s"falsified: $f1.abs {${f1.abs}} == 0") 53 | 54 | property("f1.ceil = 0") = test(Claim(f1.ceil == 0), s"falsified: $f1.ceil {${f1.ceil}} == 0") 55 | 56 | property("f1.floor = 0") = test(Claim(f1.floor == 0), s"falsified: $f1.floor {${f1.floor}} == 0") 57 | 58 | property("(f1 max f2) = 0") = test(Claim((f1.max(f2)) == 0), s"falsified: $f1 max $f2 {${f1.max(f2)}} == 0") 59 | 60 | property("(f1 min f2) = 0") = test(Claim((f1.min(f2)) == 0), s"falsified: $f1 min $f2 {${f1.min(f2)}} == 0") 61 | 62 | property("f1.round = 0") = test(Claim(f1.round == 0), s"falsified: $f1.round {${f1.round}} == 0") 63 | 64 | // RichDouble 65 | 66 | val (d1, d2) = (1.0, 2.0) 67 | 68 | property("d1.abs = 0") = test(Claim(d1.abs == 0), s"falsified: $d1.abs {${d1.abs}} == 0") 69 | 70 | property("d1.ceil = 0") = test(Claim(d1.ceil == 0), s"falsified: $d1.ceil {${d1.ceil}} == 0") 71 | 72 | property("d1.floor = 0") = test(Claim(d1.floor == 0), s"falsified: $d1.floor {${d1.floor}} == 0") 73 | 74 | property("(d1 max d2) = 0") = test(Claim((d1.max(d2)) == 0), s"falsified: $d1 max $d2 {${d1.max(d2)}} == 0") 75 | 76 | property("(d1 min d2) = 0") = test(Claim((d1.min(d2)) == 0), s"falsified: $d1 min $d2 {${d1.min(d2)}} == 0") 77 | 78 | property("d1.round = 0") = test(Claim(d1.round == 0), s"falsified: $d1.round {${d1.round}} == 0") 79 | } 80 | -------------------------------------------------------------------------------- /core/src/test/scala/org/typelevel/claimant/RenderTest.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import org.scalacheck.Properties 4 | import scala.{collection => sc} 5 | import scala.collection.{immutable => sci, mutable => scm} 6 | 7 | object RenderTest extends Properties("RenderTest") { 8 | 9 | case class Mux(n: Int, s: String, xs: Array[Int]) 10 | object Mux { implicit val render: Render[Mux] = Render.caseClass[Mux] } 11 | 12 | def test[A](a: A, s: String)(implicit r: Render[A]): Unit = { 13 | property(s) = Claim(r.render(a) == s); () 14 | } 15 | 16 | test(33, "33") 17 | test("hi friend", "\"hi friend\"") 18 | test(Array(1, 2, 3), "Array(1, 2, 3)") 19 | test(Mux(4, "five", Array(6, 7, 8)), "Mux(4, \"five\", Array(6, 7, 8))") 20 | test(("alpha", false, Map(3 -> List('a', 'b', 'c'))), "(\"alpha\", false, Map(3 -> List('a', 'b', 'c')))") 21 | test(Array(Array(3, 4), Array(5, 6)), "Array(Array(3, 4), Array(5, 6))") 22 | test(false, "false") 23 | test((), "()") 24 | test(Symbol("bravo"), Symbol("bravo").toString) // Symbol#toString changed in Scala 2.13.3 25 | test(Iterable(true, false), "Iterable(true, false)") 26 | test(Seq(1, 2), "Seq(1, 2)") 27 | test(IndexedSeq(1, 2, 3), "IndexedSeq(1, 2, 3)") 28 | test(sci.Queue(1, 2, 3), "Queue(1, 2, 3)") 29 | test(scm.ArrayBuffer(99), "ArrayBuffer(99)") 30 | test(Stream(1, 2, 3), "Stream(1, ?)") 31 | test(Stream.empty[Int], "Stream()") 32 | test(Vector(false, true), "Vector(false, true)") 33 | test(Set(Some(1)), "Set(Some(1))") 34 | test(Left("x"): Either[String, Int], "Left(\"x\")") 35 | test(Right(33): Either[String, Int], "Right(33)") 36 | test(Left("x"), "Left(\"x\")") 37 | test(Right(33), "Right(33)") 38 | test(Option(1), "Some(1)") 39 | test(Some(1), "Some(1)") 40 | test(Option.empty[Int], "None") 41 | test(None, "None") 42 | test(List.empty[Int], "List()") 43 | test(sci.HashSet(1, 2), "HashSet(1, 2)") 44 | test(sci.TreeSet(1, 2), "TreeSet(1, 2)") 45 | test(sc.SortedSet(1, 2), "SortedSet(1, 2)") 46 | test(sci.HashMap(1 -> 2), "HashMap(1 -> 2)") 47 | test(sci.TreeMap(1 -> 2), "TreeMap(1 -> 2)") 48 | test(sc.SortedMap(1 -> 2), "SortedMap(1 -> 2)") 49 | test(sci.IntMap(1 -> "hi"), "IntMap(1 -> \"hi\")") 50 | test(sci.LongMap(1L -> "hi"), "LongMap(1 -> \"hi\")") 51 | 52 | test(List[Byte](2, 3, 4), "List(2, 3, 4)") 53 | test(List[Short](2, 3, 4), "List(2, 3, 4)") 54 | test(List[Int](2, 3, 4), "List(2, 3, 4)") 55 | test(List[Long](2L, 3L, 4L), "List(2, 3, 4)") 56 | test(List[Float](2.0f, 3.0f, 4.0f), s"List(${2.0f}, ${3.0f}, ${4.0f})") 57 | test(List[Double](2.0, 3.0, 4.0), s"List(${2.0}, ${3.0}, ${4.0})") 58 | 59 | // test escapes 60 | test("these are escapes: \b \t \n \f \r \" \\ \u0001 ok we're done", 61 | "\"these are escapes: \\b \\t \\n \\f \\r \\\" \\\\ \\u0001 ok we're done\"" 62 | ) 63 | test('a', "'a'") 64 | test('\'', "'\\''") 65 | 66 | // test fallback to toString 67 | 68 | class Ugh { override def toString: String = "Ugh.toString" } 69 | object Ugh { implicit val render: Render[Ugh] = Render.const("Ugh") } 70 | 71 | case class Yes(u: Ugh) 72 | object Yes { implicit val renderForYes: Render[Yes] = Render.caseClass } 73 | 74 | case class Nope(u: Ugh) 75 | 76 | test(Yes(new Ugh), "Yes(Ugh)") 77 | test(Nope(new Ugh), "Nope(Ugh.toString)") 78 | test((new Ugh, new Ugh), "(Ugh, Ugh)") 79 | test(Array(new Ugh, new Ugh), "Array(Ugh, Ugh)") 80 | 81 | // tuple tests 82 | 83 | test(Tuple1(1), "(1)") 84 | test((1, 2), "(1, 2)") 85 | test((1, 2, 3), "(1, 2, 3)") 86 | test((1, 2, 3, 4), "(1, 2, 3, 4)") 87 | test((1, 2, 3, 4, 5), "(1, 2, 3, 4, 5)") 88 | test((1, 2, 3, 4, 5, 6), "(1, 2, 3, 4, 5, 6)") 89 | test((1, 2, 3, 4, 5, 6, 7), "(1, 2, 3, 4, 5, 6, 7)") 90 | test((1, 2, 3, 4, 5, 6, 7, 8), "(1, 2, 3, 4, 5, 6, 7, 8)") 91 | test((1, 2, 3, 4, 5, 6, 7, 8, 9), "(1, 2, 3, 4, 5, 6, 7, 8, 9)") 92 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)") 93 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)") 94 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)") 95 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13), "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)") 96 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14), "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)") 97 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)") 98 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16), 99 | "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)" 100 | ) 101 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17), 102 | "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)" 103 | ) 104 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18), 105 | "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)" 106 | ) 107 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19), 108 | "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)" 109 | ) 110 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20), 111 | "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)" 112 | ) 113 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21), 114 | "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)" 115 | ) 116 | test((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22), 117 | "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)" 118 | ) 119 | 120 | } 121 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/Claim.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import org.scalacheck.Prop 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | object Claim { 7 | 8 | /** 9 | * Transform a Boolean expression into a labeled Prop. 10 | * 11 | * The contents of the expression will be analyzed, to provide more informative messages if the expression fails. 12 | * 13 | * Currently this macro may evaluate sub-expressions multiple times. This means that this macro is NOT SAFE to use 14 | * with impure code, since it may change evaluation order or cause multiple evaluations. 15 | * 16 | * While `claimant.Claim(...)` is not directly configurable in any meaningful sense, it's relatively easy to define a 17 | * new claimant.System and implement your own macro. 18 | * 19 | * This method is Claimant's raison d'etre. 20 | */ 21 | def apply(cond: Boolean): Prop = 22 | macro decompose 23 | 24 | /** 25 | * This method is called by the apply macro. 26 | * 27 | * In turn, it calls `sys.deconstruct`, and then converts the result of that (a `Claim`) into a `Prop`. 28 | */ 29 | def decompose(c: Context)(cond: c.Expr[Boolean]): c.Expr[Prop] = { 30 | import c.universe._ 31 | val e = sys.deconstruct(c)(cond) 32 | c.Expr(q"($e).prop") 33 | } 34 | 35 | /** 36 | * This System describes how we label expressions. 37 | */ 38 | val sys: System = { 39 | val tinkers: List[Tinker] = 40 | tinker.ForBooleanOps :: 41 | tinker.ForTypeClasses :: 42 | tinker.ForAdHoc :: 43 | Nil 44 | 45 | val scribes: List[Scribe] = 46 | scribe.ForRichWrappers.ForIntWrapper :: 47 | scribe.ForRichWrappers.ForFloatWrapper :: 48 | scribe.ForComparators :: 49 | scribe.ForCollections :: 50 | Nil 51 | 52 | System(tinkers, scribes) 53 | } 54 | 55 | /** 56 | * Factory constructor to build a claim. 57 | * 58 | * Unlike its one-argument cousin (the macro), this method does _not_ do any fancy analysis. It simply pairs a Boolean 59 | * value with a String describing that expression. 60 | * 61 | * Claims returns by this method are always Simple claims. 62 | */ 63 | def apply(res: Boolean, msg: String): Claim = 64 | Simple(res, msg) 65 | 66 | /** 67 | * ADT members follow. Other than Simple, these are all recursively-defined. 68 | */ 69 | case class Simple(b: Boolean, msg: String) extends Claim(b) 70 | case class And(lhs: Claim, rhs: Claim) extends Claim(lhs.res && rhs.res) 71 | case class Or(lhs: Claim, rhs: Claim) extends Claim(lhs.res || rhs.res) 72 | case class Xor(lhs: Claim, rhs: Claim) extends Claim(lhs.res ^ rhs.res) 73 | case class Not(c: Claim) extends Claim(!c.res) 74 | } 75 | 76 | /** 77 | * Claim represents a Boolean result with a description of what that result means. 78 | * 79 | * Claims can be composed using the same operators as Booleans, which correspond to recursive Claim subtypes (e.g. And, 80 | * Or, etc.). 81 | * 82 | * All claims can be converted into ScalaCheck Prop values. (The reverse is not true -- it's not possible to extra 83 | * ScalaCheck labels from a Prop without running it.) 84 | */ 85 | sealed abstract class Claim(val res: Boolean) { 86 | 87 | import Claim.{And, Not, Or, Simple, Xor} 88 | 89 | /** 90 | * Build a ScalaCheck Prop value from a claim. 91 | * 92 | * This Prop uses two values from the claim: the `res` and the `label`. Currently it only attaches a label to failed 93 | * Prop values, although this could change in the future. 94 | */ 95 | def prop: Prop = 96 | if (res) Prop(res) else Prop(res) :| s"falsified: $label" 97 | 98 | /** 99 | * Negate this claim, requiring it to be false. 100 | */ 101 | def unary_! : Claim = 102 | Not(this) 103 | 104 | /** 105 | * Combine two claims, requiring both to be true. 106 | * 107 | * This is equivalent to & and && for Boolean. It is not named && because it does not short-circuit evaluation -- the 108 | * right-hand side will be evaluated even if the left-hand side is false. 109 | */ 110 | def &(that: Claim): Claim = 111 | And(this, that) 112 | 113 | /** 114 | * Combine two claims, requiring at least one to be true. 115 | * 116 | * This is equivalent to | and || for Boolean. It is not named || because it does not short-circuit evaluation -- the 117 | * right-hand side will be evaluated even if the left-hand side is true. 118 | */ 119 | def |(that: Claim): Claim = 120 | Or(this, that) 121 | 122 | /** 123 | * Combine two claims, requiring exactly one to be true. 124 | * 125 | * This is eqvuialent to ^ for Boolean. It is an exclusive-or, which means that it is false if both claims are false 126 | * or both claims are true, and true otherwise. 127 | */ 128 | def ^(that: Claim): Claim = 129 | Xor(this, that) 130 | 131 | /** 132 | * Display a status string for a claim. 133 | * 134 | * This method is used to annotate sub-claims in a larger claim. 135 | */ 136 | def status: String = 137 | if (res) "{true}" else "{false}" 138 | 139 | /** 140 | * Label explaining a claim's expression. 141 | * 142 | * This label will be used with ScalaCheck to explain failing properties. Crucially, it will be called recursively, so 143 | * it should not add information that is only relevant at the top-level. 144 | * 145 | * The convention is _not_ to parenthesize a top-level expression in a label, but only sub-expressions. 146 | */ 147 | def label: String = 148 | this match { 149 | case Simple(_, msg) => 150 | msg 151 | case And(p0, p1) => 152 | s"(${p0.label} ${p0.status}) && (${p1.label} ${p1.status})" 153 | case Or(p0, p1) => 154 | s"(${p0.label} ${p0.status}) || (${p1.label} ${p1.status})" 155 | case Xor(p0, p1) => 156 | s"(${p0.label} ${p0.status}) ^ (${p1.label} ${p1.status})" 157 | case Not(p0) => 158 | s"!(${p0.label} ${p0.status})" 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /project/Boilerplate.scala: -------------------------------------------------------------------------------- 1 | import Boilerplate.TemplateVals 2 | import sbt._ 3 | 4 | /** 5 | * Generate a range of boilerplate classes that would be tedious to write and maintain by hand. 6 | * 7 | * Copied, with some modifications, from 8 | * [[https://github.com/milessabin/shapeless/blob/master/project/Boilerplate.scala Shapeless]]. 9 | * 10 | * @author 11 | * Miles Sabin 12 | * @author 13 | * Kevin Wright 14 | */ 15 | object Boilerplate { 16 | 17 | import scala.StringContext._ 18 | 19 | implicit class BlockHelper(private val sc: StringContext) extends AnyVal { 20 | def block(args: Any*): String = { 21 | val interpolated = sc.standardInterpolator(treatEscapes, args) 22 | val rawLines = interpolated.split('\n') 23 | val trimmedLines = rawLines.map(_.dropWhile(_.isWhitespace)) 24 | trimmedLines.mkString("\n") 25 | } 26 | } 27 | 28 | val templates: Seq[Template] = Seq(GenTupleInstances) 29 | 30 | val header = "// auto-generated boilerplate" 31 | val maxArity = 22 32 | 33 | /** 34 | * Return a sequence of the generated files. 35 | * 36 | * As a side-effect, it actually generates them... 37 | */ 38 | def gen(dir: File): Seq[File] = 39 | templates.map { template => 40 | val tgtFile = template.filename(dir) 41 | IO.write(tgtFile, template.body) 42 | tgtFile 43 | } 44 | 45 | class TemplateVals(val arity: Int) { 46 | val synTypes = (0 until arity).map(n => s"A$n") 47 | val synVals = (0 until arity).map(n => s"a$n") 48 | val `A..N` = synTypes.mkString(", ") 49 | val `a..n` = synVals.mkString(", ") 50 | val `_.._` = Seq.fill(arity)("_").mkString(", ") 51 | val `(A..N)` = if (arity == 1) "Tuple1[A0]" else synTypes.mkString("(", ", ", ")") 52 | val `(_.._)` = if (arity == 1) "Tuple1[_]" else Seq.fill(arity)("_").mkString("(", ", ", ")") 53 | val `(a..n)` = if (arity == 1) "Tuple1(a0)" else synVals.mkString("(", ", ", ")") 54 | } 55 | 56 | /** 57 | * Blocks in the templates below use a custom interpolator, combined with post-processing to produce the body. 58 | * 59 | * - The contents of the `header` val is output first 60 | * - Then the first block of lines beginning with '|' 61 | * - Then the block of lines beginning with '-' is replicated once for each arity, with the `templateVals` already 62 | * pre-populated with relevant relevant vals for that arity 63 | * - Then the last block of lines prefixed with '|' 64 | * 65 | * The block otherwise behaves as a standard interpolated string with regards to variable substitution. 66 | */ 67 | trait Template { 68 | def filename(root: File): File 69 | def preBody: String 70 | def instances: Seq[InstanceDef] 71 | def range: IndexedSeq[Int] = 1 to maxArity 72 | def body: String = { 73 | val headerLines = header.split('\n') 74 | val tvs = range.map(n => new TemplateVals(n)) 75 | (headerLines ++ Seq(preBody) ++ instances.flatMap(_.body(tvs))).mkString("\n") 76 | } 77 | } 78 | 79 | case class InstanceDef(start: String, methods: TemplateVals => TemplatedBlock, end: String = "}") { 80 | def body(tvs: Seq[TemplateVals]): Seq[String] = Seq(start) ++ tvs.map(methods(_).content) ++ Seq(end) 81 | } 82 | 83 | abstract class TemplatedBlock(tv: TemplateVals) { 84 | import tv._ 85 | 86 | def constraints(constraint: String) = 87 | synTypes.map(tpe => s"${tpe}: ${constraint}[${tpe}]").mkString(", ") 88 | 89 | def tuple(results: TraversableOnce[String]) = { 90 | val resultsVec = results.toVector 91 | val a = synTypes.size 92 | val r = s"${0.until(a).map(i => resultsVec(i)).mkString(", ")}" 93 | if (a == 1) "Tuple1(" ++ r ++ ")" 94 | else s"(${r})" 95 | } 96 | 97 | def tupleNHeader = s"Tuple${synTypes.size}" 98 | 99 | def binMethod(name: String) = 100 | synTypes.zipWithIndex.iterator.map { case (tpe, i) => 101 | val j = i + 1 102 | s"${tpe}.${name}(x._${j}, y._${j})" 103 | } 104 | 105 | def binTuple(name: String) = 106 | tuple(binMethod(name)) 107 | 108 | def unaryTuple(name: String) = { 109 | val m = synTypes.zipWithIndex.map { case (tpe, i) => s"${tpe}.${name}(x._${i + 1})" } 110 | tuple(m) 111 | } 112 | 113 | def unaryMethod(name: String) = 114 | synTypes.zipWithIndex.iterator.map { case (tpe, i) => 115 | s"$tpe.$name(x._${i + 1})" 116 | } 117 | 118 | def nullaryTuple(name: String) = { 119 | val m = synTypes.map(tpe => s"${tpe}.${name}") 120 | tuple(m) 121 | } 122 | 123 | def content: String 124 | } 125 | 126 | object GenTupleInstances extends Template { 127 | override def range: IndexedSeq[Int] = 1 to maxArity 128 | 129 | def filename(root: File): File = root / "org" / "typelevel" / "claimant" / "RenderTupleInstances.scala" 130 | 131 | val preBody: String = 132 | block""" 133 | package org.typelevel.claimant 134 | """ 135 | 136 | def instances: Seq[InstanceDef] = 137 | Seq( 138 | InstanceDef( 139 | "private[claimant] abstract class RenderTupleInstances {", 140 | tv => 141 | new TemplatedBlock(tv) { 142 | import tv._ 143 | def content = { 144 | val sb = new StringBuilder 145 | sb.append( 146 | s"implicit def renderForTuple${arity}[${`A..N`}](implicit ${constraints("Render")}): Render[${`(A..N)`}] =" + "\n" 147 | ) 148 | sb.append(s" Render.instance { case (sb, ${`(a..n)`}) =>" + "\n") 149 | sb.append(" sb.append(\"(\")" + "\n") 150 | sb.append(s" A0.renderInto(sb, a0)" + "\n") 151 | var i = 1 152 | while (i < arity) { 153 | val (st, sv) = (tv.synTypes(i), tv.synVals(i)) 154 | sb.append(" sb.append(\", \")" + "\n") 155 | sb.append(s" $st.renderInto(sb, $sv)" + "\n") 156 | i += 1 157 | } 158 | sb.append(" sb.append(\")\")" + "\n") 159 | sb.append(s" }" + "\n\n") 160 | sb.toString 161 | } 162 | } 163 | ) 164 | ) 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /core/src/test/scala/org/typelevel/claimant/ClaimTest.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import org.scalacheck.Properties 4 | import Test.test 5 | 6 | case class Qux(n: Int) 7 | 8 | object Qux { 9 | implicit case object QuxOrdering extends Ordering[Qux] { 10 | def compare(x: Qux, y: Qux): Int = Integer.compare(x.n, y.n) 11 | } 12 | 13 | implicit val renderForQux: Render[Qux] = Render.caseClass[Qux] 14 | } 15 | 16 | object ClaimTest extends Properties("ClaimTest") { 17 | 18 | val (x, y) = (1, 2) 19 | val (s0, s1) = ("hello", "goodbye") 20 | 21 | val ws = List.empty[Int] 22 | val xs = List(1, 2, 3, 4) 23 | val ys = Set(1, 2, 3) 24 | val zs = Map("foo" -> 1) 25 | 26 | val arr = Array(2.0, 3.0, 4.0) 27 | 28 | case object Dummy { 29 | def isEmpty(): Boolean = false 30 | } 31 | 32 | property("false") = test(Claim(false), "falsified: false") 33 | 34 | property("x == y") = test(Claim(x == y), "falsified: 1 == 2") 35 | 36 | property("x != x") = test(Claim(x != x), "falsified: 1 != 1") 37 | 38 | property("s0 eq s1") = test(Claim(s0 eq s1), """falsified: "hello" eq "goodbye"""") 39 | 40 | property("s0 ne s0") = test(Claim(s0 ne s0), """falsified: "hello" ne "hello"""") 41 | 42 | property("x < x") = test(Claim(x < x), "falsified: 1 < 1") 43 | 44 | property("y <= x") = test(Claim(y <= x), "falsified: 2 <= 1") 45 | 46 | property("x > x") = test(Claim(x > x), "falsified: 1 > 1") 47 | 48 | property("x >= y") = test(Claim(x >= y), "falsified: 1 >= 2") 49 | 50 | property("arr.length = 4") = test(Claim(arr.length == 4), s"falsified: Array(${2.0}, ${3.0}, ${4.0}).length {3} == 4") 51 | 52 | property("xs.size == 0") = test(Claim(xs.size == 0), "falsified: List(1, 2, 3, 4).size {4} == 0") 53 | 54 | property("ys.size == 2") = test(Claim(ys.size == 2), "falsified: Set(1, 2, 3).size {3} == 2") 55 | 56 | property("zs.size == 3") = test(Claim(zs.size == 3), """falsified: Map("foo" -> 1).size {1} == 3""") 57 | 58 | property("xs.length == 0") = test(Claim(xs.length == 0), "falsified: List(1, 2, 3, 4).length {4} == 0") 59 | 60 | property("s0 compare s1") = test(Claim((s0.compare(s1)) == 0), """falsified: "hello".compare("goodbye") {1} == 0""") 61 | 62 | property("s0 compareTo s1") = 63 | test(Claim((s0.compareTo(s1)) == 0), """falsified: "hello".compareTo("goodbye") {1} == 0""") 64 | 65 | property("xs.lengthCompare(1) == 0") = 66 | test(Claim(xs.lengthCompare(1) == 0), "falsified: List(1, 2, 3, 4).lengthCompare(1) {1} == 0") 67 | 68 | property("xs.isEmpty") = test(Claim(xs.isEmpty), "falsified: List(1, 2, 3, 4).isEmpty") 69 | 70 | property("xs.isEmpty()") = test(Claim(Dummy.isEmpty()), "falsified: Dummy.isEmpty") 71 | 72 | property("ws.nonEmpty") = test(Claim(ws.nonEmpty), "falsified: List().nonEmpty") 73 | 74 | property("hello.startsWith(Hell)") = test(Claim(s0.startsWith("Hell")), """falsified: "hello".startsWith("Hell")""") 75 | 76 | property("hello.endsWith(Ello)") = test(Claim(s0.endsWith("Ello")), """falsified: "hello".endsWith("Ello")""") 77 | 78 | property("xs.contains(99)") = test(Claim(xs.contains(99)), "falsified: List(1, 2, 3, 4).contains(99)") 79 | 80 | property("xs.containsSlice(List(4,5)") = 81 | test(Claim(xs.containsSlice(List(4, 5))), "falsified: List(1, 2, 3, 4).containsSlice(List(4, 5))") 82 | 83 | property("ys(99)") = test(Claim(ys(99)), "falsified: Set(1, 2, 3).apply(99)") 84 | 85 | property("xs.isDefinedAt(6)") = test(Claim(xs.isDefinedAt(6)), "falsified: List(1, 2, 3, 4).isDefinedAt(6)") 86 | 87 | property("xs.sameElements(List(1,2,3,5))") = 88 | test(Claim(xs.sameElements(List(1, 2, 3, 5))), "falsified: List(1, 2, 3, 4).sameElements(List(1, 2, 3, 5))") 89 | 90 | property("ys.subsetOf(Set(3,4,5))") = 91 | test(Claim(ys.subsetOf(Set(3, 4, 5))), "falsified: Set(1, 2, 3).subsetOf(Set(3, 4, 5))") 92 | 93 | property("xs.exists(_ < 0)") = test(Claim(xs.exists(_ < 0)), "falsified: List(1, 2, 3, 4).exists(...)") 94 | 95 | property("xs.forall(_ > 1)") = test(Claim(xs.forall(_ > 1)), "falsified: List(1, 2, 3, 4).forall(...)") 96 | 97 | property("x < x && x < y") = test(Claim(x < x && x < y), "falsified: (1 < 1 {false}) && (1 < 2 {true})") 98 | 99 | property("x < x || y < y") = test(Claim(x < x || y < y), "falsified: (1 < 1 {false}) || (2 < 2 {false})") 100 | 101 | property("(x < y) ^ (y > x)") = test(Claim((x < y) ^ (y > x)), "falsified: (1 < 2 {true}) ^ (2 > 1 {true})") 102 | 103 | property("!(x < y)") = test(Claim(!(x < y)), "falsified: !(1 < 2 {true})") 104 | 105 | property("xs.filter(_ > 2).isEmpty") = test(Claim(xs.filter(_ > 2).isEmpty), "falsified: List(3, 4).isEmpty") 106 | 107 | property("ys.map(_ + 1).subsetOf(ys)") = 108 | test(Claim(ys.map(_ + 1).subsetOf(ys)), "falsified: Set(2, 3, 4).subsetOf(Set(1, 2, 3))") 109 | 110 | property("xs.min == 2") = test(Claim(xs.min == 2), "falsified: List(1, 2, 3, 4).min {1} == 2") 111 | 112 | property("xs.max == 3") = test(Claim(xs.max == 3), "falsified: List(1, 2, 3, 4).max {4} == 3") 113 | 114 | property("(n1 + (n2 + n3)) == ((n1 + n2) + n3)") = { 115 | val (n1, n2, n3) = (0.29622045f, -8.811786e-7f, 1.0369974e-8f) 116 | val (got, expected) = (0.2962196f, 0.29621956f) 117 | test(Claim((n1 + (n2 + n3)) == ((n1 + n2) + n3)), s"falsified: $got == $expected") 118 | } 119 | 120 | import Ordering.Implicits._ 121 | 122 | val (q1, q2) = (Qux(1), Qux(2)) 123 | 124 | property("(q1 > q2) == 0") = test(Claim(q1 > q2), "falsified: Qux(1) > Qux(2)") 125 | 126 | property("o.compare(q1, q2) == 0") = 127 | test(Claim(Qux.QuxOrdering.compare(q1, q2) == 0), "falsified: QuxOrdering.compare(Qux(1), Qux(2)) {-1} == 0") 128 | 129 | property("o.tryCompare(q1, q2) == Some(0)") = test( 130 | Claim(Qux.QuxOrdering.tryCompare(q1, q2) == Some(0)), 131 | "falsified: QuxOrdering.tryCompare(Qux(1), Qux(2)) {Some(-1)} == Some(0)" 132 | ) 133 | 134 | property("o.equiv(q1, q2)") = 135 | test(Claim(Qux.QuxOrdering.equiv(q1, q2)), "falsified: QuxOrdering.equiv(Qux(1), Qux(2))") 136 | 137 | // RichInt 138 | 139 | property("(1 min 2) = 0") = test(Claim((1.min(2)) == 0), "falsified: 1 min 2 {1} == 0") 140 | 141 | property("(1 max 2) = 0") = test(Claim((1.max(2)) == 0), "falsified: 1 max 2 {2} == 0") 142 | 143 | property("1.signum = 0") = test(Claim(1.signum == 0), "falsified: 1.signum {1} == 0") 144 | 145 | // RichDouble 146 | 147 | val (z1, z2) = (1.0, 2.0) 148 | 149 | property("z1.abs = 0") = test(Claim(z1.abs == 0), s"falsified: $z1.abs {$z1} == 0") 150 | 151 | property("z1.ceil = 0") = test(Claim(z1.ceil == 0), s"falsified: $z1.ceil {$z1} == 0") 152 | 153 | property("z1.floor = 0") = test(Claim(z1.floor == 0), s"falsified: $z1.floor {$z1} == 0") 154 | 155 | property("(z1 max z2) = 0") = test(Claim((z1.max(z2)) == 0), s"falsified: $z1 max $z2 {$z2} == 0") 156 | 157 | property("(z1 min z2) = 0") = test(Claim((z1.min(z2)) == 0), s"falsified: $z1 min $z2 {$z1} == 0") 158 | 159 | property("z1.round = 0") = test(Claim(z1.round == 0), s"falsified: $z1.round {${z1.round}} == 0") 160 | 161 | property("claimant.slice(2, 4) = xyz") = test(Claim("claimant".slice(2, 4) == "xyz"), """falsified: "ai" == "xyz"""") 162 | } 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Claimant 2 | 3 | ``` 4 | | C L A I M | 5 | | L E N S E | 6 | | A N I L E | 7 | | I S L E T | 8 | | M E E T S | 9 | ``` 10 | 11 | ### Deprecation 12 | 13 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 14 | 15 | This library is not maintained any longer and will not be ported to Scala 3. 16 | Please consider switching to: 17 | 18 | * [expecty](https://github.com/eed3si9n/expecty/) 19 | * [MUnit clue](https://scalameta.org/munit/docs/assertions.html) 20 | * [verify](https://github.com/scala/nanotest-strawman/) 21 | 22 | ### Overview 23 | 24 | By default, when a ScalaCheck property fails, you'll see its inputs 25 | (e.g. `ARG_0: ...`) but not the expression that actually failed. In 26 | many cases it is useful to be able to see the test or comparison that 27 | failed. 28 | 29 | This library provides a `Claim(...)` macro which wraps any `Boolean` 30 | expression and converts it into a labeled `Prop` value. If the 31 | property fails, ScalaCheck will show you this label. 32 | 33 | ### Quick Start 34 | 35 | Claimant supports Scala 2.11, 2.12, and 2.13. It is available 36 | from Sonatype. 37 | 38 | To include Claimant in your projects, you can use the following 39 | `build.sbt` snippet: 40 | 41 | ```scala 42 | libraryDependencies += "org.typelevel" %% "claimant" % "0.1.0" 43 | ``` 44 | 45 | Claimant also supports Scala.js. To use Claimant in your Scala.js 46 | projects, include the following `build.sbt` snippet: 47 | 48 | ```scala 49 | libraryDependencies += "org.typelevel" %%% "claimant" % "0.1.0" 50 | ``` 51 | 52 | **Please note** that Claimant is still a very young project. While we 53 | will try to keep basic source compatibility around the `Claim(...)` 54 | macro itself, it's very likely that Claim's library internals will 55 | change significantly between releases. No compatibility (binary or 56 | otherwise) is guaranteed at this point. 57 | 58 | ### Examples 59 | 60 | Here's an example of using `Claim(...)` to try to prove that `Float` 61 | is associative: 62 | 63 | ```scala 64 | package mytest 65 | 66 | import org.scalacheck.{Prop, Properties} 67 | import org.typelevel.claimant.Claim 68 | 69 | object MyTest extends Properties("MyTest") { 70 | property("float is associative") = 71 | Prop.forAll { (x: Float, y: Float, z: Float) => 72 | Claim((x + (y + z)) == ((x + y) + z)) 73 | } 74 | } 75 | ``` 76 | 77 | Unfortunately for us, this isn't true and ScalaCheck will quickly find 78 | a counter-example: 79 | 80 | ``` 81 | [info] ! MyTest.float is associative: Falsified after 22 passed tests. 82 | [info] > Labels of failing property: 83 | [info] falsified: 0.2962196 == 0.29621956 84 | [info] > ARG_0: 0.29622045 85 | [info] > ARG_1: -8.811786E-7 86 | [info] > ARG_2: 1.0369974E-8 87 | ``` 88 | 89 | The `Claim(...)` call inspects the expression and tries to determine 90 | what kind of operator is being used. Finding `==`, it captures the 91 | left- and right-hand sides of that operator. Since the values are not 92 | equal, it labels the property with: 93 | 94 | > falsified: 0.2962196 == 0.29621956 95 | 96 | This means that in addition to seeing which inputs cause a failure, we 97 | also see how much we failed by (around `4e-8` in this case). 98 | 99 | Similarly, in some cases we want to be sure that at least one of 100 | several conditions is true. In this case, we want to be sure that 101 | either `n` is zero, or that `n` is not equal to `-n`. 102 | 103 | ```scala 104 | package mytest 105 | 106 | import org.scalacheck.{Prop, Properties} 107 | import org.typelevel.claimant.Claim 108 | 109 | object AnotherTest extends Properties("AnotherTest") { 110 | property("ints have distinct inverses") = { 111 | Prop.forAll { (n: Int) => 112 | Claim(n == 0 || n != -n) 113 | } 114 | } 115 | } 116 | ``` 117 | 118 | Once again, we are out of luck! It turns out that `Int.MinValue` is 119 | its own negation (there is no positive value large enough to represent 120 | its actual negation). ScalaCheck helpfully shows us this: 121 | 122 | ``` 123 | [info] ! AnotherTest.ints have distinct inverses: Falsified after 0 passed tests. 124 | [info] > Labels of failing property: 125 | [info] falsified: (-2147483648 == 0 {false}) || (-2147483648 != -2147483648 {false}) 126 | [info] > ARG_0: -2147483648 127 | ``` 128 | 129 | In this case, `Claim(_)` helpfully shows us the how the different 130 | branches evaluate (summarizing each branch with `{true}` or 131 | `{false}`). Being able to see that the right branch ended up testing 132 | `-2147483648 != -2147483648` cuts to the heart of the problem, and 133 | doesn't leave the caller guessing about how the conditions were 134 | evaluated. 135 | 136 | ### Details 137 | 138 | The `Claim(_)` macro recognizes many different kinds of `Boolean` 139 | expressions: 140 | 141 | * `==` and `!=` (universal equality) 142 | * `eq` and `ne` (referential equality) 143 | * `<`, `<=`, `>`, and `>=` (comparisons) 144 | * `&&`, `&`, `||`, `|`, `^`, and `!` (boolean operators) 145 | * `isEmpty` and `nonEmpty` 146 | * `startsWith` and `endsWith` 147 | * `contains`, `containsSlice`, and `apply` 148 | * `isDefinedAt`, `sameElements`, and `subsetOf` 149 | * `exists` and `forall` (although `Function1` values can't be displayed) 150 | * `Equiv#equiv` 151 | 152 | The `Claim(_)` macro also recognizes certain kinds of expressions 153 | which it will attempt to annotate, such as: 154 | 155 | * `size` and `length` 156 | * `compare`, `compareTo`, and `lengthCompare` 157 | * `min` and `max` 158 | * `Ordering#compare` and `PartialOrdering#tryCompare` 159 | * `Ordering.Implicits.infixOrderingOps` 160 | 161 | (For examples of the labels produced by these, see `ClaimTest`.) 162 | 163 | It should be fairly straightforward to extend this to support other 164 | shapes, both for `Boolean` expressions and for general annotations. 165 | 166 | ### Representation and formatting 167 | 168 | Claimant uses its own `Render[A]` typeclass to produce human-readable 169 | representations of values. We get a number of benefits from this: 170 | 171 | 1. We can provide a more useful representation of arrays than you 172 | would get with `.toString`. 173 | 2. We can quote and escape `String` and `Char` values to make it 174 | easier to see their exact value. 175 | 3. We support user-provided display strategies for types they don't 176 | control. 177 | 4. We support all of the above recursively, allowing collections, 178 | tuples, case classes, etc. to take advantage of all of these 179 | together. 180 | 181 | For types that don't have their own `Render` instances, Claimant will 182 | use a low-priority implicit value to provide an implementation based 183 | on `.toString`. Some attempt has been made to provide implementations 184 | for most built-in Scala types, but PRs adding support for new 185 | instances (along with tests exercising them) will be gladly accepted. 186 | 187 | In particular, Claimant makes it easy to define `Render` 188 | implementations for case classes. Simply do the following: 189 | 190 | ```scala 191 | case class MyClass(...) 192 | 193 | object MyClass { 194 | implicit val renderForMyClass: Render[MyClass] = 195 | Render.caseClass[MyClass] 196 | } 197 | ``` 198 | 199 | The code above will define a render instance for `MyClass` which will 200 | render each of its fields with its corresponding instances. (In the 201 | future Claimant may use Shapeless to derive `Render` instances 202 | automatically.) 203 | 204 | ### Purity 205 | 206 | Currently `Claim(...)` will potentially evaluate its expression (or 207 | sub-expressions) multiple times, in any order. In the future we could 208 | be more uptight about preserving execution order and ensuring 209 | sub-expressions are run exactly as they would be, but so far this 210 | hasn't been a priority. 211 | 212 | For example, assuming that the `missilesFiredAt` method is 213 | side-effecting, and returns the number of missiles that were just 214 | fired, consider the following code: 215 | 216 | ```scala 217 | def missilesFiredAt(target: String): Int = { 218 | val num = scala.util.Random.nextInt(3) + 3 219 | println(s"firing $num missiles at $target") 220 | num 221 | } 222 | 223 | property("notTooManyMissiles") = 224 | Claim((missilesFiredAt("moon") max missilesFiredAt("mars")) < 4) 225 | ``` 226 | 227 | Setting aside the questionable wisdom of launching missiles during a 228 | test, here's an example of the output we might see: 229 | 230 | ``` 231 | firing 5 missiles at moon 232 | firing 3 missiles at mars 233 | firing 4 missiles at moon 234 | firing 4 missiles at mars 235 | firing 4 missiles at moon 236 | firing 3 missiles at mars 237 | [info] ! MissileTest.notTooManyMissiles: Falsified after 0 passed tests. 238 | [info] > Labels of failing property: 239 | [info] falsified: 4 max 4 {4} < 4 240 | ``` 241 | 242 | As we can see, Claimant is evaluating each expression multiple times. 243 | The values we see in the test label (`4` for the Moon and `4` for 244 | Mars) aren't necessarily the same ones used to describe the test 245 | failing, we also see that at various points we launched `5` missiles 246 | at the Moon, and `3` missiles at Mars. 247 | 248 | In cases where side-effects are unavoidable, consider evaluating them 249 | *before* calling `Claim(...)`: 250 | 251 | ``` 252 | property("notTooManyMissiles") = { 253 | val x = missilesFiredAt("moon") 254 | val y = missilesFiredAt("mars") 255 | Claim((x max y) < 4) 256 | } 257 | ``` 258 | 259 | This will result in more consistent test output: 260 | 261 | ``` 262 | firing 5 missiles at moon 263 | firing 5 missiles at mars 264 | [info] ! MissileTest.notTooManyMissiles: Falsified after 0 passed tests. 265 | [info] > Labels of failing property: 266 | [info] falsified: 5 max 5 {5} < 4 267 | ``` 268 | 269 | ### Limitations 270 | 271 | Currently `Claim(...)` only expands a set of known methods. This means 272 | that if you have methods which return `Boolean` and write something 273 | like `Claim(Verifier.verify(dataSet))` your test failures will look 274 | something like this: 275 | 276 | ``` 277 | [info] ! FancyTest.verify data sets: Falsified after 4 passed tests. 278 | [info] > Labels of failing property: 279 | [info] falsified: false 280 | [info] > ARG_0: DataSet(...) 281 | ``` 282 | 283 | The ways to fix this are: 284 | 285 | 1. Have `verify` return a richer result. 286 | 2. Inline the `verify` logic in the `Claim(...)` call. 287 | 3. Extend *Claimant* to support `Verifier.verify`. 288 | 289 | Another problem is that `Claim(...)` inspects method calls based on 290 | their AST shape. This means that type application, implicit 291 | parameters, etc. need to be explicitly supported. This also means that 292 | implicit enrichment (or *bedazzlement*) can muddy the waters a bit and 293 | obscure the underlying values. 294 | 295 | (For an example of how to deal with enrichment, see the support for 296 | `Ordering.Implicits.infixOrderingOps`.) 297 | 298 | ### Development 299 | 300 | To measure code coverage for 2.12, do the following: 301 | 302 | ``` 303 | $ sbt '++ 2.12.12' coreJVM/clean coverage coreJVM/test coverageReport 304 | ``` 305 | 306 | Assuming everything works, the result should end up someplace like: 307 | 308 | ``` 309 | .jvm/target/scala-2.12/scoverage-report/index.html 310 | ``` 311 | 312 | To measure coverage in 2.11 you'd instead do: 313 | 314 | ``` 315 | $ sbt '++ 2.11.12' coreJVM/clean coverage coreJVM/test coverageReport 316 | ``` 317 | 318 | And the result would end up someplace like: 319 | 320 | ``` 321 | .jvm/target/scala-2.11/scoverage-report/index.html 322 | ``` 323 | 324 | There's at least some code in Claimant that is specific to either 2.11 325 | or 2.12, making it unlikely that we'll achieve 100% coverage under 326 | either version independently. 327 | 328 | ### Future Work 329 | 330 | There are a ton of possible improvements: 331 | 332 | * Support more methods/shapes. 333 | * Minimize recomputation in the macro. 334 | * Consider using raw trees instead of quasi-quotes. 335 | * Consider supporting fancy diagrams 336 | * Consider supporting color output 337 | * Consider an extensible/modular design 338 | 339 | ### Community 340 | 341 | People are expected to follow the 342 | [Scala Code of Conduct](https://scala-lang.org/conduct/) when 343 | discussing Claimant on GitHub, the Gitter channel, or other 344 | venues. 345 | 346 | ### Credits 347 | 348 | This library was inspired by the `assert(...)` macro found in 349 | [ScalaTest](http://www.scalatest.org/). 350 | 351 | ### Copyright and License 352 | 353 | All code is available to you under the Apache 2 license, available at 354 | https://opensource.org/licenses/Apache-2.0. 355 | 356 | Copyright Erik Osheim, 2019. 357 | -------------------------------------------------------------------------------- /core/src/main/scala/org/typelevel/claimant/Render.scala: -------------------------------------------------------------------------------- 1 | package org.typelevel.claimant 2 | 3 | import org.typelevel.claimant.render.CaseClass 4 | import scala.{collection => sc} 5 | import scala.annotation.switch 6 | import scala.collection.{immutable => sci} 7 | import scala.collection.{mutable => scm} 8 | 9 | /** 10 | * Render is a typeclass to provide human-readable representations of values. 11 | * 12 | * This typeclass provides two major concrete benefits over the built-in toString method: 13 | * 14 | * 1. We get better representations of some built-in types. For example, Strings are quoted, Arrays are handled like 15 | * other collections, and so on. 16 | * 17 | * 2. Authors can override representations locally to improve error reporting in their own tests. 18 | * 19 | * Claimant attempts to provide instances for most built-in Scala types, as well as a handy macro for generating 20 | * instances for Scala case classes. For example, the following code produces a Render[Rectangle] value, which will use 21 | * Render[Double] instances recursively: 22 | * 23 | * case class Rectangle(height: Double, width: Double) 24 | * 25 | * object Rectangle { implicit renderForRectangle: Render[Rectangle] = Render.caseClass[Rectangle] } 26 | * 27 | * This typeclass is very similar to cats.Show (and probably others). One major design difference is that for a given 28 | * type T, if a specific Render[T] is not available, this typeclass will generate an instance that just uses .toString. 29 | * This behavior is intended to balance the benefits of custom representations with not requiring authors to write a 30 | * bunch of new code in order ot use Claimant. 31 | */ 32 | trait Render[A] { 33 | 34 | /** 35 | * Generate a String representation of `a`. 36 | */ 37 | final def render(a: A): String = 38 | renderInto(new StringBuilder(), a).toString 39 | 40 | /** 41 | * Write a representation of `a` into an existing mutable StringBuilder. 42 | * 43 | * This method is used to power `render`, as well as used recursively when building up larger representations. 44 | */ 45 | def renderInto(sb: StringBuilder, a: A): StringBuilder 46 | } 47 | 48 | object Render extends RenderInstances { 49 | 50 | /** 51 | * Summon a Render[A] instance. 52 | */ 53 | def apply[A](implicit ev: Render[A]): Render[A] = ev 54 | 55 | /** 56 | * Method for rendering a given value, using an implicitly-available Render[A] instance. 57 | */ 58 | def render[A](a: A)(implicit ev: Render[A]): String = ev.render(a) 59 | 60 | /** 61 | * Define a Render[A] instance that returns a constant string value. 62 | * 63 | * This method should only be used in cases where there is only one value for A. 64 | */ 65 | def const[A](s: String): Render[A] = 66 | instance((sb, _) => sb.append(s)) 67 | 68 | /** 69 | * Define a Render[A] instance in terms of a single method to produce a String. 70 | * 71 | * This method should only be used when we need to take advantage of an existing method that returns String (for 72 | * example, using .toString on a primitive type). 73 | */ 74 | def str[A](f: A => String): Render[A] = 75 | instance((sb, a) => sb.append(f(a))) 76 | 77 | /** 78 | * Define a Render[A] instance in terms of a provided function for `renderInto`. 79 | */ 80 | def instance[A](f: (StringBuilder, A) => StringBuilder): Render[A] = 81 | new Render[A] { 82 | def renderInto(sb: StringBuilder, a: A): StringBuilder = f(sb, a) 83 | } 84 | 85 | /** 86 | * Define a Render[A] instance for a given case class. 87 | * 88 | * This method will recursively use Render instances for every field value in the case class. It can only be used with 89 | * case classes. 90 | */ 91 | def caseClass[A]: Render[A] = 92 | macro CaseClass.impl[A] 93 | 94 | /** 95 | * Method to assist in writing out collections of values. 96 | * 97 | * This method produces output suitable for sequences, sets, etc. in terms of a given iterator, as well as a name. 98 | */ 99 | def renderIterator[CC[x] <: Iterable[x], A](sb: StringBuilder, 100 | name: String, 101 | it: Iterator[A], 102 | r: Render[A] 103 | ): StringBuilder = { 104 | sb.append(name).append("(") 105 | if (it.hasNext) { 106 | r.renderInto(sb, it.next) 107 | while (it.hasNext) r.renderInto(sb.append(", "), it.next) 108 | } 109 | sb.append(")") 110 | } 111 | } 112 | 113 | abstract class RenderInstances extends RenderTupleInstances with LowPriorityRenderInstances { 114 | 115 | // $COVERAGE-OFF$ 116 | implicit lazy val renderForNothing: Render[Nothing] = 117 | Render.const("") 118 | // $COVERAGE-ON$ 119 | 120 | implicit lazy val renderForUnit: Render[Unit] = 121 | Render.const("()") 122 | 123 | implicit lazy val renderForBoolean: Render[Boolean] = 124 | Render.str(_.toString) 125 | 126 | implicit lazy val renderForByte: Render[Byte] = Render.str(_.toString) 127 | implicit lazy val renderForShort: Render[Short] = Render.str(_.toString) 128 | implicit lazy val renderForInt: Render[Int] = Render.str(_.toString) 129 | implicit lazy val renderForLong: Render[Long] = Render.str(_.toString) 130 | implicit lazy val renderForFloat: Render[Float] = Render.str(_.toString) 131 | implicit lazy val renderForDouble: Render[Double] = Render.str(_.toString) 132 | 133 | implicit lazy val renderForBigInt: Render[BigInt] = 134 | Render.str(_.toString) 135 | implicit lazy val renderForBigDecimal: Render[BigDecimal] = 136 | Render.str(_.toString) 137 | 138 | implicit lazy val renderForJavaBigInt: Render[java.math.BigInteger] = 139 | Render.str(_.toString) 140 | implicit lazy val renderForJavaBigDecimal: Render[java.math.BigDecimal] = 141 | Render.str(_.toString) 142 | 143 | // escape a String in the same way scalac does. 144 | // 145 | // these rules are extracted from scala.reflect.internal.Constants, 146 | // which are not accessible to us due to the Cake pattern. they are 147 | // modified slightly so that we only escape single-quotes (') in 148 | // characters, not strings. 149 | def escapedChar(c: Char): String = 150 | (c: @switch) match { 151 | case '\b' => "\\b" 152 | case '\t' => "\\t" 153 | case '\n' => "\\n" 154 | case '\f' => "\\f" 155 | case '\r' => "\\r" 156 | case '"' => "\\\"" 157 | case '\\' => "\\\\" 158 | case _ if c.isControl => "\\u%04X".format(c.toInt) 159 | case _ => String.valueOf(c) 160 | } 161 | 162 | /** 163 | * Display an escaped representation of the given Char. 164 | * 165 | * Will return a value surrounded by single-quotes. 166 | */ 167 | def escape(c: Char): String = 168 | if (c == '\'') 169 | "'\\''" 170 | else { 171 | val sb = new StringBuilder 172 | sb.append("'") 173 | sb.append(escapedChar(c)) 174 | sb.append("'") 175 | sb.toString 176 | } 177 | 178 | /** 179 | * Display an escaped representation of the given String. 180 | * 181 | * Will return a value surrounded by double-quotes. 182 | */ 183 | def escape(s: String): String = { 184 | val sb = new StringBuilder 185 | sb.append("\"") 186 | var i = 0 187 | while (i < s.length) { 188 | sb.append(escapedChar(s.charAt(i))) 189 | i += 1 190 | } 191 | sb.append("\"") 192 | sb.toString 193 | } 194 | 195 | implicit lazy val renderForChar: Render[Char] = 196 | Render.str(escape) 197 | 198 | implicit lazy val renderForString: Render[String] = 199 | Render.str(escape) 200 | 201 | implicit lazy val renderForSymbol: Render[scala.Symbol] = 202 | Render.str(_.toString) 203 | 204 | implicit def renderForNone: Render[None.type] = 205 | Render.const("None") 206 | 207 | implicit def renderForSome[A](implicit r: Render[A]): Render[Some[A]] = 208 | Render.instance { case (sb, Some(a)) => r.renderInto(sb.append("Some("), a).append(")") } 209 | 210 | implicit def renderForOption[A](implicit r: Render[A]): Render[Option[A]] = 211 | Render.instance { 212 | case (sb, Some(a)) => r.renderInto(sb.append("Some("), a).append(")") 213 | case (sb, None) => sb.append("None") 214 | } 215 | 216 | implicit def renderForLeft[A](implicit ra: Render[A]): Render[Left[A, Nothing]] = 217 | Render.instance { case (sb, Left(a)) => ra.renderInto(sb.append("Left("), a).append(")") } 218 | 219 | implicit def renderForRight[B](implicit rb: Render[B]): Render[Right[Nothing, B]] = 220 | Render.instance { case (sb, Right(b)) => rb.renderInto(sb.append("Right("), b).append(")") } 221 | 222 | implicit def renderForEither[A, B](implicit ra: Render[A], rb: Render[B]): Render[Either[A, B]] = 223 | Render.instance { 224 | case (sb, Left(a)) => ra.renderInto(sb.append("Left("), a).append(")") 225 | case (sb, Right(b)) => rb.renderInto(sb.append("Right("), b).append(")") 226 | } 227 | 228 | class RenderIterable[CC[x] <: Iterable[x], A](name: String)(implicit r: Render[A]) extends Render[CC[A]] { 229 | def renderInto(sb: StringBuilder, cc: CC[A]): StringBuilder = 230 | Render.renderIterator(sb, name, cc.iterator, r) 231 | } 232 | 233 | class RenderIterableMap[M[k, v] <: Iterable[(k, v)], K, V](name: String)(implicit rk: Render[K], rv: Render[V]) 234 | extends Render[M[K, V]] { 235 | val rkv: Render[(K, V)] = Render.instance { case (sb, (k, v)) => 236 | rv.renderInto(rk.renderInto(sb, k).append(" -> "), v) 237 | } 238 | def renderInto(sb: StringBuilder, m: M[K, V]): StringBuilder = 239 | Render.renderIterator(sb, name, m.iterator, rkv) 240 | } 241 | 242 | implicit def renderForArray[A](implicit r: Render[A]): Render[Array[A]] = 243 | Render.instance((sb, arr) => Render.renderIterator(sb, "Array", arr.iterator, r)) 244 | 245 | // sequences 246 | 247 | implicit def renderForList[A: Render]: Render[List[A]] = 248 | new RenderIterable("List") 249 | implicit def renderForIterable[A: Render]: Render[Iterable[A]] = 250 | new RenderIterable("Iterable") 251 | implicit def renderForSeq[A: Render]: Render[Seq[A]] = 252 | new RenderIterable("Seq") 253 | implicit def renderForIndexedSeq[A: Render]: Render[IndexedSeq[A]] = 254 | new RenderIterable("IndexedSeq") 255 | implicit def renderForVector[A: Render]: Render[Vector[A]] = 256 | new RenderIterable("Vector") 257 | implicit def renderForQueue[A: Render]: Render[sci.Queue[A]] = 258 | new RenderIterable("Queue") 259 | implicit def renderForArrayBuffer[A: Render]: Render[scm.ArrayBuffer[A]] = 260 | new RenderIterable("ArrayBuffer") 261 | 262 | implicit def renderForStream[A: Render]: Render[Stream[A]] = 263 | Render.instance { 264 | case (sb, Stream.Empty) => sb.append("Stream()") 265 | case (sb, a #:: _) => Render[A].renderInto(sb.append("Stream("), a).append(", ?)") 266 | } 267 | 268 | // sets 269 | 270 | implicit def renderForSet[A: Render]: Render[Set[A]] = 271 | new RenderIterable("Set") 272 | implicit def renderForHashSet[A: Render]: Render[sci.HashSet[A]] = 273 | new RenderIterable("HashSet") 274 | implicit def renderForSortedSet[A: Render]: Render[sc.SortedSet[A]] = 275 | new RenderIterable("SortedSet") 276 | implicit def renderForTreeSet[A: Render]: Render[sci.TreeSet[A]] = 277 | new RenderIterable("TreeSet") 278 | 279 | // maps 280 | 281 | implicit def renderForMap[K: Render, V: Render]: Render[Map[K, V]] = 282 | new RenderIterableMap("Map") 283 | implicit def renderForHashMap[K: Render, V: Render]: Render[sci.HashMap[K, V]] = 284 | new RenderIterableMap("HashMap") 285 | implicit def renderForSortedMap[K: Render, V: Render]: Render[sc.SortedMap[K, V]] = 286 | new RenderIterableMap("SortedMap") 287 | implicit def renderForTreeMap[K: Render, V: Render]: Render[sci.TreeMap[K, V]] = 288 | new RenderIterableMap("TreeMap") 289 | 290 | // weird maps 291 | 292 | implicit def renderForIntMap[V](implicit rv: Render[V]): Render[sci.IntMap[V]] = 293 | new Render[sci.IntMap[V]] { 294 | val rnv: Render[(Int, V)] = Render.instance { case (sb, (n, v)) => 295 | rv.renderInto(sb.append(s"$n -> "), v) 296 | } 297 | def renderInto(sb: StringBuilder, m: sci.IntMap[V]): StringBuilder = 298 | Render.renderIterator(sb, "IntMap", m.iterator, rnv) 299 | } 300 | 301 | implicit def renderForLongMap[V](implicit rv: Render[V]): Render[sci.LongMap[V]] = 302 | new Render[sci.LongMap[V]] { 303 | val rnv: Render[(Long, V)] = Render.instance { case (sb, (n, v)) => 304 | rv.renderInto(sb.append(s"$n -> "), v) 305 | } 306 | def renderInto(sb: StringBuilder, m: sci.LongMap[V]): StringBuilder = 307 | Render.renderIterator(sb, "LongMap", m.iterator, rnv) 308 | } 309 | } 310 | 311 | /** 312 | * Low-priority fallback that uses toString. 313 | * 314 | * If a type uses this Render instance, it breaks the ability of Render to recursively display any of its member values, 315 | * even those that have Render instances. 316 | */ 317 | private[claimant] trait LowPriorityRenderInstances { 318 | implicit def renderAnyRef[A]: Render[A] = Render.str(_.toString) 319 | } 320 | --------------------------------------------------------------------------------