diff --git a/build.sbt b/build.sbt index 9a9b625b5..cb898b05d 100644 --- a/build.sbt +++ b/build.sbt @@ -107,6 +107,30 @@ lazy val plugin = project.in(file("plugin")) crossScalaVersions := Seq(Scala213, Scala212) ) +lazy val macroAnnotationSettings = Seq( + scalacOptions ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, v)) if v >= 13 => Seq("-Ymacro-annotations") + case _ => Nil + } + }, + libraryDependencies ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, v)) if v <= 12 => + Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)) + case _ => Nil + } + }, +) + +lazy val coreTestMacros = crossProject(JSPlatform, JVMPlatform, NativePlatform) + .crossType(CrossType.Full) + .settings(moduleName := "core-test-macros") + .settings(commonSettings) + .settings(noPublishSettings) + .configureCross(buildInfoSetup) + .settings(macroAnnotationSettings) + lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Full) .configureCross(configureJUnit) @@ -119,6 +143,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings(Compile / sourceManaged := baseDirectory.value.getParentFile / "shared" / "src" / "main" / "managed") .settings(Compile / sourceGenerators += (Compile / sourceManaged).map(Boilerplate.gen).taskValue) .settings(mimaSettings) + .dependsOn(coreTestMacros % "test->compile") + .settings(macroAnnotationSettings) lazy val coreJVM = core.jvm lazy val coreJS = core.js diff --git a/core/shared/src/main/scala/shapeless/generic.scala b/core/shared/src/main/scala/shapeless/generic.scala index 0a3a5d3ea..4ba9e67c3 100644 --- a/core/shared/src/main/scala/shapeless/generic.scala +++ b/core/shared/src/main/scala/shapeless/generic.scala @@ -916,9 +916,13 @@ trait CaseClassMacros extends ReprTypes with CaseClassMacrosVersionSpecifics { // case 3: case class case tpe if tpe.typeSymbol.asClass.isCaseClass => val companion = patchedCompanionSymbolOf(tpe.typeSymbol) + val apply = companion.typeSignature.member(TermName("apply")) val unapply = companion.typeSignature.member(TermName("unapply")) val fields = fieldsOf(tpe) - FromTo(fromApply(fields), if (unapply.isSynthetic) toUnapply(fields) else toGetters(fields)) + FromTo( + if (apply == NoSymbol) fromConstructor(fields) else fromApply(fields), + if (unapply.isSynthetic) toUnapply(fields) else toGetters(fields) + ) // case 4: exactly one matching public apply/unapply case HasApplyUnapply(args) => FromTo(fromApply(args), toUnapply(args)) diff --git a/core/shared/src/test/scala/shapeless/generic.scala b/core/shared/src/test/scala/shapeless/generic.scala index 2bd4fe8c9..49fd5dff7 100644 --- a/core/shared/src/test/scala/shapeless/generic.scala +++ b/core/shared/src/test/scala/shapeless/generic.scala @@ -157,6 +157,13 @@ package GenericTestsAux { final case class InTap[A, -B](in: B => A) extends Tap[A] final case class OutTap[A, +B](out: A => B) extends Tap[A] final case class PipeTap[A, B](in: B => A, out: A => B) extends Tap[A] + + object macroAnnotations { + case class A(i: Int, s: String) + + @generateGeneric + object A + } } class GenericTests { diff --git a/coreTestMacros/shared/src/main/scala/shapeless/generateGeneric.scala b/coreTestMacros/shared/src/main/scala/shapeless/generateGeneric.scala new file mode 100644 index 000000000..3f4000fc9 --- /dev/null +++ b/coreTestMacros/shared/src/main/scala/shapeless/generateGeneric.scala @@ -0,0 +1,35 @@ +package shapeless + +import scala.annotation.{StaticAnnotation, compileTimeOnly} +import scala.reflect.macros.blackbox +import scala.language.experimental.macros + +@compileTimeOnly("enable macro annotations") +class generateGeneric extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro GenerateGenericMacroImpl.macroTransformImpl +} + +object GenerateGenericMacroImpl { + def macroTransformImpl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = { + import c.universe._ + + def modifyObject(obj: Tree): Tree = obj match { + case q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" => + q"""$mods object $tname extends { ..$earlydefns } with ..$parents { $self => + ..$body + _root_.shapeless.Generic[${tname.toTypeName}](_root_.shapeless.Generic.materialize) + }""" + case _ => sys.error("impossible") + } + + def modify(cls: Tree, obj: Tree): Tree = q"..${Seq(cls, modifyObject(obj))}" + + annottees match { + case (cls: ClassDef) :: (obj: ModuleDef) :: Nil => modify(cls, obj) + case (cls: ClassDef) :: Nil => modify(cls, q"object ${cls.name.toTermName}") + // this works for the companion object of a sealed trait or top-level case class but not nested case class + case (obj: ModuleDef) :: Nil => modifyObject(obj) + case _ => c.abort(c.enclosingPosition, "@generateGeneric can annotate only traits, classes, and objects") + } + } +} \ No newline at end of file