milessabin / shapeless

Generic programming for Scala
Apache License 2.0
3.4k stars 533 forks source link

`Generic` is not materialized in macro-generated companion object of nested case class #1287

Closed DmytroMitin closed 7 months ago

DmytroMitin commented 2 years ago

Reproduction: Define a macro annotation. It adds shapeless.Generic[A] to the companion object of A being a class or trait (this should compile if the class is a case class or the trait is a sealed trait with case-class children). If the companion object doesn't exist the annotation creates it. The annotation can annotate not only a class/trait but a companion object itself (anyway Generic is materialized inside the object).

@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}]
      }"""
      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")
    }
  }
}

Everything compiles if the annotation annotates:

This doesn't compile if the annotation annotates:

object App {
  case class A(i: Int, s: String)

  @generateGeneric
  object A

  //implicit error;
  //!I gen: shapeless.Generic[App.A]
  //  @generateGeneric

  //scalac: object A extends scala.AnyRef {
  //  def <init>() = {
  //    super.<init>();
  //    ()
  //  };
  //  _root_.shapeless.Generic[A]
  //}

  //scalac: Generic.instance[App.A, Int :: String :: shapeless.HNil](<empty> match {
  //  case (x$macro$3 @ _) => ::(x$macro$3.i, ::(x$macro$3.s, HNil)).asInstanceOf[Int :: String :: shapeless.HNil]
  //}, <empty> match {
  //  case ::((i$macro$1 @ _), ::((s$macro$2 @ _), HNil)) => App.this.A(i$macro$1, s$macro$2)
  //})
}

If we replace _root_.shapeless.Generic[${tname.toTypeName}] with explicitly resolved _root_.shapeless.Generic[${tname.toTypeName}](_root_.shapeless.Generic.materialize) then the error is

//App.A.type does not take parameters
//  @generateGeneric

https://scastie.scala-lang.org/DmytroMitin/im8OhAEySOqOiS0d7tOxvQ/15 (toolbox is not a part of reproduction, it's just for Scastie because it doesn't support multiple files/subprojects)

It seems that Generic materialized in the macro-generated companion object of a nested case class (when the object is annotated) is typechecked when apply method doesn't exist yet.

A possible fix is https://github.com/milessabin/shapeless/pull/1286

This impacts how Circe materializes semi-automatic codecs (when implicits are macro-annotation generated).

Discovered in https://stackoverflow.com/questions/74364545/type-parameter-for-implicit-valued-method-in-scala-circe

joroKr21 commented 2 years ago

Interesting... I wonder if we could force the type checking of apply somehow. I will take a look maybe this weekend.