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

如何对Scala 3实现的宏进行单元测试?

问题根因
  • 传入测试方法的expected参数被Scala编译器自动做了内联代理处理:你看到的Ident(expected$proxy1)是编译器生成的代理引用,实际的匿名类定义被抬升到了外层作用域,直接比对generated和expected的原始AST相当于拿宏生成的表达式和一个代理引用比对,自然不相等。
  • 即使解包拿到了两个表达式的原始AST,结构完全一致的匿名类也会被编译器分配不同的合成类名,默认的Expr.equals、Term.equals以及show方法都会携带这类无关的合成信息,无法正确匹配结构相同的AST。
可行解决方案

第一步:实现AST解包工具方法

先递归剥离所有内联包装,解析代理引用指向的实际定义:

import quotes.reflect._

def unwrapTerm(term: Term)(using Quotes): Term = term match {
  case Inlined(_, _, inner) => unwrapTerm(inner)
  case ident@Ident(_) if ident.symbol.exists => 
    ident.symbol.tree match {
      case ValDef(_, _, Some(rhs)) => unwrapTerm(rhs)
      case _ => term
    }
  case _ => term
}

第二步:实现结构化AST比较逻辑

忽略合成类名、源码位置等无关差异,只比对语法结构和语义内容:

def structEqual(a: Term, b: Term)(using Quotes): Boolean = (a, b) match {
  // 匹配匿名类定义,忽略合成类名
  case (ClassDef(_, parents1, _, body1), ClassDef(_, parents2, _, body2)) =>
    parents1.map(_.tpe) == parents2.map(_.tpe) && 
    body1.zip(body2).forall { case (m1, m2) => structEqual(m1.asInstanceOf[Term], m2.asInstanceOf[Term]) }
  // 匹配方法定义
  case (DefDef(name1, params1, tpt1, rhs1), DefDef(name2, params2, tpt2, rhs2)) =>
    name1 == name2 && params1 == params2 && tpt1.tpe == tpt2.tpe && 
    rhs1.zip(rhs2).forall { case (r1, r2) => structEqual(r1, r2) }
  // 匹配方法调用
  case (Apply(fun1, args1), Apply(fun2, args2)) =>
    structEqual(fun1, fun2) && args1.zip(args2).forall { case (a1, a2) => structEqual(a1, a2) }
  // 匹配字面量
  case (Literal(c1), Literal(c2)) => c1 == c2
  // 匹配标识符
  case (Ident(name1), Ident(name2)) => name1 == name2
  // 其他节点类型可以按需求补充匹配规则
  case _ => a == b
}

第三步:修改测试宏的实现

def testMacroExpr[A](generated: Expr[A], expected: Expr[A])(using Quotes): Expr[A] = {
  val unwrappedGenerated = unwrapTerm(generated.asTerm)
  val unwrappedExpected = unwrapTerm(expected.asTerm)

  if (!structEqual(unwrappedGenerated, unwrappedExpected)) {
    report.throwError(s"AST mismatch: \nGenerated: ${unwrappedGenerated.show}\nExpected: ${unwrappedExpected.show}")
  }
  generated
}

替代简化方案

如果不需要严格校验AST结构,只想验证宏生成代码的行为符合预期,可以直接在运行时调用接口方法验证输出,这种实现更简单也更稳定:

@main def hello() = {
  val output = new java.io.ByteArrayOutputStream()
  Console.withOut(output) {
    myMacro().speak()
  }
  assert(output.toString.trim == "Hello World")
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 23:36:04