linterを作ってみよう

golang.tokyo #14

16 April 2018

dice_zu(daisuzu)

なぜlinterを作るのか

golintやgo vetなどはあるが、

といった場合、既存のツールだとカバーできないため

どうlinterを作るのか

以下のパッケージを使って静的解析する

linterを作る時の悩みどころ

1. コマンドライン引数

特定のファイルを除外したり、必要ないルールを除外したり

それぞれastを取得する処理が異なる

・ファイル: parser.ParseFile()
・ディレクトリ: parser.ParseDir()
・パッケージ: 直接astを取得することができない

2. 結果出力

ファイル名、行番号、桁番号、内容

何か見つかったら 1 以上にしたい
けどエラー扱いにしたくないケースもあるかも?

3. テスト

astが絡むテストは難しい

ast.Node を渡して、返ってきた error をチェックする?

ファイル を渡して、 stdout / stderr をチェックする?

とても面倒!

そこでhonnef.co/go/tools/lint

とても簡単にlinterを作ることができるライブラリ

megacheck などで使われている

使い方

main.goを作る

package main

import (
    "os"

    "honnef.co/go/tools/lint/lintutil"
)

func main() {
    fs := lintutil.FlagSet("mylint")
    fs.Parse(os.Args[1:])

    confs := []lintutil.CheckerConfig{
        {Checker: NewChecker(), ExitNonZero: true},
    }

    lintutil.ProcessFlagSet(confs, fs)
}

lint.Checkerを実装する

import (
    "honnef.co/go/tools/lint"
)

func NewChecker() lint.Checker {
    return &checker{}
}

type checker struct{}

func (*checker) Name() string            { return "mylint" }
func (*checker) Prefix() string          { return "ML" }
func (*checker) Init(prog *lint.Program) {}

func (c *checker) Funcs() map[string]lint.Func {
    return map[string]lint.Func{
        "ML0001": c.Check0001,
    }
}

lint.Funcを実装する

import (
    "go/ast"

    "honnef.co/go/tools/lint"
)

func (*checker) Check0001(j *lint.Job) {
    fn := func(node ast.Node) bool {
        call, ok := node.(*ast.CallExpr)
        if !ok {
            return true
        }

        j.Errorf(call, "something is wrong")
        return true
    }
    for _, f := range j.Program.Files {
        ast.Inspect(f, fn)
    }
}

テストを書く

import (
    "testing"

    "honnef.co/go/tools/lint/testutil"
)

func TestAll(t *testing.T) {
    testutil.TestAll(t, NewChecker(), "")
}

testdata/ 配下にテスト用のファイルを置いておく

func Print() {
    fmt.Println("test") // MATCH "something is wrong"
}

具体例

datastore.RunInTransaction でTransaction Contextを使っていることをチェックするため

$ gsc -target-context="MyCtx"
ctxscope.go:39:27: passing outer scope context "c" to datastore.Get() (CtxScope)
ctxscope.go:42:27: passing outer scope context "c" to datastore.Put() (CtxScope)
ctxscope.go:60:17: passing outer scope context "ctx" to get() (CtxScope)
ctxscope.go:63:14: passing outer scope context "ctx" to put() (CtxScope)
ctxscope.go:89:20: passing outer scope context "c" to datastore.Delete() (CtxScope)

Thank you

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)