Search code examples
scalascala-macrosscala-reflect

In a scala macro, how to get the full name that a class will have at runtime?


The intention is to get at run-time some info of particular classes that is available only at compile-time.

My approach was to generate the info at compile time with a macro that expands to a map that contains the info indexed by the runtime class name. Something like this:

object macros {
    def subClassesOf[T]: Map[String, Info] = macro subClassesOfImpl[T];

    def subClassesOfImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[Map[String, Info]] = {
        import ctx.universe._

        val classSymbol = ctx.weakTypeTag[T].tpe.typeSymbol.asClass
        val addEntry_codeLines: List[Tree] =
            for {baseClassSymbol <- classSymbol.knownDirectSubclasses.toList} yield {
                val key = baseClassSymbol.asType.toType.erasure.typeSymbol.fullName
                q"""builder.addOne($key -> new Info("some info"));"""
            }
        q"""
            val builder = Map.newBuilder[String, Info];
            {..$addEntry_codeLines}
            builder.result();""";
        ctx.Expr[Map[String, Info]](body_code);
    }
}

Which would we used like this:

object shapes {
    trait Shape;
    case class Box(h: Int, w: Int);
    case class Sphere(r: Int);
}

val infoMap = subclassesOf[shapes.Shape];
val box = Box(3, 7);
val infoOfBox = infoMap.get(box.getClass.getName)

The problem is that the names of the erased classes given by that macro are slightly different from the ones obtained at runtime by the someInstance.getClass.getName method. The first uses dots to separate container from members, and the second uses dollars.

scala> infoMap.mkString("\n")
val res7: String =
shapes.Box -> Info(some info)
shapes.Sphere -> Info(some info)

scala> box.getClass.getName
val res8: String = shapes$Box

How is the correct way to obtain at compile time the name that a class will have at runtime?


Solution

  • Vice versa, at runtime, having Java name of a class (with dollars) you can obtain Scala name (with dots).

    box.getClass.getName 
    // com.example.App$shapes$Box
    
    import scala.reflect.runtime
    val runtimeMirror = runtime.currentMirror
    
    runtimeMirror.classSymbol(box.getClass).fullName // com.example.App.shapes.Box
    

    This can be done even with replace

    val nameWithDot = box.getClass.getName.replace('$', '.')
    if (nameWithDot.endsWith(".")) nameWithDot.init else nameWithDot 
    // com.example.App.shapes.Box
    

    Anyway, at compile time you can try

    def javaName[T]: String = macro javaNameImpl[T]
    
    def javaNameImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[String] = {
      import ctx.universe._
      val symbol = weakTypeOf[T].typeSymbol
      val owners = Seq.unfold(symbol)(symb => 
        if (symb != ctx.mirror.RootClass) Some((symb, symb.owner)) else None
      )
      val nameWithDollar = owners.foldRight("")((symb, str) => {
        val sep = if (symb.isPackage) "." else "$"
        s"$str${symb.name}$sep"
      })
      val name = if (symbol.isModuleClass) nameWithDollar else nameWithDollar.init
      ctx.Expr[String](q"${name: String}")
    }
    
    javaName[shapes.Shape] // com.example.App$shapes$Shape
    

    One more option is to use runtime reflection inside a macro. Replace

    val key = baseClassSymbol.asType.toType.erasure.typeSymbol.fullName
    

    with

    val key = javaName(baseClassSymbol.asType.toType.erasure.typeSymbol.asClass)
    

    where

    def subClassesOfImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[Map[String, Info]] = {
      import ctx.universe._
    
      def javaName(symb: ClassSymbol): String = {
        val rm = scala.reflect.runtime.currentMirror
        rm.runtimeClass(symb.asInstanceOf[scala.reflect.runtime.universe.ClassSymbol]).getName
      }
    
      ...
    }
    

    This works only with classes existing at compile time. So the project should be organized as follows

    • subproject common. Shape, Box, Sphere
    • subproject macros (depends on common). def subClassesOf...
    • subproject core (depends on macros and common). subclassesOf[shapes.Shape]...