Usually we use static code checking tools to ensure code quality in our business projects, through static code checking tools we can find some problems in advance, such as undefined variables, type mismatches, variable scope problems, array subscript overruns, memory leaks, etc. The tools will classify the severity of the problem according to their own rules, giving different signs and hints, static code checking The static code checker helps us to find the problem as early as possible, the common static code checker tools in Go language are golang-lint, golint, these tools have developed some rules, although it can meet most of the scenarios, but sometimes we will encounter the need to do some customization rules for special scenarios, so in this article we learn how to custom linter requirements.

How is static checking implemented in Go language?

It is well known that Go language is a compiled language, and compiled language cannot be separated from lexical analysis, syntax analysis, semantic analysis, optimization, compilation and linking several stages, those who have learned the compilation principle should be familiar with the following diagram.

Compilation Principle

The compiler will translate the high-level language into machine language, will first do lexical analysis of the source code, lexical analysis is the process of converting the sequence of characters into a Token sequence, Token is generally divided into these categories: keywords, identifiers, literal (including numbers, strings), special symbols (such as the plus sign, etc.), after generating the Token sequence, the need for syntax analysis, after further processing, to generate A syntax tree with expressions as nodes, this syntax tree is what we often call AST, in the process of generating syntax tree can detect some formal errors, such as the lack of brackets, syntax analysis is completed, it is necessary to carry out semantic analysis, where all the compilation period can check the static semantics, the latter process is the intermediate code generation, target code generation and optimization, linking, here is not Detailed description, here mainly want to introduce the abstract syntax tree (AST), our static code checking tool is through the analysis of the abstract syntax tree (AST) according to the custom rules to do; so what does the abstract syntax tree look like? We can use the go/ast, go/parser, go/token packages provided by the standard library to print out the AST, or we can use the visualization tool: http://goast.yuroyoro.net/ to view the AST, and we can see the following examples of what the AST looks like.

Develop linter rules

Suppose we now want to develop such a code specification in our team, the first argument type of all functions must be Context, and we have to give a warning if it does not conform to the specification; well, now that the rule has been set, let’s now figure out how to implement it; first, a problematic example.

1
2
3
4
5
6
// example.go
package main

func add(a, b int) int {
 return a + b
}

Corresponds to AST as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
*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  .  .  .  }

way one: standard library implementation custom linter

With the AST structure above we can find out exactly which structure the function parameter type is on, as we can write the parsing code based on this structure as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
}

Then execute the following command.

1
2
$ go run ./main.go -- ./example.go
./example.go:3:1: add function first params should be Context

We can see from the output that the first argument of the function add() must be Context; this is a simple implementation, because the structure of AST is really a bit complicated, so we won’t go into detail about each structure here.

way two: go/analysis

Those who have read the above code must be a bit crazy, there are many entities exist, to develop a linter, we need to understand many entities, good thing go/analysis is encapsulated, go/analysis provides a unified interface for linter, it simplifies the integration with IDE, metalinters, code Review and other tools. For example, any go/analysis linter can be efficiently executed by go vet, and we introduce the advantages of go/analysis by way of code below.

The code structure of a new project is as follows.

1
2
3
4
5
6
7
8
.
├── firstparamcontext
│   └── firstparamcontext.go
├── go.mod
├── go.sum
└── testfirstparamcontext
    ├── example.go
    └── main.go

Add the check module code to firstparamcontext.go by adding the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
}

Then add the analyzer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)
}

Execute the following command.

1
2
$ 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

If we want to add more rules, just use golang.org/x/tools/go/analysis/multichecker to append them.

Integration to golang-cli

We can download the code of golang-cli locally and add firstparamcontext.go under pkg/golinters with the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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)
}

Then re-make a golang-cli executable file, add it to our project and that’s it.

Summary

The pkg/golinters directory in the golang-cli repository stores a lot of static checking code, the fastest way to learn a point of knowledge is to copy the code, first learn how to use it, and then slowly make it our own; this article does not do too much introduction to the AST standard library, because this part of the text description is difficult to understand, the best way or their own The best way is to read the official documentation, plus practice to understand faster.