Search code examples
scalaabstract-syntax-treescala-compiler

Inject call to base class method in trait with Scala compiler plugin


I'm trying to write a compiler plugin that would inject a call to a base class method in a trait in Scala. For the following input source:

class Component {
  def valCallback[T](ref: T, name: String): T = ???
}
trait MyTrait {
  this: Component =>
  val v = 5
}

I want the transformation result to look like:

trait MyTrait {
  this: Component =>
  val v = valCallback(5, "v")
}

This is what I've got so far:

object ValCallbackTransformer extends Transformer {
  override def transform(tree: global.Tree): global.Tree = {
    val transformed = super.transform(tree)
    transformed match {
      case cd: ClassDef if cd.mods.hasFlag(Flag.TRAIT) =>
        // ... check of self type omitted ...
        val clazz = cd.impl.self.tpt.tpe.parents.find(_.toString == "Component").get.typeSymbol
        val func = clazz.tpe.members.find(_.name.toString == "valCallback").get
        val body = cd.impl.body.map {
          case vd: ValDef if !vd.mods.isParamAccessor && vd.rhs.nonEmpty =>
            val lit = Literal(Constant(vd.getterName.toString))
            val thiz = This(clazz)
            val sel = Select(thiz, func)
            val appl = Apply(sel, List(vd.rhs, lit))
            
            thiz.tpe = clazz.tpe
            sel.tpe = func.tpe
            appl.tpe = definitions.UnitTpe
            lit.setType(definitions.StringTpe)
            treeCopy.ValDef(vd, vd.mods, vd.name, vd.tpt, appl)
          case e => e
        }
        val impl = treeCopy.Template(cd.impl, cd.impl.parents, cd.impl.self, body)
        treeCopy.ClassDef(cd, cd.mods, cd.name, cd.tparams, impl)
      case other => transformed
    }
  }
}

However, the compiler complains with the following:

Internal error: unable to find the outer accessor symbol of trait MyTrait
[error]   val v = 5

What's an outer accessor of a trait and why can't the compiler find it? How should I construct the Apply correctly?

UPDATE: I managed to get it working (see answer posted), but it's a bit confusing why I have to invoke the method on a this pointer from the trait.


Solution

  • It seems like I need to Select the method with This(<trait type>). However, since the function itself is not present inside the trait class, I need to find it separately in the self type. Working version:

    val clazz = cd.impl.symbol.owner
    val valCallbackClazz = cd.impl.self.tpt.tpe.parents.find(_.toString == "Component").get.typeSymbol
    val func = valCallbackClazz.tpe.members.find(_.name.toString == "valCallback").get
    val body = cd.impl.body.map {
      // ....
      val thiz = This(clazz)
      val sel = Select(thiz, func)
      // ...
    }