Search code examples
scalareflectionscala-reflect

Reflection does not see companion of subclass case class when defined in companion of extending trait


Given a code which takes a type, takes it's known direct subclasses, filters the ones that are case classes and then takes the companion of that case class:

def firstSubclassWithCompanion[T: TypeTag]: String = {
  val superclass = implicitly[TypeTag[T]].tpe.typeSymbol.asClass
  val caseClass = superclass.knownDirectSubclasses.map(_.asClass).filter(_.isCaseClass).head
  s"case class $caseClass has companion ${caseClass.companion}"
}

With a simple example

sealed trait With
case class WithCase() extends With

It gives the expected return

> firstSubclassWithCompanion[With]
"class WithCase has companion object WithCase"

As With trait has a WithCase subclass, which is case class that has a companion object (defined by compiler).

However, given the following example, where the subclass is defined in the companion object of the inheriting trait:

sealed trait Without
object Without {
  case class WithoutCase() extends Without
}

It doesn't return the companion object

> firstSubclassWithCompanion[Without]
"class WithoutCase has companion <none>"

It works fine if it's defined in other object.


Solution

  • Bugs should be reported at https://github.com/scala/bug/issues

    A workaround is to use caseClass.owner.typeSignature.decl(caseClass.name) instead of caseClass.companion.

    Another workaround is to translate this runtime-reflection code into a macro (compile-time reflection). Since all the classes here are defined at compile time it makes sense to use a macro.

    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    def firstSubclassWithCompanion[T]: String = macro firstSubclassWithCompanionImpl[T]
    
    def firstSubclassWithCompanionImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
      import c.universe._
      val superclass = weakTypeOf[T].typeSymbol.asClass
      val caseClass = superclass.knownDirectSubclasses.map(_.asClass).filter(_.isCaseClass).head
      val res = s"case class $caseClass has companion ${caseClass.companion}"
      q"$res"
    }