All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.specs2.matcher.MatcherMacros.scala Maven / Gradle / Ivy
package org.specs2
package matcher
import scala.reflect.macros.Context
import scala.annotation.StaticAnnotation
/**
* Macro definitions to generate matchers for the members of a type T
*/
trait MatcherMacros {
def matchA[T] = macro org.specs2.matcher.MatcherMacros.matcherMacroImpl[T]
def matchAn[T] = macro org.specs2.matcher.MatcherMacros.matcherMacroImpl[T]
class fieldMatcherBody(tree: Any) extends StaticAnnotation
}
/**
* Macros implementations
*
* The bulk of the implementation is credited to:
*
* - @travisbrown for his ["vampire methods"](http://meta.plasm.us/posts/2013/08/31/feeding-our-vampires/)
* - @retronym for his help on [range positions](https://groups.google.com/forum/#!topic/scala-internals/fmWKw5TDz98)
* - @xeno_by for his help on macros in general!
*/
object MatcherMacros extends MatcherMacros {
def matcherMacroImpl[T : c.WeakTypeTag](c: Context): c.Expr[Any] = { import c.universe._
val (matcherClassType, classDefinition) = new MakeMatchers[c.type](c).matchers[T]
val block = q"""
$classDefinition
new $matcherClassType {}
"""
c.Expr(setMacroPosition(c).transform(block))
}
def fieldMatcherImplementation[F, T : c.WeakTypeTag](c: Context)(fieldValue: c.Expr[F]) = {
import c.universe._
val (arg, body) = extractBody(c)
val prefixVal = newTermName(c.fresh)
c.Expr(setMacroPosition(c).transform {
q"""
val $prefixVal = ${c.prefix.tree}
val $arg = ${fieldValue.tree.duplicate}
${replaceThises(c)(matcherClassName[T](c), prefixVal).transform(body)}
"""})
}
def fieldMatcherImplementation2[F : c.WeakTypeTag, T : c.WeakTypeTag, R](c: Context)(fieldValue: c.Expr[F])(asResult: c.Expr[R]) = { import c.universe._
val (arg, body) = extractBody(c)
val prefixVal = newTermName(c.fresh)
c.Expr(setMacroPosition(c).transform {
q"""
implicit class ToStrictAndThen[A, B](f: Function1[A, B]) { def andThenStrict[C](g: (=>B) => C): Function1[A, C] = (a: A) => g(f(a)) }
val $prefixVal = ${c.prefix.tree}
val $arg = ${fieldValue.tree.duplicate} andThenStrict ${asResult.tree}.asResult
${replaceThises(c)(matcherClassName[T](c), prefixVal).transform(body)}
"""})(implicitly[c.WeakTypeTag[F]])
}
def matcherClassName[T : c.WeakTypeTag](c: Context) = c.universe.weakTypeOf[T].typeSymbol.name.encoded+"Matcher"
private def extractBody(c: Context) = { import c.universe._
c.macroApplication.symbol.annotations.find(_.tpe.toString.endsWith("fieldMatcherBody")).
flatMap(_.scalaArgs.map(arg => c.resetLocalAttrs(arg)).collectFirst { case Function(ValDef(_, a, _, _) :: Nil, b) => a -> b }).
getOrElse(c.abort(c.enclosingPosition, "Annotation body not provided!"))
}
private def replaceThises(c: Context)(className: String, prefixVal: c.TermName) = {
import c.universe._
new Transformer {
override def transform(tree: Tree) = tree match {
case This(qualifier) if qualifier.decoded.startsWith(className) => Ident(prefixVal)
case other => super.transform(other)
}
}
}
// TODO 2.11 Remove this after dropping 2.10.x support.
private object HasCompat { val compat = ??? }; import HasCompat._
/** set a specific position to the elements of a tree */
private def setPosition(c: Context)(position: c.Position) = {
import c.universe._
import compat._
new Transformer {
override def transform(tree: Tree) = tree match {
case t => super.transform(t).setPos(position)
}
}
}
/** set the macro application position to the elements of a tree */
private def setMacroPosition(c: Context) = setPosition(c)(c.macroApplication.pos.makeTransparent)
}
class MakeMatchers[C <: Context](val c: C) {
import c.universe._
def matchers[T: c.WeakTypeTag] = {
val typeOfT = weakTypeOf[T]
val matcherClassType = newTypeName(MatcherMacros.matcherClassName[T](c))
val fields: Iterable[c.Symbol] = typeOfT.members.filter(_.isPublic).
filter(isGetter(c)).
filterNot(isSynthetic(c)).
filter(_.owner != typeOf[Any].typeSymbol).
filter(_.owner != typeOf[Object].typeSymbol).
filter(_.owner != typeOf[Product].typeSymbol).
filter(_.owner != typeOf[Equals].typeSymbol)
val (fieldValueMatchers, fieldMatchers, fieldFunctionMatchers) = fields.map {
member =>
val fieldName = member.name.toString
val methodName = newTermName(fieldName)
val fieldType = member.typeSignature
val parameterName = newTermName(fieldName)
val valueBody = q"""(fieldValue: $fieldType) =>
addMatcher((t :$typeOfT) => new org.specs2.matcher.BeTypedEqualTo[$fieldType](fieldValue)(theValue[$fieldType](t.$parameterName).updateDescription(d => " "+$fieldName+": "+d)).toResult)"""
val matcherBody = q"""(matcherValue: org.specs2.matcher.Matcher[$fieldType]) =>
addMatcher((t :$typeOfT) => matcherValue[$fieldType](theValue[$fieldType](t.$parameterName).updateDescription(d => " "+$fieldName+": "+d)).toResult) """
val functionBody = q"""(f: $fieldType => org.specs2.execute.Result) => addMatcher((t :$typeOfT) => f(t.$parameterName)) """
val x = (
q""" @fieldMatcherBody($valueBody) def $methodName(fieldValue: $fieldType): $matcherClassType = macro org.specs2.matcher.MatcherMacros.fieldMatcherImplementation[$fieldType, $typeOfT] """,
q""" @fieldMatcherBody($matcherBody) def $methodName(fieldValue: org.specs2.matcher.Matcher[$fieldType]): $matcherClassType = macro org.specs2.matcher.MatcherMacros.fieldMatcherImplementation[$fieldType, $typeOfT] """,
q""" @fieldMatcherBody($functionBody) def $methodName[R : org.specs2.execute.AsResult](fieldValue: $fieldType => R): $matcherClassType = macro org.specs2.matcher.MatcherMacros.fieldMatcherImplementation2[$fieldType, $typeOfT, R] """
)
x: (Tree, Tree, Tree)
}.unzip3
val classDefinition = q"""
case class $matcherClassType(matcherOfT: Option[$typeOfT => org.specs2.execute.Result] = None) extends org.specs2.matcher.Matcher[$typeOfT] {
// this is a workaround for 'missing type' if a default function value is specified in the case class
private def matcherFunction = matcherOfT.getOrElse((t: $typeOfT) => org.specs2.execute.Success())
def addMatcher(f: $typeOfT => org.specs2.execute.Result): $matcherClassType = copy(Some((t: $typeOfT) => matcherFunction(t) and f(t)))
def apply[S <: $typeOfT](s: org.specs2.matcher.Expectable[S]) = {
val r = matcherFunction(s.value)
val message = r.mapMessage(m => "For "+s.description+"\n"+m).message
result(r.isSuccess, message, message, s)
}
..$fieldMatchers
..$fieldValueMatchers
..$fieldFunctionMatchers
}
"""
(matcherClassType, classDefinition)
}
private def isConstructor(c: Context) = { import c.universe._
(s: Symbol) => s match {
case m: c.universe.MethodSymbol => m.isConstructor
case other => false
}
}
private def isSynthetic(c: Context) = { import c.universe._
(s: Symbol) => s.isSynthetic
}
private def isGetter(c: Context) = { import c.universe._
(s: Symbol) => s match {
case m: c.universe.MethodSymbol => m.isGetter
case other => false
}
}
}