You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Scala 3编译器插件中正确覆写类方法?

问题

需要在Scala 3编译器插件中,为继承SpecialSerialize trait的类自动生成覆写的toJson方法。示例类代码如下:

trait SpecialSerialize {
  def toJson(sb: StringBuilder, c:SJConfig): Unit = {println("wrong")}
}

case class Person(name:String, age:Int) extends SpecialSerialize

预期生成的代码:

case class Person(name:String, age:Int) extends SpecialSerialize {
  override def toJson(sb: StringBuilder, c:SJConfig): Unit = ... // 代码实现
}

编写的插件阶段代码如下:

class ReflectionWorkerPhase extends PluginPhase {
  import tpd._

  val phaseName = "reflectionWorker"

  override val runsAfter = Set(Pickler.name)

  override def transformTypeDef(tree: TypeDef)(implicit ctx: Context): Tree =
    if tree.isClassDef && !tree.rhs.symbol.isStatic then  // only look at classes
      
      // 0. Get a FreshContext so we can set the tree to this tree. (for '{} later)
      implicit val fresh = ctx.fresh
      fresh.setTree(tree)
      QuotesCache.init(fresh)
      implicit val quotes:Quotes = QuotesImpl.apply()  // picks up fresh
      import quotes.reflect.*

        // 1. Set up method symbol, define parameters and return type
        val toJsonSymbol = Symbol.newMethod(
          Symbol.spliceOwner,
          "toJson",
          MethodType(
            List("sb","config"))( // parameter list
              _ => List( // types of the parameters
                TypeRepr.of[StringBuilder],
                TypeRepr.of[SJConfig],
                ), 
              _ => TypeRepr.typeConstructorOf(classOf[Unit])  // return type
            ),
            Flags.Override, // Note override here
            Symbol.noSymbol
          )

        // 2. Get our class' Symbol for ownership reassignment
        val classDef = tree.asInstanceOf[ClassDef]
        val classSymbol = classDef.symbol

        // 3. Define our method definition (DefDef) using our method symbol defined above
        val toJsonMethodDef = DefDef(
          toJsonSymbol, 
          {
            case List(List(sb: Term, config: Term)) => 
              given Quotes = toJsonSymbol.asQuotes
              Some({
                // Multiple quotes here intentional...
                // Real code will generate a list of quoted statements
                quoted.Expr.ofList(List(
                  '{ println("Hello") }, 
                  '{ println("World") }
                ))
              }.asTerm.changeOwner(toJsonSymbol))
          }
        ).changeOwner(classSymbol)

        // 4. Add toJsonMethodDef to tree and return
        val cd = ClassDef.copy(classDef)(
          name = classDef.name,
          constr = classDef.constructor,
          parents = classDef.parents,
          selfOpt = classDef.self,
          body = toJsonMethodDef +: classDef.body
        )
        cd.asInstanceOf[dotty.tools.dotc.ast.tpd.Tree] 
      else 
        tree
}

运行插件编译Person类时出现错误:

Exception in thread "sbt-bg-threads-1" java.lang.ClassFormatError: Duplicate method name "toJson" with signature "(Lscala.collection.mutable.StringBuilder;Lco.blocke.scala_reflection.SJConfig;)V" in class file com/foo/Person
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    ...

原因是后续编译阶段未将生成的方法识别为合法覆写,仍复制了trait中的toJson方法,导致JVM出现重复方法异常。需要修复让生成的方法被正确识别为覆写,避免重复生成。

解决方案

调整插件阶段的执行顺序
当前插件阶段runsAfter = Set(Pickler.name)执行时机过晚,编译器已经完成了trait方法的继承复制逻辑。需要将插件阶段提前到SuperAccessors之前执行,也就是在编译器生成trait方法的桥接/复制代码前介入。修改阶段配置:

override val runsAfter = Set(typer.name)
override val runsBefore = Set(SuperAccessors.name)

这样插件生成的覆写方法会被编译器优先识别,避免后续阶段再复制trait中的原方法。

正确关联覆写的目标符号
手动创建的toJsonSymbol需要明确关联到SpecialSerialize trait中的toJson方法符号,而非凭空生成新符号,这样编译器才能判定这是合法的覆写:

// 获取SpecialSerialize trait中的toJson方法符号
val specialSerializeType = TypeRepr.of[SpecialSerialize]
val traitToJsonSymbol = specialSerializeType
  .member(Symbol.requiredMethod("toJson"))
  .symbol
  .asMethod

// 创建覆写方法时,指定overrideTarget为traitToJsonSymbol
val toJsonSymbol = Symbol.newMethod(
  classSymbol, // 直接以当前类符号作为方法所属者
  "toJson",
  traitToJsonSymbol.signature, // 复用原方法签名,避免手动定义出错
  Flags.Override,
  traitToJsonSymbol // 明确标记覆写的目标方法
)

通过绑定到trait的方法符号,编译器会自动识别该方法为合法覆写,不会再复制原trait方法。

添加继承判定逻辑
在处理类之前,先判断类是否真正继承了SpecialSerialize,避免对无关类生成方法:

val classType = classSymbol.typeRef
if classType <:< TypeRepr.of[SpecialSerialize] then
  // 执行生成覆写方法的逻辑
else
  tree

修复后的完整插件阶段代码示例

class ReflectionWorkerPhase extends PluginPhase {
  import tpd._

  val phaseName = "reflectionWorker"

  // 调整阶段执行顺序
  override val runsAfter = Set(typer.name)
  override val runsBefore = Set(SuperAccessors.name)

  override def transformTypeDef(tree: TypeDef)(implicit ctx: Context): Tree =
    tree match {
      case classDef: ClassDef if !classDef.symbol.isStatic =>
        implicit val fresh = ctx.fresh
        fresh.setTree(tree)
        QuotesCache.init(fresh)
        implicit val quotes: Quotes = QuotesImpl.apply()
        import quotes.reflect.*

        val classSymbol = classDef.symbol
        val classType = classSymbol.typeRef
        val specialSerializeType = TypeRepr.of[SpecialSerialize]

        // 仅处理继承了SpecialSerialize的类
        if classType <:< specialSerializeType then
          // 获取trait中的toJson方法符号
          val traitToJsonSymbol = specialSerializeType
            .member(Symbol.requiredMethod("toJson"))
            .symbol
            .asMethod

          // 创建覆写方法的符号,关联到trait方法
          val toJsonSymbol = Symbol.newMethod(
            classSymbol,
            "toJson",
            traitToJsonSymbol.signature,
            Flags.Override,
            traitToJsonSymbol
          )

          // 生成方法体
          val toJsonMethodDef = DefDef(
            toJsonSymbol,
            {
              case List(List(sb: Term, config: Term)) =>
                Some({
                  quoted.Expr.ofList(List(
                    '{ println("Hello") },
                    '{ println("World") }
                  ))
                }.asTerm.changeOwner(toJsonSymbol))
            }
          )

          // 将新方法添加到类体中
          val updatedClassDef = ClassDef.copy(classDef)(
            body = toJsonMethodDef +: classDef.body
          )
          updatedClassDef.asInstanceOf[Tree]
        else
          classDef
      case _ => tree
    }
}

内容的提问来源于stack exchange,提问作者Greg

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 04:02:00