From aab80015b8e9672dfceff1fdce021be25b752a60 Mon Sep 17 00:00:00 2001 From: Shadaj Laddad Date: Sun, 12 Nov 2017 19:30:57 -0800 Subject: Restore ability to derive typeclasses for case classes inside other classes --- core/shared/src/main/scala/magnolia.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'core/shared/src/main') diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala index 6400002..9df3532 100644 --- a/core/shared/src/main/scala/magnolia.scala +++ b/core/shared/src/main/scala/magnolia.scala @@ -278,10 +278,10 @@ object Magnolia { val preAssignments = caseParams.map(_.typeclass) val defaults = if (!isValueClass) { - val caseClassCompanion = genericType.companion - val constructorMethod = caseClassCompanion.decl(TermName("apply")).asMethod - val indexedConstructorParams = - constructorMethod.paramLists.head.map(_.asTerm).zipWithIndex + val caseClassParameters = genericType.decls.collect { + case m: MethodSymbol if m.isCaseAccessor => m.asMethod + } + val indexedConstructorParams = caseClassParameters.map(_.asTerm).zipWithIndex indexedConstructorParams.map { case (p, idx) => -- cgit v1.2.3 From 5052e8c1a1a0c272c0406165e04bef3c89788d59 Mon Sep 17 00:00:00 2001 From: Shadaj Laddad Date: Sun, 12 Nov 2017 19:34:35 -0800 Subject: Simplify logic to directly calculate indexedConstructorParams --- core/shared/src/main/scala/magnolia.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'core/shared/src/main') diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala index 9df3532..49b2744 100644 --- a/core/shared/src/main/scala/magnolia.scala +++ b/core/shared/src/main/scala/magnolia.scala @@ -278,10 +278,9 @@ object Magnolia { val preAssignments = caseParams.map(_.typeclass) val defaults = if (!isValueClass) { - val caseClassParameters = genericType.decls.collect { - case m: MethodSymbol if m.isCaseAccessor => m.asMethod - } - val indexedConstructorParams = caseClassParameters.map(_.asTerm).zipWithIndex + val indexedConstructorParams = genericType.decls.collect { + case m: MethodSymbol if m.isCaseAccessor => m.asTerm + }.zipWithIndex indexedConstructorParams.map { case (p, idx) => -- cgit v1.2.3 From ebeca9258d5e629327abbcf67e0066a4cdeac173 Mon Sep 17 00:00:00 2001 From: Shadaj Laddad Date: Tue, 21 Nov 2017 10:23:09 -0800 Subject: Add unit tests and fix behavior with default parameters --- .gitignore | 1 + core/shared/src/main/scala/magnolia.scala | 30 ++++++++++++++++++++---------- tests/src/main/scala/tests.scala | 8 ++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) (limited to 'core/shared/src/main') diff --git a/.gitignore b/.gitignore index e7f1455..ffa0d68 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target .jvm .js .native +.idea/ diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala index 49b2744..2431e4f 100644 --- a/core/shared/src/main/scala/magnolia.scala +++ b/core/shared/src/main/scala/magnolia.scala @@ -278,16 +278,26 @@ object Magnolia { val preAssignments = caseParams.map(_.typeclass) val defaults = if (!isValueClass) { - val indexedConstructorParams = genericType.decls.collect { - case m: MethodSymbol if m.isCaseAccessor => m.asTerm - }.zipWithIndex - - indexedConstructorParams.map { - case (p, idx) => - if (p.isParamWithDefault) { - val method = TermName("apply$default$" + (idx + 1)) - q"$scalaPkg.Some(${genericType.typeSymbol.companion.asTerm}.$method)" - } else q"$scalaPkg.None" + val constructorParams = genericType.decls.collect { + case a: MethodSymbol if a.isConstructor => a + }.head.paramLists.head.map(_.asTerm) + val noDefaults = constructorParams.forall(!_.isParamWithDefault) + + if (noDefaults) { + constructorParams.map(_ => q"$scalaPkg.None") + } else { + val caseClassCompanion = genericType.companion + val constructorMethod = caseClassCompanion.decl(TermName("apply")).asMethod + val indexedConstructorParams = + constructorMethod.paramLists.head.map(_.asTerm).zipWithIndex + + indexedConstructorParams.map { + case (p, idx) => + if (p.isParamWithDefault) { + val method = TermName("apply$default$" + (idx + 1)) + q"$scalaPkg.Some(${genericType.typeSymbol.companion.asTerm}.$method)" + } else q"$scalaPkg.None" + } } } else List(q"$scalaPkg.None") diff --git a/tests/src/main/scala/tests.scala b/tests/src/main/scala/tests.scala index e453502..6810fc9 100644 --- a/tests/src/main/scala/tests.scala +++ b/tests/src/main/scala/tests.scala @@ -176,5 +176,13 @@ object Tests extends TestApp { test("serialize a value class") { Show.gen[Length].show(new Length(100)) }.assert(_ == "100") + + class ParentClass { + case class InnerClass(name: String) + + test("serialize a case class inside another class") { + implicitly[Show[String, InnerClass]].show(InnerClass("foo")) + }.assert(_ == "InnerClass(name=foo)") + } } } -- cgit v1.2.3 From 4bfd00088576c9400aa7630007ccac9670399e50 Mon Sep 17 00:00:00 2001 From: Shadaj Laddad Date: Thu, 30 Nov 2017 10:27:43 -0800 Subject: Use patchedCompanionSymbolOf from Shapeless to get companion object As suggested by @joroKr21 --- core/shared/src/main/scala/magnolia.scala | 97 ++++++++++++++++++++++--------- tests/src/main/scala/tests.scala | 13 +++-- 2 files changed, 77 insertions(+), 33 deletions(-) (limited to 'core/shared/src/main') diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala index e8350da..c935b91 100644 --- a/core/shared/src/main/scala/magnolia.scala +++ b/core/shared/src/main/scala/magnolia.scala @@ -200,6 +200,56 @@ 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 @@ -289,35 +339,24 @@ object Magnolia { val preAssignments = caseParams.map(_.typeclass) val defaults = if (!isValueClass) { - - val constructorParams = genericType.decls.collect { - case a: MethodSymbol if a.isConstructor => a - }.head.paramLists.head.map(_.asTerm) - val noDefaults = constructorParams.forall(!_.isParamWithDefault) - - if (noDefaults) { - constructorParams.map(_ => q"$scalaPkg.None") - } else { - val caseClassCompanion = genericType.companion - - // 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 = - constructorMethods.last.paramLists.head.map(_.asTerm).zipWithIndex - - indexedConstructorParams.map { - case (p, idx) => - if (p.isParamWithDefault) { - val method = TermName("apply$default$" + (idx + 1)) - q"$scalaPkg.Some(${genericType.typeSymbol.companion.asTerm}.$method)" - } else q"$scalaPkg.None" - } - + 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 = + constructorMethods.last.paramLists.head.map(_.asTerm).zipWithIndex + + indexedConstructorParams.map { + case (p, idx) => + if (p.isParamWithDefault) { + val method = TermName("apply$default$" + (idx + 1)) + q"$scalaPkg.Some(${genericType.typeSymbol.companion.asTerm}.$method)" + } else q"$scalaPkg.None" } } else List(q"$scalaPkg.None") diff --git a/tests/src/main/scala/tests.scala b/tests/src/main/scala/tests.scala index 6d883da..836a00e 100644 --- a/tests/src/main/scala/tests.scala +++ b/tests/src/main/scala/tests.scala @@ -204,13 +204,18 @@ object Tests extends TestApp { Show.gen[Length].show(new Length(100)) }.assert(_ == "100") - class ParentClass { - case class InnerClass(name: String) + case class LocalClass(name: String) test("serialize a case class inside another class") { - implicitly[Show[String, InnerClass]].show(InnerClass("foo")) - }.assert(_ == "InnerClass(name=foo)") + 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 -- cgit v1.2.3