Go
语言中常用的静态代码检查工具有golang-lint
、golint
,这些工具中已经制定好了一些规则,虽然已经可以满足大多数场景,但是有些时候我们会遇到针对特殊场景来做一些定制化规则的需求,所以本文我们一起来学习一下如何自定义linter需求;
众所周知Go
golang-lint
, golint
, beberapa peraturan telah dirumuskan dalam ini alat, walaupun mereka sudah boleh memenuhi kebanyakan senario, Tetapi kadangkala kami menghadapi keperluan untuk membuat beberapa peraturan tersuai untuk senario khas, jadi dalam artikel ini kami akan belajar cara menyesuaikan keperluan linter 🎜🎜🎜🎜Cara melaksanakan semakan statik dalam Go; bahasa? 🎜🎜Kita semua tahuGo
bahasa ialah bahasa penyusun Bahasa tersusun dan bahasa tersusun tidak dapat dipisahkan daripada peringkat analisis leksikal, analisis sintaksis, analisis semantik, pengoptimuman, dan penyusunan dan penghubung kaitPenyusun menterjemahkan bahasa peringkat tinggi ke dalam bahasa mesin, pertama sekali, analisis leksikal adalah proses menukar jujukan aksara kepada Token secara amnya dibahagikan kepada kategori ini : kata kunci , pengecam, literal (termasuk nombor, rentetan), simbol khas (seperti tanda tambah, tanda sama), jana Token
Selepas jujukan, analisis sintaks perlu dilakukan. Selepas pemprosesan selanjutnya, pokok sintaks dengan ungkapan sebagai nod dijana. Pepohon sintaks ini adalah perkara yang sering kita lakukan. sebut AST
, dalam proses menjana sintaks pepohon Anda boleh mengesan beberapa ralat formal, seperti kurungan yang hilang Selepas analisis sintaks selesai, analisis semantik diperlukan Di sini, semua semantik statik yang boleh disemak semasa tempoh penyusunan proses berikut adalah penjanaan kod perantaraan penjanaan dan pengoptimuman kod, dan memautkan , saya tidak akan menerangkannya secara terperinci di sini. Tujuan utama di sini ialah untuk memperkenalkan pokok sintaks abstrak (AST). ) mengikut peraturan tersuai; maka pokok sintaks abstrak adalah panjang Bagaimana rupanya? Kita boleh menggunakan go/ast
、go/parser
、 go/token
pakej untuk mencetakAST code>, specificKita boleh lihat contoh di bawah untuk apa yang AST
kelihatan seperti ;
// example.go package main func add(a, b int) int { return a + b }
*ast.FuncDecl { 8 . . . Name: *ast.Ident { 9 . . . . NamePos: 3:6 10 . . . . Name: "add" 11 . . . . Obj: *ast.Object { 12 . . . . . Kind: func 13 . . . . . Name: "add" // 函数名 14 . . . . . Decl: *(obj @ 7) 15 . . . . } 16 . . . } 17 . . . Type: *ast.FuncType { 18 . . . . Func: 3:1 19 . . . . Params: *ast.FieldList { 20 . . . . . Opening: 3:9 21 . . . . . List: []*ast.Field (len = 1) { 22 . . . . . . 0: *ast.Field { 23 . . . . . . . Names: []*ast.Ident (len = 2) { 24 . . . . . . . . 0: *ast.Ident { 25 . . . . . . . . . NamePos: 3:10 26 . . . . . . . . . Name: "a" 27 . . . . . . . . . Obj: *ast.Object { 28 . . . . . . . . . . Kind: var 29 . . . . . . . . . . Name: "a" 30 . . . . . . . . . . Decl: *(obj @ 22) 31 . . . . . . . . . } 32 . . . . . . . . } 33 . . . . . . . . 1: *ast.Ident { 34 . . . . . . . . . NamePos: 3:13 35 . . . . . . . . . Name: "b" 36 . . . . . . . . . Obj: *ast.Object { 37 . . . . . . . . . . Kind: var 38 . . . . . . . . . . Name: "b" 39 . . . . . . . . . . Decl: *(obj @ 22) 40 . . . . . . . . . } 41 . . . . . . . . } 42 . . . . . . . } 43 . . . . . . . Type: *ast.Ident { 44 . . . . . . . . NamePos: 3:15 45 . . . . . . . . Name: "int" // 参数名 46 . . . . . . . } 47 . . . . . . } 48 . . . . . } 49 . . . . . Closing: 3:18 50 . . . . } 51 . . . . Results: *ast.FieldList { 52 . . . . . Opening: - 53 . . . . . List: []*ast.Field (len = 1) { 54 . . . . . . 0: *ast.Field { 55 . . . . . . . Type: *ast.Ident { 56 . . . . . . . . NamePos: 3:20 57 . . . . . . . . Name: "int" 58 . . . . . . . } 59 . . . . . . } 60 . . . . . } 61 . . . . . Closing: - 62 . . . . } 63 . . . }
Context
,不符合该规范的我们要给出警告;好了,现在规则已经定好了,现在我们就来想办法实现它;先来一个有问题的示例:
package main import ( "fmt" "go/ast" "go/parser" "go/token" "log" "os" ) func main() { v := visitor{fset: token.NewFileSet()} for _, filePath := range os.Args[1:] { if filePath == "--" { // to be able to run this like "go run main.go -- input.go" continue } f, err := parser.ParseFile(v.fset, filePath, nil, 0) if err != nil { log.Fatalf("Failed to parse file %s: %s", filePath, err) } ast.Walk(&v, f) } } type visitor struct { fset *token.FileSet } func (v *visitor) Visit(node ast.Node) ast.Visitor { funcDecl, ok := node.(*ast.FuncDecl) if !ok { return v } params := funcDecl.Type.Params.List // get params // list is equal of zero that don't need to checker. if len(params) == 0 { return v } firstParamType, ok := params[0].Type.(*ast.SelectorExpr) if ok && firstParamType.Sel.Name == "Context" { return v } fmt.Printf("%s: %s function first params should be Context\n", v.fset.Position(node.Pos()), funcDecl.Name.Name) return v }
对应AST
如下:
$ go run ./main.go -- ./example.go ./example.go:3:1: add function first params should be Context
通过上面的AST
结构我们可以找到函数参数类型具体在哪个结构上,因为我们可以根据这个结构写出解析代码如下:
. ├── firstparamcontext │ └── firstparamcontext.go ├── go.mod ├── go.sum └── testfirstparamcontext ├── example.go └── main.go
然后执行命令如下:
package firstparamcontext import ( "go/ast" "golang.org/x/tools/go/analysis" ) var Analyzer = &analysis.Analyzer{ Name: "firstparamcontext", Doc: "Checks that functions first param type is Context", Run: run, } func run(pass *analysis.Pass) (interface{}, error) { inspect := func(node ast.Node) bool { funcDecl, ok := node.(*ast.FuncDecl) if !ok { return true } params := funcDecl.Type.Params.List // get params // list is equal of zero that don't need to checker. if len(params) == 0 { return true } firstParamType, ok := params[0].Type.(*ast.SelectorExpr) if ok && firstParamType.Sel.Name == "Context" { return true } pass.Reportf(node.Pos(), "''%s' function first params should be Context\n", funcDecl.Name.Name) return true } for _, f := range pass.Files { ast.Inspect(f, inspect) } return nil, nil }
通过输出我们可以看到,函数add()
第一个参数必须是Context;这就是一个简单实现,因为AST
Kaedah 1: Perpustakaan standard melaksanakan linter tersuai
AST
struktur Kita boleh mengetahui struktur mana jenis parameter fungsi dihidupkan, kerana kita boleh menulis kod parsing berdasarkan struktur ini seperti berikut : 🎜package main import ( "asong.cloud/Golang_Dream/code_demo/custom_linter/firstparamcontext" "golang.org/x/tools/go/analysis/singlechecker" ) func main() { singlechecker.Main(firstparamcontext.Analyzer) }
$ go run ./main.go -- ./example.go /Users/go/src/asong.cloud/Golang_Dream/code_demo/custom_linter/testfirstparamcontext/example.go:3:1: ''add' function first params should be Context
Parameter pertama add()
mestilah Konteks; ini ialah pelaksanaan mudah, kerana AST
Struktur ialah memang agak rumit, jadi saya tidak akan memperkenalkan setiap satu secara terperinci di sini Untuk struktur, anda boleh membaca artikel yang ditulis oleh Cao Da sebelum ini: golang dan ast🎜看过上面代码的朋友肯定有点抓狂了,有很多实体存在,要开发一个linter
,我们需要搞懂好多实体,好在go/analysis
进行了封装,go/analysis
为linter
提供了统一的接口,它简化了与IDE,metalinters,代码Review等工具的集成。如,任何go/analysis
linter都可以高效的被go vet
执行,下面我们通过代码方式来介绍go/analysis
的优势;
新建一个项目代码结构如下:
. ├── firstparamcontext │ └── firstparamcontext.go ├── go.mod ├── go.sum └── testfirstparamcontext ├── example.go └── main.go
添加检查模块代码,在firstparamcontext.go
添加如下代码:
package firstparamcontext import ( "go/ast" "golang.org/x/tools/go/analysis" ) var Analyzer = &analysis.Analyzer{ Name: "firstparamcontext", Doc: "Checks that functions first param type is Context", Run: run, } func run(pass *analysis.Pass) (interface{}, error) { inspect := func(node ast.Node) bool { funcDecl, ok := node.(*ast.FuncDecl) if !ok { return true } params := funcDecl.Type.Params.List // get params // list is equal of zero that don't need to checker. if len(params) == 0 { return true } firstParamType, ok := params[0].Type.(*ast.SelectorExpr) if ok && firstParamType.Sel.Name == "Context" { return true } pass.Reportf(node.Pos(), "''%s' function first params should be Context\n", funcDecl.Name.Name) return true } for _, f := range pass.Files { ast.Inspect(f, inspect) } return nil, nil }
然后添加分析器:
package main import ( "asong.cloud/Golang_Dream/code_demo/custom_linter/firstparamcontext" "golang.org/x/tools/go/analysis/singlechecker" ) func main() { singlechecker.Main(firstparamcontext.Analyzer) }
命令行执行如下:
$ go run ./main.go -- ./example.go /Users/go/src/asong.cloud/Golang_Dream/code_demo/custom_linter/testfirstparamcontext/example.go:3:1: ''add' function first params should be Context
如果我们想添加更多的规则,使用golang.org/x/tools/go/analysis/multichecker
追加即可。
我们可以把golang-cli
的代码下载到本地,然后在pkg/golinters
下添加firstparamcontext.go
,代码如下:
import ( "golang.org/x/tools/go/analysis" "github.com/golangci/golangci-lint/pkg/golinters/goanalysis" "github.com/fisrtparamcontext" ) func NewfirstparamcontextCheck() *goanalysis.Linter { return goanalysis.NewLinter( "firstparamcontext", "Checks that functions first param type is Context", []*analysis.Analyzer{firstparamcontext.Analyzer}, nil, ).WithLoadMode(goanalysis.LoadModeSyntax) }
然后重新make
一个golang-cli
可执行文件,加到我们的项目中就可以了;
golang-cli
仓库中pkg/golinters
目录下存放了很多静态检查代码,学会一个知识点的最快办法就是抄代码,先学会怎么使用的,慢慢再把它变成我们自己的;本文没有对AST
标准库做过多的介绍,因为这部分文字描述比较难以理解,最好的办法还是自己去看官方文档、加上实践才能更快的理解。
本文所有代码已经上传:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/custom_linter
Atas ialah kandungan terperinci Cara menyesuaikan linter (alat semakan statik) dalam bahasa Go. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!