You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Scala编译器插件/宏递归包装方法调用的编译错误排查与解决

我来给你分享两个靠谱的解决方案,不管是用宏注解还是修正后的编译器插件,都能搞定自动包装ExternalApi.createX调用的需求,还能处理嵌套、代码块里的调用,同时解决你之前遇到的编译器内部错误问题:

方案一:宏注解(推荐,更可控)

宏注解可以精准扫描指定范围内的代码树,替换符合条件的调用,而且不需要深入Scala编译器内部的复杂逻辑,不容易触发内部错误。

第一步:定义带TypeTag的process方法

先把MyApi.process改成支持自动推导TypeTag的版本,方便从T的类型生成元数据:

import scala.reflect.runtime.universe._

object MyApi {
  // 自动推导T的TypeTag,用于生成元数据
  def process[T](tc: TypeConstructor[T])(implicit tt: TypeTag[T]): Processed[T] = {
    val metadata = generateMetadata(tt.tpe)
    Processed(tc, metadata)
  }

  // 这里实现你的元数据生成逻辑,比如提取类型名称、泛型参数等
  private def generateMetadata(tpe: Type): Metadata = {
    Metadata(s"Type: ${tpe.toString}, Args: ${tpe.typeArgs.map(_.toString).mkString(",")}")
  }
}

// 辅助类型定义,和你的原有结构对齐
case class Metadata(value: String)
case class TypeConstructor[T](value: T)
case class Processed[T](tc: TypeConstructor[T], metadata: Metadata)

第二步:实现宏注解

写一个宏注解,用来自动扫描并替换所有ExternalApi.createX的调用:

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

// 编译时提示必须启用宏注解
@compileTimeOnly("请启用Scala宏注解插件以使用此功能")
class autoProcess extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro AutoProcessMacro.impl
}

object AutoProcessMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    // 自定义遍历器,递归处理所有子树(支持嵌套、代码块)
    val transformer = new Transformer {
      override def transform(tree: Tree): Tree = tree match {
        // 匹配所有ExternalApi.create开头的方法调用
        case Apply(Select(Ident(TermName("ExternalApi")), TermName(methodName)), args)
          if methodName.startsWith("create") =>
          // 生成MyApi.process的调用,自动注入TypeTag隐式参数
          q"MyApi.process($tree)"
        // 递归处理其他所有节点
        case _ => super.transform(tree)
      }
    }

    // 处理被注解的元素(类、方法、代码块都可以)
    annottees.map(transformer.transform) match {
      case Nil => c.abort(c.enclosingPosition, "没有可处理的代码元素")
      case single :: Nil => single
      case multiple => q"..$multiple"
    }
  }
}

第三步:使用宏注解

只需要给需要自动包装的代码范围加上@autoProcess注解即可:

// 给整个对象加注解,对象内所有createX调用都会被自动包装
@autoProcess
object BusinessLogic {
  // 会被转为:MyApi.process(ExternalApi.createUser())
  val userTc = ExternalApi.createUser()

  // 嵌套调用也会被递归处理:MyApi.process(ExternalApi.createOrder(MyApi.process(ExternalApi.createProduct())))
  val orderTc = ExternalApi.createOrder(ExternalApi.createProduct())

  // 代码块内的调用同样会被处理
  def createComplex(): TypeConstructor[Complex] = {
    val temp = ExternalApi.createTemp()
    ExternalApi.createComplex(temp, "param")
  }
}

宏注解的优势

  • 可控性强:可以选择性地给需要处理的代码块、类、方法加注解,不需要全局生效
  • 逻辑简单:不需要深入Scala编译器内部的阶段细节,降低出错概率
  • 调试友好:宏的错误提示相对编译器插件更清晰

方案二:修正后的编译器插件(全局生效)

如果你需要全局自动包装所有ExternalApi.createX调用,不需要修改业务代码,那可以用编译器插件,但要避开你之前踩的坑:不要在typer阶段修改未完成类型检查的树,应该把变换阶段放在typer之后,确保所有类型信息都已解析完成。

编译器插件实现

import scala.tools.nsc
import scala.tools.nsc.Global
import scala.tools.nsc.plugins.Plugin
import scala.tools.nsc.plugins.PluginComponent
import scala.tools.nsc.transform.Transform

class AutoProcessPlugin(val global: Global) extends Plugin {
  import global._

  val name = "auto-process"
  val description = "自动将ExternalApi.createX调用包装为MyApi.process"
  val components = List[PluginComponent](AutoProcessComponent)

  private object AutoProcessComponent extends PluginComponent with Transform {
    val global: AutoProcessPlugin.this.global.type = AutoProcessPlugin.this.global
    val phaseName = "auto-process"
    // 关键:把阶段放在typer之后,确保所有类型都已检查完成
    override val runsAfter = List("typer")
    override val runsBefore = List("pickler")

    def newTransformer(unit: CompilationUnit): Transformer = new AutoProcessTransformer(unit)

    class AutoProcessTransformer(unit: CompilationUnit) extends Transformer {
      override def transform(tree: Tree): Tree = tree match {
        // 匹配ExternalApi.create开头的调用,此时tree.tpe已经完全解析
        case Apply(Select(Ident(TermName("ExternalApi")), TermName(methodName)), args)
          if methodName.startsWith("create") =>
          // 从已类型化的树中提取TypeConstructor[T]的泛型参数T
          val tType = tree.tpe.typeArgs.head
          // 生成TypeTag的隐式参数树
          val typeTagTree = Apply(
            TypeApply(
              Select(Ident(TermName("scala")), TermName("reflect.runtime.universe.typeTag")),
              List(TypeTree(tType))
            ),
            Nil
          )
          // 生成MyApi.process的调用
          Apply(
            TypeApply(
              Select(Ident(TermName("MyApi")), TermName("process")),
              List(TypeTree(tType))
            ),
            List(tree, typeTagTree)
          )
        // 递归处理所有子树
        case _ => super.transform(tree)
      }
    }
  }
}

插件的关键修正点

  1. 阶段位置:把变换阶段放在typer之后,避免操作未完成类型检查的TypeTree,解决你之前遇到的genLoad异常
  2. 类型获取:直接从已类型化的tree.tpe中提取泛型参数,而不是手动构造未初始化的TypeTree
  3. 隐式参数生成:正确生成TypeTag的隐式参数,确保MyApi.process能获取到正确的类型信息

方案对比

方案适用场景优点缺点
宏注解局部代码需要自动包装可控性强、调试友好需要修改业务代码加注解
编译器插件全局所有代码需要自动包装无侵入、无需修改业务代码调试难度高、依赖编译器版本

注意事项

  • 确保ExternalApi.createX的返回类型确实是TypeConstructor[T],这样才能正确提取T的类型生成元数据
  • 如果createX有重载方法,需要在匹配逻辑中添加参数类型判断,避免误匹配
  • 宏注解需要在sbt中启用宏插件:addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)(根据你的Scala版本调整)

内容的提问来源于stack exchange,提问作者mikołak

火山引擎 最新活动