Scala macro can't find my java class

309 views Asked by At

I'm creating a scala macro to automatically generate case classes from POJOs (in order to make working with avro a little bit nicer).

Everything "works" except that the compiler - during macro expansion only - chokes on my java class. If I comment out the call to the macro, everything compiles fine.

My question is: how do I generate code in the macro such that the compiler resolves my own java classes?

Example error message:

[info] Compiling 1 Scala source to /Users/marcin/development/repos/problemdemo/target/scala-2.11/classes...
typeArgs: scala.collection.immutable.List()
fieldType:class scala.reflect.internal.Trees$Literal|"java.lang.Object"
class scala.reflect.internal.Names$TypeName_S
DEBUG:java.util.ArrayList|java.lang.Object
typeArgs: scala.collection.immutable.List(com.squarefoot.Pojo)
fieldType:class scala.reflect.internal.Trees$Literal|"java.util.ArrayList"
class scala.reflect.internal.Names$TypeName_R
DEBUG:java.util.ArrayList|java.util.ArrayList
typeArgs: scala.collection.immutable.List()
fieldType:class scala.reflect.internal.Trees$Literal|"scala.Int"
class scala.reflect.internal.Names$TypeName_S
DEBUG:java.util.ArrayList|scala.Int
Expr[Any](case class Demo extends scala.Product with scala.Serializable {
  <caseaccessor> <paramaccessor> val qux: "java.lang.Object" = _;
  <caseaccessor> <paramaccessor> val baz: java.util.ArrayList[com.squarefoot.Pojo] = _;
  <caseaccessor> <paramaccessor> val bar: "scala.Int" = _;
  def <init>(qux: "java.lang.Object", baz: java.util.ArrayList[com.squarefoot.Pojo], bar: "scala.Int") = {
    super.<init>();
    ()
  }
})
[error] /Users/marcin/development/repos/problemdemo/src/main/scala/com/squarefoot/converters/problemdemo.scala:10: not found: type com.squarefoot.Pojo
[error] @Caseify(classOf[com.squarefoot.Pojo])
[error]  ^
[error] one error found

problemdemo/avroschemas/src/main/java/com/squarefoot/Pojo.java:

package com.squarefoot;

public class Pojo {
    public int bar;
    public java.util.ArrayList<Pojo> baz;
    public java.lang.Object qux;
}

problemdemo/src/main/scala/com/squarefoot/converters/problemdemo.scala:

package com.squarefoot.converters

import com.squarefoot.Pojo

class Foomin {
  val foobar: java.util.List[Int]
  val foobaz: com.squarefoot.Pojo
}

@Caseify(classOf[com.squarefoot.Pojo])
case class Demo()

problemdemo/macros/src/main/scala/com/squarefoot/converters/Caseify.scala:

package com.squarefoot.converters

import scala.language.experimental.macros
import scala.annotation.StaticAnnotation
import scala.reflect.macros.Context

/** 
  *  Generate case class from POJO
  *  ex:
  *  @Caseify(classOf[com.squarefoot.incominglisting])
  *  case class Incominglisting()

  * NOTE that the type parameter to classOf must be provided as a fully
  * qualified name, otherwise the macro code here won't be able to find it.
  * 
  * Generates a case class with the same members as the public, non-static
  * members of the pojo
  * 
  * Note that you must have all types used in the POJO in scope where the macro
  * is invoked
  */

class Caseify[T](source: Class[T]) extends StaticAnnotation {
  def macroTransform(annottees: Any*) = macro CaseifyMacro.expand_impl[T]
}

object CaseifyMacro {
  /** generate case class from POJO */
  def expand_impl[T](c: Context)(annottees: c.Expr[Any]*) = {
    import c.universe._

    // macro expand the macro expression itself to extract param
    val source: Class[T] = c.prefix.tree match {
      case q"new Caseify($param)" => c.eval[Class[T]](c.Expr(param))
    }

    val rm = scala.reflect.runtime.currentMirror


    val vars =
      rm.classSymbol(source).toType.members.map(_.asTerm).
        filter(_.isVar).filter(_.isPublic)

    val fields = vars.map({f=>
      val fieldName = TermName(f.name.toString)

      val fieldType = tq"${f.typeSignature.typeConstructor.typeSymbol.fullName}"
      val rawTypeArgs = f.typeSignature.typeArgs.map(a=>TypeName(a.toString))
      val typeArgs = tq"${rawTypeArgs}"
      println("typeArgs: "+typeArgs.toString)
      println("fieldType:"+fieldType.getClass.toString+"|"+fieldType.toString)
      println(f.typeSignature.typeSymbol.asType.name.getClass.toString)
      val arraylistname = tq"java.util.ArrayList"
      println("DEBUG:"+tq"${arraylistname}".toString+"|"+f.typeSignature.typeConstructor.typeSymbol.fullName)
      q"val $fieldName: $fieldType"
      if(rawTypeArgs.nonEmpty) {
        val appliedFieldType = tq"${arraylistname}[..$rawTypeArgs]"
        q"val $fieldName: $appliedFieldType"
      }
      else
         q"val $fieldName: $fieldType"
    })

    annottees.map(_.tree) match {
      case List(q"case class $newname()") => {
        val q = c.Expr[Any](
        // Add your own logic here, possibly using arguments on the annotation.
          q"""
          case class $newname(..$fields)
        """)
        println(q.toString)
        q
      }
      // Add validation and error handling here.
    }
  }
}

Sbt files:

problemdemo/build.sbt

name := "data-importer"


addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

scalaVersion := "2.11.8"
val avroVersion = "1.8.1"

lazy val root =
        project.in( file(".") )
          .aggregate(avroschemas, macros).dependsOn(macros, avroschemas)

lazy val macros = project.dependsOn(avroschemas)

lazy val avroschemas = project



libraryDependencies ++= Seq(
  "org.scala-lang" % "scala-reflect" % scalaVersion.value
)


// better error reporting
scalacOptions in Test ++= Seq("-Yrangepos")

run in Compile := Defaults.runTask(fullClasspath in Compile, mainClass in (Compile, run), runner in (Compile, run))

problemdemo/macros/build.sbt

name := "data-importer-macros"


addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

organization := "com.squarefoot"
scalaVersion := "2.11.3"



libraryDependencies ++= Seq(
  "org.scala-lang" % "scala-reflect" % scalaVersion.value
)

scalacOptions in Test ++= Seq("-Yrangepos")

problemdemo/avroschemas/build.sbt

name := "data-importer-avroschemas"

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

organization := "com.squarefoot"
scalaVersion := "2.11.8"


// better error reporting
scalacOptions in Test ++= Seq("-Yrangepos")

run in Compile := Defaults.runTask(fullClasspath in Compile, mainClass in (Compile, run), runner in (Compile, run))
0

There are 0 answers