Slide 1

Slide 1 text

Welcome to Linter Go Conference 2019 Autumn Junki Kaneko

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

1. 背景

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

2. Go における静的解析

Slide 10

Slide 10 text

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. マシンコードの生成

Slide 11

Slide 11 text

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を元にチェックを 行う

Slide 12

Slide 12 text

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を元にチェックを 行う

Slide 13

Slide 13 text

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" }

Slide 14

Slide 14 text

3. カスタムLinterの開発

Slide 15

Slide 15 text

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, ... )

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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にも提供したい場合はここで型を宣言する }

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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 }

Slide 21

Slide 21 text

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", ) }

Slide 22

Slide 22 text

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"

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

参考になる 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/

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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