Go 分析框架:Go 团队开发的模块化静态分析

Hacker News Top 工具

摘要

Go 分析框架为 Go 代码的静态分析提供模块化接口,支持可复用的检查器,可应用于 vet、IDE 和构建系统等多种工具。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/07/27 01:42

# analysis 包 - golang.org/x/tools/go/analysis - Go Packages 来源:https://pkg.go.dev/golang.org/x/tools/go/analysis 包 analysis 定义了模块化静态分析与分析驱动程序之间的接口。 #### 背景¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Background) 静态分析是一个函数,它检查 Go 代码包并报告一组诊断(通常是代码中的错误),也可能产生其他结果,例如建议的重构或其他事实。报告错误的分析通常被称为“检查器”。例如,printf 检查器报告 fmt.Printf 格式字符串中的错误。 “模块化”分析是一种每次检查一个包的分析,但它可以保存来自低层级包的信息,并在检查高层级包时使用,类似于工具链中的单独编译。printf 检查器是模块化的:当它发现诸如 log.Fatalf 这样的函数委托给 fmt.Printf 时,它会记录这一事实,并检查对该函数的调用,包括从其他包发出的调用。 通过实现一个通用接口,来自不同来源的检查器可以很容易地被选择、集成和重用,涵盖广泛的驱动程序,包括命令行工具(如 vet)、文本编辑器和 IDE、构建和测试系统(如 go build、Bazel 或 Buck)、测试框架、代码审查工具、代码库索引器(如 SourceGraph)、文档查看器(如 godoc)、大型代码库的批处理管道等。 #### Analyzer¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Analyzer) API 中的主要类型是Analyzer (https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer)。Analyzer 静态描述了一个分析函数:其名称、文档、标志、与其他分析器的关系,以及当然,其逻辑。 要定义分析,用户声明一个类型为 Analyzer 的(逻辑上常量)变量。以下是 go/analysis/passes/ 子目录中一个分析器的典型示例: `` package unusedresult var Analyzer = &analysis.Analyzer{ Name: "unusedresult", Doc: "检查对某些函数的调用结果是否未被使用", Run: run, ... } func run(pass *analysis.Pass) (interface{}, error) { ... } `` 分析驱动程序是一个程序(如 vet),它运行一组分析并打印它们报告的诊断。驱动程序程序必须导入其所需的 Analyzer 列表。通常每个 Analyzer 位于单独的包中。要向现有驱动程序添加新的 Analyzer,只需在列表中添加另一个项: `` import ( "unusedresult"; "nilness"; "printf" ) var analyses = []*analysis.Analyzer{ unusedresult.Analyzer, nilness.Analyzer, printf.Analyzer, } `` 驱动程序可以使用名称、标志和文档来提供描述其执行的分析的在线帮助。文档注释包含简短的摘要行,后面可选地跟有解释段落。 除了上面展示的字段,Analyzer (https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer) 类型还有更多字段: `` type Analyzer struct { Name string Doc string Flags flag.FlagSet Run func(*Pass) (interface{}, error) RunDespiteErrors bool ResultType reflect.Type Requires []*Analyzer FactTypes []Fact } `` Flags 字段声明了一组命名的(全局)标志变量,用于控制分析行为。与 vet 不同,分析标志不直接在命令行 FlagSet 中声明;由驱动程序负责设置标志变量。对于单个分析 a,其驱动程序可能会在命令行上直接将其标志 f 公开为 -f,而对于多个分析,驱动程序可能会在标志名称前加上分析名称(-a.f)以避免歧义。IDE 可能通过图形界面公开标志,而批处理管道可能通过配置文件配置它们。请参见“findcall”分析器以了解标志的使用示例。 RunDespiteErrors 标志指示分析是否能够处理类型错误的代码。如果不能,如果存在解析或类型错误,驱动程序将跳过分析。可选的 ResultType 字段指定此分析计算并可供其他分析使用的结果值的类型。Requires 字段指定此分析依赖的分析列表,以及它可能访问的结果,并约束驱动程序运行分析的顺序。FactTypes 字段在模块化部分讨论。analysis 包提供了 Validate 函数,用于对 Analyzer 执行基本健全性检查,例如确保其 Requires 图是无环的,其事实和结果类型是唯一的等。 最后,Run 字段包含一个函数,驱动程序调用它在单个包上执行分析。驱动程序将 Pass 类型的一个实例传递给它。 #### Pass¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Pass) Pass (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass) 描述单个工作单元:将特定的 Analyzer 应用于特定的 Go 代码包。Pass 向 Analyzer 的 Run 函数提供有关正在分析的包的信息,并向 Run 函数提供用于报告诊断和其他信息返回给驱动程序的操作。 `` type Pass struct { Fset *token.FileSet Files []*ast.File OtherFiles []string IgnoredFiles []string Pkg *types.Package TypesInfo *types.Info ResultOf map[*Analyzer]interface{} Report func(Diagnostic) ... } `` Fset、Files、Pkg 和 TypesInfo 字段为单个 Go 代码包提供语法树、类型信息和源代码位置。 OtherFiles 字段提供非 Go 文件(如作为此包一部分的汇编文件)的名称。类似地,IgnoredFiles 字段提供当前构建配置下不属于此包但可能属于其他构建配置的 Go 和非 Go 源文件的名称。可以使用 Pass.ReadFile 读取这些文件的内容;有关加载非 Go 文件并根据它们报告诊断的示例,请参见“asmdecl”或“buildtags”分析器。 ResultOf 字段提供由此分析器所需的分析器计算的结果,这些需求在其 Analyzer.Requires 字段中表达。驱动程序首先运行所需的分析器,并将它们的结果在此映射中提供。每个 Analyzer 必须返回其 Analyzer.ResultType 字段中描述的类型值。例如,“ctrlflow”分析器返回 *ctrlflow.CFGs,它为包中的每个函数提供控制流图(参见 golang.org/x/tools/go/cfg);“inspect”分析器返回一个值,使其他 Analyzer 能够更高效地遍历包的语法树;“buildssa”分析器构建 SSA 形式的中间表示。每个这些 Analyzer 扩展了后续 Analyzer 的能力,而无需向核心 API 添加依赖,因此分析工具只为其需要的扩展付出代价。 Report 函数发出一个诊断,一个与源位置关联的消息。对于大多数分析,诊断是它们的主要结果。为了方便,Pass 提供了一个辅助方法 Reportf,通过格式化字符串来报告新的诊断。Diagnostic 定义为: `` type Diagnostic struct { Pos token.Pos Category string // 可选 Message string } `` 可选的 Category 字段是一个短标识符,用于在分析产生多种诊断时对消息类型进行分类。 Diagnostic (https://pkg.go.dev/golang.org/x/tools/go/analysis#Diagnostic) 结构没有指示严重性的字段,因为用户对分析器及其诊断的相对重要性的看法差异很大。该框架的设计不要求每个 Analyzer 负责识别其诊断的严重性。相反,我们期望驱动程序允许用户根据产生诊断的 Analyzer 和可选的 Category,根据用户偏好来定制诊断的过滤和优先级排序。 大多数 Analyzer 检查带类型的 Go 语法树,但少数(如 asmdecl 和 buildtag)检查 Go 源文件的原始文本,甚至非 Go 文件(如汇编)。要根据原始文本文件的行报告诊断,请使用以下序列: `` content, err := pass.ReadFile(filename) if err != nil { ... } tf := fset.AddFile(filename, -1, len(content)) tf.SetLinesForContent(content) ... pass.Reportf(tf.LineStart(line), "oops") `` #### 使用事实的模块化分析¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Modular_analysis_with_Facts) 为了提高效率和可伸缩性,大型程序通常使用单独编译构建:程序的单元被单独编译,并且仅在其依赖项之一发生变化时才重新编译;独立的模块可以并行编译。同样的技术可以应用于静态分析,以获得相同的好处。这样的分析被称为“模块化”。 编译器的类型检查器是模块化静态分析的一个例子。我们希望应用于 Go 程序的许多其他检查器可以被理解为替代的或非标准的类型系统。例如,vet 的 printf 检查器推断函数是否具有“printf 包装器”类型,并对这类函数的调用应用更严格的检查。此外,它记录哪些函数是 printf 包装器,供后续分析阶段通过归纳识别其他 printf 包装器使用。一个本身不有趣但作为达到有趣结果(如诊断)的垫脚石的结果(如“f 是一个 printf 包装器”)被称为事实 (https://pkg.go.dev/golang.org/x/tools/go/analysis#Fact)。 analysis API 允许分析定义新类型的事实,将这些类型的事实与当前包中声明的对象(命名实体)关联,或与包整体关联,并查询与对象或包关联的给定类型的现有事实。 使用事实的 Analyzer 必须声明其类型: `` var Analyzer = &analysis.Analyzer{ Name: "printf", FactTypes: []analysis.Fact{new(isWrapper)}, ... } type isWrapper struct{} // => *types.Func f “是一个 printf 包装器” `` 驱动程序程序确保在分析包之前生成针对该包依赖项的事实,并负责将事实从一个包传播到另一个包,可能跨越地址空间。因此,事实必须可序列化。API 要求驱动程序使用 gob 编码,这是一种高效、健壮、自描述的二进制协议。如果默认编码不合适,事实类型可以实现 GobEncoder/GobDecoder 接口。事实应该是无状态的。由于序列化的事实可能出现在构建输出中,事实的 gob 编码必须是确定性的,以避免在使用内容寻址缓存的构建系统中出现虚假缓存未命中。驱动程序对给定分析阶段导出的所有事实进行一次 gob 编码调用,以便保留多个事实引用的共享数据结构的拓扑结构。 Pass 类型具有导入和导出事实的函数,事实可以与对象或包关联: `` type Pass struct { ... ExportObjectFact func(types.Object, Fact) ImportObjectFact func(types.Object, Fact) bool ExportPackageFact func(fact Fact) ImportPackageFact func(*types.Package, Fact) bool } `` Analyzer 只能导出与当前包或其对象关联的事实,但它可以导入来自作为当前包导入依赖的任何包或对象的事实。 从概念上讲,ExportObjectFact(obj, fact) 将事实插入到以 (obj, TypeOf(fact)) 对为键的隐藏映射中,而 ImportObjectFact 函数从该映射检索条目并将其值复制到 fact 指向的变量中。此方案假设事实的具体类型是一个指针;此假设由 Validate 函数检查。请参见“printf”分析器以了解对象事实的使用示例。 一些驱动程序实现(例如基于 Bazel 和 Blaze 的实现)目前不将分析器应用于标准库的包。因此,为了获得最佳结果,分析器作者不应依赖于标准包的分析事实可用。例如,尽管 printf 检查器能够在分析 log 包时推断出 log.Printf 是一个 printf 包装器,但此事实内置于分析器中,以便即使在不对标准包应用分析器的驱动程序中运行时,也能正确检查对 log.Printf 的调用。我们希望在将来消除此限制。 #### 测试 Analyzer¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Testing_an_Analyzer) analysistest 子包提供了用于测试 Analyzer 的实用工具。用几行代码,就可以在测试数据文件包上运行分析器,并检查它是否报告了所有预期的诊断和事实(且没有更多)。期望使用输入代码中的 “// want ...” 注释表达。 #### 独立命令¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Standalone_commands) 分析器以包的形式提供,驱动程序程序应导入这些包。vet 命令导入一组几个分析器,但用户可能希望定义自己的执行额外检查的分析命令。为了简化创建分析命令的任务(无论是用于单个分析器还是整套分析器),我们提供了 singlechecker 和 multichecker 子包。 singlechecker 包为运行一个分析器的命令提供了 main 函数。按照惯例,每个分析器(例如 go/analysis/passes/findcall)都应附带一个基于 singlechecker 的命令,例如 go/analysis/passes/findcall/cmd/findcall,其完整定义如下: `` package main import ( "golang.org/x/tools/go/analysis/passes/findcall" "golang.org/x/tools/go/analysis/singlechecker" ) func main() { singlechecker.Main(findcall.Analyzer) } `` 提供多个分析器的工具可以以类似的方式使用 multichecker,向其传递 Analyzer 列表。 - [func Validate(analyzers [\]*Analyzer) error](https://pkg.go.dev/golang.org/x/tools/go/analysis#Validate) - type Analyzer (https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer) - - func (a *Analyzer) String() string (https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer.String) - type CycleInRequiresGraphError (https://pkg.go.dev/golang.org/x/tools/go/analysis#CycleInRequiresGraphError) - - func (e *CycleInRequiresGraphError) Error() string (https://pkg.go.dev/golang.org/x/tools/go/analysis#CycleInRequiresGraphError.Error) - type Diagnostic (https://pkg.go.dev/golang.org/x/tools/go/analysis#Diagnostic) - type Fact (https://pkg.go.dev/golang.org/x/tools/go/analysis#Fact) - type Module (https://pkg.go.dev/golang.org/x/tools/go/analysis#Module) - type ModuleError (https://pkg.go.dev/golang.org/x/tools/go/analysis#ModuleError) - type ObjectFact (https://pkg.go.dev/golang.org/x/tools/go/analysis#ObjectFact) - type PackageFact (https://pkg.go.dev/golang.org/x/tools/go/analysis#PackageFact) - type Pass (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass) - - func (pass *Pass) ReportRangef(rng Range, format string, args ...any) (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass.ReportRangef) - func (pass *Pass) Reportf(pos token.Pos, format string, args ...any) (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass.Reportf) - func (pass *Pass) String() string (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass.String) - type Range (https://pkg.go.dev/golang.org/x/tools/go/analys

相似文章

Golang 代码审查笔记 II

Lobsters Hottest

来自 elttam 的后续博客文章,介绍了提高安全性的新 Go 语言特性、在代码审计中发现的有问题的编码模式(footguns),以及用于捕获这些模式的 Semgrep 规则。

Go 工具链的 Fuzzing 分支

Lobsters Hottest

Trail of Bits 推出了 gosentry,这是一个面向模糊测试的 Go 工具链分支,集成 LibAFL 以增强路径约束求解、结构化模糊测试及漏洞检测能力,同时保留标准测试工作流程。

JetBrains Go现代编码规范指南

GitHub Trending (daily)

JetBrains发布了一个GitHub仓库,其中包含编写现代Go代码的代码代理指南,涵盖了Go 1.27的所有功能,并与Junie和Claude Code等工具集成。