この記事は「BASE Advent Calendar 2018」24日目の記事です。
はじめに
BASEでエンジニアとしてインターンをしている池田です。今日はクリスマスイブということで、以前から気になっていたグリューワイン(ドイツのクリスマスに欠かせないホットワイン)をクリスマスマーケットに飲みに来ています。
普段はBASE BANKというBASEの100%子会社にて金融事業の立ち上げを行っています。BASE BANKのプロダクトのAPIはGoを使って書かれているのですが、BASEでのGoの使用はこのプロジェクトが初だったので、開発基盤整備の一貫で様々なlinterの導入を行いました。その際にlinterの内部がどうなっているかに興味を持ち、すごく簡単なlinterの仕組みを実際に作ってみたので、今回はその知見をお話しします。
Goと静的解析
Goには、標準パッケージとして静的解析を行うためのgo
パッケージが存在します。
また何よりGoは文法がシンプルな静的型付け言語なので、静的解析をとても簡単に行うことができました。
今回はその中でも、字句解析を行うgo/token
、構文解析を行いAST(abstract syntax tree: 抽象構文木)を取り扱うgo/ast
やgo/parser
パッケージを用います。
今回は、megacheck
というlinterを用いた際に実際に注意された、
bool値で条件分岐するときは、 if x == false {...} じゃなくて if !x {...} の方がいい
といったことを検出してみたいと思います。 今回の静的解析の簡単な流れを下図に示します。
抽象構文木のノードを探索していき、条件に当てはまるものがあったら警告するといった感じです。
やってみる
では実際にコードを見ていきましょう。 下がコード全体です。少しずつ解説していきます。
func main() { // 指定したファイルの構文解析を行う fset := token.NewFileSet() f, err := parser.ParseFile(fset, "sample.go", nil, 0) if err != nil { log.Fatal("Error:", err) } // 抽象構文木を探索する ast.Inspect(f, func(n ast.Node) bool { // 二項演算子ではない場合は無視 expr, ok := n.(*ast.BinaryExpr) if !ok { return true } // == の場合 if expr.Op == token.EQL { xIdent, ok := expr.X.(*ast.Ident) // Expr型をast.Ident型に型アサーション if !ok { return true } yIdent, ok := expr.Y.(*ast.Ident) // Expr型をast.Ident型に型アサーション if !ok { return true } // 識別子を文字列として取り出す xString := xIdent.String() yString := yIdent.String() switch yString { case "true": fmt.Printf("- got: %s == %s\n+ want: %s\n", xString, yString, xString) case "false": fmt.Printf("- got: %s == %s\n+ want: !%s\n", xString, yString, xString) } return true } return true }) }
ファイルの読み込みと構文解析
// 指定したファイルの構文解析を行う fset := token.NewFileSet() f, err := parser.ParseFile(fset, "sample.go", nil, 0) if err != nil { log.Fatal("Error:", err) }
まずはsample.go
というファイルを指定してparser.ParseFile
により構文解析を行います。
package main func sample() { isAvailable := false if isAvailable == false { return } }
ここで、「あれ、まず字句解析しないの?」と思った方もいらっしゃるかもしれません。
実はgo/parser
パッケージが内部で字句解析を行うため、直接字句解析を行うことはあまりないのです。
抽象構文木の探索
続いて、構文解析した結果により作られた抽象構文木を扱います。
// 抽象構文木を探索する ast.Inspect(f, func(n ast.Node) bool { ... } )
ast.Inspect
は抽象構文木のノードをトラバースする関数です。この中のfunc(n ast.Node) bool { ... }
で、抽象構文木の各ノードに対して一律の処理をかけます。
実際にどのような処理をするか見ていきましょう。
// 二項演算子ではない場合は無視 expr, ok := n.(*ast.BinaryExpr) if !ok { return true }
まずはast.Node
型のノードをast.BinaryExpr
型に変換しています。
ast.BinaryExprは+
や==
などのような二項演算子を表す型です。今回はx == false
のような箇所を検出したいので、この型に変換できない場合は無視して次に進みます。(なぜ最初にast.BinaryExp型に変換できるノードを探しているのかは下で説明します)
ast.BinaryExpr
は以下のような構造体です。
type BinaryExpr struct { X Expr // 左のオペランド OpPos token.Pos // オペレーターの位置(ファイルの何行目の何バイト目か) Op token.Token // オペレーター Y Expr // 右のオペランド }
また下図は二項演算を抽象構文木にした場合の図で、BinaryExpr構造体のフィールド(X, Y, Op)との対応が見て取れると思います。
図を見てわかる通り、二項演算では演算子が親ノード、オペランドが子ノードになるため、このステップではまずast.BinaryExp型に変換できるノードを探していました。
続いて、上記の結果返ってきたast.BinaryExp型の値に対して処理をかけます。
// == の場合 if expr.Op == token.EQL { xIdent, ok := expr.X.(*ast.Ident) // ast.Expr型をast.Ident型に型アサーション if !ok { return true } yIdent, ok := expr.Y.(*ast.Ident) // ast.Expr型をast.Ident型に型アサーション if !ok { return true } // 識別子を文字列として取り出す xString := xIdent.String() yString := yIdent.String() switch yString { case "true": fmt.Printf("- got: %s == %s\n+ want: %s\n", xString, yString, xString) case "false": fmt.Printf("- got: %s == %s\n+ want: !%s\n", xString, yString, xString) } return true }
今回は簡単にx == y
のような部分だけ検出したいので、まずオペレーターOp
がtoken.Token
型の==
と等しいか調べています。
そしてオペレーターが==
だった場合に、次は各オペランドをast.Ident
という型に変換しているのがわかるかと思います。
ここでIdentが何かというと、識別子
を表します。
上図のように、字句解析のあとの変数名や関数名、bool値は識別子を表すast.Ident
型、1
や"dog"
など数値リテラルや文字リテラルはast.BasicLit
型になります。
今回はある変数をboolと比較している箇所を評価したいため、その条件に合致するようなオペランド対を探しています。
あとは、該当するような箇所があったらその旨を出力して終わりです。
実際にsample.go
にこのlinterをかけた結果を見てみましょう。
# go run main.go - got: isAvailable == false + want: !isAvailable
ご覧のように、sample.go
内でbool値と直接比較している箇所を検出できています。
終わりに
今回は、標準の静的解析パッケージであるgo
パッケージを使って簡単な静的解析ツールを試作してみました。
go
パッケージを使えば、lintの他にもコードの自動生成や自動フォーマットをすることもできます。
またこのパッケージのソースコードを読むことでコンパイラや言語処理系の勉強にもなるので、静的解析はGoの中でも面白いトピックの一つなのではないでしょうか。
では、よいクリスマスをお過ごしください!