Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fully automatic tree / class hierarchy generators #152

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ lazy val sharedSettings = mimaDefaultSettings ++ Seq(

crossScalaVersions := Seq("2.10.4", "2.11.5"),

libraryDependencies <+= (scalaVersion)("org.scala-lang" % "scala-reflect" % _),

unmanagedSourceDirectories in Compile += {
if (scalaVersion.value.startsWith("2.11."))
(baseDirectory in LocalRootProject).value / "src" / "main" / "scala-2.11"
else if (scalaVersion.value.startsWith("2.10."))
(baseDirectory in LocalRootProject).value / "src" / "main" / "scala-2.10"
else ???
},

unmanagedSourceDirectories in Test ++= {
if (scalaVersion.value.startsWith("2.11."))
Seq((baseDirectory in LocalRootProject).value / "src" / "test" / "scala-2.11")
else if (scalaVersion.value.startsWith("2.10."))
Seq()
else ???
},

previousArtifact := Some("org.scalacheck" % "scalacheck_2.11" % "1.12.1"),

unmanagedSourceDirectories in Compile += (baseDirectory in LocalRootProject).value / "src" / "main" / "scala",
Expand Down
3 changes: 3 additions & 0 deletions src/main/scala-2.10/org/scalacheck/GenTree.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.scalacheck
/** no content for Scala 2.10 */
trait GenTree
77 changes: 77 additions & 0 deletions src/main/scala-2.11/org/scalacheck/GenMacros.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.scalacheck
class GenMacroImpls(val c: reflect.macros.blackbox.Context){
import c.universe._

def partialTree[T: c.WeakTypeTag]: Tree = treeHelper[T](true)

def tree[T: c.WeakTypeTag]: Tree = treeHelper[T](false)

/** Generates a list of all known classes and traits in an inheritance tree. Does not include subclasses of non-sealed ones. */
private def linerizedInheritanceTree(symbol:Symbol): Seq[Symbol] = {
def children = symbol.asClass.knownDirectSubclasses.flatMap(linerizedInheritanceTree)
symbol match{
case s if s.isModuleClass => Seq(s)
case s if s.isClass => // && s.asClass.isCaseClass
Seq(s) ++ children
case s =>
c.abort(c.enclosingPosition, s"Can create generator for $s.")
}
}

private def caseObject(sym: Symbol) =
sym.asInstanceOf[reflect.internal.Symbols#Symbol].sourceModule.asInstanceOf[Symbol]

private val scalacheck = q"_root_.org.scalacheck"
private val Gen = q"$scalacheck.Gen"
private val Arbitrary = q"$scalacheck.Arbitrary.apply"
private def Arbitrary(T: Type) = tq"$scalacheck.Arbitrary[$T]"

private def treeHelper[T: c.WeakTypeTag](allowOpenHierarchies: Boolean): Tree = {
val T = weakTypeOf[T]
val symbol = T.typeSymbol

val classes = linerizedInheritanceTree(symbol)

if(symbol.isClass && symbol.asClass.isAbstract && !symbol.asClass.isSealed)
c.abort(c.enclosingPosition, s"$T is abstract but not sealed. No subclasses can be found.")

val gens = classes.collect{
case s if s.isModuleClass => q"$Gen.const(${caseObject(s)})"

case s if !s.isClass =>
c.abort(c.enclosingPosition, s"Can not create generator for non-class type $s.")

case s: ClassSymbol if !allowOpenHierarchies && !s.isFinal && !s.isSealed =>
c.abort(c.enclosingPosition, s"Can not create generator for non-sealed, non-final $s. Use partialTree if you cannot make all classes and traits sealed or finaled. Be aware that partialTree does not generate subclasses of non-sealed classes or traits.")

case s: ClassSymbol if !s.isAbstract =>
val paramLists = s.typeSignature.decls.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor => m
}.get.paramLists

if(paramLists.flatten.size > 22){
c.abort(c.enclosingPosition, s"Can not create generator for class with more than 22 paramters: $s")
} else if(paramLists.flatten.size > 0){
val identsAndTypes = paramLists.map( _.map( s => (TermName(c.freshName("v")), s.typeSignature) ) )
val args = identsAndTypes.flatten.map{ case (n,t) => q"$n: $t" }
val idents = identsAndTypes.map( _.map(_._1) )
q"$Gen.resultOf((..$args) => new $s(...$idents))"
} else {
q"$Gen.wrap($Gen.const(new ${s}))" // wrap to generate different object instances
}
}

if(gens.isEmpty)
c.abort(c.enclosingPosition, s"No concrete classes found extending $T. (Only subclasses of sealed traits and classes can be found.)")

val arb = TermName(c.freshName("arb"))

// using lzy to allow circular recursion
q"""{
/** implicit to automatically wire recursive datastructure */
implicit def $arb: ${Arbitrary(T)} = $Arbitrary($Gen.lzy($Gen.oneOf(..$gens)))
$arb.arbitrary
}
"""
}
}
17 changes: 17 additions & 0 deletions src/main/scala-2.11/org/scalacheck/GenTree.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.scalacheck
import scala.language.experimental.macros
trait GenTree{
/** Generator that produces instances of the given class and its subclasses
using their primary constructors, skipping traits and abstract classes.

NOTE: Expects all involed traits and classes to be sealed or final.
*/
def tree[T]: Gen[T] = macro GenMacroImpls.tree[T]

/** Generator that produces instances of the given class and its subclasses
using their primary constructors, skipping traits and abstract classes.

NOTE: Does not see subclasses of non-sealed classes and traits.
*/
def partialTree[T]: Gen[T] = macro GenMacroImpls.partialTree[T]
}
2 changes: 1 addition & 1 deletion src/main/scala/org/scalacheck/Gen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ sealed trait Gen[+T] {

}

object Gen extends GenArities{
object Gen extends GenArities with GenTree{

//// Private interface ////

Expand Down
48 changes: 48 additions & 0 deletions src/test/scala-2.11/org/scalacheck/examples/Examples211.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.scalacheck.examples

import org.scalacheck.{Prop,Properties}

object Examples211 extends Properties("Examples211") {
import org.scalacheck.Gen

property("closed hierarchy") = {
sealed abstract class Tree
final case class Node(left: Tree, right: Tree, v: Int) extends Tree
case object Leaf extends Tree

Prop.forAll(Gen.tree[Tree]){
case Leaf => true
case Node(_,_,_) => true
}

sealed trait A
sealed case class B(i: Int, s: String) extends A
case object C extends A
sealed trait D extends A
final case class E(a: Double, b: Option[Float]) extends D
case object F extends D
sealed abstract class Foo extends D
case object Baz extends Foo
final class Bar extends Foo
final class Baz(i1: Int)(s1: String) extends Foo

Prop.forAll(Gen.tree[A]){
case _:A => true
}

Prop.forAll(Gen.tree[D]){
case _:D => true
}
}

property("open hierarchy") = {
sealed abstract class Tree
case class Node(left: Tree, right: Tree, v: Int) extends Tree
case object Leaf extends Tree

Prop.forAll(Gen.partialTree[Tree]){
case Leaf => true
case Node(_,_,_) => true
}
}
}