All Downloads are FREE. Search and download functionalities are using the official Maven repository.

izumi.thirdparty.internal.boopickle.PicklerMaterializersImpl.scala Maven / Gradle / Ivy

package izumi.thirdparty.internal.boopickle

import scala.reflect.macros.blackbox

private[izumi] object PicklerMaterializersImpl {

  private val logmacro = {
    val prop = System.getProperty("boopickle.logmacro")
    prop != null && prop.toLowerCase == "true"
  }
  private var stats: Map[String, Int] = Map()
  private var overallCount            = 0
  private def logStatistics(x: String): Unit = {
    stats += ((x, 1 + stats.getOrElse(x, 0)))
    overallCount += 1
    val count = stats(x)
    if (count == 1) {
      println(s"Boopickle macro: overall: $overallCount, class: $x")
    } else {
      println(s"Boopickle macro: overall: $overallCount, counter: $count, generating: $x")
    }
  }

  private def pickleSealedTrait(c: blackbox.Context)(tpe: c.universe.Type): c.universe.Tree = {
    import c.universe._

    val concreteTypes = findConcreteTypes(c)(tpe)
    val name          = TermName(c.freshName("TraitPickler"))

    q"""
      implicit object $name extends _root_.izumi.thirdparty.internal.boopickle.CompositePickler[$tpe] {
        ..$concreteTypes
      }
      $name
    """
  }

  private def findConcreteTypes(c: blackbox.Context)(tpe: c.universe.Type): Seq[c.universe.Tree] = {
    import c.universe._

    val sym = tpe.typeSymbol.asClass
    // must be a sealed trait
    if (!sym.isSealed) {
      val msg = s"The referenced trait ${sym.name} must be sealed. For non-sealed traits, create a pickler " +
        "with boopickle.CompositePickler. You may also get this error if a pickler for a class in your type hierarchy cannot be found."
      c.abort(c.enclosingPosition, msg)
    }

    if (sym.knownDirectSubclasses.isEmpty) {
      val msg = s"The referenced trait ${sym.name} does not have any sub-classes. This may " +
        "happen due to a limitation of scalac (SI-7046) given that the trait is " +
        "not in the same package. If this is the case, the pickler may be " +
        "defined using boopickle.CompositePickler directly."
      c.abort(c.enclosingPosition, msg)
    }

    // find all implementation classes in the trait hierarchy
    def findSubClasses(p: c.universe.ClassSymbol): Set[c.universe.ClassSymbol] = {
      p.knownDirectSubclasses.flatMap { sub =>
        val subClass = sub.asClass
        if (subClass.isTrait)
          findSubClasses(subClass)
        else
          Set(subClass) ++ findSubClasses(subClass)
      }
    }
    // sort class names to make sure they are always in the same order
    val result = findSubClasses(sym).toSeq.sortBy(_.name.toString).map { s =>
      if (s.typeParams.isEmpty) {
        q"""addConcreteType[$s]"""
      } else {
        val t = unifyClassWithTrait(c)(tpe, s)
        q"""addConcreteType[$t]"""
      }
    }
    result
  }

  private def pickleValueClass(c: blackbox.Context)(tpe: c.universe.Type): c.universe.Tree = {
    import c.universe._

    val parameter = tpe.typeSymbol.asClass.primaryConstructor
      .typeSignatureIn(tpe)
      .paramLists
      .head
      .head

    val parameterType = parameter.typeSignature
    val parameterTerm = parameter.name.toTermName

    q"""
      _root_.izumi.thirdparty.internal.boopickle.Default.transformPickler[$tpe, $parameterType](v => new $tpe(v))(v => v.$parameterTerm)
    """
  }

  def materializePickler[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[Pickler[T]] = {
    import c.universe._

    val tpe = weakTypeOf[T]

    if (logmacro) {
      logStatistics(tpe.toString)
    }

    if (!tpe.typeSymbol.isClass)
      throw new RuntimeException(s"Enclosure: ${c.enclosingPosition.toString}, type = $tpe")

    val sym = tpe.typeSymbol.asClass

    // special handling of sealed traits
    if ((sym.isTrait || sym.isAbstract) && !sym.fullName.toString.startsWith("scala")) {
      return c.Expr[Pickler[T]](pickleSealedTrait(c)(tpe))
    }

    // special handling of value classes
    if (sym.isDerivedValueClass) {
      return c.Expr[Pickler[T]](pickleValueClass(c)(tpe))
    }

    if (!sym.isCaseClass) {
      c.error(
        c.enclosingPosition,
        s"Cannot materialize pickler for non-case class: $tpe. If this is a collection, the error can refer to the class inside.")
      return c.Expr[Pickler[T]](q"null")
    }

    val pickleLogic = if (sym.isModuleClass) {
      // no need to write anything for case objects
      q"""()"""
    } else {
      val accessors = (tpe.decls collect {
        case acc: MethodSymbol if acc.isCaseAccessor => acc
      }).toList

      val pickleFields = for {
        accessor <- accessors
      } yield {
        val fieldTpe = accessor.typeSignatureIn(tpe).finalResultType
        q"""state.pickle[$fieldTpe](value.${accessor.name})"""
      }

      q"""
        val ref = state.identityRefFor(value)
        if(ref.isDefined) {
          state.enc.writeInt(-ref.get)
        } else {
          state.enc.writeInt(0)
          ..$pickleFields
          state.addIdentityRef(value)
        }
      """
    }

    val unpickleLogic = if (sym.isModuleClass) {
      c.parse(sym.fullName)
    } else {
      val accessors = tpe.decls.collect {
        case acc: MethodSymbol if acc.isCaseAccessor => acc
      }.toList

      val unpickledFields = for {
        accessor <- accessors
      } yield {
        val fieldTpe = accessor.typeSignatureIn(tpe).finalResultType
        q"""state.unpickle[$fieldTpe]"""
      }
      q"""
        val ic = state.dec.readInt
        if(ic == 0) {
          val value = new $tpe(..$unpickledFields)
          state.addIdentityRef(value)
          value
        } else if(ic < 0) {
          state.identityFor[$tpe](-ic)
        } else {
          state.codingError(ic)
        }
      """
    }

    val name = TermName(c.freshName("Pickler"))

    val result = q"""
      implicit object $name extends _root_.izumi.thirdparty.internal.boopickle.Pickler[$tpe] {
        override def pickle(value: $tpe)(implicit state: _root_.izumi.thirdparty.internal.boopickle.PickleState): Unit = { $pickleLogic; () }
        override def unpickle(implicit state: _root_.izumi.thirdparty.internal.boopickle.UnpickleState): $tpe = $unpickleLogic
      }
      $name
    """

    c.Expr[Pickler[T]](result)
  }

  private def unifyClassWithTrait(c: blackbox.Context)(ttrait: c.universe.Type, classSym: c.universe.ClassSymbol) = {

    val tclass             = classSym.toType
    val traitSeenFromClass = tclass.baseType(ttrait.typeSymbol)

    tclass.substituteTypes(traitSeenFromClass.typeArgs.map(_.typeSymbol), ttrait.typeArgs)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy