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

Welcome_to_Linter

 Welcome_to_Linter

38483449b10e3eacb5bddad0f59a5541?s=128

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

  2. 自己紹介 金子 淳貴 • SWET (Software Engineer in Test) •

    普段の業務 ◦ Go プロダクトの品質改善 ◦ CI や開発プロセスの改善 ◦ それらに伴うツールやサービス開発
  3. この発表のゴール • Goのプロダクトにおいて、 Linter 導入やカスタムLinterの開発に関しての 一通り必要な知識を知ること

  4. Agenda 1. 背景 2. Go における静的解析 3. カスタムLinter の開発 4.

    プロダクトに Linter を導入する
  5. 1. 背景

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

  7. 開発における起こりがちな問題 • コードレビューで本質的ではない指摘が多くなってしまいがち • コーディングスタイル • 命名規則 • etc... •

    コード品質が開発者によって違う • エラーを毎回チェックしていないコード • panicが入っているミドルウェア • etc...
  8. Linter を使って解決 • 一定のコード品質を機械的に担保できる • 一貫性 • コーディングスタイルや命名規則 • 簡潔性

    • 無駄な処理が入っていないか、複雑すぎないか • etc... • コードレビューする際の初期品質が向上 • コードレビューの負荷軽減
  9. 2. Go における静的解析

  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. マシンコードの生成
  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を元にチェックを 行う
  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を元にチェックを 行う
  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" }
  14. 3. カスタムLinterの開発

  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, ... )
  16. カスタムLinterの開発手順 1. analysis.Analyzer を実装する 2. analysistest を使って analysis.Analyzer をテストする 3.

    コマンド化
  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にも提供したい場合はここで型を宣言する }
  18. 分析器専用の analysis.Analyzer ★ analysis/passes/inspect : ASTを扱える ◦ *inspector.Inspector ★ analysis/passes/ctrlflow

    : 制御フローグラフを扱える ◦ *cfg.CFG ★ analysis/passes/buildssa : SSA形式を扱える ◦ *ssa.Package, []*ssa.Function
  19. analysis.Analyzer.Run • シグネチャ: func(*Pass) (interface{}, error) • 引数: analysis.Pass •

    この構造体から検査対象のパッケージのデータを取得し、その結果を格納する • 戻り値: interface{}, error • 第一戻り値に先ほど初期化時に定義した ResultTypeの型の値を返すことで、 他のAnalyzerからその値を利用することができる
  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 }
  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", ) }
  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"
  23. analysis.Analyzer のコマンドエントリーポイント ★ analysis/unitchecker.Main ◦ Goの内部 (cmd/go/internal/work)で利用するためのエントリーポイント ▪ ユーザは基本的には go

    vet -vettool 経由でしか扱えない ★ analysis/singlechecker.Main ◦ 単独のコマンドラインとしても実行できる ▪ go vet経由でも扱える ★ analysis/multichecker.Main ◦ 単独のコマンドラインとしても実行できる ▪ go vet経由でも扱える
  24. go vet -vettool • go vet コマンドのデフォルトの動作 • デフォルトではunitcheckerで実装しているgo tool

    vet($GOTOOLDIR/vet)を -vettoolに設定して実行している • -vettoolで 特定のLinterを指定した場合、go tool vetは実行されない
  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/
  26. 4. プロダクトにLinterを導入する

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

    • テストと同様に常にレッドである状態が続いてしまうと麻痺してしまう
  28. プロダクトに導入するステップ 1. CI を導入する (今回は触れません) 2. 既存の Linter を導入する 3. CI

    に組み込む 4. ドメイン固有のルールが必要になったら Linterの開発を検討する
  29. 既存の Linter • go vet • コンパイラで検知できないエラーを検知( Printfのフォーマットと引数の検査 , etc...)

    • golint • コーディングスタイルの検査 • golangci-lint • 様々な Linter のアグリゲーター 他にも様々なLinterツールがある https://github.com/golangci/awesome-go-linters
  30. 既存の Linter を導入する - golangci-lint • https://github.com/golangci/golangci-lint • どのルールを適用するか決める •

    .golangci.yml をプロジェクトルートに作成 • https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml • サポートされている全てのオプションが説明されている
  31. CI に組み込む - reviewdog • https://github.com/reviewdog/reviewdog • Linter の結果を GitHub

    などの Pull Request 差分にコメントしてくれるツール
  32. ドメイン固有の カスタムLinter • golangci-lintとカスタムLinterを1コマンドで実行できるようにする • 手元でも簡単に確認できるように • makeやshell scriptを使うと良い •

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

    CI と reviewdog を使って継続的にフィードバックできるようにする
  34. ご清聴ありがとうございました