Go 的 `golang.org/x/tools/go/analysis` 文档定义了一个模块化静态分析接口:分析器一次检查一个 Go 软件包、报告诊断结果,并可通过统一的应用程序编程接口交换结果。该框架的设计目标,是让同一个分析器能够被集成到 `vet` 等命令行工具、编辑器、集成开发环境、构建系统、测试框架、代码审查工具以及更大规模的批处理流水线中。

Go 编程语言官方标识
Go 标识点明了本文所讨论的分析 API 所属的编程语言。Go project / pkg.go.dev

开篇介绍的是 Go 的包分析接口,因此这枚官方标识能帮助读者确认本文讨论的语言。来源:pkg.go.dev。

分析器由一个 `Analyzer` 值表示,其中描述了分析器的名称、文档、标志、依赖项、Fact 类型和执行函数。驱动程序会为单个软件包接收一个 `Pass`,提供语法与类型信息,先运行所需的分析器,并通过 `Pass.ResultOf` 公开其结果。随后,分析器通过该 Pass 报告带有源代码位置信息的诊断结果。这种分离使工具作者能够自行选择运行哪些检查,而无需针对某个特定接口重写每个检查器。

该框架实现可扩展性的核心理念是 `Fact`。Fact 是一种中间结果,它本身未必对开发者有直接用途,却可以帮助其他分析得出结论。文档以 `printf` 检查器为例:它能够识别某个函数是否为 `printf` 包装器、导出相应 Fact,并让后续的软件包分析对经该包装器转调用的调用实施更严格的检查。

Fact 可以与某个具名对象关联,也可以与整个软件包关联。驱动程序负责为依赖项生成 Fact,并跨软件包边界传播这些 Fact,包括在不同进程之间进行传播。由于 Fact 可能存储在构建输出中,因此该应用程序编程接口要求采用确定性的 gob 序列化;文档还建议分析器作者让 Fact 保持无状态,以免基于内容寻址的构建缓存出现不必要的未命中。

该设计有意不将诊断严重性纳入核心 `Diagnostic` 类型。文档指出,用户和工具对严重性的偏好各不相同,因此驱动程序应根据生成诊断结果的分析器及其可选类别,提供过滤和优先级排序功能。这一选择使分析器接口保持相对精简,但也意味着检查器的输出不会自动成为按优先级排列的缺陷列表。

该框架也存在实际边界。文档所述的某些驱动程序实现,包括基于 Bazel 和 Blaze 的驱动程序,目前不会分析 Go 标准库中的软件包。因此,`printf` 检查器为 `log.Printf` 内置了一个 Fact,以便在标准库软件包未经分析时,仍能检查相关调用。这是一项明确设计的变通方案,并不能证明所有驱动程序都提供完全相同的软件包覆盖范围。

文档还区分了检查带类型语法树的分析器与读取原始源代码或非 Go 文件的分析器。`asmdecl` 和 `buildtag` 等示例表明,该框架能够处理普通类型信息之外的内容,但分析器作者仍须自行定义检查器检查哪些内容,以及如何报告发现结果。该应用程序编程接口提供了一份契约,并不保证能够可靠地推断出程序的每一种可能属性。

在测试方面,`analysistest` 子包会针对夹具软件包运行分析器,并通过 `// want` 注释检查预期的诊断结果和 Fact。对于独立使用场景,`singlechecker` 和 `multichecker` 分别为单个分析器或一组分析器提供入口。这些设施使该框架能够用于单个内部工具之外的更多场景,但并未免除验证分析器假设及其依赖图的需要。

该框架更广泛的意义在于其可组合性。团队只需将分析器加入列表,就能把检查器添加到现有驱动程序中;工具构建者则可以在不同环境中复用分析器。现有证据能够确认文档所述的接口、示例以及驱动程序应承担的责任,但不能证明其采用率、性能基准,也不能证明 Go 工具生态中的每个工具都以相同方式实现了该框架。