为go vet添加一个新的分析器,用于检查append后面的值缺失

2023年 10月 5日 61.5k 0

缘起:

2023年5月25号,知名Go技术专家,WA语言联合发起人,多本技术书籍的合著者柴大,遇到一个append忘记写要添加的元素,只写了第一个参数(要追加的切片)的问题,可以编译通过,但大概率不是开发者本意。目前go vet缺失这样的检测项,柴大知道我对Go源码一直充满热枕,建议尝试实现。

即对于以下情况,希望在执行go vet检测时,可以抛出提示

func AppendsTest() {
	sli := []string{"a", "b", "c"}
	sli = append(sli) 
}

最开始我甚至不知道这种情况竟然可以编译通过,也不清楚如何为vet新增一个分析项。不过凭借一腔热情,通过分析源码,检索资料,询问ChatGPT后,几个小时后的第二天凌晨,就实现了初版功能。但后来的集成&修改较大,在此做相关记述。

注: 凹语言(凹读音“ wa ”)是国内 资深 Gopher 针对 WASM 平台设计的通用编程语言。作为 WASM 原生的编程语言,天然对浏览器环境亲和,同时支持 Linux 、 macOS 和 Windows 等主流操作系统,此外通过 LLVM 后端对本地应用和单片机等环境提供支持。有兴趣的开发者可以关注 https://wa-lang.org/guide/

目标:

添加一个新的分析器,用于在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 
    

    其中 是要检查的 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 函数调用,参数数量:%dn", 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新增了一个检测项~

    完整提交:

    go-review.googlesource.com/c/tools/+/4…

    go-review.googlesource.com/c/go/+/4984…

    评审阶段:

    问题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 函数调用,参数数量:%dn", 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 函数调用,参数数量:%dn", argsLen)
    					}
    				} else {
    					argsLen := len(call.Args)
    					fmt.Printf("发现用户自定义的 append 函数调用,参数数量:%dn", argsLen)
    				}
    			}
    		}
    
    		return true
    	})
    
    }
    

    运行后输出:

    发现用户自定义的 append 函数调用,参数数量:4
    
    发现用户自定义的 append 函数调用,参数数量:1
    

    在修改后的代码中,添加了类型检查的步骤。通过使用types.Configtypes.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的来改: pkg.go.dev/golang.org/…

    这个比较好解决,即不要像 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("发现二元表达式:%sn", 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/analysisgolang.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 函数调用,参数数量:%dn", 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 函数调用,参数数量:%dn", 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等,在此略过。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论