linter的原理是通过静态代码分析,发现其中隐藏的错误或者不符合规范的地方,然后给暴露出来,提升系统的稳定性。linter扫描的过程如下:首先进行词法分析得到一系列token,然后通过语法分析得到抽象语法树,接着通过inspector或者visitor模式提取我们感兴趣的语法单元,结合我们的规范,对比发现其中的差异,将差异暴露出来。
那么如何定义一个linter呢,首先我们从一个简单的demo开始,目标是扫描出函数第一个参数不是context.Context的函数,它可以作为我们代码提交后的lint工具。demo如下:
package main
import(
"fmt"
)
func add(a, b int) int {
return a b
}
func main() {
add(1, 2)
fmt.Println(add(1, 2))
}
我们的的linter可以简单这么实现
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 Contextn",
v.fset.Position(node.Pos()), funcDecl.Name.Name)
return v
}
通过visitor模式,获取函数的第一个参数,判断类型不是我们需要的类型,就报错。执行结果如下:
% go run ./json/linter/exp1/main.go -- ./json/linter/demo/main.go
./json/linter/demo/main.go:4:1: add function first params should be Context
上述过程虽然能够满足我们的需求,但是,没法集成到通用的linter工具里面,我们可以使用golang官方的包"golang.org/x/tools/go/analysis"进行实现
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 Contextn",
funcDecl.Name.Name)
return true
}
for _, f := range pass.Files {
ast.Inspect(f, inspect)
}
return nil, nil
}
然后用signlechecker来验证下它的功能
package main
import (
"golang.org/x/tools/go/analysis/singlechecker"
"learn/json/linter/exp2/firstparamcontext"
)
func main() {
singlechecker.Main(firstparamcontext.Analyzer)
}
执行结果如下:
% go run ./json/linter/exp2/main.go -- ./json/linter/demo/main.go
/Users/xiazemin/bilibili/live/learn/json/linter/demo/main.go:4:1: ''add' function first params should be Context
exit status 3
我们一般是使用https://github.com/golangci/golangci-lint来实现代码扫描的,我们的linter工具如何集成到golangci-lint里面呢?
首先,我们可以定义好linter工具
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 Contextn",
funcDecl.Name.Name)
return true
}
for _, f := range pass.Files {
ast.Inspect(f, inspect)
}
return nil, nil
}
然后在golang-cli仓库中pkg/golinters目录下引入我们的linter
package golinters
import (
"golang.org/x/tools/go/analysis"
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
"github.com/xiazemin/firstparamcontext"
)
func NewfirstparamcontextCheck() *goanalysis.Linter {
return goanalysis.NewLinter(
"firstparamcontext",
"Checks that functions first param type is Context",
[]*analysis.Analyzer{firstparamcontext.Analyzer},
nil,
).WithLoadMode(goanalysis.LoadModeSyntax)
}
紧接着在learn/json/linter/golangci-lint/pkg/lint/lintersdb/manager.go中引入,否则在命令行中看不到
lcs := []*linter.Config{
linter.NewConfig(golinters.NewfirstparamcontextCheck()).
WithSince("0.0.0").
WithPresets(linter.PresetBugs).
WithLoadForGoAnalysis().
WithURL("github.com/xiazemin/firstparamcontext"),
然后进行编译,注意把makefile里面//export GOPROXY = https://proxy.golang.org替换为//export GOPROXY = https://proxy.cn
cd golangci-lint
% make
或者
go build -o golangci-lint ./cmd/golangci-lint
然后到我们的demo目录下测验下
% ../golangci-lint/golangci-lint linters
// Disabled by your configuration linters:
// firstparamcontext: Checks that functions first param type is Context [fast: false, auto-fix: false]
查看下lint结果
```
% ../golangci-lint/golangci-lint run -E firstparamcontext
main.go:8:1: ''add' function first params should be Context (firstparamcontext)
func add(a, b int) int {
^
main.go:13:2: SA4017: add doesn't have side effects and its return value is ignored (staticcheck)
add(1, 2)
^
`