Можно ли с помощью макроса изменить сгенерированный код вызова экземпляра структурной типизации?

Например, как следующий код:

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

Условная компиляция Scala

Аннотации макроса для переопределения toString функции Scala

Как объединить несколько операций импорта в scala?

Другие вопросы по тегам