golang.tokyo #22+Okayama.go/Sendai.go 概要まとめ ~静的解析ハンズオン~


はじめに

golang.tokyo #22+Okayama.go/Sendai.go - connpass
このイベントに行ってまいりました。
内容としては、このイベントの概要となります。
今回のテーマは、静的解析でした。

登壇者:
@tenntenn
資料:
A Tour of Static Analysis - Google スライド

このページについて | golang.tokyo コードラボ

そもそも静的解析ってなんの役に立つ

  • go vetとかgolintみたいなツールを作るのに向いている
  • コンパイラじゃ見つけてくれないバグを探すのに使える
  • Goの文法に詳しくなれる

静的解析の流れ

  • 字句解析
    文字列をトークンへ!! 字句解析で、文字列の塊だったソースコードが予約語のfuncなのか、識別子(変数名や関数名など)なのか、数値リテラルなのかなどを区別することができるトークンの塊に変換される。

字句解析についてはgo/parserパッケージ内でgo/scannerパッケージを用いて行われています。

go/parserで自動でやられるので点線表記みたいですね

  • 構文解析
    トークンをASTへ!! 構文解析を行うと、どの部分が関数定義で、どの部分がその引数の定義なのか、などを抽象構文木から取得することができるようになる。
  • 型チェック
    最後に型チェックを行うことで、抽象構文木から型情報を抽出
    型チェックは次の3つの工程から成る。
    1.識別子の解決
    2.型の推論
    3.定数の評価
    この3つの工程を行い、型情報を抽出することで、どの変数(識別子)がどういうデータ型でどこで定義され、どこで使用されているかなどを知ることができる。

Gopherを探せ!ハンズオン

資料:
Gopherを探せ!ハンズオン資料

概要

go@_gopher.go
package main

import (
        "fmt"
)

type Gopher struct {
        Gopher string `json:"gopher"`
}

func main() {
        const gopher = "GOPHER"
        gogopher := GOPHER()
        gogopher.Gopher = gopher
        fmt.Println(gogopher)
}

func GOPHER() (gopher *Gopher) {
        gopher = &Gopher{Gopher: "gopher"}
        return
}

上記のファイルから、名前付き型かつGopherという名前の識別子だけを
サーチするにはどうしたらいいかが

  • grepコマンドの限界
  • 式の構造解析
  • ファイルの構造解析
  • 型チェック

という流れで学べる、すごくわかりやすいハンズオンになっていました。

ハンズオンを書き換えて、理解を深める

ここからは個人で勝手にやった内容となります。
リテラルの"gopher"文字列をサーチするように書き換えてみます。

package main

import (
    "fmt"
    "go/ast"
    "go/importer"
    "go/parser"
    "go/token"
    "go/types"
    "log"
    "strconv"
)

func main() {
    // ファイルごとのトークンの位置を記録するFileSetを作成する
    fset := token.NewFileSet()

    // ファイル単位で構文解析を行う
    f, err := parser.ParseFile(fset, "_gopher.go", nil, 0)
    if err != nil {
        log.Fatal("Error:", err)
    }

    // 識別子が定義または利用されてる部分を記録する
    defsOrUses := map[*ast.Ident]types.Object{}
    info := &types.Info{
        Defs: defsOrUses,
        Uses: defsOrUses,
    }

    // 型チェックを行うための設定
    config := &types.Config{
        Importer: importer.Default(),
    }

    // 型チェックを行う
    _, err = config.Check("main", fset, []*ast.File{f}, info)
    if err != nil {
        log.Fatal("Error:", err)
    }

    // 抽象構文木を深さ優先で探索する
    ast.Inspect(f, func(n ast.Node) bool {

        // リテラルではない場合は無視
        basic, ok := n.(*ast.BasicLit)
        if !ok {
            return true
        }

        basicValue,err := strconv.Unquote(basic.Value)
        if err !=nil{
            panic(err)
        }

        //リテラルが"gopher"という値でなければ無視
        if basicValue  != "gopher" && basicValue != `json:"gopher"` {
            return true
        }

        fmt.Println(fset.Position(basic.Pos()))

        return true
    })
}

takafk9@narikawakiyoshinoMacBook-Pro [16時39分00秒] [~/go/src/github.com/golangtokyo/codelab/find-gophers/3_typecheck] [master *]
-> % go run main.go
_gopher.go:8:16
_gopher.go:19:27

きちんとリテラルを捕まえてますね

structタグのastが

Tag: *ast.BasicLit {
 ValuePos: 70
    Kind: STRING
    Value: "`json:\"gopher\"`"
}

なようなので、

if basicValue  != "gopher" && basicValue != `json:"gopher"`

という規制で、タグ内の"gopher"も捕まえられました
(slackで回答していただいた方々、ありがとうございました!)

感想

静的解析についてかなりディープで難しい領域に思っていましたが、今回のハンズオンで
少しイメージが変わりました。
(この流れをモジュール化したAnalyzerまで手を出せなかったので、時間あるときに、そちらもやっていきたい)
golang.tokyoのイベントにお邪魔するのは初めてなのですが、Slackでも疑問に即レスで答えていただけて、非常に素晴らしいイベントでした。
ちょっとastにおこして解析するだけで、かなり文法の知識を深められたので、
今度は自作で静的解析ツールを作りたいと思います。

ありがとうございました!

その他有益な資料まとめ

analysis.Analyzerを使っている今回のようなサンプルプログラム

Goの式の定義

静的解析のサンプルコード

tenntennさんが作ったAnalyzerを使った静的解析CLIを作り始めるのに便利奴

knsh14さんによるanalysisパッケージの解説記事

Goの標準パッケージではじめる静的解析入門①準備編 · mom0tomo

Goにおける静的解析のモジュール化について

analysis pkgh設計者(“プログラミング言語Go”執筆者)によるtypesの説明

motemenさんによるgo/astパッケージやgo/typesパッケージ

余談

Gopherオタクになりたて人間には最高のサイト、、、