diff options
author | Jon Pretty <jon.pretty@propensive.com> | 2017-12-02 16:55:09 +0000 |
---|---|---|
committer | Jon Pretty <jon.pretty@propensive.com> | 2017-12-02 16:55:09 +0000 |
commit | bd5f2dc411b1d63b9ee4dfec6476ba27d8410e2a (patch) | |
tree | 035718067cb0ba32a3fc5f31453d40e510e09348 | |
parent | bf5fbc0373ffdff5bd7b269e2c0709fd92819641 (diff) | |
parent | fba0c1cb94d4d67c48db8fb283c5dc1fedbf215a (diff) | |
download | magnolia-bd5f2dc411b1d63b9ee4dfec6476ba27d8410e2a.tar.gz magnolia-bd5f2dc411b1d63b9ee4dfec6476ba27d8410e2a.tar.bz2 magnolia-bd5f2dc411b1d63b9ee4dfec6476ba27d8410e2a.zip |
Merge branch 'master' into virtual-params
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | CONTRIBUTORS | 3 | ||||
-rw-r--r-- | core/shared/src/main/scala/interface.scala | 13 | ||||
-rw-r--r-- | core/shared/src/main/scala/magnolia.scala | 138 | ||||
-rw-r--r-- | examples/shared/src/main/scala/default.scala | 3 | ||||
-rw-r--r-- | examples/shared/src/main/scala/show.scala | 6 | ||||
-rw-r--r-- | examples/shared/src/main/scala/typename.scala | 19 | ||||
-rw-r--r-- | tests/src/main/scala/tests.scala | 93 |
8 files changed, 241 insertions, 35 deletions
@@ -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") () } } |