Search code examples
scalascala-macrosscala-reflectscala-macro-paradise

Preserve method parameter names in scala macro


I have an interface:

trait MyInterface {
  def doSomething(usefulName : Int) : Unit
}

I have a macro that iterates over the methods of the interface and does stuff with the method names and parameters. I access the method names by doing something like this:

val tpe = typeOf[MyInterface]

// Get lists of parameter names for each method
val listOfParamLists = tpe.decls
  .filter(_.isMethod)
  .map(_.asMethod.paramLists.head.map(sym => sym.asTerm.name))

If I print out the names for doSomething's parameters, usefulName has become x$1. Why is this happening and is there a way to preserve the original parameter names?

I am using scala version 2.11.8, macros paradise version 2.1.0, and the blackbox context.

The interface is actually java source in a separate sbt project that I control. I have tried compiling with:

javacOptions in (Compile, compile) ++= Seq("-target", "1.8", "-source", "1.8", "-parameters")

The parameters flag is supposed to preserve the names, but I still get the same result as before.


Solution

  • This has nothing to do with macros and everything to do with Scala's runtime reflection system. In a nutshell, Java 8 and Scala 2.11 both wanted to be able to look up parameter names and each implemented their reflection system to do it.

    This works just fine if everything is Scala and you compile it together (duh!). Problems arise when you have a Java class that has to be compiled separately.

    Observations and Problem

    First thing to notice is that the -parameters flag is only since Java 8, which is about as old as Scala 2.11. So Scala 2.11 is probably not using this feature to lookup method names... Consider the following

    • MyInterface.java compiled with javac -parameters MyInterface.java

      public interface MyInterface {
        public int doSomething(int bar);
      }
      
    • MyTrait.scala compiled with scalac MyTrait.scala

      class MyTrait {
        def doSomething(bar: Int): Int
      }
      

    Then, we can use MethodParameterSpy to inspect the parameter information name that the Java 8 -parameter flag is supposed to give us. Running it on the Java compiled interface, we get (and here I abbreviated some of the output)

    public abstract int MyInterface.doSomething(int)

    Parameter name: bar

    but in the Scala compiled class, we only get

    public abstract int MyTrait.doSomething(int)

    Parameter name: arg0

    Yet, Scala has no problem looking up its own parameter names. That tells us that Scala is actually not relying on this Java 8 feature at all - it constructs its own runtime system for keeping track of parameter names. Then, it comes as no surprise that this doesn't work for classes from Java sources. It generates the names x$1, x$2, ... as placeholders, the same way that Java 8 reflection generates the names arg0, arg1, ... as placeholders when we inspected a compiled Scala trait. (And if we had not passed -parameters, it would have generated those names even for MyInterface.java.)

    Solution

    The best solution (that works in 2.11) I can come up with to get the parameter names of a Java class is to use Java reflection from Scala. Something like

    $ javac -parameters MyInterface.java
    $ jar -cf MyInterface.jar MyInterface.class
    $ scala -cp MyInterface.jar
    scala> :pa
    // Entering paste mode (ctrl-D to finish)
    
    import java.lang.reflect._
    
    Class.forName("MyInterface")
      .getDeclaredMethods
      .map(_.getParameters.map(_.getName))
    
    // Exiting paste mode, now interpreting.
    
    res: Array[Array[String]] = Array(Array(bar))
    

    Of course, this will only work if you have the -parameter flag (else you get arg0).

    I should probably also mention that if you don't know if your method was compiled from Java or from Scala, you can always call .isJava (For example: typeOf[MyInterface].decls.filter(_.isMethod).head.isJava) and then branch on that to either your initial solution or what I propose above.

    Future

    Mercifully, this is all a thing of the past in Scala 2.12. If I am correctly reading this ticket, that means that in 2.12 your code will work for Java classes compiled with -parameter and, my Java reflection hack will also work for Scala classes.

    All's well that ends well?