【Go言語】自作した静的解析ツールをGitHub Actionsで実行する
本記事では、
- Goで静的解析ツールを自作する
- 自作した静的解析ツールをGitHub Actionsで実行する
方法についてまとめます。
自作した静的解析ツール
oncenv
os.Getenv()
が実行されていることを指摘するoncenvというツールを作りました。os.Getenv()
は環境変数を取得するためのメソッドです。
自分のユースケースではプロセス起動時に一度環境変数を取得できれば十分なため、
func request() {
apiHost := os.Getenv("API_HOST")
resp, _ := http.Get(apiHost + "hoge")
...
}
と書いてリクエストごとにホストを環境変数から取得するよりも、
var apiHost string
func init() {
apiHost = os.Getenv("API_HOST")
}
func request() {
resp, _ := http.Get(apiHost + "hoge")
...
}
と書いた方が値を使い回せるようになりますし、os.Getenv()
内で取るロックの回数を減らせるので、多少パフォーマンスも向上するでしょう。
実装
Goの静的解析ツールの実装についてはこちらの記事が非常に丁寧に書かれており参考になりました。
上記の記事でも解説されていますが、Goの静的解析ツールはanalysis.Analyzer
を実装して作ります。
最終的なディレクトリ構成は以下の様になっています。
.
├── cmd
│ └── oncenv
│ └── main.go
├── go.mod
├── go.sum
├── oncenv.go
├── oncenv_test.go
└── testdata
└── src
└── a
└── a.go
以下で実装について解説していきます。
テストデータの作成
まずは静的解析ツールに期待する挙動をテストにします。Analyzer
のテストはgolang.org/x/tools/go/analysis/analysistest
パッケージを使うことで簡単に書くことができます。
analysistest
のRun()
、RunWithSuggestedFixes()
は指定したディレクトリ配下のパッケージに対して、テストを実行します。テストデータのディレクトリパスを返すanalysistest.TestData()
がtestdataディレクトリのパスを返すので、testdata配下にテスト用のコードを書いていきます。
package main
import "os"
func init() {
os.Getenv("A")
}
type a string
func (a a) a() {
os.Getenv("A") // want "os\\.Getenv\\(\\) is used in inappropriate place"
}
func main() {
os.Getenv("A") // want "os\\.Getenv\\(\\) is used in inappropriate place"
}
ソースコード内に// want ...
形式のコメントを書くことで、Analyzer
の診断結果と期待するメッセージが一致するかをテストしてくれます。今回はinit関数外でos.Getenv()
を使っている箇所でメッセージを出力することを期待しています。
// want
に続くメッセージは正規表現で書く必要があるため、.
や(
は適切にエスケープします。今回の場合、// want "os.Getenv() is used in inappropriate place"
などと書くと期待するテストができません。
Analyzerの実装
テストを書けたらいよいよAnalyzerの実装をしていきます。
Name
とDoc
はそれぞれツール名とツールの説明で、ツールの実態をRun
に実装していきます。
var Analyzer = &analysis.Analyzer{
Name: "oncenv", // ツール名
Doc: "oncenv detects os.Getenv in inappropriate place", // ツールの説明
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
for _, f := range pass.Files {
if strings.Contains(f.Name.Name, "_test") {
continue
}
for _, decl := range f.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok && decl.Name.Name != "init" { // init()以外のメソッド宣言部を探索
ast.Inspect(decl, func(n ast.Node) bool {
if fun, ok := n.(*ast.SelectorExpr); ok { // ast.Inspect()でノードを探索していく
if x, ok := fun.X.(*ast.Ident); ok {
if x.Name == "os" && fun.Sel.Name == "Getenv" { // os.Getenv()の呼び出しの場合、メッセージ出力
pass.Report(analysis.Diagnostic{
Pos: fun.Pos(),
Message: "os.Getenv() is used in inappropriate place",
})
}
}
}
return true
})
}
}
}
return nil, nil
}
実装は単純で、メソッド宣言部を順番に探索して*ast.SelectorExpr
が見つかったときに、その呼び出しがos.Getenv()
か否かを確認しています。
今回は簡易的にパッケージ名に_test
が含まれるか否かでテスト内のos.Getenv()
を検知しない様にしています。テストのパッケージ名に必ず_test
がついてる訳ではないため、本来はメソッド名などからテスト内の呼び出しであることを判定する必要があると思います。
go vetで実行できるようにする
go vetはGoに標準で組み込まれている静的解析ツールです。自作した静的解析ツールもgo vetコマンドから呼び出すことができます。
unitchecker
を使って、go vetから実行できる静的解析ツール(vettool)を作ります。
package main
import (
"github.com/ytakaya/oncenv"
"golang.org/x/tools/go/analysis/unitchecker"
)
func main() { unitchecker.Main(oncenv.Analyzer) }
unitchecker.Main()
には複数のAnalyzer
を渡すことができ、必要なAnalyzer
を列挙することで自分だけのvettoolを作ることができます。
上記のソースコードをビルドして、
$ go vet -vettool={実行ファイル}
とすると、自作のvettoolを実行することができます。
オプションを必要とするAnalyzer
(例えばfaindcall)をgo vetから実行する場合は、
$ go vet -vettool={実行ファイル} -findcall.name=println
の形式でプログラム引数を渡します。findcallはオプションで渡された関数の呼び出しを検出する静的解析ツールです。
今回は自作したoncenv
だけを実行するvettoolを作りました。
GitHub Actionsで実行できるようにする
テストや静的解析をCIで実行することで、コード品質の向上が期待できます。今回はGitHub Actionsで自作の静的解析ツールを実行する方法をまとめます。
こちらがサンプルのリポジトリです、以下でワークフローについて説明します。go vetを実行する
上記で開発したvettoolをGitHub Actionsで実行します。以下にワークフローの一部を掲載します。
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: install vettool
run : GOBIN=$(pwd) go install github.com/ytakaya/vettool-example@latest
- name: run vet
run: go vet -vettool=$(pwd)/vettool-example
単純に自分が使いたいAnalyzer
をまとめたvettoolをインストールしてgo vetコマンドで実行するだけです。
あとはプルリクエストなどをフックに実行して、いずれかのAnalyzer
の解析に引っかかればGitHub Actionsも失敗してくれます。
こんな感じで失敗してくれます。
privateリポジトリのvettoolを使う
publicリポジトリのvettoolを導入するのは上記の様に簡単でした。しかし、チームの独自ルールを反映した静的解析ツールを使う場合などは、vettoolをprivateリポジトリに置きたい場合もあります。privateリポジトリのvettoolをGitHub Actionsで実行するのは一手間必要です。本題からは少しそれますが、以下の様なステップを追加することで、privateリポジトリのvettoolを実行できます。
- name: Configure git for private modules
env:
TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
run: git config --global url."https://ytakaya:${TOKEN}@github.com".insteadOf "https://github.com"
- name: install vettool
run : GOBIN=$(pwd) GOPRIVATE="github.com/ytakaya/vettool-example" go install github.com/ytakaya/vettool-example@latest
PERSONAL_ACCESS_TOKEN
はGitHubのトークンページで、privateリポジトリの読み取り権限をつけたトークンを発行し、リポジトリのシークレットに登録した値です。
アクセストークンやGOPRIVATEに関しては以下に記載があります。
まとめ
Goで静的解析ツールを実装する方法と、それをGitHub Actionsで実行する方法を紹介しました。簡単にASTをいじれる方法が提供されているのが良いと思いました。チーム内の独自ルールへの違反やよくあるレビューの指摘などを静的解析で発見できる様にすると、開発効率が上がったりしそうだなと感じました!
Discussion