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.PassTypesInfo.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
}

必要なのはTypeBodyなので、どちらも同じ方法で探索できます。

戻り値の型は*ast.FuncTypeResultsから取得できます。 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の静的解析をしてみましたが、想像以上に簡潔にできてよいですね。 なにかのお役に立てば幸いです。