Можно ли с помощью макроса изменить сгенерированный код вызова экземпляра структурной типизации?
Например, как следующий код:
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)
}
Я просматриваю байт-код с помощью декомпилятора и замечаю, что компиляция генерирует java-интерфейс Test.Class в виде псевдокода:
trait Class
{
val f1: Int
}
и класс Test$$anon$1, реализующий псевдокод Test.Class как:
class Test$$anon$1 extends Class
{
val f1: Int = 1
val f2: String = "Class"
}
а затем компилятор инициализирует переменную 'c' как:
c = new Test$$anon$1()
затем вызывает член 'f1' как обычный вызов:
println(c.f1)
но он вызывает f2 с помощью отражения:
println(reflMethod(c, f2))
Здесь, поскольку определение анонимного класса Test$$anon$1 видно в той же области, можно ли использовать макрос для изменения сгенерированного кода, чтобы вызвать "f2" как обычное поле, избегая отражения?
Я просто хочу изменить код вызова в той же области, не хочу изменять код отражения в разных областях, например экземпляр структурной типизации в качестве аргумента при вызове функции. Так что я думаю, что теоретически это возможно. Но я не знаком с макросом scala, предложения и примеры кода приветствуются. Благодарность!
1 ответ
Макросов (точнее, аннотаций к макросам, потому что макросы def не имеют отношения к этой задаче) недостаточно. Вы хотите переписать не класс (признак, объект) или его параметр или член, а локальные выражения. Вы можете сделать это либо с помощью подключаемого модуля компилятора (см. Также) во время компиляции, либо с помощью генерации кода Scalameta перед компиляцией.
Если вы выбираете Scalameta, то на самом деле вы хотите переписать свои выражения семантически, а не синтаксически, потому что вы хотите перейти от локального выражения new Class...
к определению trait Class...
и проверьте, есть ли там нужные участники. Итак, вам нужен Scalameta + SemanticDB. Более удобно использовать Scalameta + SemanticDB со Scalafix (см. Также раздел для пользователей).
Вы можете создать собственное правило перезаписи. Затем вы можете использовать его либо для переписывания кода на месте, либо для генерации кода (см. Ниже).
правила /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
}
}
в / 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 (после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
)
проект / plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.16")
Другие примеры:
https://github.com/olafurpg/scalafix-codegen
https://github.com/DmytroMitin/scalafix-codegen
https://github.com/DmytroMitin/scalameta-demo
Аннотации макроса для переопределения toString функции Scala