为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等,在此略过。

相关推荐
0zxm9 分钟前
06 - Django 视图view
网络·后端·python·django
m0_7482571810 分钟前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
小_太_阳1 小时前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
智慧老师1 小时前
Spring基础分析13-Spring Security框架
java·后端·spring
搬码后生仔3 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
凡人的AI工具箱3 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
Lx3523 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
小池先生4 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
百罹鸟4 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
小蜗牛慢慢爬行5 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate