Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Welcome_to_Linter

 Welcome_to_Linter

Junki Kaneko

October 28, 2019
Tweet

More Decks by Junki Kaneko

Other Decks in Technology

Transcript

  1. Welcome to Linter
    Go Conference 2019 Autumn
    Junki Kaneko

    View Slide

  2. 自己紹介
    金子 淳貴
    ● SWET (Software Engineer in Test)
    ● 普段の業務
    ○ Go プロダクトの品質改善
    ○ CI や開発プロセスの改善
    ○ それらに伴うツールやサービス開発

    View Slide

  3. この発表のゴール
    • Goのプロダクトにおいて、 Linter 導入やカスタムLinterの開発に関しての
    一通り必要な知識を知ること

    View Slide

  4. Agenda
    1. 背景
    2. Go における静的解析
    3. カスタムLinter の開発
    4. プロダクトに Linter を導入する

    View Slide

  5. 1. 背景

    View Slide

  6. そもそも Linter とは ?
    コンパイラで検出できないエラーや構文を静的解析によって検査するツールのこと

    View Slide

  7. 開発における起こりがちな問題
    • コードレビューで本質的ではない指摘が多くなってしまいがち
    • コーディングスタイル
    • 命名規則
    • etc...
    • コード品質が開発者によって違う
    • エラーを毎回チェックしていないコード
    • panicが入っているミドルウェア
    • etc...

    View Slide

  8. Linter を使って解決
    • 一定のコード品質を機械的に担保できる
    • 一貫性
    • コーディングスタイルや命名規則
    • 簡潔性
    • 無駄な処理が入っていないか、複雑すぎないか
    • etc...
    • コードレビューする際の初期品質が向上
    • コードレビューの負荷軽減

    View Slide

  9. 2. Go における静的解析

    View Slide

  10. Go のコンパイルの流れ(簡易版)
    1. ソースコード
    2. トークン (go/token)
    a. ソースコードを読み込み、トークンへ変換する (go/scanner, go/token)
    3. AST (go/ast)
    a. トークンを抽象構文木 (AST) へ変換する (go/parser)
    b. 型チェック (go/types, go/constant)
    4. SSA形式 (golang.org/x/tools/go/ssa)
    5. マシンコードの生成

    View Slide

  11. Go のコンパイルの流れ(簡易版)
    1. ソースコード
    2. トークン (go/token)
    a. ソースコードを読み込み、トークンへ変換する (go/scanner, go/token)
    3. AST (go/ast)
    a. トークンを抽象構文木 (AST) へ変換する (go/parser)
    b. 型チェック (go/types, go/constant)
    4. SSA形式 (golang.org/x/tools/go/ssa)
    5. マシンコードの生成
    基本的にLinterはAST, SSAを元にチェックを
    行う

    View Slide

  12. Go のコンパイルの流れ(簡易版)
    1. ソースコード
    2. トークン (go/token)
    a. ソースコードを読み込み、トークンへ変換する (go/scanner, go/token)
    3. AST (go/ast)
    a. トークンを抽象構文木 (AST) へ変換する (go/parser)
    b. 型チェック (go/types, go/constant)
    4. SSA形式 (golang.org/x/tools/go/ssa)
    5. マシンコードの生成
    基本的にLinterはAST, SSAを元にチェックを
    行う

    View Slide

  13. Go の抽象構文木 (AST)
    0 *ast.File {
    1 . Package: helloworld/helloworld.go:1:1
    2 . Name: *ast.Ident {
    3 . . NamePos: helloworld/helloworld.go:1:9
    4 . . Name: "helloworld"
    5 . }
    6 . Decls: []ast.Decl (len = 1) {
    7 . . 0: *ast.FuncDecl {
    8 . . . Name: *ast.Ident {
    9 . . . . NamePos: helloworld/helloworld.go:3:6
    10 . . . . Name: "HelloWorld"
    11 . . . . Obj: *ast.Object {
    12 . . . . . Kind: func
    13 . . . . . Name: "HelloWorld"
    14 . . . . . Decl: *(obj @ 7)
    15 . . . . }
    16 . . . }
    17 . . . Type: *ast.FuncType {
    18 . . . . Func: helloworld/helloworld.go:3:1
    19 . . . . Params: *ast.FieldList {
    20 . . . . . Opening: helloworld/helloworld.go:3:16
    21 . . . . . Closing: helloworld/helloworld.go:3:17
    22 . . . . }
    23 . . . . Results: *ast.FieldList {
    24 . . . . . Opening: -
    25 . . . . . List: []*ast.Field (len = 1) {
    26 . . . . . . 0: *ast.Field {
    27 . . . . . . . Type: *ast.Ident {
    28 . . . . . . . . NamePos: helloworld/helloworld.go:3:19
    29 . . . . . . . . Name: "string"
    30 . . . . . . . }
    31 . . . . . . }
    32 . . . . . }
    33 . . . . . Closing: -
    . . . . .
    package helloworld
    func HelloWorld() string {
    return "Hellow World"
    }

    View Slide

  14. 3. カスタムLinterの開発

    View Slide

  15. Go における Linter
    - golang.org/x/tools/go/analysis パッケージによってフレームワーク化されている
    - analysis.Analyzer が1モジュールとなっている
    - go tool vetも複数のanalysis.Analyzerを利用して実装している
    - Analyzerの中で依存関係を持たせ、
    得られた結果を他の Analyzerで利用することが可能になっている
    package main
    import (
    "cmd/internal/objabi"
    "golang.org/x/tools/go/analysis/unitchecker"
    "golang.org/x/tools/go/analysis/passes/asmdecl"
    "golang.org/x/tools/go/analysis/passes/assign"
    "golang.org/x/tools/go/analysis/passes/atomic"
    "golang.org/x/tools/go/analysis/passes/bools"
    "golang.org/x/tools/go/analysis/passes/buildtag"
    "golang.org/x/tools/go/analysis/passes/cgocall"
    ...
    )
    func main() {
    objabi.AddVersionFlag()
    unitchecker.Main(
    asmdecl.Analyzer,
    assign.Analyzer,
    atomic.Analyzer,
    bools.Analyzer,
    buildtag.Analyzer,
    cgocall.Analyzer,
    ...
    )

    View Slide

  16. カスタムLinterの開発手順
    1. analysis.Analyzer を実装する
    2. analysistest を使って analysis.Analyzer をテストする
    3. コマンド化

    View Slide

  17. analysis.Analyzer
    • 静的解析をモジュール化する際の 1モジュール単位となるもの
    • Linter を開発する際は Analyzer 単位で実装していく
    var Analyzer = &analysis.Analyzer{
    Name: "sample", // Analyzerの名前
    Doc: "this is sample", // Analyzerについての説明
    Requires: []*analysis.Analyzer{inspect.Analyzer}, // 依存するAnalyzerのリスト
    Run: run, // 実際の解析処理をここに記述する(パッケージ単位で実行される)
    FactTypes: nil, // このAnalyzer内で複数パッケージを跨いだデータ共有等を行いたい場合に利用する
    ResultType: nil, // このAnalyzerが解析した結果を他のAnalyzerにも提供したい場合はここで型を宣言する
    }

    View Slide

  18. 分析器専用の analysis.Analyzer
    ★ analysis/passes/inspect : ASTを扱える
    ○ *inspector.Inspector
    ★ analysis/passes/ctrlflow : 制御フローグラフを扱える
    ○ *cfg.CFG
    ★ analysis/passes/buildssa : SSA形式を扱える
    ○ *ssa.Package, []*ssa.Function

    View Slide

  19. analysis.Analyzer.Run
    • シグネチャ: func(*Pass) (interface{}, error)
    • 引数: analysis.Pass
    • この構造体から検査対象のパッケージのデータを取得し、その結果を格納する
    • 戻り値: interface{}, error
    • 第一戻り値に先ほど初期化時に定義した ResultTypeの型の値を返すことで、
    他のAnalyzerからその値を利用することができる

    View Slide

  20. analysis.Analyzer で ASTを扱う
    • analysis/passes/inspect.Analyzer をRequiresフィールドに含める
    • golang.org/x/tools/go/ast/inspectorパッケージの inspector.Inspector を使って
    ASTのNodeに対しての処理を記述していく
    func run(pass *analysis.Pass) (interface{}, error) {
    // inspect.Analyzerが提供する結果を利用する
    inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    // 処理を適用したいast.Nodeの種類をフィルタリングできる
    nodeFilter := []ast.Node{
    (*ast.CallExpr)(nil),
    }
    inspect.Preorder(nodeFilter, func(n ast.Node) {
    // Node毎の処理を記述
    })
    return nil, nil
    }

    View Slide

  21. analysis/analysistest を使ったテスト
    • testdata ディレクトリを作成するとそこを GOPATHとして扱うことができる
    • ディレクトリ配下に対して、作成した Linter を実行することができる
    tests
    ├── testdata
    │ └── src
    │ ├── a
    │ │ ├── a.go
    │ │ ├── a_test.go
    │ │ └── ax_test.go
    │ ├── b
    │ │ └── b.go
    │ ├── b_x_test
    │ │ └── b_test.go
    │ └── divergent
    │ ├── buf.go
    │ └── buf_test.go
    ├── tests.go
    └── tests_test.go
    golang.org/x/tools/go/analysis/passes/tests より
    // tests_test.go
    func Test(t *testing.T) {
    testdata := analysistest.TestData()
    analysistest.Run(t, testdata, tests.Analyzer,
    "a", // loads "a", "a [a.test]", and "a.test"
    "b_x_test", // loads "b" and "b_x_test"
    "divergent",
    )
    }

    View Slide

  22. analysis/analysistest を使ったテスト - Assertion
    • コメントでアサーションを行うことができる
    tests
    ├── testdata
    │ └── src
    │ ├── a
    │ │ ├── a.go
    │ │ ├── a_test.go
    │ │ └── ax_test.go
    │ ├── b
    │ │ └── b.go
    │ ├── b_x_test
    │ │ └── b_test.go
    │ └── divergent
    │ ├── buf.go
    │ └── buf_test.go
    ├── tests.go
    └── tests_test.go
    // testdata/src/a/a_test.go
    func ExamplePuffer_suffix () {} // want "ExamplePuffer_suffix refers to unknown identifier:
    Puffer"
    func ExampleFoo () {} // OK because a.Foo exists
    func ExampleBar () {} // want "ExampleBar refers to unknown identifier: Bar"

    View Slide

  23. analysis.Analyzer のコマンドエントリーポイント
    ★ analysis/unitchecker.Main
    ○ Goの内部 (cmd/go/internal/work)で利用するためのエントリーポイント
    ■ ユーザは基本的には go vet -vettool 経由でしか扱えない
    ★ analysis/singlechecker.Main
    ○ 単独のコマンドラインとしても実行できる
    ■ go vet経由でも扱える
    ★ analysis/multichecker.Main
    ○ 単独のコマンドラインとしても実行できる
    ■ go vet経由でも扱える

    View Slide

  24. go vet -vettool
    • go vet コマンドのデフォルトの動作
    • デフォルトではunitcheckerで実装しているgo tool vet($GOTOOLDIR/vet)を
    -vettoolに設定して実行している
    • -vettoolで 特定のLinterを指定した場合、go tool vetは実行されない

    View Slide

  25. 参考になる analysis.Analyzer の実装や資料
    • analysis.Analyzer のテンプレート生成ツール
    • https://github.com/gostaticanalysis/skeleton
    • analysis.Analyzer のサンプル
    • https://github.com/golang/tools/tree/master/go/analysis/passes
    • GoのためのGo
    • https://motemen.github.io/go-for-go-book/

    View Slide

  26. 4. プロダクトにLinterを導入する

    View Slide

  27. プロダクトに導入する際の注意点
    • テストと同様に、開発フローの一部として継続的に回す必要がある
    • CI に組み込む
    • 開発初期段階から導入しておくべき
    • 開発途中から導入する際はルールを少なくして、徐々に増やしていくと良い
    • テストと同様に常にレッドである状態が続いてしまうと麻痺してしまう

    View Slide

  28. プロダクトに導入するステップ
    1. CI を導入する (今回は触れません)
    2. 既存の Linter を導入する
    3. CI に組み込む
    4. ドメイン固有のルールが必要になったら Linterの開発を検討する

    View Slide

  29. 既存の Linter
    • go vet
    • コンパイラで検知できないエラーを検知( Printfのフォーマットと引数の検査 , etc...)
    • golint
    • コーディングスタイルの検査
    • golangci-lint
    • 様々な Linter のアグリゲーター
    他にも様々なLinterツールがある
    https://github.com/golangci/awesome-go-linters

    View Slide

  30. 既存の Linter を導入する - golangci-lint
    • https://github.com/golangci/golangci-lint
    • どのルールを適用するか決める
    • .golangci.yml をプロジェクトルートに作成
    • https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
    • サポートされている全てのオプションが説明されている

    View Slide

  31. CI に組み込む - reviewdog
    • https://github.com/reviewdog/reviewdog
    • Linter の結果を GitHub などの Pull Request 差分にコメントしてくれるツール

    View Slide

  32. ドメイン固有の カスタムLinter
    • golangci-lintとカスタムLinterを1コマンドで実行できるようにする
    • 手元でも簡単に確認できるように
    • makeやshell scriptを使うと良い
    • reviewdogでフィードバックできるようにする

    View Slide

  33. Summary
    • 一般的なLintルールは golangci-lint を使うと良い
    • 独自のカスタムLinterが必要になったら analysis.Analyzer を使って実装
    • CI と reviewdog を使って継続的にフィードバックできるようにする

    View Slide

  34. ご清聴ありがとうございました

    View Slide