前言
asong
Gogolang-lintgolint
Go语言中的静态检查是如何实现?
Go
TokenASTgo/astgo/parsergo/tokenASTASTAST
制定linter规则
Context
// example.go
package main
func add(a, b int) int {
return a + b
}
AST
*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 . . . }
方式一:标准库实现custom linter
AST
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
}
然后执行命令如下:
$ go run ./main.go -- ./example.go
./example.go:3:1: add function first params should be Context
add()AST
方式二:go/analysis
lintergo/analysisgo/analysislintergo/analysisgo vetgo/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
golang-clipkg/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)
}
makegolang-cli
总结
golang-clipkg/golintersAST
本文所有代码已经上传:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/custom_linter
好啦,本文到这里就结束了,我是asong,我们下期见。