nilがnilじゃないのでerrorになるのを静的解析で検出する

Goアドベントカレンダーその2の穴埋めです。

TL; DR

作りました: https://github.com/makiuchi-d/ptrtoerr

なぜ必要なのか

まずは次のコードを実行してみてください。

https://play.golang.org/p/j4ffNK4Xx84

example.go
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つを検出することにしました。
というか作った後で関数引数のことを思い出しました。他にあったらこっそり教えてください。

error型へポインタを代入している場所を検出

skeletonがいろいろ準備してくれているので、抽象構文木(AST)のノードに注目するところから実装していきます。

代入文はast.Nodeの型が*ast.AssignStmtになっています。

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と比較することで判定できます。

errType
var errType = types.Universe.Lookup("error").Type()

右辺の判定は、TypeOf()で取得したtypes.Type*types.Pointerであればポインタ型です。
あとは、左辺がerrorかつ右辺がポインタの場所n.Pos()をReportすれば完了です。

checkAssign()
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)があるので、両方探索します。

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です。

ast.FuncType
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のものです。

ast.ReturnStmt
type ReturnStmt struct {
    Return  token.Pos // position of "return" keyword
    Results []Expr    // result expressions; or nil
}

戻り値も複数あるので、Resultsはスライスになっています。
ここで先程メモしていた何番目がerror型かの情報を使い、その場所がポインタ型だったら報告すれば完了です。

checkFuncReturn()
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の静的解析をしてみましたが、想像以上に簡潔にできてよいですね。
なにかのお役に立てば幸いです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account