aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJon Pretty <jon.pretty@propensive.com>2017-12-02 16:55:09 +0000
committerJon Pretty <jon.pretty@propensive.com>2017-12-02 16:55:09 +0000
commitbd5f2dc411b1d63b9ee4dfec6476ba27d8410e2a (patch)
tree035718067cb0ba32a3fc5f31453d40e510e09348
parentbf5fbc0373ffdff5bd7b269e2c0709fd92819641 (diff)
parentfba0c1cb94d4d67c48db8fb283c5dc1fedbf215a (diff)
downloadmagnolia-bd5f2dc411b1d63b9ee4dfec6476ba27d8410e2a.tar.gz
magnolia-bd5f2dc411b1d63b9ee4dfec6476ba27d8410e2a.tar.bz2
magnolia-bd5f2dc411b1d63b9ee4dfec6476ba27d8410e2a.zip
Merge branch 'master' into virtual-params
-rw-r--r--.gitignore1
-rw-r--r--CONTRIBUTORS3
-rw-r--r--core/shared/src/main/scala/interface.scala13
-rw-r--r--core/shared/src/main/scala/magnolia.scala138
-rw-r--r--examples/shared/src/main/scala/default.scala3
-rw-r--r--examples/shared/src/main/scala/show.scala6
-rw-r--r--examples/shared/src/main/scala/typename.scala19
-rw-r--r--tests/src/main/scala/tests.scala93
8 files changed, 241 insertions, 35 deletions
diff --git a/.gitignore b/.gitignore
index e7f1455..63d54ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ target
.jvm
.js
.native
+.idea
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index f495d8d..28be510 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -1,3 +1,4 @@
Jon Pretty <jon.pretty@propensive.com> [@propensive](https://twitter.com/propensive/)
Loïc Descotte
-
+Georgi Krastev <jorokr21@gmail.com> [@joro_kr](https://twitter.com/joro_kr/)
+Shadaj Laddad [@shadaj](https://twitter.com/shadajl) \ No newline at end of file
diff --git a/core/shared/src/main/scala/interface.scala b/core/shared/src/main/scala/interface.scala
index 25dd1fd..5043680 100644
--- a/core/shared/src/main/scala/interface.scala
+++ b/core/shared/src/main/scala/interface.scala
@@ -34,7 +34,7 @@ trait Param[Typeclass[_], Type] {
/** the type of the parameter being represented
*
- * For exmaple, for a case class,
+ * For example, for a case class,
* <pre>
* case class Person(name: String, age: Int)
* </pre>
@@ -46,6 +46,17 @@ trait Param[Typeclass[_], Type] {
/** the name of the parameter */
def label: String
+ /** flag indicating a repeated (aka. vararg) parameter
+ *
+ * For example, for a case class,
+ * <pre>
+ * case class Account(id: String, emails: String*)
+ * </pre>
+ * the [[Param]] instance corresponding to the `emails` parameter would be `repeated` and have a
+ * [[PType]] equal to the type `Seq[String]`. Note that only the last parameter of a case class
+ * can be repeated. */
+ def repeated: Boolean
+
/** the typeclass instance associated with this parameter
*
* This is the instance of the type `Typeclass[PType]` which will have been discovered by
diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala
index bcb695f..9f3bda8 100644
--- a/core/shared/src/main/scala/magnolia.scala
+++ b/core/shared/src/main/scala/magnolia.scala
@@ -65,11 +65,14 @@ object Magnolia {
* */
def gen[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
import c.universe._
- import scala.util.{Try, Success, Failure}
+ import internal._
val magnoliaPkg = q"_root_.magnolia"
val scalaPkg = q"_root_.scala"
+ val repeatedParamClass = definitions.RepeatedParamClass
+ val scalaSeqType = typeOf[Seq[_]].typeConstructor
+
val prefixType = c.prefix.tree.tpe
def companionRef(tpe: Type): Tree = {
@@ -99,7 +102,7 @@ object Magnolia {
val typeConstructor = getTypeMember("Typeclass")
- def getMethod[T](termName: String): Option[MethodSymbol] = {
+ def getMethod(termName: String): Option[MethodSymbol] = {
val term = TermName(termName)
val cls = c.prefix.tree.tpe.baseClasses.find(_.asType.toType.decl(term) != NoSymbol)
cls.map { c =>
@@ -197,15 +200,65 @@ object Magnolia {
}
}
+ // From Shapeless: https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/generic.scala#L698
+ // Cut-n-pasted (with most original comments) and slightly adapted from
+ // https://github.com/scalamacros/paradise/blob/c14c634923313dd03f4f483be3d7782a9b56de0e/plugin/src/main/scala/org/scalamacros/paradise/typechecker/Namers.scala#L568-L613
+ def patchedCompanionSymbolOf(original: c.Symbol): c.Symbol = {
+ // see https://github.com/scalamacros/paradise/issues/7
+ // also see https://github.com/scalamacros/paradise/issues/64
+
+ val global = c.universe.asInstanceOf[scala.tools.nsc.Global]
+ val typer = c.asInstanceOf[scala.reflect.macros.runtime.Context].callsiteTyper.asInstanceOf[global.analyzer.Typer]
+ val ctx = typer.context
+ val owner = original.owner
+
+ import global.analyzer.Context
+
+ original.companion.orElse {
+ import global.{ abort => aabort, _ }
+ implicit class PatchedContext(ctx: Context) {
+ trait PatchedLookupResult { def suchThat(criterion: Symbol => Boolean): Symbol }
+ def patchedLookup(name: Name, expectedOwner: Symbol) = new PatchedLookupResult {
+ override def suchThat(criterion: Symbol => Boolean): Symbol = {
+ var res: Symbol = NoSymbol
+ var ctx = PatchedContext.this.ctx
+ while (res == NoSymbol && ctx.outer != ctx) {
+ // NOTE: original implementation says `val s = ctx.scope lookup name`
+ // but we can't use it, because Scope.lookup returns wrong results when the lookup is ambiguous
+ // and that triggers https://github.com/scalamacros/paradise/issues/64
+ val s = {
+ val lookupResult = ctx.scope.lookupAll(name).filter(criterion).toList
+ lookupResult match {
+ case Nil => NoSymbol
+ case List(unique) => unique
+ case _ => aabort(s"unexpected multiple results for a companion symbol lookup for $original#{$original.id}")
+ }
+ }
+ if (s != NoSymbol && s.owner == expectedOwner)
+ res = s
+ else
+ ctx = ctx.outer
+ }
+ res
+ }
+ }
+ }
+ ctx.patchedLookup(original.asInstanceOf[global.Symbol].name.companionName, owner.asInstanceOf[global.Symbol]).suchThat(sym =>
+ (original.isTerm || sym.hasModuleFlag) &&
+ (sym isCoDefinedWith original.asInstanceOf[global.Symbol])
+ ).asInstanceOf[c.universe.Symbol]
+ }
+ }
+
def directInferImplicit(genericType: c.Type, typeConstructor: Type): Option[Typeclass] = {
val genericTypeName: String = genericType.typeSymbol.name.decodedName.toString.toLowerCase
lazy val assignedName: TermName = TermName(c.freshName(s"${genericTypeName}Typeclass"))
lazy val typeSymbol = genericType.typeSymbol
lazy val classType = if (typeSymbol.isClass) Some(typeSymbol.asClass) else None
- lazy val isCaseClass = classType.map(_.isCaseClass).getOrElse(false)
- lazy val isCaseObject = classType.map(_.isModuleClass).getOrElse(false)
- lazy val isSealedTrait = classType.map(_.isSealed).getOrElse(false)
+ lazy val isCaseClass = classType.exists(_.isCaseClass)
+ lazy val isCaseObject = classType.exists(_.isModuleClass)
+ lazy val isSealedTrait = classType.exists(_.isSealed)
lazy val primitives = Set(typeOf[Double],
typeOf[Float],
@@ -214,7 +267,8 @@ object Magnolia {
typeOf[Int],
typeOf[Long],
typeOf[Char],
- typeOf[Boolean])
+ typeOf[Boolean],
+ typeOf[Unit])
lazy val isValueClass = genericType <:< typeOf[AnyVal] && !primitives.exists(_ =:= genericType)
@@ -244,9 +298,10 @@ object Magnolia {
sym.paramLists.head.map(_.name.decodedName.toString)
}.getOrElse(List("name", "subtypes"))
+ val className = s"${genericType.typeSymbol.owner.fullName}.${genericType.typeSymbol.name.decodedName}"
+
val result = if (isCaseObject) {
val obj = companionRef(genericType)
- val className = genericType.typeSymbol.name.decodedName.toString
val parameters = caseClassParamNames.map {
case "name" => q"$className"
@@ -263,23 +318,29 @@ object Magnolia {
case m: MethodSymbol if m.isCaseAccessor || (isValueClass && m.isParamAccessor) =>
m.asMethod
}
- val className = genericType.typeSymbol.name.decodedName.toString
case class CaseParam(sym: c.universe.MethodSymbol,
+ repeated: Boolean,
typeclass: c.Tree,
paramType: c.Type,
ref: c.TermName)
- val caseParamsReversed: List[CaseParam] = caseClassParameters.foldLeft(List[CaseParam]()) {
- case (acc, param) =>
+ val caseParamsReversed = caseClassParameters.foldLeft[List[CaseParam]](Nil) {
+ (acc, param) =>
val paramName = param.name.decodedName.toString
- val paramType = param.returnType.substituteTypes(genericType.etaExpand.typeParams,
- genericType.typeArgs)
+ val paramTypeSubstituted = param.typeSignatureIn(genericType).resultType
+
+ val (repeated, paramType) = paramTypeSubstituted match {
+ case TypeRef(_, `repeatedParamClass`, typeArgs) =>
+ true -> appliedType(scalaSeqType, typeArgs)
+ case tpe =>
+ false -> tpe
+ }
val predefinedRef = acc.find(_.paramType == paramType)
val caseParamOpt = predefinedRef.map { backRef =>
- CaseParam(param, q"()", paramType, backRef.ref) :: acc
+ CaseParam(param, repeated, q"()", paramType, backRef.ref) :: acc
}
caseParamOpt.getOrElse {
@@ -292,7 +353,7 @@ object Magnolia {
val ref = TermName(c.freshName("paramTypeclass"))
val assigned = q"""val $ref = $derivedImplicit"""
- CaseParam(param, assigned, paramType, ref) :: acc
+ CaseParam(param, repeated, assigned, paramType, ref) :: acc
}
}
@@ -304,10 +365,17 @@ object Magnolia {
val preAssignments = caseParams.map(_.typeclass)
val defaults = if (!isValueClass) {
- val caseClassCompanion = genericType.companion
- val constructorMethod = caseClassCompanion.decl(TermName("apply")).asMethod
+ val caseClassCompanion = patchedCompanionSymbolOf(genericType.typeSymbol).asModule.info
+
+ // If a companion object is defined with alternative apply methods
+ // it is needed get all the alternatives
+ val constructorMethods =
+ caseClassCompanion.decl(TermName("apply")).alternatives.map(_.asMethod)
+
+ // The last apply method in the alternatives is the one that belongs
+ // to the case class, not the user defined companion object
val indexedConstructorParams =
- constructorMethod.paramLists.head.map(_.asTerm).zipWithIndex
+ constructorMethods.last.paramLists.head.map(_.asTerm).zipWithIndex
indexedConstructorParams.map {
case (p, idx) =>
@@ -319,22 +387,23 @@ object Magnolia {
} else List(q"$scalaPkg.None")
val assignments = caseParams.zip(defaults).zipWithIndex.map {
- case ((CaseParam(param, typeclass, paramType, ref), defaultVal), idx) =>
+ case ((CaseParam(param, repeated, typeclass, paramType, ref), defaultVal), idx) =>
val paramMethod: Tree = getMethod("param").map { sym =>
q"${c.prefix}.param"
}.getOrElse(q"$magnoliaPkg.Magnolia.param[$typeConstructor, $genericType, $paramType]")
val paramNames = getMethod("param").map { sym =>
sym.paramLists.head.map(_.name.decodedName.toString)
- }.getOrElse(List("name", "typeclass", "default", "dereference"))
+ }.getOrElse(List("name", "repeated", "typeclass", "default", "dereference"))
val parameters: List[Tree] = paramNames.map {
case "name" => q"${param.name.decodedName.toString}"
+ case "repeated" => q"$repeated"
case "typeclass" => q"$ref"
case "default" => q"$defaultVal"
case "dereference" => q"_.${param.name}"
case other =>
- c.abort(c.enclosingPosition, s"magnolia: method 'param' has an unexpected parameter with name '$other'; permitted parameter names: name, typeclass, default, dereference")
+ c.abort(c.enclosingPosition, s"magnolia: method 'param' has an unexpected parameter with name '$other'; permitted parameter names: default, dereference, name, repeated, typeclass")
}
q"""$paramsVal($idx) = $paramMethod(..$parameters)"""
}
@@ -348,7 +417,8 @@ object Magnolia {
($fnVal: $liftedParamType => Any) =>
new $genericType(..${caseParams.zipWithIndex.map {
case (typeclass, idx) =>
- q"$fnVal($paramsVal($idx)).asInstanceOf[${typeclass.paramType}]"
+ val arg = q"$fnVal($paramsVal($idx)).asInstanceOf[${typeclass.paramType}]"
+ if (typeclass.repeated) q"$arg: _*" else arg
}})
"""
}
@@ -370,10 +440,13 @@ object Magnolia {
} else if (isSealedTrait) {
val genericSubtypes = classType.get.knownDirectSubclasses.to[List]
val subtypes = genericSubtypes.map { sub =>
- val typeArgs = sub.asType.typeSignature.baseType(genericType.typeSymbol).typeArgs
- val mapping = typeArgs.zip(genericType.typeArgs).toMap
- val newTypeParams = sub.asType.toType.typeArgs.map(mapping(_))
- appliedType(sub.asType.toType.typeConstructor, newTypeParams)
+ val subType = sub.asType.toType // FIXME: Broken for path dependent types
+ val typeParams = sub.asType.typeParams
+ val typeArgs = thisType(sub).baseType(genericType.typeSymbol).typeArgs
+ val mapping = (typeArgs.map(_.typeSymbol), genericType.typeArgs).zipped.toMap
+ val newTypeArgs = typeParams.map(mapping.withDefault(_.asType.toType))
+ val applied = appliedType(subType.typeConstructor, newTypeArgs)
+ existentialAbstraction(typeParams, applied)
}
if (subtypes.isEmpty) {
@@ -415,10 +488,10 @@ object Magnolia {
}
val parameters = sealedTraitParamNames.map {
- case "name" => q"$genericTypeName"
+ case "name" => q"""${s"${genericType.typeSymbol.owner.fullName}.${genericType.typeSymbol.name.decodedName}"}"""
case "subtypes" => q"$subtypesVal: $scalaPkg.Array[$liftedSubtypeType]"
}
-
+
Some {
Typeclass(
genericType,
@@ -446,9 +519,9 @@ object Magnolia {
val genericType: Type = weakTypeOf[T]
val currentStack: Stack =
- recursionStack.get(c.enclosingPosition).getOrElse(Stack(Map(), List(), List()))
+ recursionStack.getOrElse(c.enclosingPosition, Stack(Map(), List(), List()))
- val directlyReentrant = Some(genericType) == currentStack.frames.headOption.map(_.genericType)
+ val directlyReentrant = currentStack.frames.headOption.exists(_.genericType == genericType)
if (directlyReentrant) throw DirectlyReentrantException()
@@ -457,14 +530,14 @@ object Magnolia {
emittedErrors += error
val trace = error.path.mkString("\n in ", "\n in ", "\n \n")
- val msg = s"magnolia: could not derive ${typeConstructor} instance for type " +
+ val msg = s"magnolia: could not derive $typeConstructor instance for type " +
s"${error.genericType}"
c.info(c.enclosingPosition, msg + trace, true)
}
}
- val result: Option[Tree] = if (!currentStack.frames.isEmpty) {
+ val result: Option[Tree] = if (currentStack.frames.nonEmpty) {
findType(genericType) match {
case None =>
directInferImplicit(genericType, typeConstructor).map(_.tree)
@@ -508,16 +581,19 @@ object Magnolia {
* This method is intended to be called only from code generated by the Magnolia macro, and
* should not be called directly from users' code. */
def param[Tc[_], T, P](name: String,
+ repeated: Boolean,
typeclass: Tc[P],
default: => Option[P],
dereference: T => P) = {
val typeclassVal = typeclass
val defaultVal = default
val dereferenceVal = dereference
+ val repeatedVal = repeated
new Param[Tc, T] {
type PType = P
def label: String = name
+ def repeated: Boolean = repeatedVal
def default: Option[PType] = defaultVal
def typeclass: Tc[PType] = typeclassVal
def dereference(t: T): PType = dereferenceVal(t)
diff --git a/examples/shared/src/main/scala/default.scala b/examples/shared/src/main/scala/default.scala
index aa9dedc..bce11d6 100644
--- a/examples/shared/src/main/scala/default.scala
+++ b/examples/shared/src/main/scala/default.scala
@@ -31,6 +31,9 @@ object Default {
/** default value for ints; 0 */
implicit val int: Default[Int] = new Default[Int] { def default = 0 }
+ /** default value for sequences; the empty sequence */
+ implicit def seq[A]: Default[Seq[A]] = new Typeclass[Seq[A]] { def default = Seq.empty }
+
/** generates default instances of [[Default]] for case classes and sealed traits */
implicit def gen[T]: Default[T] = macro Magnolia.gen[T]
}
diff --git a/examples/shared/src/main/scala/show.scala b/examples/shared/src/main/scala/show.scala
index c9ce09f..ee43dfc 100644
--- a/examples/shared/src/main/scala/show.scala
+++ b/examples/shared/src/main/scala/show.scala
@@ -93,4 +93,10 @@ object Show extends GenericShow[String] {
implicit val int: Show[String, Int] = new Show[String, Int] {
def show(s: Int): String = s.toString
}
+
+ /** show typeclass for sequences */
+ implicit def seq[A](implicit A: Show[String, A]): Show[String, Seq[A]] =
+ new Show[String, Seq[A]] {
+ def show(as: Seq[A]): String = as.iterator.map(A.show).mkString("[", ",", "]")
+ }
}
diff --git a/examples/shared/src/main/scala/typename.scala b/examples/shared/src/main/scala/typename.scala
new file mode 100644
index 0000000..aaa40f7
--- /dev/null
+++ b/examples/shared/src/main/scala/typename.scala
@@ -0,0 +1,19 @@
+package magnolia.examples
+
+import language.experimental.macros
+
+import magnolia._
+
+trait TypeName[T] { def name: String }
+
+object TypeName {
+ type Typeclass[T] = TypeName[T]
+ def combine[T](ctx: CaseClass[TypeName, T, _]): TypeName[T] =
+ new TypeName[T] { def name: String = ctx.typeName }
+
+ def dispatch[T](ctx: SealedTrait[TypeName, T]): TypeName[T] =
+ new TypeName[T] { def name: String = ctx.typeName }
+
+ implicit def gen[T]: TypeName[T] = macro Magnolia.gen[T]
+}
+
diff --git a/tests/src/main/scala/tests.scala b/tests/src/main/scala/tests.scala
index 110e29d..7b1fbe3 100644
--- a/tests/src/main/scala/tests.scala
+++ b/tests/src/main/scala/tests.scala
@@ -1,13 +1,13 @@
package magnolia.tests
+import language.experimental.macros
+
import magnolia._
import estrapade._
import contextual.data.scalac._
import contextual.data.fqt._
import contextual.data.txt._
-import scala.util._
-
sealed trait Tree[+T]
case class Leaf[+L](value: L) extends Tree[L]
case class Branch[+B](left: Tree[B], right: Tree[B]) extends Tree[B]
@@ -37,11 +37,39 @@ case object Blue extends Color
case class `%%`(`/`: Int, `#`: String)
+case class Param(a: String, b: String)
+case class Test(param: Param)
+object Test {
+ def apply(): Test = Test(Param("", ""))
+
+ def apply(a: String)(implicit b: Int): Test = Test(Param(a, b.toString))
+
+ def apply(a: String, b: String): Test = Test(Param(a, b))
+}
+
+sealed trait Politician[+S]
+case class Accountable[+S](slogan: S) extends Politician[S]
+case class Corrupt[+S, +L <: Seq[Company]](slogan: S, lobby: L) extends Politician[S]
+
+sealed trait Box[+A]
+case class SimpleBox[+A](value: A) extends Box[A]
+case class LabelledBox[+A, L <: String](value: A, var label: L) extends Box[A]
+
+case class Account(id: String, emails: String*)
+
+case class Portfolio(companies: Company*)
+
+
object Tests extends TestApp {
def tests() = for(i <- 1 to 1000) {
import examples._
+ test("construct a Show product instance with alternative apply functions") {
+ import examples._
+ Show.gen[Test].show(Test("a", "b"))
+ }.assert(_ == """Test(param=Param(a=a,b=b))""")
+
test("construct a Show product instance") {
import examples._
Show.gen[Person].show(Person("John Smith", 34))
@@ -161,6 +189,16 @@ object Tests extends TestApp {
| in parameter 'alpha' of product type Beta
|"""))
+ test("not attempt to instantiate Unit when producing error stack") {
+ scalac"""
+ import magnolia.examples._
+ case class Gamma(unit: Unit)
+ Show.gen[Gamma]
+ """
+ }.assert(_ == TypecheckError(txt"""magnolia: could not find typeclass for type Unit
+ | in parameter 'unit' of product type Gamma
+ |"""))
+
test("typenames and labels are not encoded") {
implicitly[Show[String, `%%`]].show(`%%`(1, "two"))
}.assert(_ == "%%(/=1,#=two)")
@@ -177,6 +215,57 @@ object Tests extends TestApp {
Show.gen[Length].show(new Length(100))
}.assert(_ == "100")
+ // Corrupt being covariant in L <: Seq[Company] enables the derivation for Corrupt[String, _]
+ test("show a Politician with covariant lobby") {
+ Show.gen[Politician[String]].show(Corrupt("wall", Seq(Company("Alice Inc"))))
+ }.assert(_ == "Corrupt(slogan=wall,lobby=[Company(name=Alice Inc)])")
+
+ // LabelledBox being invariant in L <: String prohibits the derivation for LabelledBox[Int, _]
+ test("can't show a Box with invariant label") {
+ scalac"Show.gen[Box[Int]]"
+ }.assert { _ == TypecheckError(
+ txt"""magnolia: could not find typeclass for type L
+ | in parameter 'label' of product type magnolia.tests.LabelledBox[Int, _ <: String]
+ | in coproduct type magnolia.tests.Box[Int]
+ |""")
+ }
+
+ class ParentClass() {
+ case class LocalClass(name: String)
+
+ test("serialize a case class inside another class") {
+ implicitly[Show[String, LocalClass]].show(LocalClass("foo"))
+ }.assert(_ == "LocalClass(name=foo)")
+
+ case class LocalClassWithDefault(name: String = "foo")
+
+ test("construct a default case class inside another class") {
+ Default.gen[LocalClassWithDefault].default
+ }.assert(_ == LocalClassWithDefault("foo"))
+ }
+
+ new ParentClass()
+
+ test("show an Account") {
+ Show.gen[Account].show(Account("john_doe", "john.doe@yahoo.com", "john.doe@gmail.com"))
+ }.assert(_ == "Account(id=john_doe,emails=[john.doe@yahoo.com,john.doe@gmail.com])")
+
+ test("construct a default Account") {
+ Default.gen[Account].default
+ }.assert(_ == Account(""))
+
+ test("show a Portfolio of Companies") {
+ Show.gen[Portfolio].show(Portfolio(Company("Alice Inc"), Company("Bob & Co")))
+ }.assert(_ == "Portfolio(companies=[Company(name=Alice Inc),Company(name=Bob & Co)])")
+
+ test("sealed trait typeName should be complete and unchanged") {
+ TypeName.gen[Color].name
+ }.assert(_ == "magnolia.tests.Color")
+
+ test("case class typeName should be complete and unchanged") {
+ implicit val stringTypeName: TypeName[String] = new TypeName[String] { def name = "" }
+ TypeName.gen[Fruit].name
+ }.assert(_ == "magnolia.tests.Fruit")
()
}
}