当前位置: 首页 > 科技观察

Go语言如何自定义linter(静态检查工具)

时间:2023-03-13 17:47:53 科技观察

前言大家好,我是asong;通常我们在业务项目中使用静态代码检查工具来保证代码质量。通过静态代码检查工具,我们可以提前发现一些问题,比如变量未定义、类型不匹配、变量作用域问题、数组下标越界、内存泄漏等,该工具会对问题的严重程度进行分级根据自己的规则,给出不同的标志和提示。静态代码检查帮助我们尽早发现问题。Go语言中常用的静态代码检查工具有golang-lint和golint。这些工具中制定了一些规则。虽然它们可以满足大部分场景,但是有时候我们会遇到需要针对特殊场景做一些自定义规则的需求,那么本文就来学习一下如何自定义linter需求;Go语言中的静态检查是如何实现的?众所周知,Go语言是一种编译型语言,而编译型语言是分离出来的,没有开启词法分析、句法分析、语义分析、优化、编译链接等阶段,学过编译原理的朋友应该是熟悉下图:编译器将高级语言翻译成机器语言,首先会对源代码进行词法分析,词法分析就是将字符序列转化为Token序列的过程。Token一般分为这几类:关键字、标识符、字面量(包括数字和字符串)、特殊符号(如加号、等号),Token序列生成后,需要进行语法分析。进一步处理后,生成以表达式为节点的语法树。这个语法树就是我们常说的AST。在生成语法树的过程中,可以检测到一些形式错误。错误,如缺少括号,语法分析完成后,需要进行语义分析。在这里勾选,编译时可以检查所有的静态语义。后续过程是中间代码生成、目标代码生成和优化、链接,这里不再详细描述,这里的主要目的是介绍抽象语法树(AST)。我们的静态代码检查工具是通过根据自定义规则分析抽象语法树(AST)来完成的;那么抽象语法树长什么样子呢?我们可以使用标准库提供的go/ast、go/parser、go/token包来打印出AST,也可以使用可视化工具:http://goast.yuroyoro.net/查看AST,我们可以看到特定AST的以下示例如下所示;制定linter规则假设我们想在我们的团队中制定这样的代码规范。所有函数的第一个参数类型必须是Context,不符合规范我们会给出警告;好吧,现在规则已经制定好了,现在我们来看看如何实现它;让我们从一个有问题的例子开始://例子。gopackagemainfuncadd(a,bint)int{returna+b}对应AST如下:*ast.FuncDecl{8...名称:*ast.Ident{9....名称位置:3:610....名称:“添加”11....对象:*ast.Object{12.....种类:func13。....Name:"add"//随意数名14.....声明:*(obj@7)15。...}16。..}17。..输入:*ast.FuncType{18....功能:3:119....参数:*ast.FieldList{20.....开场:3:921.....列表:[]*ast.Field(len=1){22......0:*ast.Field{23.......名称:[]*ast.Ident(len=2){24........0:*ast.Ident{25.........姓名位置:3:1026.........名称:“一”27.........对象:*ast.Object{28..........种类:var29。.........名称:“一”30..........声明:*(obj@22)31。........}32。.......}33。.......1:*ast.Ident{34.........姓名位置:3:1335.........名称:“b”36.........对象:*ast.Object{37..........种类:var38。.........名称:“b”39..........声明:*(obj@22)40。........}41。.......}42。......}43。......输入:*ast.Ident{44........姓名位置:3:1545........Name:"int"//参数名46.......}47。.....}48。....}49。....结束时间:3:1850。...}51。...结果:*ast.FieldList{52.....开幕式:-53。....列表:[]*ast.Field(len=1){54......0:*ast.Field{55.......输入:*ast.Ident{56........名称位置:3:2057........名称:“int”58。.....}59。....}60。...}61。.......}方法一:标准库实现自定义linter通过上面的AST结构,我们可以找出函数参数类型在哪个结构中,因为我们可以根据这个结构编写解析代码如下:packagemainimport("fmt""go/ast""go/parser""go/token""log""os")funcmain(){v:=visitor{fset:token.NewFileSet()}for_,filePath:=rangeos.Args[1:]{iffilePath=="--"{//能够像"gorunmain.go--input.go"continue}f,err:=parser.ParseFile(v.fset,filePath,nil,0)iferr!=nil{log.Fatalf("无法解析文件%s:%s",filePath,err)}ast.Walk(&v,f)}}typevisitorstruct{fset*token.FileSet}func(v*visitor)Visit(nodeast.Node)ast.Visitor{funcDecl,ok:=node.(*ast.FuncDecl)if!ok{returnv}params:=funcDecl.Type.Params.List//getparams//列表等于零,不需要检查。iflen(params)==0{returnv}firstParamType,ok:=params[0].Type.(*ast.SelectorExpr)ifok&&firstParamType.Sel.Name=="Context"{返回v}fmt.Printf("%s:%sfunctionfirstparamsshouldbeContext\n",v.fset.Position(node.Pos()),funcDecl.Name.Name)returnv}然后执行命令如下:$gorun。/main.go--./example.go./example.go:3:1:addfunctionfirstparamsshouldbeContext通过输出我们可以看到函数add()的第一个参数必须是Context;这是一个简单的实现,因为AST的结构有点复杂,这里就不一一介绍了。是一个结构体,可以看曹大之前写的一篇文章:golang和ast方法二:go/analysis看了上面代码的朋友肯定有点抓狂,实体很多,要开发一个linter,我们需要搞实体我了解很多。幸运的是,go/analysis已经对它进行了封装。Go/analysis为linter提供了统一的接口,简化了与IDE、metalinters、codereview等工具的集成例如,任何go/analysislinter都可以被govet高效执行。下面我们通过代码来介绍go/analysis的优势;新项目的代码结构如下:.├──firstparamcontext│└──firstparamcontext.go├──go.mod├──go.sum└──testfirstparamcontext├──example.go└──main.go添加检查模块代码,在firstparamcontext.go中添加如下代码:Doc:"ChecksthatfunctionsfirstparamtypeisContext",Run:run,}funcrun(pass*analysis.Pass)(interface{},error){inspect:=func(nodeast.Node)bool{funcDecl复制代码,ok:=node.(*ast.FuncDecl)if!ok{returntrue}params:=funcDecl.Type.Params.List//getparams//列表等于零,不需要检查。如果len(params)==0{returntrue}firstParamType,ok:=params[0].Type.(*ast.SelectorExpr)ifok&&firstParamType.Sel.Name=="Context"{returntrue}pass.Reportf(node.Pos(),"''%s'functionfirstparamsshouldbeContext\n",funcDecl.Name.Name)returntrue}for_,f:=rangepass.Files{ast.Inspect(f,inspect)}returnnil,nil}然后添加分析器:packagemainimport("asong.cloud/Golang_Dream/code_demo/custom_linter/firstparamcontext""golang.org/x/tools/go/analysis/singlechecker")funcmain(){singlechecker.Main(firstparamcontext.Analyzer)}命令行执行如下:$gorun./main.go--./example.go/Users/go/src/asong.cloud/Golang_Dream/code_demo/custom_linter/testfirstparamcontext/example.go:3:1:''add'functionfirstparamsshouldbeContextif我们要添加更多的规则,使用golang.org/x/tools/go/analysis/multichecker来添加集成到golang-cli中,我们可以下载golang-cli的代码到本地,然后在pkg/golints下添加firstparamcontext.go,代码如下:import("golang.org/x/tools/go/analysis""github.com/golangci/golangci-lint/pkg/golinters/goanalysis""github.com/fisrtparamcontext")funcNewfirstparamcontextCheck()*goanalysis.Linter{returngoanalysis.NewLinter("firstparamcontext","首先检查函数paramtypeisContext",[]*analysis.Analyzer{firstparamcontext.Analyzer},nil,).WithLoadMode(goanalysis.LoadModeSyntax)}然后重新制作一个golang-cli可执行文件并添加到我们的项目中;总结golang-cli仓库pkg/golints目录下存放了很多静态检查代码。学习一个知识点最快的方法就是复制代码,先学会使用,然后慢慢变成我们自己的;本文对AST标准库并没有做过多的介绍,因为这部分的文字描述比较难理解,最好的办法是自己阅读官方文档,加练习理解更快。本文所有代码已上传:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/custom_linter