溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Go語言自定義linter靜態(tài)檢查工具怎么實現(xiàn)

發(fā)布時間:2022-05-31 13:51:39 來源:億速云 閱讀:265 作者:iii 欄目:開發(fā)技術(shù)

今天小編給大家分享一下Go語言自定義linter靜態(tài)檢查工具怎么實現(xiàn)的相關(guān)知識點,內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

    Go語言中的靜態(tài)檢查是如何實現(xiàn)?

    眾所周知Go語言是一門編譯型語言,編譯型語言離不開詞法分析、語法分析、語義分析、優(yōu)化、編譯鏈接幾個階段,學(xué)過編譯原理的朋友對下面這個圖應(yīng)該很熟悉:

    Go語言自定義linter靜態(tài)檢查工具怎么實現(xiàn)

    編譯器將高級語言翻譯成機(jī)器語言,會先對源代碼做詞法分析,詞法分析是將字符序列轉(zhuǎn)換為Token序列的過程,Token一般分為這幾類:關(guān)鍵字、標(biāo)識符、字面量(包含數(shù)字、字符串)、特殊符號(如加號、等號),生成Token序列后,需要進(jìn)行語法分析,進(jìn)一步處理后,生成一棵以 表達(dá)式為結(jié)點的 語法樹,這個語法樹就是我們常說的AST,在生成語法樹的過程就可以檢測一些形式上的錯誤,比如括號缺少,語法分析完成后,就需要進(jìn)行語義分析,在這里檢查編譯期所有能檢查靜態(tài)語義,后面的過程就是中間代碼生成、目標(biāo)代碼生成與優(yōu)化、鏈接,這里就不詳細(xì)描述了,這里主要是想引出抽象語法樹(AST),我們的靜態(tài)代碼檢查工具就是通過分析抽象語法樹(AST)根據(jù)定制的規(guī)則來做的;那么抽象語法樹長什么樣子呢?我們可以使用標(biāo)準(zhǔn)庫提供的go/ast、go/parser、go/token包來打印出AST,

    查看AST,具體AST長什么樣我們可以看下文的例子;

    制定linter規(guī)則

    假設(shè)我們現(xiàn)在要在我們團(tuán)隊制定這樣一個代碼規(guī)范,所有函數(shù)的第一個參數(shù)類型必須是Context,不符合該規(guī)范的我們要給出警告;好了,現(xiàn)在規(guī)則已經(jīng)定好了,現(xiàn)在我們就來想辦法實現(xiàn)它;先來一個有問題的示例:

    // example.go
    package main
    func add(a, b int) int {
     return a + b
    }

    對應(yīng)AST如下:

    *ast.FuncDecl {
         8  .  .  .  Name: *ast.Ident {
         9  .  .  .  .  NamePos: 3:6
        10  .  .  .  .  Name: "add"
        11  .  .  .  .  Obj: *ast.Object {
        12  .  .  .  .  .  Kind: func
        13  .  .  .  .  .  Name: "add" // 函數(shù)名
        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" // 參數(shù)名
        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  .  .  .  }

    方式一:標(biāo)準(zhǔn)庫實現(xiàn)custom linter

    通過上面的AST結(jié)構(gòu)我們可以找到函數(shù)參數(shù)類型具體在哪個結(jié)構(gòu)上,因為我們可以根據(jù)這個結(jié)構(gòu)寫出解析代碼如下:

    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
    }

    然后執(zhí)行命令如下:

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

    通過輸出我們可以看到,函數(shù)add()第一個參數(shù)必須是Context;這就是一個簡單實現(xiàn),因為AST的結(jié)構(gòu)實在是有點復(fù)雜,就不在這里詳細(xì)介紹每個結(jié)構(gòu)體了,可以看曹大之前寫的一篇文章:golang
    和 ast

    方式二:go/analysis

    看過上面代碼的朋友肯定有點抓狂了,有很多實體存在,要開發(fā)一個linter,我們需要搞懂好多實體,好在go/analysis進(jìn)行了封裝,go/analysis為linter
    提供了統(tǒng)一的接口,它簡化了與IDE,metalinters,代碼Review等工具的集成。如,任何go/analysislinter都可以高效的被go
    vet執(zhí)行,下面我們通過代碼方式來介紹go/analysis的優(yōu)勢;

    新建一個項目代碼結(jié)構(gòu)如下:

    .
    ├── 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)
    }

    命令行執(zhí)行如下:

    $ 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

    如果我們想添加更多的規(guī)則,使用golang.org/x/tools/go/analysis/multichecker追加即可。

    集成到golang-cli

    我們可以把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可執(zhí)行文件,加到我們的項目中就可以了。

    以上就是“Go語言自定義linter靜態(tài)檢查工具怎么實現(xiàn)”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注億速云行業(yè)資訊頻道。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI