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 Context\n",
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 Context\n",
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 Context\n",
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)
^
```