Swift 宏在编译期间读取语法并生成新语法。它适合消除可机械推导的重复代码,却也容易把大量领域规则塞进编译器插件入口,导致测试只能比较整段展开文本。更稳健的设计是让宏入口保持薄:解析声明、调用纯规则、把结果转换成 SwiftSyntax 节点并报告诊断。命名、成员选择和冲突判断等规则则放在普通 Swift 模块中。

先判断问题是否真的需要宏

协议扩展、泛型、property wrapper 或普通代码生成器能解决的问题,通常比宏更容易阅读与调试。宏的优势在于它能看到源码结构,并在调用位置产生编译期诊断。若生成结果取决于网络、当前时间、文件系统或构建机器状态,就会破坏可重复构建,也让缓存与审查变得困难。

使用者应能从宏声明和文档预测它会新增什么。公开宏需要说明角色、允许的输入、生成成员、名称规则和错误信息。若必须展开代码才能发现宏偷偷加入网络请求或改变业务语义,接口就太深了。

把语法适配层压到最薄

假设 @MemberwiseInit 根据存储属性生成初始化器。插件入口可以把 SwiftSyntax 节点转换成一个简单描述,再交给普通规则:

struct StoredProperty: Equatable {
    let name: String
    let type: String
    let hasDefault: Bool
}

struct InitializerPlan: Equatable {
    let parameters: [StoredProperty]
    let accessLevel: String?
}

func makePlan(
    properties: [StoredProperty],
    existingInitializer: Bool,
    accessLevel: String?
) throws -> InitializerPlan {
    guard !existingInitializer else { throw MacroRuleError.conflict }
    return InitializerPlan(
        parameters: properties.filter { !$0.hasDefault },
        accessLevel: accessLevel
    )
}

这个函数不导入编译器插件接口,能用普通单元测试覆盖空类型、默认值、访问级别和冲突。真正的 expansion 只负责识别存储属性、调用 makePlan、建立语法节点。这样 SwiftSyntax API 变化只影响适配层,规则测试不会全部重写。

不要为了测试方便把语法过早压成无结构字符串。类型文本可以在某些规划层使用,但生成阶段应构造语法节点并让格式化器处理空格与换行。字符串拼接容易产生转义、注释、泛型和 attributes 的边界错误。

诊断属于宏的公开接口

错误不只是 throw。宏应把诊断附着到最有帮助的语法节点,给出稳定标识、清楚消息,并在安全时提供 fix-it。对不支持的输入要拒绝,不要猜测后生成几乎正确的代码。诊断文本也需要测试,因为使用者会依赖它理解构建失败。

区分“规则不允许”和“插件自身故障”。前者给出面向使用者的说明;后者应保留足够上下文供维护者定位,但不能崩溃编译器进程。若发现未知语法形态,保守诊断通常比强制转换更可靠。

使用三层测试

第一层测试纯规划函数,覆盖组合边界,速度最快。第二层给宏 expansion 输入短源码,比较格式化后的展开与诊断,验证语法适配。第三层在样例 target 中编译并运行生成代码,证明访问控制、类型检查和运行语义正确。不是每个输入都需要昂贵的集成测试,但每种宏角色至少应有成功与失败样例。

快照更新必须人工审查。生成结果改变可能是预期格式变化,也可能是宏扩大了公开 API。不要用“一键接受全部快照”代替判断。还应固定工具链与兼容的 SwiftSyntax 版本,按支持的 Swift 版本矩阵构建。

控制生成表面

生成的名称应可预测并避免与使用者声明冲突。宏能否访问 private 成员、生成声明的访问级别、泛型约束和属性传播都要明确。优先生成少量、可阅读的代码;如果一次展开产生数百行或递归应用多个宏,编译时间与诊断质量都会恶化。

版本升级时,把规则变化写进变更记录,并提供迁移路径。宏展开属于源代码兼容性的一部分:即使宏调用文本没变,新增成员也可能触发重载歧义或协议一致性变化。

结论

可测试的 Swift 宏不是靠更大的快照,而是靠清楚分层。普通 Swift 规则决定“应该生成什么”,SwiftSyntax 适配层决定“如何读取和构造语法”,插件边界负责诊断。把副作用排除在展开之外,控制生成表面,再用规则、展开和集成三层测试,宏才能成为可维护的编译期工具,而不是难以审查的代码魔法。