缘起:
2023年5月25号,知名Go技术专家,WA语言联合发起人,多本技术书籍的合著者柴大,遇到一个append忘记写要添加的元素,只写了第一个参数(要追加的切片)的问题,可以编译通过,但大概率不是开发者本意。目前go vet缺失这样的检测项,柴大知道我对Go源码一直充满热枕,建议尝试实现。
即对于以下情况,希望在执行go vet检测时,可以抛出提示
func AppendsTest() {
sli := []string{"a", "b", "c"}
sli = append(sli)
}
最开始我甚至不知道这种情况竟然可以编译通过,也不清楚如何为vet新增一个分析项。不过凭借一腔热情,通过分析源码,检索资料,询问ChatGPT后,几个小时后的第二天凌晨,就实现了初版功能。但后来的集成&修改较大,在此做相关记述。
目标:
添加一个新的分析器,用于在append后检查缺失值
目前,如果append时没有添加第二个参数,执行go vet时不会有提示。 添加一个分析器来检测这种情况
下面 由缓至急循循而进
go vet的作用
官方文档
Go vet 是 Go 语言内置的静态分析工具,用于检查 Go 代码中的常见错误和潜在问题。它可以帮助开发者在编译代码之前发现一些常见的错误,并提供修复建议。
以下是 Go vet 的一些常见用途和功能:
-
错误检查:Go vet 可以检查代码中的语法错误、类型错误、未使用的变量和导入包、无法到达的代码等。它能够捕捉到一些编译器可能会忽略的问题,从而帮助开发者在编译之前就能够发现这些潜在错误。
-
常见问题检测:Go vet 还可以检查代码中的常见问题,例如无用的 if 条件、无法到达的代码、错误的函数调用等。它可以帮助开发者避免一些常见的错误模式和不良实践。
-
代码规范检查:Go vet 还可以检查代码是否符合 Go 语言的规范和最佳实践。它可以发现一些与代码风格和约定不符的地方,例如不必要的类型转换、不恰当的命名风格等。通过检查代码的规范性,可以提高代码的可读性和可维护性。
Go vet 只是一个静态分析工具,它无法检测所有类型的问题。一些更复杂的逻辑错误、并发问题和运行时错误需要使用其他工具和技术进行检测和调试。
go vet
工具可以检查代码中的一些常见错误,例如:
-
使用 defer
语句时传递函数参数的常见错误。 -
在使用 sync.WaitGroup
时,忘记在协程内调用Done
方法的常见错误。 -
在使用 fmt.Printf
时,格式化字符串中的占位符和参数不匹配的常见错误。
除了这些常见错误之外,go vet
工具还可以检查代码中的其他一些潜在问题,例如:
-
未使用的变量和导入包。 -
可能的空指针解引用。 -
可能的竞态条件等。
通过运行 go vet
工具,可以帮助开发者及时发现代码中的问题,并减少因这些问题导致的错误和不必要的麻烦。
vet 这个名称可能源于英文单词 "veterinary",意为 "兽医";或者"veto",意为 "否决"
可通过此链接,查看Go的各个版本中,vet的变化
要运行 Go vet,可以使用以下命令:
go vet <package>
其中 <package>
是要检查的 Go 包的路径。Go vet 将会分析指定包及其依赖的代码,并输出潜在的问题和修复建议。
Go vet 是一个帮助开发者在编译之前发现代码错误和潜在问题的工具,它可以提高代码的质量和可靠性,并帮助开发者编写更健壮的 Go 代码。
Go 1.12 - 2019 年 2 月 版本在analysis包基础上重写了 go vet 命令,这个包有着更大的灵活性,允许开发人员编写自己的代码检查工具。
go vet 命令可作为一系列不同源代码分析工具的基础。
举几个例子,go vet会输出提示
当使用 Go vet 进行静态代码分析时,它可能会输出一些提示来指出代码中的潜在问题。以下是一些示例:
-
未使用的变量:
package main
import "fmt"
func main() {
unusedVar := 42 // 声明了一个未使用的变量
fmt.Println("Hello, Go!")
}
运行 go vet 命令后,会输出以下提示:
# command-line-arguments
./main.go:6:6: unusedVar declared but not used
这个提示表明变量 unusedVar 被声明但未被使用,可能是代码中的一个错误或者不必要的代码。
-
错误的函数调用:
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
fmt.Printl("Oops!") // 错误的函数名
}
运行 go vet 命令后,会输出以下提示:
# command-line-arguments
./main.go:7:5: Printl call has possible misspelling: fmt.Printl refers to unknown identifier 'Printl'; did you mean Println?
这个提示指出在第 7 行的函数调用 fmt.Printl("Oops!")
可能是一个拼写错误,建议使用 fmt.Println("Oops!")
。
-
错误的类型转换:
package main
import "fmt"
func main() {
var x float64 = 3.14
y := int(x) // 错误的类型转换
fmt.Println(y)
}
运行 go vet 命令后,会输出以下提示:
# command-line-arguments
./main.go:7:10: possible misuse of conversion
这个提示表明在第 7 行的类型转换 int(x) 可能存在问题,建议检查转换的正确性。
以上只是一些简单的示例,Go vet 可以检查更多的问题并提供相应的提示。通过运行 go vet 命令并检查输出,开发者可以及早发现代码中的潜在问题,并根据提示进行修复和改进。
append参数的问题
package main
func main() {
sli1 := []string{"a", "b", "c"}
sli2 := make([]string, 0)
for _, val := range sli1 {
print(val)
sli2 = append(sli2)
}
}
append的第一个参数是切片,后面是...
可选参数。那如上,append()中遗漏了要追加的元素,是完全符合语法规范的,能正常编译通过。
src/buildin/buildin.go:
// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
//
// As a special case, it is legal to append a string to a byte slice, like this:
//
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
但往往这不是想要的效果,并且由于编译期不会抛错,不能马上发现问题。想必很多开发者都"中过招",甚至一些知名开源项目如docker,也有这样的bug
于是提了一个issue: proposal: cmd/vet: add a new analyzer for check missing values after append
同时写好了代码,并本地自测ok
vet这块的维护者评论了 Related #30040.
早在2019年2月份,就有开发者提出了类似提案(proposal: Go 2: ban calling append with a single argument),但最终没有定论。看起来Rob Pike不支持新增一个vet项来检测该问题,Ian Lance Taylor则较支持。
参与讨论的开发者还列举了更多这样的错误case:
Having a quick look with SQL and the Google Cloud BigQuery GitHub Activity Data - I was able to find some regexp matches (120 different lines, 1623 total matches), examples:
https://github.com/flynn/flynn/blob/1033a42b27c1acee8d8bac742a0d65291ce31b2c/test/test_cli.go#L692
https://github.com/moby/moby/blob/f8e29fdd683f8d1a79344df08342261c3991ad1b/image/rootfs.go#L41
https://github.com/yanzay/autohome/blob/9b98aa56c498e21fd580dbbcd5459cdc0c15f0c6/modules/arduino/arduino.go#L95
https://github.com/frezadev/colony-manager/blob/6ec588b081b9aab1d519cc27c3358ea26d942c92/controller/application.go#L988
https://github.com/xingzhou/kubernetes/blob/master/pkg/kubelet/rkt/rkt_test.go
https://github.com/akram/origin/blob/1b26a414ce1c2a4ca4f4491722a48a138aab6d3c/vendor/github.com/docker/docker/daemon/graphdriver/aufs/aufs_test.go#L768
https://github.com/haxorof/terraform-provider-openstack/blob/b6e0cc9732f4379ac39a1506fbbd6f5cfea1438f/vendor/github.com/hashicorp/hcl2/hcl/merged.go#L174
代码实现
4年前不了了之的问题,估计现在也不会很快有定论。
抛开这些暂且不谈,先看看代码如何实现 为go vet新增一个检测项,用来分析append切片时,是不是遗漏了要追加的元素
先借助chatgpt:
go vet的源代码是在go的哪个目录?
Go vet 工具的源代码位于 Go 语言的源代码库中,具体而言,它位于 go/src/cmd/vet
目录下。在标准的 Go 安装中,该目录会包含 vet 包和与之相关的文件。
你可以在以下路径找到 Go vet 的源代码:
go/src/cmd/vet
在这个目录中,你会找到 Go vet 工具的实现文件和其他相关文件,包括命令行解析、静态分析逻辑等。这些文件一起构成了 Go vet 工具的代码。
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"cmd/internal/objabi"
"golang.org/x/tools/go/analysis/unitchecker"
"golang.org/x/tools/go/analysis/passes/asmdecl"
"golang.org/x/tools/go/analysis/passes/assign"
"golang.org/x/tools/go/analysis/passes/atomic"
"golang.org/x/tools/go/analysis/passes/bools"
"golang.org/x/tools/go/analysis/passes/buildtag"
"golang.org/x/tools/go/analysis/passes/cgocall"
"golang.org/x/tools/go/analysis/passes/composite"
"golang.org/x/tools/go/analysis/passes/copylock"
"golang.org/x/tools/go/analysis/passes/directive"
"golang.org/x/tools/go/analysis/passes/errorsas"
"golang.org/x/tools/go/analysis/passes/framepointer"
"golang.org/x/tools/go/analysis/passes/httpresponse"
"golang.org/x/tools/go/analysis/passes/ifaceassert"
"golang.org/x/tools/go/analysis/passes/loopclosure"
"golang.org/x/tools/go/analysis/passes/lostcancel"
"golang.org/x/tools/go/analysis/passes/nilfunc"
"golang.org/x/tools/go/analysis/passes/printf"
"golang.org/x/tools/go/analysis/passes/shift"
"golang.org/x/tools/go/analysis/passes/sigchanyzer"
"golang.org/x/tools/go/analysis/passes/slog"
"golang.org/x/tools/go/analysis/passes/stdmethods"
"golang.org/x/tools/go/analysis/passes/stringintconv"
"golang.org/x/tools/go/analysis/passes/structtag"
"golang.org/x/tools/go/analysis/passes/testinggoroutine"
"golang.org/x/tools/go/analysis/passes/tests"
"golang.org/x/tools/go/analysis/passes/timeformat"
"golang.org/x/tools/go/analysis/passes/unmarshal"
"golang.org/x/tools/go/analysis/passes/unreachable"
"golang.org/x/tools/go/analysis/passes/unsafeptr"
"golang.org/x/tools/go/analysis/passes/unusedresult"
)
func main() {
objabi.AddVersionFlag()
unitchecker.Main(
asmdecl.Analyzer,
assign.Analyzer,
atomic.Analyzer,
bools.Analyzer,
buildtag.Analyzer,
cgocall.Analyzer,
composite.Analyzer,
copylock.Analyzer,
directive.Analyzer,
errorsas.Analyzer,
framepointer.Analyzer,
httpresponse.Analyzer,
ifaceassert.Analyzer,
loopclosure.Analyzer,
lostcancel.Analyzer,
nilfunc.Analyzer,
printf.Analyzer,
shift.Analyzer,
sigchanyzer.Analyzer,
slog.Analyzer,
stdmethods.Analyzer,
stringintconv.Analyzer,
structtag.Analyzer,
tests.Analyzer,
testinggoroutine.Analyzer,
timeformat.Analyzer,
unmarshal.Analyzer,
unreachable.Analyzer,
unsafeptr.Analyzer,
unusedresult.Analyzer,
)
}
什么意思?
这段代码是 Go vet 工具的主要入口代码。它导入了多个分析器(analyzers),每个分析器都是 Go vet 工具中的一个检查功能。然后,它使用 unitchecker.Main 函数注册这些分析器,并在运行时执行它们。
这段代码的主要作用是注册和运行一系列的分析器,这些分析器将在代码静态分析过程中进行检查。每个分析器都实现了 golang.org/x/tools/go/analysis.Analyzer 接口,它定义了分析器应该提供的功能和行为。
例如,assign.Analyzer 分析器用于检查可能的赋值错误,unusedresult.Analyzer 用于检查未使用的函数调用结果,copylock.Analyzer 用于检查复制锁的错误使用等等。通过将这些分析器传递给 unitchecker.Main 函数,它们将在代码静态分析期间被执行。
整个过程的目的是通过静态分析来帮助开发者发现潜在的错误、不良实践和代码质量问题。当你在命令行中运行 go vet 命令时,实际上就是执行了这段代码,并调用了这些分析器进行代码分析和检查。
这段代码还包含了 objabi.AddVersionFlag() 的调用,它用于为 Go vet 工具添加版本标志。
总结一下,这段代码是 Go vet 工具的主入口,负责注册和执行一系列分析器来进行代码静态分析和检查。
想新增一个分析器,用于检测append(sli)后面没有追加值的情况,需要怎么写?
要新增一个分析器来检测 append(sli) 后面没有追加值的情况,你需要完成以下步骤:
创建新的分析器文件:首先,在 cmd/vet 目录下创建一个新的分析器文件,例如 appendcheck.go。
导入必要的包:在 appendcheck.go 文件中,导入所需的包,包括 "golang.org/x/tools/go/analysis" 和其他必要的包,例如 "go/ast" 和 "go/token"。
实现分析器:定义一个新的分析器类型,并实现 analysis.Analyzer 接口的方法。你需要实现 Run 方法,在其中编写检查逻辑。可以参考其他现有分析器的实现作为参考。
package main
import (
"go/ast"
"go/token"
"golang.org/x/tools/go/analysis"
)
var appendCheckAnalyzer = &analysis.Analyzer{
Name: "appendcheck",
Doc: "check for missing values after append",
Run: runAppendCheck,
}
func runAppendCheck(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
// 检查是否为 append 函数调用
if call, ok := node.(*ast.CallExpr); ok {
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == "append" {
// 检查 append 函数调用的参数列表是否为空
if len(call.Args) == 1 {
pass.Reportf(call.Lparen, "append called without values to append")
}
}
}
}
return true
})
}
return nil, nil
}
func main() {
// ...
unitchecker.Main(appendCheckAnalyzer)
}
在上面的代码中,我们定义了一个名为 appendCheckAnalyzer 的新分析器,并实现了 Run 方法。在 Run 方法中,我们使用 ast.Inspect 函数遍历抽象语法树(AST),并检查每个函数调用是否为 append 函数调用。如果是,我们检查其参数列表的长度是否为 1。如果长度为 1,则意味着没有追加的值,我们会使用 pass.Reportf 方法报告一个问题。
注册分析器:在 main 函数中,将新的分析器 appendCheckAnalyzer 传递给 unitchecker.Main 函数,以便在运行时注册和执行该分析器。
构建和运行:将 appendcheck.go 文件和其他必要的文件添加到你的 Go 项目中,并构建执行文件。然后,你可以使用 go vet 命令来运行新的分析器,例如:
go vet ./...
这样,新的分析器就会检查代码中是否存在 append(sli) 后没有追加值的情况,并报告相应的问题。
(chatgpt给的代码有误,此处需要自行开发&调试)
实践一下:
对于
package main
func main() {
sli1 := []string{"a", "b", "c"}
sli2 := make([]string, 0)
for _, val := range sli1 {
print(val)
sli2 = append(sli2)
}
}
执行go vet
之后不会有任何提示~
在src/cmd/vet下新建一个appendcheck.go
内容如下:
package main
import (
"fmt"
"go/ast"
//"go/token"
"golang.org/x/tools/go/analysis"
//"golang.org/x/tools/go/analysis/unitchecker"
)
var AppendCheckAnalyzer = &analysis.Analyzer{
Name: "appendcheck",
Doc: "check for missing values after append",
Run: runAppendCheck,
}
func runAppendCheck(pass *analysis.Pass) (interface{}, error) {
//print("到了这里了!")
//fmt.Printf("%#v", pass)
for _, node := range pass.Files {
//print("也到了这里了!!!")
ast.Inspect(node, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
argsLen := len(call.Args)
if argsLen == 1 {
fmt.Printf("发现 append 函数调用,参数数量:%d\n", argsLen)
}
}
}
return true
})
//ast.Inspect(file, func(node ast.Node) bool {
// // 检查是否为 append 函数调用
// //print(66666666)
// if call, ok := node.(*ast.CallExpr); ok {
// //print(7777777777)
// if sel, ok2 := call.Fun.(*ast.SelectorExpr); ok2 {
// print(888888)
// ident, ok3 := sel.X.(*ast.Ident)
// print("ident.Name:", ident.Name)
// //if ok3 && ident.Name == "append" {
// if ok3 && ident.Name == "fmt" {
// print("222222222222")
// print("参数长度为:", len(call.Args))
// // 检查 append 函数调用的参数列表是否为空
// if len(call.Args) == 1 {
// pass.Reportf(call.Lparen, "append called without values to append")
// }
// }
// }
// }
// return true
//})
}
return nil, nil
}
//func main() {
// // ...
// unitchecker.Main(AppendCheckAnalyzer)
//}
同时在同目录的main.go中新增AppendCheckAnalyzer
这一行
切回到src目录下,执行 ./all.bash
在IDE中配置最新的goroot,而后再执行go vet:
功能算是实现了,但参考其他的Analyzer,代码都是在golang.org/x/tools/go/analysis/passes/具体的Analyzer名
下面
所以不能直接将逻辑在Go主库上实现,而要在x/tools上面,Go主库import相应的功能
观察一下x/tools
golang.org/x/tools这个仓库中 go/analysis和gopls/internal/lsp/analysis有什么区别?
在 golang.org/x/tools 仓库中,go/analysis 和 gopls/internal/lsp/analysis 包是用于静态代码分析的两个不同的包,它们有以下区别:
-
go/analysis 包:这是 Go 语言官方提供的用于编写静态代码分析工具的包。它提供了一组标准的接口和工具,用于创建、注册和执行各种代码分析器。这些分析器可以用于检查代码中的错误、不良实践、性能问题等。go/analysis 包提供了一种通用的框架,使得开发者能够方便地创建自定义的代码分析器,并与其他工具(如 go vet、golangci-lint 等)集成。这个包被广泛用于 Go 代码分析工具的开发。
-
gopls/internal/lsp/analysis 包:这个包是 gopls(Go 语言的官方 Language Server Protocol 实现)内部使用的代码分析包。gopls 是一个提供代码编辑功能的工具,用于提供代码补全、语法检查、重构等功能。gopls 使用 analysis 包进行代码分析,以便在编辑器中提供实时的代码建议和错误提示。gopls/internal/lsp/analysis 包提供了一些特定于 gopls 的分析功能和扩展,以满足 gopls 的特定需求和集成。
总结起来,go/analysis 包是一个通用的静态代码分析框架,用于创建自定义的代码分析器和工具,而 gopls/internal/lsp/analysis 包是 gopls 工具内部使用的代码分析包,为 gopls 提供了与编辑器集成的代码分析功能。这两个包的目标和用途略有不同,但都用于静态代码分析。
想给go新增一个vet分析器,应该加在哪个目录下?
如果你想为 Go 新增一个 vet 分析器,你应该将代码放在 go/src/cmd/vet 目录下,这是 Go 语言源代码库中负责 vet 工具的目录。
在这个目录下,你可以创建一个新的 Go 源代码文件,例如 myvet.go,并在其中实现你的自定义 vet 分析器。
假设你的自定义 vet 分析器用于检查某种特定的代码模式或规范,你可以按照以下步骤进行操作:
进入 go/src/cmd/vet 目录:打开终端或命令行界面,切换到 Go 源代码库中的 cmd/vet 目录。
创建新的分析器文件:在 cmd/vet 目录下创建一个新的 Go 源代码文件,例如 myvet.go。
在 myvet.go 文件中实现分析器逻辑:在 myvet.go 文件中,你可以定义和实现你的自定义 vet 分析器。你需要导入必要的包和依赖,然后编写你的分析器逻辑。
注册分析器:在 myvet.go 文件中,确保你的分析器被注册到 unitchecker.Main 函数中。可以参考已有的分析器文件,将你的分析器添加到类似的位置。
构建和安装 vet 工具:使用适当的命令构建和安装 vet 工具,确保你的自定义分析器被包含在其中。例如,在 go/src/cmd/vet 目录下运行 go install 命令。
完成上述步骤后,你的自定义 vet 分析器就会被包含在 Go 的标准 vet 工具中,可以使用 go vet 命令来运行它。
pass.ReportRangef 报错是因为该函数名应为 pass.Reportf,而不是 pass.ReportRangef
请将以下行:
pass.ReportRangef(call.Lparen, "called without values to append")
修改为:
pass.Reportf(call.Lparen, "called without values to append")
这样修改后,应该能够正确调用 pass.Reportf 函数,并输出相应的错误报告。
在静态代码分析器中,pass.Reportf 函数用于生成问题报告,它的第一个参数是与问题相关的位置信息,例如在这种情况下是 call.Lparen,即 append 函数调用的左括号位置。第二个参数是具体的问题描述。
请注意,在你的代码中,使用 fmt.Printf 输出了一个调试信息,但最好是使用 pass.Reportf 来生成标准的静态代码分析报告。这样可以保持代码一致,并使得报告与其他分析器一起展示。
call.Lparen和call.Rparen有什么区别?
call.Lparen 和 call.Rparen 分别表示函数调用中的左括号和右括号的位置。
具体来说:
-
call.Lparen 是一个 token.Pos 类型的值,表示函数调用表达式中左括号的位置信息。 -
call.Rparen 也是一个 token.Pos 类型的值,表示函数调用表达式中右括号的位置信息。
在 Go 的抽象语法树(AST)中,函数调用表达式由函数名称和括号包围的参数列表组成。对于函数调用 append(sli) 来说:
-
call.Lparen 将指向左括号的位置。 -
call.Rparen 将指向右括号的位置。
这些位置信息可以在静态代码分析器中使用,用于报告问题的准确位置。例如,通过将 pass.Reportf 的第一个参数设置为 call.Lparen 或 call.Rparen,可以将问题报告与具体的括号位置相关联,以帮助开发人员定位代码中的问题。
总结起来,call.Lparen 和 call.Rparen 是用于表示函数调用表达式中左括号和右括号位置的标识符,用于在静态代码分析中准确定位问题的位置。
实践
在go/analysis/passes下新建一个appendcheck文件夹,新建一个appendcheck.go,内容如下:
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package appendcheck defines an Analyzer that detects
// if there is only one variable in append.
package appendcheck
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
const Doc = "check for missing values after append"
var Analyzer = &analysis.Analyzer{
Name: "appendcheck",
Doc: Doc,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appendcheck",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
for _, node := range pass.Files {
ast.Inspect(node, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
if len(call.Args) == 1 {
pass.Reportf(call.Rparen, "called without values to append")
}
}
}
return true
})
}
return nil, nil
}
可以参考go/analysis/passes/timeformat 和 go/analysis/passes/bools,在相应main.go中填写
此时回到Go项目中进行验证,很容易得知x/tools的代码都在和vet同目录的vendor中。如下图,将在x/tools中写好的appendcheck文件夹复制到go/src/cmd/vendor/golang.org/x/tools/go/analysis/passes下
此时注释掉main.go中的AppendCheckAnalyzer
,新增一行appendcheck.Analyzer
,
而后执行./all.bash
回到demo代码中,执行go vet
当然在x/tools中还需要增加相应测试,这个先不谈。
这样就给go vet新增了一个检测项~
完整提交:
https://go-review.googlesource.com/c/tools/+/498475
https://go-review.googlesource.com/c/go/+/498416
评审阶段:
问题1: Check that it is a call to the builtin function append and not another function named append
Check that it is a call to the builtin function append and not another function named append, i.e.
if fun, ok := pass.TypesInfo.Uses[id].(*types.Builtin); ok { ... }
是什么意思?
这段代码片段是Go语言中的一段代码,用于检查是否调用了内置函数append
,而不是一个名为append
的其他函数。
解释该代码段的逻辑如下:
-
pass.TypesInfo
是一个用于类型信息的结构体或对象,Uses
是其中的一个字段。这个字段存储了程序中标识符(id
)与其使用位置相关联的信息。 -
通过
pass.TypesInfo.Uses[id]
,可以获取与标识符id
关联的使用信息。 -
使用断言(type assertion)操作符
.(*types.Builtin)
将获取的使用信息转换为types.Builtin
类型。这是因为内置函数append
属于types.Builtin
类型。 -
如果转换成功,那么表示使用的是内置函数
append
,可以在if
语句块内执行相应的逻辑。
总而言之,这段代码的目的是判断一个标识符是否引用了内置函数append
,并在满足条件时执行特定的逻辑。
举例说明:
下面是一个使用该代码片段的示例:
package main
import (
"fmt"
"go/types"
"golang.org/x/tools/go/analysis"
)
func main() {
analyzer := &appendAnalyzer{}
analysis.Run(analyzer, "path/to/package")
}
type appendAnalyzer struct{}
func (a *appendAnalyzer) Run(pass *analysis.Pass) (interface{}, error) {
for _, stmt := range pass.Nodes {
if callExpr, ok := stmt.(*ast.CallExpr); ok {
if id, ok := callExpr.Fun.(*ast.Ident); ok {
if fun, ok := pass.TypesInfo.Uses[id].(*types.Builtin); ok && fun.Name() == "append" {
fmt.Println("Calling the built-in append function.")
}
}
}
}
return nil, nil
}
在上述示例中,我们定义了一个名为appendAnalyzer
的结构体,并实现了Run
方法。在Run
方法中,我们遍历AST中的语句,检查是否有调用内置函数append
的表达式。如果是调用内置函数append
,则打印一条消息。
该示例展示了如何在自定义的静态分析器(使用golang.org/x/tools/go/analysis
包)中使用该代码片段来检测代码中的内置函数append
的调用。请注意,这只是一个示例,实际使用时可能需要根据具体需求进行适当修改和扩展。
解决
言外之意,就是之前的代码,会把用户自定义的append方法也纳入检测。 验证一下:
假设自定义了一个append方法,如下
package main
func main() {
sli := []int{1, 2, 3}
sli = append(sli, 4, 5, 6)
sli = append(sli)
}
func append(args ...interface{}) []int {
println(args)
return []int{}
}
检测
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func main() {
src := `
package main
func main() {
sli := []int{1, 2, 3}
sli = append(sli, 4, 5, 6)
sli = append(sli)
}
func append(args ...interface{}) []int {
println(args)
return []int{}
}`
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, "", src, parser.ParseComments)
if err != nil {
fmt.Println("Failed to parse file:", err)
return
}
ast.Inspect(node, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
argsLen := len(call.Args)
fmt.Printf("发现 append 函数调用,参数数量:%d\n", argsLen)
}
}
return true
})
}
输出:
发现 append 函数调用,参数数量:4
发现 append 函数调用,参数数量:1
进行修改:
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
)
func main() {
src := `
package main
func main() {
sli := []int{1, 2, 3}
sli = append(sli, 4, 5, 6)
sli = append(sli)
}
func append(args ...interface{}) []int {
println(args)
return []int{}
}`
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, "", src, parser.ParseComments)
if err != nil {
fmt.Println("Failed to parse file:", err)
return
}
// 创建类型检查器(这部分为新增)
conf := types.Config{}
info := &types.Info{}
_, err = conf.Check("", fset, []*ast.File{node}, info)
if err != nil {
fmt.Println("Failed to type-check file:", err)
return
}
ast.Inspect(node, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
fmt.Println()
if obj, ok := info.ObjectOf(ident).(*types.Builtin); ok {
if obj.Name() == "append" {
argsLen := len(call.Args)
fmt.Printf("发现内建的 append 函数调用,参数数量:%d\n", argsLen)
}
} else {
argsLen := len(call.Args)
fmt.Printf("发现用户自定义的 append 函数调用,参数数量:%d\n", argsLen)
}
}
}
return true
})
}
运行后输出:
发现用户自定义的 append 函数调用,参数数量:4
发现用户自定义的 append 函数调用,参数数量:1
在修改后的代码中,添加了类型检查的步骤。通过使用types.Config
和types.Info
,可以对Go代码进行类型检查。在检查完成后,使用info.ObjectOf
函数获取标识符对应的对象,并通过类型断言判断该对象是否为types.Builtin
类型(内建函数)。如果是内建函数append
,则说明它不是用户自定义的函数;否则,它是用户自定义的函数。
通过这种方式,可以区分内建的append
函数和用户自定义的append
函数。
实际上这段代码,会一直走到 发现用户自定义的 append 函数调用这里。。所以还是有问题
问题2:Use MustExtractDoc. https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/internal/analysisutil#MustExtractDoc . See other analyzers for examples.
可以参考assign.go的来改: https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/assign
这个比较好解决,即不要像 bools这个Analyzer一样,直接把文档写在上面;而要像assign一样,使用//go:embed doc.go
,在doc.go中更详细地描述这个Analyzer的作用
问题3: Use x/tools/go/analysis/passes/inspect
See nilfunc for an example of using inspector.Preorder https://cs.opensource.google/go/x/tools/+/refs/tags/v0.10.0:go/analysis/passes/nilfunc/nilfunc.go;l=34-39
好吧,搜了下tools/go/analysis/passed目录pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
这个关键字....确实应该统一“格式”
golang中下面这段代码什么作用?请举例说明
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.BinaryExpr)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {}
这段代码用于在Go语言的静态分析框架中,使用inspector
来对AST节点进行预定义类型的先序遍历。
让我们来逐行解释这段代码的作用:
-
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
:首先,我们从pass.ResultOf
中获取与inspect.Analyzer
相关联的结果,并将其转换为*inspector.Inspector
类型。这里假设inspect.Analyzer
是一个静态分析器,其执行结果包含了inspector
,该inspector
用于遍历AST。 -
nodeFilter := []ast.Node{ (*ast.BinaryExpr)(nil), }
:我们创建一个AST节点过滤器nodeFilter
,其中只包含一个元素,即*ast.BinaryExpr
类型的节点。这个过滤器指定了我们只关心二元表达式节点。 -
inspect.Preorder(nodeFilter, func(n ast.Node) {}
:使用Preorder
方法进行先序遍历,遍历的起点是AST根节点。我们传递了节点过滤器nodeFilter
和一个匿名函数作为参数。匿名函数会在遍历到满足过滤器条件的节点时被调用。
举个例子,假设我们有以下Go代码片段:
package main
import "fmt"
func main() {
a := 1 + 2
b := a * 3
fmt.Println(b)
}
如果我们将上述代码传递给一个自定义的静态分析器,并使用上述代码片段作为分析的输入,可以使用上述代码片段中的代码来遍历AST并找到二元表达式节点。
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
func main() {
analyzer := &exampleAnalyzer{}
analyzers := []*analysis.Analyzer{analyzer}
analysis.Run(analyzers)
}
type exampleAnalyzer struct{}
func (a *exampleAnalyzer) Run(pass *analysis.Pass) (interface{}, error) {
nodeFilter := []ast.Node{
(*ast.BinaryExpr)(nil),
}
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
inspect.Preorder(nodeFilter, func(n ast.Node) {
// 处理二元表达式节点
binaryExpr := n.(*ast.BinaryExpr)
fmt.Printf("发现二元表达式:%s\n", binaryExpr)
})
return nil, nil
}
func (a *exampleAnalyzer) String() string {
return "exampleAnalyzer"
}
在上述示例中,我们定义了一个自定义的静态分析器exampleAnalyzer
,并在Run
方法中使用inspect.Preorder
进行AST的先序遍历。在处理函数中,我们将满足过滤器条件的节点强制转换为*ast.BinaryExpr
类型,并打印出来。
当运行上述代码时,我们会发现在遍历AST期间,会找到两个二元表达式节点,并打印出它们的信息。
请注意,上述示例中涉及到了使用Go的静态分析器框架和相关库(例如golang.org/x/tools/go/analysis
、golang.org/x/tools/go/ast/inspector
等)。在实际使用中,您可能需要进行适当的导入和配置,以便正确运行和应用静态分析器。
给的代码完全无法编译..
帮我使用此,实现检测append后面的元素数量的功能
帮我使用
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.BinaryExpr)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {}
实现检测append后面的元素数量的功能
当我们使用inspect.Preorder
结合inspector
来实现检测append
后面的元素数量的功能时,可以按照以下步骤进行:
-
导入必要的包和库:
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
-
创建自定义的静态分析器:
type appendAnalyzer struct{}
-
实现静态分析器的 Run
方法:
func (a *appendAnalyzer) Run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.CallExpr)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
callExpr := n.(*ast.CallExpr)
// 检查是否是 append 函数调用
if ident, ok := callExpr.Fun.(*ast.Ident); ok && ident.Name == "append" {
// 获取元素数量
argsLen := len(callExpr.Args)
fmt.Printf("发现 append 函数调用,参数数量:%d\n", argsLen)
}
})
return nil, nil
}
-
实现静态分析器的 String
方法:
func (a *appendAnalyzer) String() string {
return "appendAnalyzer"
}
-
在 main
函数中创建静态分析器集合并运行分析器:
func main() {
analyzer := &appendAnalyzer{}
analyzers := []*analysis.Analyzer{
inspect.Analyzer,
analyzer,
}
analysis.Run(analyzers)
}
完整的示例代码如下:
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
type appendAnalyzer struct{}
func (a *appendAnalyzer) Run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.CallExpr)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
callExpr := n.(*ast.CallExpr)
// 检查是否是 append 函数调用
if ident, ok := callExpr.Fun.(*ast.Ident); ok && ident.Name == "append" {
// 获取元素数量
argsLen := len(callExpr.Args)
fmt.Printf("发现 append 函数调用,参数数量:%d\n", argsLen)
}
})
return nil, nil
}
func (a *appendAnalyzer) String() string {
return "appendAnalyzer"
}
func main() {
analyzer := &appendAnalyzer{}
analyzers := []*analysis.Analyzer{
inspect.Analyzer,
analyzer,
}
analysis.Run(analyzers)
}
以上代码会在检测到append
函数调用时打印出其后面的元素数量。请注意,此代码中使用了Go的静态分析器框架和相关库。确保在运行代码之前已正确导入并安装所需的包和库。
这两段质量都不咋高
直接在tools里面修改,再找个go源码项目,将改好的tools的append这个目录按照之前的操作,复制到go/src/cmd的vendor的golang.org/x/tools/go/analysis/passer目录下,并修改go/src/cmd/vet/main.go
或者用最初那种方式,在go/src/cmd/vet/
下新建appendcheck.go,把内容贴过去,同时在main.go中增加AppendCheckAnalyzer,
最后都要编译这个Go源码项目
关于这两种方式的差异,以及如何用新的方式实现检测append后面值缺失的功能,直接参考 使用go/analysis包实现自定义的linter
最后可用的代码:
package append
import (
_ "embed"
"go/ast"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
"golang.org/x/tools/go/ast/inspector"
)
//go:embed doc.go
var doc string
var Analyzer = &analysis.Analyzer{
Name: "append",
Doc: analysisutil.MustExtractDoc(doc, "append"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/append",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.CallExpr)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
call := n.(*ast.CallExpr)
if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
if _, ok := pass.TypesInfo.Uses[ident].(*types.Builtin); ok {
if len(call.Args) == 1 {
pass.ReportRangef(call, "append with no values")
}
}
}
})
return nil, nil
}
go/analysis/passes/inspect分析器提供了一个构建块,可以使用它来代替ast.Inspect()或ast.Walk()来遍历语法文件。但是,如果有多个分析器,并且每个分析器都必须遍历语法树的话,则效率不是很高!
go/analysis/passes/inspect包比ast.Inspect()快得多,因为它在底层使用golang.org/x/tools/go/ast/inspector包。
最后还需要更新vendor等,在此略过。
本文由 mdnice 多平台发布