nilがnilじゃないのでerrorになるのを静的解析で検出する
Goアドベントカレンダーその2の穴埋めです。
TL; DR
作りました: https://github.com/makiuchi-d/ptrtoerr
なぜ必要なのか
まずは次のコードを実行してみてください。
https://play.golang.org/p/j4ffNK4Xx84
package main
import "fmt"
type MyErr struct{}
func (*MyErr) Error() string {
return "MyErr"
}
func F1() *MyErr {
return nil
}
func F2() error {
return F1()
}
func main() {
err := F2()
if err != nil {
fmt.Println("Error!")
}
}
F1()
がnilを返しているのでF2()
もnilを返すのですが、返ってきたerr
はnilにならずに"Error!"
が表示されます。
不思議ですね!
そうです。 これはGoに詳しいみなさんならよくご存知の、nilポインタを入れたinterfaceはnilではないというお話です。
Goでは、型変換は基本的に明示的にしなければならないのですが、interface型への変換だけは例外的に暗黙に行われます。
このコードでは、F1()
の戻り値は*MyDrr
型(ポインタ型)ですが、F2()
ではerror型(interface)になっています。
つまり、F2()
のreturn文で暗黙的な型変換が行われています。
Goのnilリテラルには、ポインタとしてのnilと、interfaceとしてのnilの2つの意味があります。 さらにinterfaceであるerror型はinterfaceとしてのnilと比較することでエラー判定をするため、 非エラーのつもりでポインタとしてのnilを入れてしまうとエラーとみなされてしまいます。
このようなミスは、上のexample.goでも示したとおりコンパイルも通りますし、見た目にもわかりにくいです。 そこで静的解析です。
nilポインタをerror型に入れている場所を探せばよいのですが、ポインタがnilかどうかは実行時でないとわからないため、 ポインタ型をerror型に入れている場所を探すことにしました。 また、error以外のinterface型へポインタを入れることは普通によくあることなので、error型限定です。
静的解析で検出する
作ったものはこちらです: https://github.com/makiuchi-d/ptrtoerr
まず手始めに、GoStaticAnalysis skeletonでコード生成しました。 これにより、最初からロジックに集中できてとても便利ですね。
検出すべきものを洗い出す
ポインタ型をerror型に入れている場所を検出したいのですが、このような型変換が起こるのは次のようなケースです。
- 変数などへの代入
- return文
- 関数の引数
error型としてよく使われていて、問題になりそうなのは代入とreturn文でしょう。 今回はこの2つを検出することにしました。 というか作った後で関数引数のことを思い出しました。他にあったらこっそり教えてください。
追記 型変換が起こる場所がもう一つありました。
- 初期化付きの変数宣言
初期化は=
で書くので*ast.AssignStmt
になるのかと思ったら違いました。*ast.ValueSpec
に含まれました。
リポジトリのこのコミットで対応していますので、興味のある方はご覧ください。
error型へポインタを代入している場所を検出
skeletonがいろいろ準備してくれているので、抽象構文木(AST)のノードに注目するところから実装していきます。
代入文はast.Node
の型が*ast.AssignStmt
になっています。
type AssignStmt struct {
Lhs []Expr
TokPos token.Pos // position of Tok
Tok token.Token // assignment token, DEFINE
Rhs []Expr
}
ご存知のようにGo言語は複数変数にまとめて代入できるので、左辺(Lhs
)と右辺(Rhs
)はスライスです。
検出したいのはポインタ型をerror型へ代入しているところなので、左辺がerrorかつ右辺がポインタのものを探します。
両辺のそれぞれの型はanalysis.Pass
のTypesInfo.Typeof()
で取得できます。
error型かどうかは、次のように予め取得しておいたerrorのtypes.Type
と比較することで判定できます。
var errType = types.Universe.Lookup("error").Type()
右辺の判定は、TypeOf()
で取得したtypes.Type
が*types.Pointer
であればポインタ型です。
あとは、左辺がerrorかつ右辺がポインタの場所n.Pos()
をReportすれば完了です。
func checkAssign(pass *analysis.Pass, n *ast.AssignStmt) {
for i := range n.Lhs {
lt := pass.TypesInfo.TypeOf(n.Lhs[i])
rt := pass.TypesInfo.TypeOf(n.Rhs[i])
_, rtIsPtr := rt.(*types.Pointer)
if lt == errType && rtIsPtr {
pass.Reportf(n.Pos(), "Assign pointer to error")
}
}
}
error型としてポインタをreturnしている場所を検出
まず関数の定義から戻り値の型を調べ、関数本体の中のreturn文を探して型を調べる、という流れになります。
Goでは通常の関数定義(*ast.FuncDecl
)の他に関数リテラル(*ast.FuncLit
)があるので、両方探索します。
type FuncDecl struct {
Doc *CommentGroup // associated documentation; or nil
Recv *FieldList // receiver (methods); or nil (functions)
Name *Ident // function/method name
Type *FuncType // function signature: parameters, results, and position of "func" keyword
Body *BlockStmt // function body; or nil for external (non-Go) function
}
type FuncLit struct {
Type *FuncType // function type
Body *BlockStmt // function body
}
必要なのはType
とBody
なので、どちらも同じ方法で探索できます。
戻り値の型は*ast.FuncType
のResults
から取得できます。
Goの関数は複数戻り値をとれるので、Results.List
の各要素のType
を見て、何番目がerror型かメモしておきます。
戻り値が無い時はResults
はnilです。
type FuncType struct {
Func token.Pos // position of "func" keyword (token.NoPos if there is no "func")
Params *FieldList // (incoming) parameters; non-nil
Results *FieldList // (outgoing) results; or nil
}
次にBody
からreturn文を探します。
再帰的に構文木を辿っていくのですが、自分でコードを書かなくてもast.Inspect()
がやってくれます。
ひとつ注意点として、探索しているBodyの中に関数リテラルがあったとき、その中のreturn文は無視しなくてはなりません。
これは単純に、ノードが*ast.FuncLit
だったらその先を探索しないようにfalse
を返せばよいです。
return文はast.Node
が*ast.ReturnStmt
のものです。
type ReturnStmt struct {
Return token.Pos // position of "return" keyword
Results []Expr // result expressions; or nil
}
戻り値も複数あるので、Results
はスライスになっています。
ここで先程メモしていた何番目がerror型かの情報を使い、その場所がポインタ型だったら報告すれば完了です。
func checkFuncReturn(pass *analysis.Pass, t *ast.FuncType, b *ast.BlockStmt) {
if t.Results == nil {
return
}
var idxs []int
for i, r := range t.Results.List {
if pass.TypesInfo.TypeOf(r.Type) == errType {
idxs = append(idxs, i)
}
}
if len(idxs) == 0 {
return
}
ast.Inspect(b, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.FuncLit:
return false
case *ast.ReturnStmt:
for _, i := range idxs {
_, isPtr := pass.TypesInfo.TypeOf(n.Results[i]).(*types.Pointer)
if isPtr {
pass.Reportf(n.Pos(), "Return pointer as error")
}
}
}
return true
})
}
動かしてみる
最初に示したexample.goを静的解析してみます。
$ ptrtoerr example.go
./example.go:16:2: Return pointer as error
16行目のF2()
のreturn文が検出されました。
まとめ
error型にポインタを入れている、ミスしやすいコードを静的解析で検出することができました。 はじめてGoの静的解析をしてみましたが、想像以上に簡潔にできてよいですね。 なにかのお役に立てば幸いです。