4

OUTLINE

I have an API that looks something like this:

package com.example

object ExternalApi {

  def create[T <: SpecialElement](elem: T): TypeConstructor[T] =
    TypeConstructor(elem)

  def create1[T <: SpecialElement](elem: T): TypeConstructor[T] =
    TypeConstructor(elem)

  def create2[T <: SpecialElement](elem: T): TypeConstructor[T] =
    TypeConstructor(elem)
  //...

}

object MyApi {

  def process[T <: TypeConstructor[_ <: SpecialElement]](
      l: T,
      metadata: List[String]): T = {
    println("I've been called!")
    //do some interesting stuff with the List's type parameter here
    l
  }

}

case class TypeConstructor[E](elem: E)

trait SpecialElement

The ExternalApi (which is actually external to my lib, so no modifying that) has a series of calls that I'd like to automatically wrap with MyApi.process calls, with the metadata argument derived from the final type of T.

To illustrate, the calls to be wrapped could have any form, including nested calls, and calls within other AST subtree types (such as Blocks), e.g. :

package com.example.test

import com.example.{ExternalApi, SpecialElement}

object ApiPluginTest extends App {
  //one possible form
  val targetList = ExternalApi.create(Blah("name"))

  //and another
  ExternalApi.create2(ExternalApi.create1(Blah("sth")).elem)

  //and yet another
  val t = {

    val sub1 = ExternalApi.create(Blah("anything"))

    val sub2 = ExternalApi.create1(sub1.elem)

    sub2
  }

}

case class Blah(name: String) extends SpecialElement

Since compiler plugins handle matching structures within ASTs recursively "for free", I've decided to go with them.

However, due to the fact that I need to match a specific type signature, the plugin follows the typer phase.

Here's the code of the PluginComponent:

package com.example.plugin

import com.example.{SpecialElement, TypeConstructor}

import scala.tools.nsc.Global
import scala.tools.nsc.plugins.PluginComponent
import scala.tools.nsc.transform.Transform

class WrapInApiCallComponent(val global: Global)
    extends PluginComponent
    with Transform {
  protected def newTransformer(unit: global.CompilationUnit) =
    WrapInApiCallTransformer

  val runsAfter: List[String] = List("typer") //since we need the type
  val phaseName: String       = WrapInApiCallComponent.Name

  import global._

  object WrapInApiCallTransformer extends Transformer {
    override def transform(tree: global.Tree) = {
      val transformed = super.transform(tree)
      transformed match {
        case call @ Apply(_, _) =>
          if (call.tpe != null && call.tpe.finalResultType <:< typeOf[
                TypeConstructor[_ <: SpecialElement]]) {
            println(s"Found relevant call $call")

            val typeArguments = call.tpe.typeArgs.map(_.toString).toList

            val listSymbOf = symbolOf[List.type]
            val wrappedFuncSecondArgument =
              q"$listSymbOf.apply(..$typeArguments)"

            val apiObjSymbol = symbolOf[com.example.MyApi.type]

            val wrappedCall =
              q"$apiObjSymbol.process[${call.tpe.finalResultType}]($call, $wrappedFuncSecondArgument)"

            //explicit typing, otherwise later phases throw NPEs etc.
            val ret = typer.typed(wrappedCall)
            println(showRaw(ret))
            println("----")
            ret
          } else {
            call
          }
        case _ => transformed
      }
    }
  }
}

object WrapInApiCallComponent {
  val Name = "api_embed_component"
}

This seems to resolve identifiers, as well as types, correctly, outputting e.g.:

Apply(TypeApply(Select(TypeTree().setOriginal(Ident(com.example.MyApi)), TermName("process")), List(TypeTree())), List(Apply(TypeApply(Select(Select(Select(Ident(com), com.example), com.example.MyApi), TermName("create")), List(TypeTree())), List(Apply(Select(Ident(com.example.test.Blah), TermName("apply")), List(Literal(Constant("name")))))), Apply(TypeApply(Select(TypeTree().setOriginal(Ident(scala.collection.immutable.List)), TermName("apply")), List(TypeTree())), List(Literal(Constant("com.example.test.Blah"))))))

Unfortunately, I get an error during compilation starting with:

scala.reflect.internal.FatalError: 
[error] 
[error]   Unexpected tree in genLoad: com.example.MyApi.type/class scala.reflect.internal.Trees$TypeTree at: RangePosition([projectpath]/testPluginAutoWrap/compiler_plugin_test/src/main/scala/com/example/test/ApiPluginTest.scala, 108, 112, 112)
[error]      while compiling: [projectpath]/testPluginAutoWrap/compiler_plugin_test/src/main/scala/com/example/test/ApiPluginTest.scala
[error]         during phase: jvm
[error]      library version: version 2.12.4
[error]     compiler version: version 2.12.4
[error]   reconstructed args: -Xlog-implicits -classpath [classpath here]
[error] 
[error]   last tree to typer: TypeTree(class String)
[error]        tree position: line 23 of [projectpath]/testPluginAutoWrap/compiler_plugin_test/src/main/scala/com/example/test/ApiPluginTest.scala

QUESTION

It looks I'm screwing something up with type definitions, but what is it?

Specifically:

How do I correctly wrap every ExternalApi.createX call with an MyApi.process call, constrained by the requirements provided above?


NOTES

  1. Given the amount of boilerplate required, I've set up a complete example project. It's available here.
  2. The answer does not have to define a compiler plugin. If you're able to cover all the relevant calls with a macro, that is fine as well.
  3. Originally the wrapped call was to something like: def process[T <: TypeConstructor[_ <: SpecialElement] : TypeTag](l: T): T, the setup here is actually a workaround. So if you are able to generate a wrapped call of this type, i.e. one that includes a runtime TypeTag[T], that's fine as well.
mikołak
  • 9,605
  • 1
  • 48
  • 70

0 Answers0