如何对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
相关产品推荐
相关产品推荐

