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

缘起:

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

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

go 复制代码
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 的一些常见用途和功能:

  1. 错误检查:Go vet 可以检查代码中的语法错误、类型错误、未使用的变量和导入包、无法到达的代码等。它能够捕捉到一些编译器可能会忽略的问题,从而帮助开发者在编译之前就能够发现这些潜在错误。

  2. 常见问题检测:Go vet 还可以检查代码中的常见问题,例如无用的 if 条件、无法到达的代码、错误的函数调用等。它可以帮助开发者避免一些常见的错误模式和不良实践。

  3. 代码规范检查: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 复制代码
go vet <package>

其中 <package> 是要检查的 Go 包的路径。Go vet 将会分析指定包及其依赖的代码,并输出潜在的问题和修复建议。

Go vet 是一个帮助开发者在编译之前发现代码错误和潜在问题的工具,它可以提高代码的质量和可靠性,并帮助开发者编写更健壮的 Go 代码。

Go 1.12 - 2019 年 2 月 版本在analysis包基础上重写了 go vet 命令,这个包有着更大的灵活性,允许开发人员编写自己的代码检查工具。

go vet 命令可作为一系列不同源代码分析工具的基础。

举几个例子,go vet会输出提示

当使用 Go vet 进行静态代码分析时,它可能会输出一些提示来指出代码中的潜在问题。以下是一些示例:

  • 未使用的变量:
go 复制代码
package main

import "fmt"

func main() {
    unusedVar := 42 // 声明了一个未使用的变量
    fmt.Println("Hello, Go!")
}

运行 go vet 命令后,会输出以下提示:

go 复制代码
# command-line-arguments
./main.go:6:6: unusedVar declared but not used

这个提示表明变量 unusedVar 被声明但未被使用,可能是代码中的一个错误或者不必要的代码。

  • 错误的函数调用:
go 复制代码
package main

import "fmt"

func main() {
    fmt.Println("Hello, Go!")
    fmt.Printl("Oops!") // 错误的函数名
}

运行 go vet 命令后,会输出以下提示:

go 复制代码
# 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!")

  • 错误的类型转换:
go 复制代码
package main

import "fmt"

func main() {
    var x float64 = 3.14
    y := int(x) // 错误的类型转换
    fmt.Println(y)
}

运行 go vet 命令后,会输出以下提示:

go 复制代码
# command-line-arguments
./main.go:7:10: possible misuse of conversion

这个提示表明在第 7 行的类型转换 int(x) 可能存在问题,建议检查转换的正确性。

以上只是一些简单的示例,Go vet 可以检查更多的问题并提供相应的提示。通过运行 go vet 命令并检查输出,开发者可以及早发现代码中的潜在问题,并根据提示进行修复和改进。

append参数的问题

go 复制代码
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:

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:

bash 复制代码
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 的源代码:

bash 复制代码
go/src/cmd/vet

在这个目录中,你会找到 Go vet 工具的实现文件和其他相关文件,包括命令行解析、静态分析逻辑等。这些文件一起构成了 Go vet 工具的代码。

go 复制代码
// 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 方法,在其中编写检查逻辑。可以参考其他现有分析器的实现作为参考。

go 复制代码
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 命令来运行新的分析器,例如:

shell 复制代码
go vet ./...

这样,新的分析器就会检查代码中是否存在 append(sli) 后没有追加值的情况,并报告相应的问题。

(chatgpt给的代码有误,此处需要自行开发&调试)

实践一下:

对于

go 复制代码
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

内容如下:

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 包是用于静态代码分析的两个不同的包,它们有以下区别:

  1. go/analysis 包:这是 Go 语言官方提供的用于编写静态代码分析工具的包。它提供了一组标准的接口和工具,用于创建、注册和执行各种代码分析器。这些分析器可以用于检查代码中的错误、不良实践、性能问题等。go/analysis 包提供了一种通用的框架,使得开发者能够方便地创建自定义的代码分析器,并与其他工具(如 go vet、golangci-lint 等)集成。这个包被广泛用于 Go 代码分析工具的开发。

  2. 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

请将以下行:

go 复制代码
pass.ReportRangef(call.Lparen, "called without values to append")

修改为:

go 复制代码
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,内容如下:

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/timeformatgo/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

go 复制代码
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的其他函数。

解释该代码段的逻辑如下:

  1. pass.TypesInfo是一个用于类型信息的结构体或对象,Uses是其中的一个字段。这个字段存储了程序中标识符(id)与其使用位置相关联的信息。

  2. 通过pass.TypesInfo.Uses[id],可以获取与标识符id关联的使用信息。

  3. 使用断言(type assertion)操作符.(*types.Builtin)将获取的使用信息转换为types.Builtin类型。这是因为内置函数append属于types.Builtin类型。

  4. 如果转换成功,那么表示使用的是内置函数append,可以在if语句块内执行相应的逻辑。

总而言之,这段代码的目的是判断一个标识符是否引用了内置函数append,并在满足条件时执行特定的逻辑。

举例说明:

下面是一个使用该代码片段的示例:

go 复制代码
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方法,如下

go 复制代码
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{}

}

检测

go 复制代码
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
	})

}

输出:

go 复制代码
发现 append 函数调用,参数数量:4
发现 append 函数调用,参数数量:1

进行修改:

go 复制代码
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
	})

}

运行后输出:

go 复制代码
发现用户自定义的 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中下面这段代码什么作用?请举例说明

go 复制代码
	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	nodeFilter := []ast.Node{
		(*ast.BinaryExpr)(nil),
	}
	inspect.Preorder(nodeFilter, func(n ast.Node) {}

这段代码用于在Go语言的静态分析框架中,使用inspector来对AST节点进行预定义类型的先序遍历。

让我们来逐行解释这段代码的作用:

  1. inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector):首先,我们从pass.ResultOf中获取与inspect.Analyzer相关联的结果,并将其转换为*inspector.Inspector类型。这里假设inspect.Analyzer是一个静态分析器,其执行结果包含了inspector,该inspector用于遍历AST。

  2. nodeFilter := []ast.Node{ (*ast.BinaryExpr)(nil), }:我们创建一个AST节点过滤器nodeFilter,其中只包含一个元素,即*ast.BinaryExpr类型的节点。这个过滤器指定了我们只关心二元表达式节点。

  3. inspect.Preorder(nodeFilter, func(n ast.Node) {}:使用Preorder方法进行先序遍历,遍历的起点是AST根节点。我们传递了节点过滤器nodeFilter和一个匿名函数作为参数。匿名函数会在遍历到满足过滤器条件的节点时被调用。

举个例子,假设我们有以下Go代码片段:

go 复制代码
package main

import "fmt"

func main() {
	a := 1 + 2
	b := a * 3
	fmt.Println(b)
}

如果我们将上述代码传递给一个自定义的静态分析器,并使用上述代码片段作为分析的输入,可以使用上述代码片段中的代码来遍历AST并找到二元表达式节点。

go 复制代码
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/analysisgolang.org/x/tools/go/ast/inspector等)。在实际使用中,您可能需要进行适当的导入和配置,以便正确运行和应用静态分析器。

给的代码完全无法编译..

帮我使用此,实现检测append后面的元素数量的功能

帮我使用

go 复制代码
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后面的元素数量的功能时,可以按照以下步骤进行:

  1. 导入必要的包和库:
go 复制代码
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"
)
  1. 创建自定义的静态分析器:
go 复制代码
type appendAnalyzer struct{}
  1. 实现静态分析器的Run方法:
go 复制代码
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
}
  1. 实现静态分析器的String方法:
go 复制代码
func (a *appendAnalyzer) String() string {
	return "appendAnalyzer"
}
  1. main函数中创建静态分析器集合并运行分析器:
go 复制代码
func main() {
	analyzer := &appendAnalyzer{}

	analyzers := []*analysis.Analyzer{
		inspect.Analyzer,
		analyzer,
	}

	analysis.Run(analyzers)
}

完整的示例代码如下:

go 复制代码
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

最后可用的代码:

go 复制代码
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等,在此略过。

相关推荐
追逐时光者3 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_3 小时前
敏捷开发流程-精简版
前端·后端
苏打水com4 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧5 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧5 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧5 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧5 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧5 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng6 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6016 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring