Skip to content

Instantly share code, notes, and snippets.

@xeno-by
Created November 3, 2012 19:28
Show Gist options
  • Save xeno-by/4008389 to your computer and use it in GitHub Desktop.
Save xeno-by/4008389 to your computer and use it in GitHub Desktop.
// for the story behind this snippet of code see:
// http://stackoverflow.com/questions/13185405/structural-subtyping-reflection
// requires Scala 2.10.0-RC1 or higher
// you also need to have scala-reflect.jar and scala-compiler.jar on your classpath
import scala.reflect.runtime.universe._
object Test extends App {
val f = (r: {val s: String}) => r.s
// we want to build a function `mock` which:
// * takes `fn`, a T => R, with T being a structural type
// * takes `mocker`, a Symbol => Any, which produces a mock value for a given declaration
// * returns the result of invoking `fn` with an argument generated by `mocker`
//
// example:
// * `fn`: (r: {def s: String}) => r.s
// * `mocker`: sym => sym.name.toString
// * the result will be "s"
def mock[T: TypeTag, R](fn: T => R, mocker: Symbol => Any): R = {
// having annotated T with the TypeTag context bound
// we persist full information about its type to be available at runtime
// afterwards we pattern match against that type using the types declared in the reflection API
// info about the reflection API is available here: http://docs.scala-lang.org/overviews/reflection/overview.html
// this stuff required scala-reflect.jar, which means Scala 2.10.0-RC1 or higher
val RefinedType(parents, decls) = typeOf[T]
// we're going to generate a mock argument for `fn` at runtime using the reflection API
// to do that we first create a toolbox, i.e. a runtime Scala compiler
// this functionality requires scala-compiler.jar
// more information about toolboxes is available via the aforementioned link
import scala.tools.reflect.ToolBox
val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
// to generate a class with toolboxes, we need to create an abstract syntax tree for it
// see http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/index.html#scala.reflect.api.Printers
// to understand how I figured out what exactly is the shape of the tree I need to build
// (for simplicity I assume that the structural type only contains nullary defs)
val emptyCtor = DefDef(NoMods, nme.CONSTRUCTOR, Nil, List(Nil), TypeTree(), Block(List(Apply(Select(Super(This(tpnme.EMPTY), tpnme.EMPTY), nme.CONSTRUCTOR), Nil)), Literal(Constant(()))))
val classDef = ClassDef(NoMods, newTypeName("Mock"), Nil, Template(parents map TypeTree, emptyValDef, emptyCtor +: decls.toList.map(decl => {
val NullaryMethodType(typeOfVal) = decl.typeSignature
// free terms allow to refer to runtime values in the generated code
// creation syntax isn't pretty, but it works
// though we'll most likely improve the syntax in the future
val mockValueOfVal = build.newFreeTerm(decl.name.toString, mocker(decl))
build.setTypeSignature(mockValueOfVal, typeOfVal)
// a nasty caveat - if one doesn't provide any flags to the method creator
// (effectively leaving it public by default)
// then the compiler somehow decides to make the method private (probably because this is a local class)
// which leads to NoSuchMethodExceptions later
// therefore we trick the compiler into not touching visibility by making the method protected
// these little things is what makes reflection experimental in 2.10.0
DefDef(Modifiers(Flag.PROTECTED), decl.name.toTermName, Nil, Nil, TypeTree(typeOfVal), Ident(mockValueOfVal))
})))
// now we wrap the class definition into a snippet that instantiates it
// and then use the toolbox to compile and evaluate that snippet
val code = Block(List(classDef), Apply(Select(New(Ident(newTypeName("Mock"))), nme.CONSTRUCTOR), List()))
println(code)
val mockInstance = tb.eval(code).asInstanceOf[T]
// finally, the invocation
fn(mockInstance)
}
val fn = (r: {def s: String}) => r.s
val result = mock(fn, sym => sym.name.toString)
println(result)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment