For example as the following code:
object Test extends App
{
trait Class
{
val f1: Int
}
val c = new Class {
val f1: Int = 1
val f2: String = "Class"
}
println(c.f1)
println(c.f2)
}
I look into the bytecode with a decompiler, and notice that the compile generate a java interface 'Test.Class' as pseudo code:
trait Class
{
val f1: Int
}
and a class 'Test$$anon$1' implemeting 'Test.Class', pseudo code as:
class Test$$anon$1 extends Class
{
val f1: Int = 1
val f2: String = "Class"
}
and then the compiler initiaize the variable 'c' as:
c = new Test$$anon$1()
then calls the member 'f1' as normal invocation:
println(c.f1)
but it calls 'f2' using reflection:
println(reflMethod(c, f2))
Here, since the definition of the anonymous class 'Test$$anon$1' is visible in the same scope, is it possible to use macro to change the generated code to invoke 'f2' as normal field avoiding reflection?
I just want to change the invocation code in the same scope, not want to change the reflection code across scopes e.g. structual-typing instance as argument in function call. So I think it is possible in theory. But I am not familiar with scala macro, suggestions and code examples are appreciated. Thanks!
Macros (more precisely, macro annotations because def macros are irrelevant to this task) are not enough. You want to rewrite not class (trait, object) or its parameter or member but local expressions. You can do this either with compiler plugin (see also) at compile time or with Scalameta code generation before compile time.
If you choose Scalameta then actually you want to rewrite your expressions semantically rather than syntactically because you want to go from local expression new Class...
to the definition trait Class...
and check whether there are proper members there. So you need Scalameta + SemanticDB. More convenient is to use Scalameta + SemanticDB with Scalafix (see also section for users).
You can create your own rewriting rule. Then you can use it either for rewriting your code in-place or for code generation (see below).
rules/src/main/scala/MyRule.scala
import scalafix.v1._
import scala.meta._
class MyRule extends SemanticRule("MyRule") {
override def isRewrite: Boolean = true
override def description: String = "My Rule"
override def fix(implicit doc: SemanticDocument): Patch = {
doc.tree.collect {
case tree @ q"new { ..$stats } with ..$inits { $self => ..$stats1 }" =>
val symbols = stats1.collect {
case q"..$mods val ..${List(p"$name")}: $tpeopt = $expr" =>
name.syntax
}
val symbols1 = inits.headOption.flatMap(_.symbol.info).flatMap(_.signature match {
case ClassSignature(type_parameters, parents, self, declarations) =>
Some(declarations.map(_.symbol.displayName))
case _ => None
})
symbols1 match {
case None => Patch.empty
case Some(symbols1) if symbols.forall(symbols1.contains) => Patch.empty
case _ =>
val anon = Type.fresh("anon$meta$")
val tree1 =
q"""
class $anon extends ${template"{ ..$stats } with ..$inits { $self => ..$stats1 }"}
new ${init"$anon()"}
"""
Patch.replaceTree(tree, tree1.syntax)
}
}.asPatch
}
}
in/src/main/scala/Test.scala
object Test extends App
{
trait Class
{
val f1: Int
}
val c = new Class {
val f1: Int = 1
val f2: String = "Class"
}
println(c.f1)
println(c.f2)
}
out/target/scala-2.13/src_managed/main/scala/Test.scala (after sbt out/compile
)
object Test extends App
{
trait Class
{
val f1: Int
}
val c = {
class anon$meta$2 extends Class {
val f1: Int = 1
val f2: String = "Class"
}
new anon$meta$2()
}
println(c.f1)
println(c.f2)
}
build.sbt
name := "scalafix-codegen-demo"
inThisBuild(
List(
scalaVersion := "2.13.2",
addCompilerPlugin(scalafixSemanticdb),
scalacOptions ++= List(
"-Yrangepos"
)
)
)
lazy val rules = project
.settings(
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % "0.9.16"
)
lazy val in = project
lazy val out = project
.settings(
sourceGenerators.in(Compile) += Def.taskDyn {
val root = baseDirectory.in(ThisBuild).value.toURI.toString
val from = sourceDirectory.in(in, Compile).value
val to = sourceManaged.in(Compile).value
val outFrom = from.toURI.toString.stripSuffix("/").stripPrefix(root)
val outTo = to.toURI.toString.stripSuffix("/").stripPrefix(root)
Def.task {
scalafix
.in(in, Compile)
// .toTask(s" ProcedureSyntax --out-from=$outFrom --out-to=$outTo")
.toTask(s" --rules=file:rules/src/main/scala/MyRule.scala --out-from=$outFrom --out-to=$outTo")
.value
(to ** "*.scala").get
}
}.taskValue
)
project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.16")
Other examples:
https://github.com/olafurpg/scalafix-codegen
https://github.com/DmytroMitin/scalafix-codegen
https://github.com/DmytroMitin/scalameta-demo