1 of 322

16. 静的解析とコード生成

The Go gopher was designed by Renée French.

The gopher stickers was made by Takuya Ueda.

Licensed under the Creative Commons 3.0 Attributions license.

2 of 322

注意事項と免責事項

  • 利用は個人の学習の範囲内でお願いします
    • この資料は個人の学習を目的とした利用に限ります
    • この資料を使った講義等を行う場合は事前に@tenntennに�許可を得てください
    • 生成AIを用いたサービスに学習させ、それを配布する行為を禁じます
  • 免責事項
    • この資料を元に発生した問題、この資料を参考にして作成した�ソフトウェア等に基づく問題について作成者は責任を負いません

2

3 of 322

質問について

  • Gophers Slackの#japanチャンネルでお願いします
    • Slackへの招待URL: https://invite.slack.golangbridge.org/
    • @tenntennまでメンションをください
    • ※質問への回答はすぐに行われるわけではないので予めご了承ください

3

4 of 322

上田拓也

Go ビギナーズ

Go Conference

Google Developer Expert (Go)

一般社団法人 Gophers Japan 代表理事

バックエンドエンジニアとして日々Goを書いている。Google Developer Expert (Go)。一般社団法人Gophers Japan代表。Go Conference主催者。大学時代にGoに出会い、それ以来のめり込む。人類をGopherにしたいと考え、Goの普及に取り組んでいる。複数社でGoに関する技術アドバイザーをしている。マスコットのGopherの絵を描くのも好き。

作者

5 of 322

【PR】企業向け研修や技術アドバイザー

  • Goに関する研修・講義
    • 初学者から中級者以上向けの講義を行えます
    • プログラミング言語Go完全入門をベースにカスタマイズ可能です
  • 技術アドバイザー
    • PRのレビュー
    • 週1回1時間程度のMeetやZoomでの相談
  • 依頼方法
    • フォームからお問い合わせください
    • 短期・長期のどちらでも契約が可能です
    • 講義・ハンズオン、技術相談などを組み合わせることも可能です
    • 実績等はhttps://tenntenn.dev/ja/job/をご覧ください

6 of 322

目次

  1. 静的解析を行う理由
  2. ast-grepによる静的解析
  3. 静的解析クイックスタート
  4. 構文解析
  5. 型チェック
  6. コード生成
  7. 静的単一代入形式
  8. パッケージ情報の取得
  9. go/analysis詳細
  10. コールグラフとポインタ解析
  11. 型パラメタを含むコードの解析

6

7 of 322

16.1. 静的解析を行う理由

7

8 of 322

Goのプログラムが実行されるまで

8

コーディング

コンパイル

実行

01010010100010100011001000101001

ソースコード

オブジェクトコード

$ go build main.go

Hello, 世界

コードが正しい場合

コーディング

コンパイル

問題のあるソースコード

問題の修正

$ go build main.go

Error: ...

コンパイルエラー

コードに誤りがある場合

9 of 322

コンパイルエラーにならないバグ

9

実装・考慮漏れ

実装ミス

実行時エラー

仕様を満たしていない

仕様にバグがある

考慮が漏れている

文法エラーではないミス

設定ミス

ライブラリの使い方ミス

メモリリーク

ゴルーチンリーク

パニック

10 of 322

コンパイル以外でバグを見つける

10

コーディング

010100101000101000

コンパイル

010100101000101000

デプロイ

リリース

影響小

影響大

テスト

監視

QA

単体テストや結合テスト

機械的に動作確認

人の手による確認

実装漏れや品質を確認

人の手による確認

実装漏れや品質を確認

静的解析

リリース後の本番環境で問題の原因を

見つけるのは非常に困難になる

11 of 322

静的解析と動的解析

  • 静的解析
    • プログラムを実行せずに解析すること
    • ソースコードの構造や意味を解析する
    • 例:Linter、コード補完、 コードフォーマッタ
  • 動的解析
    • プログラムを実行して解析すること
    • 実行時 変数 状態や関数 実行順などを検証
    • 例:Race Detector、ゴールーチンの可視化

11

ソースコードを文字列の塊として

解析せず静的解析を行う理由とは?

12 of 322

"Gopher"を探せ!

  • ソースコードから"Gopher"という文字列を探そう

12

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

}

13 of 322

みんな大好きgrepコマンド

  • grepコマンドを使って検索してみる
    • ソースコードに含まれている文字列を検索できる

13

$ grep Gopher main.go

Gopher string `json:"gopher"`

gogopher := Gopher()

gogopher.Gopher = gopher

func Gopher() (gopher *GOPHER) {

gopher = &GOPHER{Gopher: "gopher"}

14 of 322

Gopher関数を探せ!

  • ソースコードからGopher関数を探そう

14

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

}

15 of 322

Gopher関数を探すには?

  • grepコマンドだと文字列としてしか検索できない
    • 文字列以上の情報が得られない
    • 関数名であるという情報にはたどり着けない
  • Goのソースコードとして理解する必要
    • ソースコードの構造や意味を知ることができる
    • 文字列以上の情報が得られる

15

ソースコードとして理解するために

静的解析が必要となる

16 of 322

Goでよく使われる静的解析ツール

  • 実行せずにソースコードを解析するツール

16

go vet

バグといえるレベルの誤りを検出

errcheck

エラー処理のミスを検出

statickcheck

サードパーティ製の静的解析ツールのセット

golangci-lint

サードパーティ製のLinter Runner

gosec

セキュリティチェック

17 of 322

go vet

  • コンパイラでは発見できないバグを見つける
    • go testを走らせれば自動で実行される(Go1.10から)
      • 実行される項目は少なくなるため注意

17

package main

import "fmt"

func main() {

fmt.Printf("%s\n", 100) // %sに数値は指定できない

}

18 of 322

ルール違反はツールで検出する

  • ルールを作る場合はツールも作る
    • 人の手でコーディングルールなどを守っているかをチェックしない
    • ツールに任せることで検出漏れを防ぐ
  • 例:context.Contextをフィールドにしない
    • https://github.com/gostaticanalysis/ctxfield
    • コンテキストはラップして値を更新していくもの
    • 古いコンテキストを使用してしまう恐れがある
      • トランザクションの掛け忘れ
      • タイムアウトの掛け忘れ
  • 例:○○という関数は使わない
    • https://github.com/gostaticanalysis/called
    • 指定した関数の呼び出し部分を検出する

18

19 of 322

静的解析を行う理由

  • 早い段階でバグを見つける
    • コンパイルより前にバグを発見できる
  • 文字列ではなくソースコードとして解析する
    • 文字列として解析するだけでは分からない情報が手に入る
  • ルールをチェックする
    • ルールだけ決めても厳守されるとは限らない
    • 人間の目だとチェックが大変
  • チェックを自動化する
    • テストの自動化のように静的解析もCIで自動化する
    • 自動でかかるので手動で行う負担(精神的負担も含め)が減る

19

20 of 322

16.2. ast-grepによる静的解析

20

21 of 322

ast-grepとは?

  • 簡単に静的解析が行える
    • ソースコードとして検索が可能
    • Linter、リファクタリング、コード生成
  • プログラミング言語を問わない
    • tree-sitterを使用しているため多くの言語に対応
    • バックエンドもフロントエンドも1つのツールでできる
  • 抽象構文木のトラバースに特化
    • 型チェックや静的単一代入(SSA)形式などは扱わない
    • シンプルなチェックに限定される

公式ページ

22 of 322

抽象構文木(AST)

  • ソースコードを木構造で表現したもの
    • 文法上の構造を示している
    • ノードの種類や構造を調べて静的解析を行う

AST: Abstract Syntax Tree

v + 1

+

v

1

二項演算式

識別子

整数リテラル

ソースコード

抽象構文木(AST)

構文解析

23 of 322

Playground

インストール不要

視覚的に確認

シェアリンク

24 of 322

ルールの記述方法

  • パターンとYAMLで記述できる
    • YAMLの方が柔軟でより扱いやすい
    • 特定のノードがマッチするようにルールを記述していく

# YAML Rule is more powerful!

# https://ast-grep.github.io/guide/rule-config.html#rule

language: Go

severity: error

rule:

## ここにルールを記述していく

25 of 322

kind:ノードの種類を指定

  • ノートの種類を指定する
    • tree-sitterのノードの種類を指定する
    • 最初はPlaygroundで確認すると分かりやすい
      • Playgroundで該当のソースコードをクリックするとノードが選択される
      • ノードをクリックすると種類名がコピーできる

# YAML Rule is more powerful!

# https://ast-grep.github.io/guide/rule-config.html#rule

language: Go

severity: error

rule:

kind: call_expression # 関数呼び出し式

26 of 322

regex:正規表現のマッチ

  • ノードに対応するソースコードにマッチさせる
    • 一緒にkindルールが必要
    • 正規表現はRustのものを使う

# YAML Rule is more powerful!

# https://ast-grep.github.io/guide/rule-config.html#rule

language: Go

severity: error

rule:

kind: type_identifier

regex: u?int[1-9]* # 整数型かどうか

27 of 322

【TRY】log.Fatalの呼び出し

  • log.Fatal関数を呼び出している箇所を見つけよう

package main

import "log"

func main() {

log.Fatal("NG") // NG

log.Print("OK") // OK

}

28 of 322

has:ルールを満たすノードを持つ

  • 子ノードにルールを満たすノードを持つか
    • stopByを指定するとどこまで調べるか指定できる
      • stopBy: neighbor 直属の子ノードまで
      • stopBy: end    葉ノードまで

# YAML Rule is more powerful!

# https://ast-grep.github.io/guide/rule-config.html#rule

language: Go

severity: error

rule:

kind: package_clause

has:

kind: package_identifier

regex: "[A-Z]" # パッケージ名に大文字を使っている

29 of 322

not:否定

  • ルールに当てはまらない場合
    • 当てはまらない正規表現を書くより簡単

# YAML Rule is more powerful!

# https://ast-grep.github.io/guide/rule-config.html#rule

language: Go

severity: error

rule:

kind: package_clause

not:

has:

kind: package_identifier

regex: "^main$" # mainパッケージ以外

30 of 322

【TRY】フィールド名のない構造体リテラル

  • フィールド名が指定されていない構造体リテラル
    • 構造体リテラルはcomposit_literal
    • keyed_elementでフィールド名の指定を表現している

package main

type T struct {

N int

}

func main() {

var _ = T{N:100} // OK

var _ = T{100} // NG

}

31 of 322

inside:親ノードがルールを満たすか

  • 親ノードにルールを満たすノードを持つか
    • stopByを指定するとどこまで調べるか指定できる
      • stopBy: neighbor 直属の親ノードまで
      • stopBy: end    ルートノードまで

# YAML Rule is more powerful!

# https://ast-grep.github.io/guide/rule-config.html#rule

language: Go

severity: error

rule: # for文の中のdefer文

kind: defer_statement

inside:

kind: for_statement

stopBy: end

32 of 322

【TRY】関数内でのregexp.MustCompileの呼び出し①

  • 関数内のregexp.MustCompileの呼び出しを見つける
    • パッケージ変数の初期化はOK

package main

import "regexp"

var _ = regexp.MustCompile("OK") // OK

func main() {

var _ = regexp.MustCompile("NG") // NG

}

33 of 322

any:どれかにマッチする

  • 複数のルールのいずれかにマッチ
    • 全部にマッチするのはall

# YAML Rule is more powerful!

# https://ast-grep.github.io/guide/rule-config.html#rule

language: Go

rule: ## varでの宣言か:=での宣言化

any:

- kind: var_declaration

- kind: short_var_declaration

34 of 322

【TRY】関数内でのregexp.MustCompileの呼び出し②

  • メソッド内での呼び出しもNG

package main

import "regexp"

var _ = regexp.MustCompile("OK") // OK

func main() {

var _ = regexp.MustCompile("NG") // NG

}

type T struct{}

func (T) M() {

var _ = regexp.MustCompile("NG") // NG

}

35 of 322

【TRY】関数内でのregexp.MustCompileの呼び出し③

  • init関数内での呼び出しはOK

package main

import "regexp"

var _ = regexp.MustCompile("OK") // OK

func main() {

var _ = regexp.MustCompile("NG") // NG

}

func init() {

var _ = regexp.MustCompile("OK") // OK

}

36 of 322

CLIツールのインストールと実行

$ brew install ast-grep

$ cargo install ast-grep --locked

Homebrew (macOS)

cargo

※ cargoはRustのパッケージマネージャ

$ ast-grep scan --rule rule.yml main.go

実行

37 of 322

16.3. 静的解析クイックスタート

37

38 of 322

静的解析ツールを自作する

  • 自作する必要性
    • プロジェクトの独自ルール
      • 例:インポートルール
        • 決められたパッケージからしかインポートできない
        • レイヤードアーキテクチャのレイヤーをまたぐインポートの場合など
    • 特定のライブラリの使い方を検証したい
    • 欲しい静的解析ツールがない
      • かゆいところに手が届くものがない
      • なければ作るしかない
      • レビューで毎回指摘しているようなものはツールにする

39 of 322

静的解析ツールを作ってみよう!

  • 2つめの結果を受け取らないとパニックが発生しうる
    • 型アサーションが不可能な場合にパニックが発生

39

package main

func main() {

var v interface{} = "hello"

n := v.(int) // パニックする

println(n)

}

どうやったらこのバグを発見できる?

40 of 322

ソースコードの構造を解析する

n := v.(int)

Assign Stmt

n,ok := v.(int)

Type Assert Expr

Assign Stmt

Ident

Type Assert Expr

Ident

Ident

Lhs

Rhs

Rhs

Lhs

ソースコード

抽象構文木(AST)

構文解析

構文解析

※ 完全に見つけるにはもうちょっと複雑

41 of 322

静的解析ツール開発の流れ

42 of 322

goパッケージ

42

go/ast

抽象構文木(AST)を提供

go/build

パッケージに関する情報を集める

go/build/constraint

build constraintをパースする機能を提供

go/constant

定数に関する型を提供

go/doc

ドキュメントをASTから取り出す

go/doc/comment

ドキュメントコメントをパースする機能を提供

go/format

コードフォーマッタ機能を提供

go/importer

コンパイラに適したImporterを提供

go/parser

構文解析 機能を提供

go/printer

AST 表示機能を提供

go/scanner

字句解析 機能を提供

go/token

トークンに関する型を提供

go/types

型チェックに関する機能を提供

go/version

Goのバージョンをパースする機能を提供

43 of 322

x/tools/goパッケージ

43

analysis

静的解析ツールをモジュール化するパッケージ

ast

AST関連のユーティリティ

buildutil

標準ライブラリのgo/buildパッケージに関するユーティリティ

callgraph

call graph関連

cfg

control flow graph関連

gcexportdata

gc(Go Compiler)のexport dataに関する機能

packages

パッケージ情報の収集から構文解析、型チェックまでを行うパッケージ

pointer

ポインタ解析

ssa

Static Single Assignment (SSA) 関連

types

型情報関連

44 of 322

静的解析とコンパイラ

  • goパッケージはコンパイラは基本独立している
    • あくまで静的解析用のパッケージ
    • GoのコンパイラはもともとCで書かれていた
    • コンパイラで用いているデータ構造と異なる場合がある
      • 特に静的単一代入形式はまったく異なる
  • コンパイラの実装が歩み寄っている
    • goパッケージの一部を使用している箇所がある
    • 実装がほとんど同じ箇所も増えてきている

44

45 of 322

Goにおける静的解析のフェーズ

  • 静的解析はいくつかのフェーズに分かれている
    • 後のフェーズにいくこと、さらに詳しい情報が手に入る
    • 各フェーズで手に入る情報を使い分けながら解析する
    • 各フェーズで手に入る情報の紐付けの仕方がキモ
      • このノードの型情報は?など

45

構文解析

型チェック

静的単一代入形式

ポインタ解析

46 of 322

字句解析- go/scanner,go/token

  • 入力された文字列をトークンとして分解

46

IDENT

ADD

INT

トークン

ソースコード:

v + 10

47 of 322

構文解析 - go/parser,go/ast

  • トークンを抽象構文木(AST)に変換
    • AST: Abstract Syntax Tree

47

v + 10

IDENT

ADD

INT

ソースコード:

+

v

10

BinaryExpr

Ident

BasicLit

トークン:

抽象構文木(AST):

48 of 322

例:import文の重複

  • 同じインポートパスのimport文を見つける
    • 別名をつけると同じインポートパスのパッケージをインポート可能
    • https://github.com/gostaticanalysis/dupimport

48

package main

import fmt1 "fmt"

import fmt2 "fmt"

func main() {

fmt1.Println("Hello")

fmt2.Println("World")

}

*File

[]Decl

*GenDecl

*FuncDecl

*ImportSpec

49 of 322

構文解析で分からないこと

  • 型情報
    • 型の不一致など
    • 変数の型や式の型
  • 定数式の結果
    • 定数式の計算や定数の型
  • 識別子の解決
    • どの識別子がどこで定義されているのか
    • どの識別子がどこで使用されているのか

49

BinaryExpr

100 + "hello"

BasicLit

100

BasicLit

"hello"

100 + "hello"

型が合わなくても文法上は問題ない

50 of 322

抽象構文木(AST)を使った解析

  • 書き方に注目する
    • どのように書いたか?が重要な場合
    • 書き方のスタイルをチェックする場合に便利
  • 文字列比較はしない
    • 特定のパッケージや型、関数の特定の文字列比較をしない
    • エイリアスや再代入によって名前が変わる可能性がある
    • 型情報を使うべき

50

51 of 322

型チェック - go/types,go/constant

  • 型情報を抽象構文木から抽出
    • 識別子の解決
    • 型の推論
    • 定数の評価

51

n := 100 + 200

m := n + 300

定数の評価

= 300

型の推論

-> int

識別子の解決

52 of 322

型チェックで分かること

  • 型情報
    • 型の不一致など
    • 変数の型や式の型
  • 定数式の結果
    • 定数式の計算や定数の型
  • 識別子の解決
    • どの識別子がどこで定義されているのか
    • どの識別子がどこで使用されているのか

52

53 of 322

例:不要な識別子の判別

  • 使用されていない識別子を見つける
    • https://github.com/gostaticanalysis/unused
    • パッケージ外に公開されていないパッケージ変数や関数が対象
    • どこからも参照されていないものを探す
    • 機械的に削除しても問題ない

53

package mypkg

// 使用されていない

func f() { println("f") }

// エクスポートされている

func G() { println("G") }

削除しても問題ない

54 of 322

例:コンテキストを構造体に保持

  • コンテキストを構造体に保持しているものを見つける
    • https://github.com/gostaticanalysis/ctxfield
    • フィールドの型がcontext.Context型を実装しているか
    • インタフェースの実装のために止む得ない場合を除く
    • コンテキストが古くなることを防ぐ

54

package mypkg

import "context"

// NG

type S struct {

ctx context.Context

}

フィールドに保持するのは

好ましくない

55 of 322

型情報を使った解析

  • 識別子に対応するオブジェクトを特定する
    • 抽象構文木を元に型情報を取得する
    • 識別子に対応するオブジェクト(型や関数、変数など)を特定
    • 特定したオブジェクトに対する操作がチェックできる
  • 処理や値を流れは追えない
    • 抽象構文木と型情報では処理の流れを追うのは難しい
    • 値がどのように変化していくのか追うのも難しい
    • 処理や値の流れを追うには静的単一代入形式(SSA)が必要

55

56 of 322

静的解析ツールのモジュール化

  • golang.org/x/tools/go/analysisパッケージ
    • 静的解析ツールのモジュール化を提供するパッケージ
    • Go1.12からgo vetでも使われるようになった

57 of 322

go/analysis使う利点

  • 共通部分は自動で行われる
    • 構文解析から型チェックまでは自動で行われる
    • パッケージ名からソースコード群を見つけるなども自動
    • 自分の作りたい静的解析ツールのアルゴリズムに集中できる
    • パッケージ単位で処理がされる
  • モジュール化されているので使いまわしできる
    • Analyzerという単位で静的解析ツールを作る
    • ライブラリ的な振る舞いをするAnalyzerも作れる
    • 依存するAnalyzerを指定することで別のAnalyzerから�解析結果が使える
    • 同じAnalyzerは1度しか実行されない
    • 各Analyzerはゴールーチンで実行される

57

58 of 322

analysis.Analyzer

  • go/analysisの静的解析の1つの単位を表す構造体
    • Runフィールドに処理の本体を書く
    • Requiresに依存するAnalyzerを書く

type Analyzer struct {

Name string

Doc string

URL string

Flags flag.FlagSet

Run func(*Pass) (any, error)

RunDespiteErrors bool

Requires []*Analyzer

ResultType reflect.Type

FactTypes []Fact

}

59 of 322

analysis.Pass

  • 静的解析に使う情報が入った構造体
    • Analyzer.Runフィールドの引数で用いられる

type Pass struct {

Analyzer *Analyzer

Fset *token.FileSet // ファイル上の位置を扱うための情報

Files []*ast.File // ファイル単位の抽象構文木(AST)

OtherFiles []string

IgnoredFiles []string

Pkg *types.Package // パッケージ単位の型情報

TypesInfo *types.Info // ASTのノードと型情報の組み合わせを保持

TypesSizes types.Sizes

TypeErrors []types.Error

Module *Module // パッケージが存在するモジュールの情報

Report func(Diagnostic)

ResultOf map[*Analyzer]any // 依存するAnalyzerの解析結果

ReadFile func(filename string) ([]byte, error)

ImportObjectFact func(obj types.Object, fact Fact) bool

ImportPackageFact func(pkg *types.Package, fact Fact) bool

ExportObjectFact func(obj types.Object, fact Fact)

ExportPackageFact func(fact Fact)

AllPackageFacts func() []PackageFact

AllObjectFacts func() []ObjectFact

}

60 of 322

analysis.Diagnostic

  • token.Pos(位置)に関連付けられた静的解析結果
    • 任意の位置へのエラーを表現するために使う
      • 例:〇行目に〇〇というエラーがあります
  • (*analysis.Pass).Reportfメソッド
    • (*analysis.Pass).Reportフィールドのラッパー
    • Diagnosticを生成するメソッド
    • fmt.Fprintf感覚で使える

func (pass *Pass) Reportf(pos token.Pos, format string, args ...any)

61 of 322

簡単なAnalyzerの例

61

var Analyzer = &analysis.Analyzer{

Name: "simple",

Doc: "simple is simple Analyzer",

Run: run,

Requires: []*analysis.Analyzer{inspect.Analyzer},

}

func run(pass *analysis.Pass) (any, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

inspect.Preorder([]ast.Node{new(ast.Ident)}, func(n ast.Node) {

switch n := n.(type) {

case *ast.Ident:

if n.Name == "gopher" {

pass.Reportf(n.Pos(), "identifier is gopher")

}

}

})

return nil, nil

}

62 of 322

簡単なAnalyzerの例(イテレータ版)

62

var Analyzer = &analysis.Analyzer{

Name: "simple",

Doc: "simple is simple Analyzer",

Run: run,

Requires: []*analysis.Analyzer{inspect.Analyzer},

}

func run(pass *analysis.Pass) (any, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

for n := range inspect.PreorderSeq((*ast.Ident)(nil)) {

switch n := n.(type) {

case *ast.Ident:

if n.Name == "gopher" {

pass.Reportf(n.Pos(), "identifier is gopher")

}

}

}

return nil, nil

}

63 of 322

analysistestパッケージ

  • Analyzerのテストを簡単に行うためのパッケージ
    • 実際の解析対象となるソースコードをファイルとして用意すればよい
    • テストデータはtestdata以下に用意する
    • コメントによって該当行にDiagnosticが出力されるかチェックする

func Test(t *testing.T) {

testdata := analysistest.TestData()

analysistest.Run(t, testdata, fourcetypeassert.Analyzer, "a")

}

64 of 322

テストデータ

  • wantで始まるコメントで期待する結果を書く
    • 対応するDiagnosticが出力されていればOK
    • want以降は正規表現が使用できる
      • .などは\.のようにエスケープする必要がある
      • ""の代わりに``を使用した方が書きやすい
        • 例:want `log\.Fatal must not call`

package a

func f() {

var a any

_ = a.(int) // want "must not do fource type assertion"

_, _ = a.(int) // OK

switch a := a.(type) { // OK

case int:

println(a)

}

}

65 of 322

unitchecker

  • unitchecker.Main
    • 複数のAnalyzerからなるmain関数を提供する
    • Analyzerはゴールーチンで1回だけ実行される
    • 作成した実行可能ファイルはgo vet -vettoolコマンドから呼ぶ
    • 各種設定はgo vetからもらう

package main

import (

"example.com/sample"

"golang.org/x/tools/go/analysis/unitchecker"

)

func main() {

unitchecker.Main(sample.Analyzer)

}

66 of 322

go vetからの実行

  • go vet-vettoolオプションを用いる
    • unitchecker.Main関数を呼び出す実行可能ファイルを指定できる
    • 相対パスではなく、絶対パス
      • $(which myvet)myvetコマンドの絶対パスを取得
    • フラグは-called.funcs=log.Fatalのように指定できる

66

$ go vet -vettool=$(which myvet) pkgname

67 of 322

自作のAnalyzerコレクションを作る

  • unitchecker.Main関数に複数のAnalyzerを設定する
    • プロジェクト固有のLinterを作成する

67

package main

import (

"github.com/gostaticanalysis/forcetypeassert"

"github.com/gostaticanalysis/nofmt"

"github.com/gostaticanalysis/notest"

"github.com/gostaticanalysis/vetgen/analyzers"

"golang.org/x/tools/go/analysis/unitchecker"

)

func main() {

unitchecker.Main(append(

analyzers.Govet(), // go vetと同じもの

nofmt.Analyzer,

notest.Analyzer,

forcetypeassert.Analyzer,

)...)

}

68 of 322

【TRY】自作のAnalyzerコレクション

68

69 of 322

skeleton

  • go/analysis用のスケルトンコードジェネレータ
    • https://github.com/gostaticanalysis/skeleton
    • 簡単に静的解析ツールを始めることができる
    • Analyzer、テストコード、main.goの雛形作ってくれる
    • -kindオプションで雛形を変更可
      • inspect, ssa, packages, codegenなど

$ skeleton example.com/myanalyzer

myanalyzer

├── cmd

│ └── myanalyzer

│ └── main.go

├── myanalyzer.go

├── myanalyzer_test.go

└── testdata

└── src

└── a

└── a.go

70 of 322

skeletonを使えば簡単につくれる

  • 思い立ったらすぐに作れる

簡単なものは1時間もかからず

開発が可能

71 of 322

skeletonを使った開発の流れ

  • テスト駆動開発で行える

71

雛形を作る

テストデータを作る

テストを動かす

Analyzerを修正

package a

func f() {

var a any

_ = a.(int) // want "NG"

}

$ skeleton example.com/sample

$ go test

72 of 322

【TRY】skeletonのインストール

  • skeletonをインストールしてみよう
    • インストールできたら動かしてみよう

72

$ go install github.com/gostaticanalysis/skeleton/v2@latest

$ skeleton example.com/sample

$ cd sample

$ go mod tidy

$ go test

73 of 322

golangci-lintのプラグイン

  • 標準のpluginパッケージを用いる
    • GetAnalyzersメソッドを持つAnalyzerPlugin変数を提供する
    • golangci-lintで使っているライブラリとバージョンを揃える
      • analysisパッケージなど

73

package main

import (

"github.com/gostaticanalysis/unused"

"golang.org/x/tools/go/analysis"

)

var AnalyzerPlugin analyzerPlugin

type analyzerPlugin struct{}

func (analyzerPlugin) GetAnalyzers() []*analysis.Analyzer {

return []*analysis.Analyzer{ unused.Analyzer }

}

$ go build -buildmode=plugin -o path_to_plugin_dir main.go

74 of 322

skeletonでgolangci-lintプラグイン作成

  • skeletonが自動生成するmain関数を使えば簡単
    • pluginディレクトリ以下に生成される
    • ビルド時に-ldflagsオプションでフラグが指定できる

74

$ go build -buildmode=plugin \

-ldflags "-X 'main.flags=-funcs=log.Fatal'" \

-o path_to_plugin_dir myanalyzer/plugin/myanalyzer

75 of 322

go/analysis/passesパッケージ

  • go vetで使われるAnalyzerや便利なライブラリを提供
    • golang.org/x/tools/go/analysis/passesパッケージ

75

パッケージ名

説明

buildssaパッケージ

静的単一代入(SSA)形式を生成する

ctrlflowパッケージ

Control Flow Graph(CGF)を生成する

inspectパッケージ

抽象構文木の探索を行う

76 of 322

gostaticanalysis

  • Goの静的解析に関するリポジトリを集めたもの
    • https://github.com/gostaticanalysis
    • 便利なライブラリやAnalyzerを提供している

77 of 322

【TRY】静的解析ツールを作ろう!

  • インポート文の重複を検出しよう
    • 同じパスのimport文を見つける

77

package main

import fmt1 "fmt"

import fmt2 "fmt" // want "NG"

func main() {

fmt1.Println("Hello")

fmt2.Println("World")

}

*File

[]Decl

*GenDecl

*FuncDecl

*ImportSpec

78 of 322

ヒント:インポート文の重複を検出

  • skeletonでひな形を作る
    • dupimport.gorun関数を実装する
  • 1つずつファイルを調べる
    • 引数pass[]*ast.File型のFilesフィールドを調べる
  • ファイルごとの重複をチェックする
    • ast.File構造体のImportsフィールドを調べる
    • Importsフィールドは[]*ast.ImportSpec
    • 重複チェックはマップを使う
  • インポート文のパスをチェックする
    • ast.ImportSpec構造体のPathフィールドを調べる
    • Pathフィールドは*ast.BasicLit
    • ast.BasicLit型のValueフィールドを取得する
    • strconv.Unquote関数を掛けて""を取り除く

78

79 of 322

16.3. 構文解析

79

80 of 322

構文解析 - go/parser,go/ast

  • トークンを抽象構文木(AST)に変換
    • AST: Abstract Syntax Tree

80

v + 10

IDENT

ADD

INT

ソースコード:

+

v

10

BinaryExpr

Ident

BasicLit

トークン:

抽象構文木(AST):

81 of 322

構文解析で分からないこと

  • 型情報
    • 型の不一致など
    • 変数の型や式の型
  • 定数式の結果
    • 定数式の計算や定数の型
  • 識別子の解決
    • どの識別子がどこで定義されているのか
    • どの識別子がどこで使用されているのか

81

BinaryExpr

100 + "hello"

BasicLit

100

BasicLit

"hello"

100 + "hello"

型が合わなくても文法上は問題ない

82 of 322

言語仕様を読もう

  • 抽象構文木の扱うためにはGoの構文に詳しくなろう
    • https://go.dev/ref/spec
    • 最低限EBNFを流し読みする
    • 構文を深く理解すれば抽象構文木を自由に扱える
      • どういうノードで構成されているか
      • どういう情報がどういうノードから取得できるか

82

83 of 322

抽象構文木(AST)の取得

  • go/parserパッケージを用いる
    • parser.ParseExprFrom関数
      • 式をパースする
    • parser.ParseExpr関数
      • 式をパースする
      • parser.ParseExprFrom関数の簡易版
    • parser.ParseFile関数
      • ファイル単位でパースする
    • parser.ParseDir関数
      • ディレクトリ単位でパースする
      • 中でparser.ParseFile関数を呼んでいる

83

非推奨

go/analysisパッケージが自動で行う

84 of 322

手動で構文解析を行う理由

  • Linter系はgo/analysisパッケージを使う
    • 自動で構文解析と型チェックを行ってくれる
  • コード生成やgo/analysisパッケージが使えない場合
    • go/packagesパッケージ(後述)を使って構文解析を行う
  • 式単位でパースしたい場合
  • 1ファイルだけパースしたい場合

84

85 of 322

token.Pos

  • token.Pos型はファイル上の位置を表す
    • ファイルを跨いだ位置を表す
    • トークンや抽象構文木のノードの位置を表す
    • 単なる整数なので大小比較ができる
    • token.FileSet型によって管理される
    • token.FileSet型で管理されているファイルで一意の値になる

85

// tokenパッケージにおける定義

type Pos int

const NoPos Pos = 0

86 of 322

token.FileSet

  • ファイルごとの位置情報を管理する
    • 構文解析時に出力引数として渡される
    • トークンのファイル上の位置を記録していく
    • ファイルごとのサイズ(オフセット)を管理
    • token.Pos型の値からどのファイルの何行目などの情報が得られる

86

1

ファイルA

ファイルB

100バイト

100

token.Pos型の値

200バイト

87 of 322

式単位の構文解析

  • parser.ParseExpr関数を用いる
  • parser.ParseExprFrom関数を用いてもできる

87

expr, err := parser.ParseExpr(`v + 1`)

if err != nil { /* エラー処理 */ }

/* exprを解析する処理 */

fset := token.NewFileSet() // ファイル情報

src := []byte(`v + 1`)

f := "" // ファイル名(式なので不要)

m := 0 // モード(式なので不要)

expr, err := parser.ParseExprFrom(fset, f, s, m)

88 of 322

ファイル単位の構文解析

  • parser.ParseFile関数を用いる
    • 第3引数はstring型、io.Reader型、[]byte型のいずれか
    • 第3引数がnilの場合は第2引数のパスを開く

88

const src = `

package main

var v = 100

func main() {

println(v+1)

}`

fs := token.NewFileSet()

// ParseFile(fset *token.FileSet, filename string, src any, mode Mode) (*ast.File, error)

f, err := parser.ParseFile(fs, "my.go", src, 0)

if err != nil { /* エラー処理 */ }

/* f を解析する処理 */

89 of 322

parser.Mode

  • 構文解析器の挙動を変えるフラグ

89

定数名

説明

parser.PackageClauseOnly

package句で構文解析を止める

parser.ImportsOnly

import定義で構文解析を止める

parser.ParseComments

コメントも解析をしASTに加える

parser.Trace

構文解析のトレース情報を表示する

parser.DeclarationErrors

定義エラー(多重定義など)を報告する

parser.SpuriousErrors

parser.AllErrorsと同じで後方互換のため

parser.AllErrors

最初の10個だけではなくすべてのエラーを報告する

90 of 322

ディレクトリ単位の構文解析

  • parser.ParseDir関数を用いる
    • 第3引数をnilにすると.goファイルだけを対象とする
    • パッケージ名と*ast.Pacakge型の値のマップが取得できる

90

fset := token.NewFileSet()

path := filepath.Join(runtime.GOROOT(), "src", "fmt")

filter := func(info os.FileInfo) bool { return info.Name() == "format.go" }

pkgs, err := parser.ParseDir(fset, path, filter, 0)

if err != nil { /* エラー処理 */ }

for name, pkg := range pkgs {

fmt.Printf("======= %s =======\n", name)

for fname := range pkg.Files {

fmt.Println(fname)

}

}

モード

非推奨

91 of 322

抽象構文木のダンプ

  • ast.Print関数とast.Fprint関数を用いる
    • ast.Fprint関数はフィルターがセットできる
    • 値がnilのフィールドを表示しない場合はast.NotNilFilterを使う

91

fset := token.NewFileSet()

expr, err := parser.ParseExprFrom(fset, "", `v + 1`, 0)

if err != nil { /* エラー処理 */ }

ast.Print(fset, expr)

var filter ast.FieldFilter = func(name string, v reflect.Value) bool {

return v.Kind() == reflect.Int

}

err = ast.Fprint(os.Stdout, fset, expr, filter)

if err != nil { /* エラー処理 */ }

92 of 322

GoAst Viewer

  • Web上でGoコードの抽象構文木を表示するツール

92

93 of 322

astree

  • 抽象構文木(AST)を表示を行う

93

$ astree a.go

File

├── Doc

├── Package = a.go:1:1

├── Name

│ └── Ident

│ ├── NamePos = a.go:1:9

│ ├── Name = main

│ └── Obj

├── Decls (length=1)

│ └── FuncDecl

・・・

└── Unresolved (length=0)

package main

import (

_ "embed"

"go/parser"

"go/token"

"os"

"github.com/knsh14/astree"

)

//go:embed a.go

var src string

func main() {

fs:= token.NewFileSet()

f, err := parser.ParseFile(fs, "a.go", src, 0)

if err != nil { panic(err) }

astree.File(os.Stdout, fs, f)

}

94 of 322

【TRY】抽象構文木の確認

  • 以下のコードの抽象構文木をダンプしてみよう
    • どんなノードがあるか確かめてみよう
    • name := "Gopher"はどんなノードで構成されているか?

94

package main

import "fmt"

var msg string

func main() {

msg = "Hello"

name := "Gopher"

fmt.Println(msg, name)

}

95 of 322

抽象構文木のノード

  • ast.Nodeインタフェースを実装した型
    • 子ノードの持ち方は型によって異なる
    • ファイル単位の場合は*ast.File型がルートノードとなる

95

type Node interface {

Pos() token.Pos

End() token.Pos

}

+

v

10

BinaryExpr

Ident

BasicLit

96 of 322

ノードの種類 −1−

96

ノードの種類

説明

パッケージを表すノード

ast.Package

ファイルを表すノード

ast.File

宣言を表すノード

ast.Declインタフェースを実装した型

宣言の詳細を表すノード

ast.Specインタフェースを実装した型

文を表すノード

ast.Stmtインタフェースを実装した型

式を表すノード

ast.Exprインタフェースを実装した型

型リテラルを表すノード

ast.ArrayType型やast.FuncType型など

非推奨

97 of 322

ノードの種類 −2−

97

ノードの種類

説明

リテラルを表すノード

ast.BasicLit型やast.FuncLit型など

識別子を表すノード

ast.Identast.Expr型を実装

コメントを表すノード

ast.Comment型とast.CommentGroup

case句を表すノード

ast.CaseClause型とast.CommClause

フィールドを表すノード

ast.Field型とast.FieldList

...を表すノード

ast.Ellipsis

※ 実際に実装しているのは*ast.Ident

98 of 322

パッケージを表すノード

  • ast.Package
    • スコープやインポートしているパッケージの情報が取れる
    • parser.ParseDir関数から取得できる

98

type Package struct {

Name string

Scope *Scope

Imports map[string]*Object

Files map[string]*File

}

非推奨

99 of 322

ファイルを表すノード

  • ast.File
    • ファイル単位の情報を保持する
    • parser.ParseFile関数から取得できる

99

type File struct {

Doc *CommentGroup // 関連付けられたドキュメント

Package token.Pos // packageキーワードの場所

Name *Ident // パッケージ名

Decls []Decl // パッケージスコープでの宣言群

FileStart token.Pos // ファイルの開始位置

FileEnd token.Pos // ファイルの終了位置

Scope *Scope // このファイルだけのパッケージスコープ // 非推奨

Imports []*ImportSpec // このファイルでインポートしているもの

Unresolved []*Ident // ファイル内の未解決な識別子

Comments []*CommentGroup // ファイル内のコメントすべて

GoVersion string // 最小のGoのバージョン

}

100 of 322

宣言を表すノード

  • ast.Declインタフェースを実装した型

100

型名

説明

*ast.BadDecl

エラーを含む不完全な宣言

*ast.GenDecl

インポート、型、変数、定数などの宣言

宣言の詳細はSpecインタフェースを実装した型で定義

*ast.FuncDecl

関数宣言

101 of 322

ast.GenDecl

  • インポート、型、変数、定数の宣言を表す
    • Tokフィールドで種類を判断
    • Specsフィールドに定義の詳細を保持

101

type GenDecl struct {

Doc *CommentGroup // 関連付けられたコメント

TokPos token.Pos // トークンの位置

Tok token.Token // IMPORT, CONST, TYPE, VARのどれか

Lparen token.Pos // '('がある場合の位置

Specs []Spec // 定義の詳細

Rparen token.Pos // ')'の位置(あれば)

}

102 of 322

宣言の詳細を表すノード

  • ast.Specインタフェースを実装した型
    • ast.GenDecl構造体のSpecsフィールドから取得
    • *ast.ImportSpec型の値はast.File構造体からも取得可能

102

型名

説明

*ast.ImportSpec

パッケージのインポート定義

*ast.ValueSpec

定数定義と変数定義

*ast.TypeSpec

型定義

103 of 322

【TRY】パッケージ変数の数

  • パッケージの変数を数えよう
    • ファイルにいくつあるか数えよう
    • 変数ができたら定数の数も数えてみよう

103

104 of 322

ast.FuncDecl

  • 関数定義を表す
    • Recvフィールドで関数かメソッドか判断できる
    • 引数などはast.FuncType構造体から取得する
    • 関数のボディはast.BlockStmt型のBodyフィールドに保持される

104

type FuncDecl struct {

Doc *CommentGroup // 関連付けられたコメント

Recv *FieldList // レシーバ(nilの場合は関数)

Name *Ident // 関数/メソッド名

Type *FuncType // 関数のシグニチャ(引数、戻り値など)

Body *BlockStmt // 関数のボディでGoの関数じゃない場合はnil

}

105 of 322

【TRY】パッケージ関数の一覧

  • パッケージ関数の一覧を表示しよう
    • ファイルに定義されているパッケージ関数の名前を表示しよう
    • init関数が複数ある場合にどうなるか確認してみよう
    • _という名前の関数が複数ある場合にどうなるか確認してみよう
    • Bodyがnilの関数が定義できるか考えよう

105

106 of 322

文を表すノード −1−

  • ast.Stmtインタフェースを実装した型

106

型名

説明

*ast.BadStmt

エラーを含む不完全な文

*ast.DeclStmt

constvartypeで定数、変数、型などの宣言文

*ast.EmptyStmt

空の文。文が書ける場所で省略した場合など

*ast.LabeledStmt

ラベルのついた文

*ast.ExprStmt

単一の式からなる文

*ast.SendStmt

チャネルの送信文(式ではないことに注意)

107 of 322

文を表すノード −2−

  • ast.Stmtインタフェースを実装した型

107

型名

説明

*ast.IncDecStmt

++--での増減を行う文(式ではないことに注意)

*ast.AssignStmt

=を使った代入文と:=での宣言+代入も含む

*ast.GoStmt

goキーワードによる新規ゴールーチンでの関数呼び出し

*ast.DeferStmt

deferキーワードによる関数呼び出し

*ast.ReturnStmt

return

*ast.BranchStmt

breakcontinuefallthroughgotoなどによる分岐文

*ast.BlockStmt

ブロック文(複文)

*ast.IfStmt

if

108 of 322

文を表すノード −3−

  • ast.Stmtインタフェースを実装した型

108

型名

説明

*ast.SwitchStmt

switch

*ast.TypeSwitchStmt

型スイッチのswitch

*ast.SelectStmt

select

*ast.CaseClause

switch文のcase句

*ast.CommClause

select文のcase句

*ast.ForStmt

for

*ast.RangeStmt

range句を持つfor

109 of 322

【TRY】for文の中のdefer

  • for文の中でdefer文を使っている部分を見つけるLinterを作成してください
    • Control Flow Graph(CFG)を使わないと完全に特のは難しいです
    • 一旦はfor文直下に書いてあるものだけ対応してください
    • 余裕があったらfor range文も対応してください

109

func f() {

defer func() {}() // OK

for range 10 {

defer func() {}() // want "NG"

if false {

// 本当は見つけてほしいけど難しい

defer func() {}() // want "NG"

}

}

}

110 of 322

【TRY】循環複雑度を求めよう

  • 循環複雑度
    • プログラムの複雑度を測る指標
      • 制御フローグラフ(Control Flow Graph)を用いる
      • どのくらい分岐や繰り返しがあるか分かる
      • E – N + 2P
        • E:エッジの数
        • N:ノードの数(基本ブロックの数)
        • Pは単一の関数の場合は1

110

func f2(x, y int) int {

if x == 2 { if y == 2 { if x+y == 4 { return x + y } } }

return 0

}

111 of 322

式を表すノード −1−

  • ast.Exprインタフェースを実装した型

111

型名

説明

*ast.BadExpr

エラーを含む不完全な式

*ast.ParenExpr

()でくくられた式

*ast.SelectorExpr

v.Mなどのフィールドやメソッドを参照する式

*ast.IndexExpr

[]で配列、スライス、マップの要素にアクセスするインデクサの式

または、型パラメタおよび型引数

*ast.IndexListExpr

型パラメタおよび型引数(複数ある場合、例:[X, Y any])

*ast.SliceExpr

スライス演算式(例:slice[1:3]

*ast.TypeAssertExpr

型アサーションの式

*ast.CallExpr

関数呼び出しと型変換(キャスト)の式

112 of 322

式を表すノード −2−

  • ast.Exprインタフェースを実装した型

112

型名

説明

*ast.StarExpr

ポインタのデリファレンスを行う式とポインタ型

*ast.UnaryExpr

単項演算式(-100など)

*ast.BinaryExpr

2項演算式(v + 1など)

*ast.KeyValueExpr

キーとバリューを:で区切ったような表現の式

*ast.BasicLitなど

リテラル

*ast.Ident

識別子

*ast.Ellipsis

可変長引数や配列の長さの...

113 of 322

型リテラルを表すノード

  • 型が記述される場所に対応するノード
    • 変数定義の型や型定義など
    • 名前付き型はast.Ident型で表される

113

型名

説明

ast.ArrayType

配列型またはスライス型の型リテラル

ast.StructType

構造体型の型リテラル

ast.FuncType

関数型の型リテラル

ast.InterfaceType

インタフェース型の型リテラル

ast.MapType

マップ型の型リテラル

ast.ChanType

チャネル型の型リテラル

// スライスの型リテラル

var ns []int

114 of 322

リテラルを表すノード

  • 名前の付いていない値
    • ast.Exprインタフェースも実装している
    • 構造体リテラルやスライスリテラルはコンポジットリテラルとして�言語仕様上でも定義されている
    • ast.BasicLit構造体のValueフィールドから値を取り出す場合はstrconvパッケージを使う
      • 文字列はstrconv.Unquote関数を用いる
    • nil、truefalse*ast.Ident型になる

114

型名

説明

*ast.BasicLit

数値リテラルや文字列リテラル

*ast.FuncLit

関数リテラル(無名関数、クロージャ)

*ast.CompositeLit

配列、スライス、構造体、マップのコンポジットリテラル

115 of 322

【TRY】空文字を返す関数

  • 空文字を返すパッケージ関数を探そう
    • ast.FuncDecl構造体のBodyフィールドから*ast.BlockStmt型の値を得る
    • return "" している箇所を探そう
      • これ以外で空文字を返すパターンを考えよう
      • どうやったら検出できるか考えよう

115

116 of 322

識別子を表すノード

  • ast.Ident
    • 識別子(=変数名、関数名、定数名、型名など)
    • Nameフィールドが同じでも同じ識別子を指すとは限らない
    • IsExportedメソッドで公開されてるか取得できる
    • ast.Exprインタフェースも実装している
    • niltruefalse、iotaなども識別子として扱われる

116

type Ident struct {

NamePos token.Pos // 識別子の位置

Name string // 識別子名

Obj *Object // 対応する*ast.Object型の値

}

117 of 322

【TRY】非公開なパッケージ関数

  • 非公開なパッケージ関数の一覧を表示しよう
    • ファイルに定義されている非公開パッケージ関数の名前を表示しよう
    • (*ast.Ident).IsExportedメソッドを使おう
    • メソッドは除く

117

118 of 322

【TRY】大きな関数

  • パッケージ関数の一覧をサイズを表示しよう
    • ファイルに定義されているパッケージ関数の名前を表示しよう
    • 関数の大きさの指標を考えよう
      • 行数
      • バイト数
      • 文の数
    • 大きな順に並べ替えよう

118

119 of 322

ast.Object

  • 名前の付いたものを表す
    • ast.Identは名前そのものを表す

119

type Object struct {

Kind ObjKind // 種類

Name string // 名前

Decl interface{} // 対応するField, XxxSpec, FuncDecl, LabeledStmt, AssignStmt, Scope

Data interface{} // オブジェクト固有のデータ

Type interface{} // 型情報のためのプレースホルダ。nilである場合がほとんど。

}

const (

Bad ObjKind = iota // エラー処理のための値

Pkg // パッケージ

Con // 定数

Typ // 型

Var // 変数

Fun // 関数またはメソッド

Lbl // ラベル

)

Kindの値

Dataの型

Dataの値

Pkg

*ast.Scope

パッケージスコープ

Con

int

iotaための連番

実はドキュメントが間違ってる可能性があり。要調査

非推奨

120 of 322

スコープ

  • ast.Scope
    • スコープごとの*ast.Object型の値を管理する
    • パッケージスコープとユニバーススコープのみ
    • 更に詳しい情報がほしい場合は型チェックを行う
    • ast.Package構造体とast.File構造体から取得できる

120

type Scope struct {

Outer *Scope // このスコープを内包しているスコープ

Objects map[string]*Object // 名前とオブジェクトのマップ

}

ast.Objectast.Scopeは非推奨になる予定(#52463

非推奨

121 of 322

ノードの種類で処理を分ける

  • 型スイッチを用いる
    • 再帰呼び出しも合わせて行うことが多い

121

func traverse(n ast.Node) {

switch n := n.(type) {

case *ast.Ident:

fmt.Println(n.Name)

case *ast.BinaryExpr:

traverse(n.X)

traverse(n.Y)

case *ast.UnaryExpr:

traverse(n.X)

}

}

122 of 322

木構造の深さ優先探索

  • 抽象構文木の探索に用いることが多い
    • ルートノードから子ノードからさらにその子ノードへ辿っていく
    • 葉までたどり着いたらバックトラックして他の子ノードに進む
    • 再帰で書かれることが多い

122

0

1

4

2

3

123 of 322

抽象構文木の走査

  • ast.Inspect関数を用いる
    • 深さ優先探索で各ノードに関数を適用する

123

n, err := parser.ParseExpr(`v + 1`)

if err != nil { /* エラー処理 */ }

ast.Inspect(n, func(n ast.Node) bool {

if n != nil {

// 型名を出力

fmt.Printf("%T\n", n)

}

// falseを返すと子ノードの探索をしない

return true

})

0

1

4

2

3

124 of 322

【TRY】ast.Inspect関数で探索

  • Gopherという関数(メソッドは除く)を探そう
    • ast.Inspect関数でGopherという識別子を探す
    • *ast.Ident型のノードを見つけ、Nameフィールドを調べる
    • ast.Ident構造体のObjフィールドからオブジェクトを取得
    • ast.Object構造体のKindフィールドを調べる

124

※ast.Objectは非推奨になる予定

125 of 322

抽象構文木の走査

  • ast.Walk関数を用いる
    • ast.Visitorインタフェースを引数に取る
    • 探索アルゴリズムを切り替えたい場合に使う

125

expr, err := parser.ParseExpr("v+1")

if err != nil { /* エラー処理 */ }

// 二項演算式に使われている識別子を探す

var v ast.Visitor

v = VisitFunc(func(n ast.Node) (w ast.Visitor) {

if _, ok := n.(*ast.BinaryExpr); !ok { return v }

// 二項演算式を見つけたら次は識別子を探す

w = VisitFunc(func(n ast.Node) ast.Visitor {

if ident, ok := n.(*ast.Ident); ok { fmt.Println(ident.Name) }

return w

})

return w

})

ast.Walk(v, expr)

// ast.Visitorインタフェースを実装

type VisitFunc func(n ast.Node) (w ast.Visitor)

func (v VisitFunc) Visit(n ast.Node) (w ast.Visitor) {

return v(n)

}

126 of 322

抽象構文木の走査 Preorderメソッド

  • x/tools/go/ast/inspectorパッケージを用いる
    • Preorderメソッドでフィルターをかけて探索ができる
    • ast.Expr型などのインタフェース型でフィルターできない

126

const src = "package main\n func main() { println(`hello`)}"

fset := token.NewFileSet()

f, err := parser.ParseFile(fset, "my.go", src, 0)

if err != nil { /* エラー処理 */ }

inspect := inspector.New([]*ast.File{f})

typs := []ast.Node{new(ast.CallExpr)}

inspect.Preorder(typs, func(n ast.Node) {

fmt.Printf("%T\n", n)

})

127 of 322

抽象構文木の走査 Nodesメソッド

  • x/tools/go/ast/inspectorパッケージを用いる
    • Nodesメソッドで探索のゆき・かえりをハンドリングできる

127

const src = "package main\n func main() { println(`hello`)}"

fset := token.NewFileSet()

f, err := parser.ParseFile(fset, "my.go", src, 0)

if err != nil { /* エラー処理 */ }

inspect := inspector.New([]*ast.File{f})

typs := []ast.Node{new(ast.CallExpr)}

inspect.Nodes(typs, func(n ast.Node, push bool) bool {

fmt.Printf("%v %T\n", push, n); return true

})

128 of 322

抽象構文木の走査 WithStackメソッド

  • x/tools/go/ast/inspectorパッケージを用いる
    • WithStackメソッドで親ノードを取得できる

128

const src = "package main\n func main() { println(`hello`)}"

fset := token.NewFileSet()

f, err := parser.ParseFile(fset, "my.go", src, 0)

if err != nil { /* エラー処理 */ }

inspect := inspector.New([]*ast.File{f})

typs := []ast.Node{new(ast.CallExpr)}

inspect.WithStack(typs, func(n ast.Node, push bool, stack []ast.Node) bool {

if !push { return false }

for i := range stack { fmt.Printf("%T ", stack[i])}

fmt.Println(); return true

})

129 of 322

抽象構文木の走査 Cursor型

  • x/tools/go/ast/inspectorパッケージを用いる
    • Cursor型の値を(*Inspector).Rootメソッドから取得する
    • 親・祖先ノード:Parent, Enclosing
    • 子ノード:Child, Children,FirstChild, LastChild
    • 兄弟ノード:PrevSibling, NextSibling;
    • 子孫ノード:FindByPos, FindNode,Inspect, Preorder.

129

const src = "package main\n func main() { println(`hello`)}"

fset := token.NewFileSet()

f, err := parser.ParseFile(fset, "my.go", src, 0)

if err != nil { /* エラー処理 */ }

inspect := inspector.New([]*ast.File{f})

typs := []ast.Node{new(ast.CallExpr)}

inspect.Preorder(typs, func(n ast.Node) {

fmt.Printf("%T\n", n)

})

130 of 322

ノードの置換や挿入

  • x/tools/go/ast/astutil.Apply関数を用いる
    • astutil.Cursor型を使って置換などができる

130

expr, err := parser.ParseExpr(`a+b`)

if err != nil { /* エラー処理 */ }

n := astutil.Apply(expr, func(cr *astutil.Cursor) bool {

switch cr.Name() {

case "X": cr.Replace(&ast.BasicLit{ Kind:token.INT, Value:"10"})

case "Y": cr.Replace(&ast.BasicLit{ Kind:token.INT, Value:"20"})

}

return true

}, nil)

// 10 + 20になっている

ast.Print(nil, n)

131 of 322

ノードのある行数などを取得する

  • (*token.FileSet).Positionメソッドを用いる
    • token.Position型の値を取得できる

131

const src = "package main\n func main() { println(`hello`)}"

fset := token.NewFileSet()

f, err := parser.ParseFile(fset, "my.go", src, 0)

if err != nil { /* エラー処理 */ }

inspect := inspector.New([]*ast.File{f})

typs := []ast.Node{new(ast.CallExpr)}

inspect.Preorder(typs, func(n ast.Node) {

p := fset.Position(n.Pos())

fmt.Println(p)

})

type Position struct {

Filename string

Offset int

Line int

Column int

}

132 of 322

ノードに対応するコメントの取得

  • ast.CommentMap型を用いる

132

const src = `package main

func main() {

v := 100 // v is int value

fmt.Println(v+1)

}`

fset := token.NewFileSet()

file, err := parser.ParseFile(fset, "main.go", src, parser.ParseComments)

if err != nil { /* エラー処理 */ }

cmap := ast.NewCommentMap(fset, file, file.Comments)

ast.Inspect(file, func(n ast.Node) bool {

switch n := n.(type) {

case *ast.AssignStmt:

for _, cg := range cmap[n] { fmt.Println(cg.Text()) }

}

return true

})

133 of 322

ルートノードからへのパスを取得

  • astutil.PathEnclosingInterval関数を使う

133

const src = `package main

var v = 100

func main() {

fmt.Println(v+1)

}`

fset := token.NewFileSet()

file, err := parser.ParseFile(fset, "my.go", src, 0)

if err != nil { /* エラー処理 */ }

fset.Iterate(func(f *token.File) bool {

if f.Name() != "my.go" { return true }

pos := token.Pos(f.LineStart(2) + 4)

path, exact := astutil.PathEnclosingInterval(file, pos, pos)

if exact { for _, n := range path { fmt.Printf("%T\n", n) } }

return true

})

134 of 322

抽象構文木からソースコードを生成

  • go/format.Node関数を用いる
    • gofmtと同じ書式でフォーマットされる

134

expr, err := parser.ParseExpr(`v+1`)

if err != nil { /* エラー処理 */ }

fset := token.NewFileSet()

var buf bytes.Buffer

err = format.Node(&buf, fset, expr)

if err != nil { /* エラー処理 */ }

// v + 1

fmt.Println(buf.String())

135 of 322

【TRY】四則演算式の評価

  • 四則演算式を計算(評価)する
    • parser.ParseExpr関数を用いる
    • 括弧付きの四則演算式を構文解析し評価する
    • float64型の値として評価する

135

v, err := eval("1 + 2 * 3")

if err != nil { /* エラー処理 */ }

// 7

fmt.Println(v)

136 of 322

【TRY】タグ付きの非公開フィールド

  • タグ付きなのにフィールドが非公開なものを探す
    • まずはparser.ParseFile関数を使ってやってみる
    • できたらgo/analysisパッケージを使ってやってみる
    • タグの種類がjsonxmlに限定された場合も行う

136

type Person struct {

// タグが付いているのに非公開なのでNG

name string `json:"name"`

}

137 of 322

【TRY】引数の多い関数

  • 引数が5つ以上の関数を探そう
    • まずはparser.ParseFile関数を使ってやってみる
    • できたらgo/analysisパッケージを使ってやってみる
    • オプションで個数を設定できるようにしてみよう

137

138 of 322

16.4. 型チェック

138

139 of 322

型チェック - go/types,go/constant

  • 型情報を抽象構文木から抽出
    • 識別子の解決
    • 型の推論
    • 定数の評価

139

n := 100 + 200

m := n + 300

定数の評価

= 300

型の推論

-> int

識別子の解決

140 of 322

型チェックで分かること

  • 型情報
    • 型の不一致など
    • 変数の型や式の型
  • 定数式の結果
    • 定数式の計算や定数の型
  • 識別子の解決
    • どの識別子がどこで定義されているのか
    • どの識別子がどこで使用されているのか

140

141 of 322

型チェック

  • (*types.Config).Checkで型チェックを行う

141

/* 型チェックのためのConfigを初期化 */

cfg := &types.Config{Importer: importer.Default()}

info := &types.Info{

/* TODO: 結果を保持するためのmapを初期化 */

}

pkg, err := cfg.Check("main", fs, []*ast.File{f}, info)

if err != nil {

/* エラー処理 */

}

/* TODO: pkgやinfoを使う処理 */

142 of 322

types.Config

  • 型チェックで用いる各種設定を保持

142

type Config struct {

// trueにすると関数のボディの型チェックを行わない

IgnoreFuncBodies bool

// import "C"を偽装してエラーが起きなくする

FakeImportC bool

// 型チェック時のエラーをハンドリングするための関数

Error func(err error)

// パッケージのインポートに使うImporter

Importer Importer

// アーキテクチャごとに変わる型のサイズを設定

Sizes Sizes

// 使っていないimportをチェックするかどうか

DisableUnusedImportCheck bool

}

143 of 322

式単位で型チェックを行う

  • types.CheckExpr関数を用いる

143

fset := token.NewFileSet()

expr, err := parser.ParseExprFrom(fset, "my.go", `int(1)+2`, 0)

if err != nil { /* エラー処理 */ }

info := &types.Info{ Types:map[ast.Expr]types.TypeAndValue{} }

err = types.CheckExpr(fset, nil, token.NoPos, expr, info)

if err != nil { /* エラー処理 */ }

for expr, tv := range info.Types {

var buf bytes.Buffer

types.WriteExpr(&buf, expr)

fmt.Println(&buf, ":", tv.Type, tv.Value)

}

144 of 322

types.Infoで型チェックの結果を保持する

144

type Info struct {

// 式とその型および定数式の場合は値を対応させたマップ

Types map[ast.Expr]TypeAndValue

// 定義された識別子とそれに対応するオブジェクトのマップ

Defs map[*ast.Ident]Object

// 使用された識別子とそれに対応するオブジェクトのマップ

Uses map[*ast.Ident]Object

// 型スイッチなどで暗黙に定義されたオブジェクトのマップ

Implicits map[ast.Node]Object

// v.Mなどのセレクタ式と*types.Selectionのマップ

Selections map[*ast.SelectorExpr]*Selection

// ノードとそのノードに関連付けられたスコープのマップ

Scopes map[ast.Node]*Scope

// パッケージ変数の初期化処理を初期化順で保持したスライス

InitOrder []*Initializer

}

145 of 322

types.Type型とtypes.Object

  • types.Type
    • Goの型を表すインタフェース
    • 名前付き型や組み込み型、コンポジット型などに対応
  • types.Object
    • 変数や関数などに対応する
    • 名前をつける対象となるものが多い
    • 型名などもオブジェクト扱い
    • オブジェクトが型を持つ

145

var n int

オブジェクト: *types.TypeName

型: *types.Basic

オブジェクト: *types.Var

型: *types.Basic

146 of 322

types.Type

  • Goの型を表すインターフェース
    • 型情報を表す
    • 具体的な情報は実装した型が持つ
    • (types.Object).Typeメソッドから取得できる

146

type Type interface {

// underlying typeの取得

Underlying() Type

// fmt.Stringerインタフェースを実装するため

String() string

}

147 of 322

Underlying type

  • すべての型はUnderlying typeを持つ
    • int型やstring型などの組み込み型は自分自身
    • 型リテラルで記述された型も自分自身
    • Defined typeはtype Y XXのUnderlying type
    • 型階層が作れないようになっている
    • Underlying typeのメソッドセットを引き継ぐ
      • 例:type I interface{ F() }type T I

147

int

[]string

struct {N int}

X

type X struct {N int}

Y

type Y X

148 of 322

types.Typeを実装した型 −1−

148

型名

説明

*types.Basic

int型などの基本型

*types.Array

配列

*types.Slice

スライス

*types.Struct

構造体

*types.Pointer

ポインタ

*types.Tuple

変数や引数など変数のタプルを表す。Goの型にはない

149 of 322

types.Typeを実装した型 −2−

149

型名

説明

*types.Signature

関数型。メソッドも含む

*types.Interface

インタフェース

*types.Map

マップ

*types.Chan

チャネル

*types.Named

typeキーワードで定義した名前付き型(ユーザ定義型)

150 of 322

types.Object

  • 型情報自体だけでは保持できない情報を持つ
    • 関数や変数など
    • Typeメソッドから型情報も取れる

150

type Object interface {

Parent() *types.Scope // 属するスコープ

Pos() token.Pos // ソースコード上の位置

Pkg() *types.Package // 所属するパッケージ

Name() string // 名前

Type() Type // 型情報

Exported() bool // パッケージ外に公開されているかどうか

Id() string // e.g. F, main.f

String() string // fmt.Stringerインタフェースを実装するため

}

151 of 322

types.Objectを実装した型

151

型名

説明

*types.PkgName

パッケージ名

*types.Const

定数

*types.TypeName

型名

*types.Var

変数(フィールドや引数、名前付き戻り値も含む)

*types.Func

関数

*types.Label

ラベル

*types.Builtin

組み込み関数(組み込み型は含まない)

*types.Nil

nil

152 of 322

識別子の定義箇所と使用箇所

  • types.Info構造体のDefsUsesフィールドを用いる
    • *ast.Identが別でも同じ識別子であれば同じオブジェクトになる

152

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

nodeFilter := []ast.Node{(*ast.Ident)(nil)}

inspect.Preorder(nodeFilter, func(n ast.Node) {

switch n := n.(type) {

case *ast.Ident:

fmt.Println(n)

fmt.Println("Defs:", pass.TypesInfo.Defs[n])

fmt.Println("Uses:", pass.TypesInfo.Uses[n])

}

})

return nil, nil

}

153 of 322

型の情報を調べる関数

153

関数名

説明

types.Identical

同じ型かどうか

types.IdenticalIgnoreTags

構造体タグを無視して比較する

types.AssertableTo

型アサーションでキャスト可能かどうか

types.AssignableTo

代入可能かどうか

types.Comparable

比較可能かどうか

types.ConvertibleTo

キャスト可能かどうか

types.Implements

インタフェースを実装しているかどうか

types.IsInterface

インタフェースかどうか

154 of 322

型の比較

  • types.Identical関数を用いる
    • 型が同じかどうかをチェックする
    • 名前などで比較してはダメ

154

func Identical(x, y types.Type) bool

155 of 322

式から型と値を取得する

  • types.Info構造体のTypesフィールドを用いる
    • 定数式の場合は値も取得できる

155

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

inspect.Preorder(nil, func(n ast.Node) {

switch expr := n.(type) {

case ast.Expr:

if tv, ok := pass.TypesInfo.Types[expr]; ok {

fmt.Println(expr, tv.Type, tv.Value)

}

}

})

return nil, nil

}

156 of 322

セレクタ式から型情報を取得する

  • types.Info構造体のSelectionsフィールドを用いる
    • フィールドやメソッドへのセレクタ式がとれる

156

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

nodeFilter := []ast.Node{(*ast.SelectorExpr)(nil)}

inspect.Preorder(nodeFilter, func(n ast.Node) {

switch n := n.(type) {

case *ast.SelectorExpr:

s := pass.TypesInfo.Selections[n]

fmt.Println(n, s)

}

})

return nil, nil

}

157 of 322

暗黙に定義されたオブジェクトの取得

  • types.Info構造体のImplicitsフィールドを用いる
    • 型スイッチなどで暗黙的に生成されたオブジェクトを取得

157

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

inspect.Preorder(nil, func(n ast.Node) {

obj := pass.TypesInfo.Implicits[n]

if obj != nil {

fmt.Println(n, obj)

}

})

return nil, nil

}

158 of 322

初期化順を取得する

  • types.Info構造体のInitOrderフィールドを用いる
    • パッケージの初期化順が取れる

158

func run(pass *analysis.Pass) (interface{}, error) {

initOrder := pass.TypesInfo.InitOrder

for _, initializer := range initOrder {

fmt.Println(initializer)

}

return nil, nil

}

159 of 322

式から型を取得する

  • *types.Info型のTypeOfメソッドを用いる
    • Types/Defs/Usesフィールドのいずれかから得る

159

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

inspect.Preorder(nil, func(n ast.Node) {

expr, _ := n.(ast.Expr)

typ := pass.TypesInfo.TypeOf(expr)

if typ == nil { return }

var buf bytes.Buffer

types.WriteExpr(&buf, expr)

fmt.Println(&buf, typ)

})

return nil, nil

}

160 of 322

識別子からオブジェクトを取得する

  • *types.Info型のObjectOfメソッドを用いる
    • Defs/Usesフィールドのいずれかから得る

160

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

inspect.Preorder([]ast.Node{new(ast.Ident)}, func(n ast.Node) {

ident, _ := n.(*ast.Ident)

obj := pass.TypesInfo.ObjectOf(ident)

if obj == nil { return }

fmt.Println(ident, obj)

})

return nil, nil

}

161 of 322

【TRY】不要なパッケージ関数の判別

  • 使用されていない関数を見つける
    • パッケージ外に公開されていないパッケージ関数が対象
    • どこからも参照されていないものを探す

161

package mypkg

// 使用されていない

func f() { println("f") }

// エクスポートされている

func G() { println("G") }

削除しても問題ない

162 of 322

基本型を取得する

  • types.Typ変数を用いる

162

var Typ = []*Basic{// 基本型を表す*types.Basic型のスライス

Invalid: {Invalid, 0, "invalid type"}, Bool: {Bool, IsBoolean, "bool"},

Int: {Int, IsInteger, "int"}, Int8: {Int8, IsInteger, "int8"},

Int16: {Int16, IsInteger, "int16"}, Int32: {Int32, IsInteger, "int32"},

Int64: {Int64, IsInteger, "int64"}, Uint: {Uint, IsInteger | IsUnsigned, "uint"},

Uint8: {Uint8, IsInteger | IsUnsigned, "uint8"}, Uint16: {Uint16, IsInteger | IsUnsigned, "uint16"},

Uint32: {Uint32, IsInteger | IsUnsigned, "uint32"}, Uint64: {Uint64, IsInteger | IsUnsigned, "uint64"},

Uintptr: {Uintptr, IsInteger | IsUnsigned, "uintptr"}, Float32: {Float32, IsFloat, "float32"},

Float64: {Float64, IsFloat, "float64"}, Complex64: {Complex64, IsComplex, "complex64"},

Complex128: {Complex128, IsComplex, "complex128"}, String: {String, IsString, "string"},

UnsafePointer: {UnsafePointer, 0, "Pointer"},

UntypedBool: {UntypedBool, IsBoolean | IsUntyped, "untyped bool"},

UntypedInt: {UntypedInt, IsInteger | IsUntyped, "untyped int"},

UntypedRune: {UntypedRune, IsInteger | IsUntyped, "untyped rune"},

UntypedFloat: {UntypedFloat, IsFloat | IsUntyped, "untyped float"},

UntypedComplex: {UntypedComplex, IsComplex | IsUntyped, "untyped complex"},

UntypedString: {UntypedString, IsString | IsUntyped, "untyped string"},

UntypedNil: {UntypedNil, IsUntyped, "untyped nil"},

}

163 of 322

型なしの定数

  • 型なしの定数や定数式の型
    • *types.Basic型の値
    • types.Typ変数から取得できる

163

説明

types.Typ[types.UntypedBool]

truefalseなどの型

types.Typ[types.UntypedInt]

100200などの型

types.Typ[types.UntypedRune]

'''A'などの型

types.Typ[types.UntypedFloat]

1.5などの型

types.Typ[types.UntypedComplex]

5i1+5iなどの型

types.Typ[types.UntypedString]

"Hello"などの型

types.Typ[types.UntypedNil]

nilの型

164 of 322

基本型のプロパティを取得する

  • types.BasicInfo型を用いる
    • (*types.Basic).Infoメソッドから取得できる

164

const (

IsBoolean BasicInfo = 1 << iota // boolかどうか

IsInteger // 整数かどうか

IsUnsigned // uintなど符号なしかどうか

IsFloat // 浮動小数点数かどうか

IsComplex // 複素数かどうか

IsString // 文字列かどうか

IsUntyped // 型なしの定数かどうか

// <, <=, >, >=で大小比較できるか

IsOrdered = IsInteger | IsFloat | IsString

// 数値かどうか

IsNumeric = IsInteger | IsFloat | IsComplex

// 定数として扱えるか

IsConstType = IsBoolean | IsNumeric | IsString

)

165 of 322

【TRY】int型の式を見つける

  • int型の式をすべてみつける
    • int型はtypes.Typ変数から取得する
    • 式から型を取得するには(*types.Info).TypeOfメソッドを使う
    • 型の比較はtypes.Identical関数を用いる
    • untyped intについても探してみよう
    • analysisパッケージを使ってもよい

165

166 of 322

スコープ

  • types.Scope型を用いる
    • スコープは入れ子になっている

166

ユニバース

パッケージ

ファイル

ブロック

167 of 322

ユニバーススコープの取得

  • types.Universe変数を用いる
    • error型などの組み込み型などを取得できる
    • 組み込み関数なども取得できる

167

// println関数を指すオブジェクトを取得する

obj := types.Universe.Lookup("println")

fmt.Println(obj)

168 of 322

ノードからスコープを取得する

  • types.Info構造体のScopesフィールドを用いる
    • ノードに関連付けられたスコープが取得できる

168

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

inspect.Preorder(nil, func(n ast.Node) {

s := pass.TypesInfo.Scopes[n]

if s != nil {

fmt.Println(n, s)

}

})

return nil, nil

}

169 of 322

スコープからオブジェクトの取得

  • (*types.Scope).Lookupメソッドを用いる
    • LookupParentメソッドを用いれば親を辿っていって見つける

169

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

inspect.Preorder(nil, func(n ast.Node) {

s := pass.TypesInfo.Scopes[n]

if s == nil { return }

if obj := s.Lookup("gopher"); obj != nil {

fmt.Println(obj)

}

})

return nil, nil

}

170 of 322

子スコープの取得

  • (*types.Scope).Childメソッドを用いる
    • NumChildrenメソッドで子スコープの数を取得できる
    • types.Universe変数から子スコープは辿れないので注意

170

func allScopes(s *types.Scope) {

fmt.Println(s)

for i := 0; i < s.NumChildren(); i++ {

allScopes(s.Child(i))

}

}

func run(pass *analysis.Pass) (interface{}, error) {

allScopes(pass.Pkg.Scope())

return nil, nil

}

171 of 322

スコープ内の識別子を取得

  • (*types.Scope).Namesメソッドを用いる
    • []string型で取得できる
    • types.Object型の値が欲しい場合はLookupメソッドを使う

171

func run(pass *analysis.Pass) (interface{}, error) {

for _, n := range pass.Pkg.Scope().Names() {

fmt.Println(n)

}

return nil, nil

}

172 of 322

【TRY】名前の短いパッケージ変数

  • 名前の短いパッケージ変数を探す
    • パッケージスコープを取得する
    • パッケージスコープに定義されたオブジェクトを調べる
    • オブジェクトが変数であれば*types.Var型になる
    • 名前がN文字以下の場合をエラーにする

172

173 of 322

一番内側のスコープを取得する

  • (*types.Scope).Innermostメソッドを用いる
    • スコープの中でもっとも内側のスコープを取得する

173

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

inspect.Preorder([]ast.Node{new(ast.Ident)}, func(n ast.Node) {

if id, ok := n.(*ast.Ident); ok && id.Name == "gopher" {

s := pass.Pkg.Scope().Innermost(id.Pos())

if s == nil { return }

for _, n := range s.Names() { fmt.Println(n) }

}

})

return nil, nil

}

174 of 322

パッケージ外のオブジェクトを取得

  • (*types.Package).Importsメソッドから探す
    • インポートしているパッケージのスコープから探す

174

func run(pass *analysis.Pass) (interface{}, error) {

// fmt.Stringerを探す

for _, p := range pass.Pkg.Imports() {

if p.Path() == "fmt" {

obj := p.Scope().Lookup("Stringer")

fmt.Println(obj)

}

}

return nil, nil

}

175 of 322

スコープの比較

  • *types.Scope型の値を==で比較する
    • 特定のパッケージのオブジェクトをすべて取るなどができる

175

func run(pass *analysis.Pass) (interface{}, error) {

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

var fmtpkg *types.Package

for _, p := range pass.Pkg.Imports() {

if p.Path() == "fmt" { fmtpkg = p }

}

if fmtpkg == nil { return nil, nil } // fmtパッケージをインポートしてない

inspect.Preorder([]ast.Node{new(ast.Ident)}, func(n ast.Node) {

id, _ := n.(*ast.Ident)

obj := pass.TypesInfo.ObjectOf(id)

// obj != nil && obj.Pkg() == fmtpkg でもよい

if obj != nil && obj.Parent() == fmtpkg.Scope() { fmt.Println(obj) }

})

return nil, nil

}

176 of 322

analysisutil.LookupFromImports関数

  • インポートしているパッケージから探す
    • インポートパスと名前を指定して探すことができる
    • types.Object型で取得できる

176

func run(pass *analysis.Pass) (interface{}, error) {

// fmt.Stringerを探す

pkgs := pass.Pkg.Imports()

obj := analysisutil.LookupFromImports(pkgs, "fmt", "Stringer")

fmt.Println(obj)

return nil, nil

}

177 of 322

analysisutil.TypeOf関数

  • インポートパスと名前で型を探せる
    • types.Type型で取得できる
    • 存在しないかパッケージをインポートしてない場合はnilが返る

177

func run(pass *analysis.Pass) (interface{}, error) {

// fmt.Stringerを探す

typ := analysisutil.TypeOf(pass, "fmt", "Stringer")

fmt.Println(typ)

return nil, nil

}

178 of 322

analysisutil.ObjectOf関数

  • インポートパスと名前で型を探せる
    • types.Object型で取得できる
    • 存在しないかパッケージをインポートしてない場合はnilが返る

178

func run(pass *analysis.Pass) (interface{}, error) {

// fmt.Printlnを探す

obj := analysisutil.ObjectOf(pass, "fmt", "Println")

fmt.Println(obj)

return nil, nil

}

179 of 322

名前付きの型とUnderlying type

  • 名前付き型はtypes.Named型で表される
    • Underlying typeを取得したい場合はUnderlyingメソッドを呼ぶ
    • 型ごとの特性を調べたい場合は基底型を取得する
    • メソッドが取得できる

179

func run(pass *analysis.Pass) (interface{}, error) {

typ := analysisutil.TypeOf(pass, "fmt", "Stringer")

// *types.Named *types.Interface

fmt.Printf("%T %T\n", typ, typ.Underlying())

return nil, nil

}

180 of 322

インターフェース

  • types.Interface型で表される
    • メソッドセットなどが取得できる

180

関数名

説明

types.AssertableTo

型アサーションでキャスト可能かどうか

types.Implements

インタフェースを実装しているかどうか

types.IsInterface

インタフェースかどうか

181 of 322

インタフェースの埋め込みを取得する

  • (*types.Interface).EmbeddedTypeメソッドを使う
    • 数はNumEmbeddedsメソッドから取得できる

181

func run(pass *analysis.Pass) (interface{}, error) {

typ := analysisutil.TypeOf(pass, "io", "ReadWriter")

iface := typ.Underlying().(*types.Interface)

for i := 0; i < iface.NumEmbeddeds(); i++ {

fmt.Println(iface.EmbeddedType(i))

}

return nil, nil

}

182 of 322

【TRY】errorインタフェースの実装

  • errorインタフェースを実装している型を取得する
    • 組み込み型のerror型を取得する
    • パッケージスコープから定義している型を調べる
    • types.Implementsで実装しているか調べる
    • レシーバがポインタの場合を忘れないようにする
    • analysisutil.ImplementsError関数は使わない

182

183 of 322

analysisutil.ImplementsError関数

  • errorインタフェースを実装しているか調べる
    • 型を指定するだけでよい

183

func run(pass *analysis.Pass) (interface{}, error) {

obj := analysisutil.ObjectOf(pass, "os", "ErrNotExist")

if obj == nil { return nil, nil }

ok := analysisutil.ImplementsError(obj.Type())

fmt.Println(ok) // true

return nil, nil

}

184 of 322

analysisutil.Interfaces関数

  • 定義されているインタフェースをすべて取得
    • 基底型の*types.Interface型のマップが取得できる
    • *types.Named型ではない

184

func run(pass *analysis.Pass) (interface{}, error) {

for _, iface := range analysisutil.Interfaces(pass.Pkg) {

fmt.Println(iface)

}

return nil, nil

}

185 of 322

フィールドまたはメソッドを取得する

  • types.LookupFieldOrMethod関数から取得できる
    • 埋め込みされた型のフィールドやメソッドも取得できる
    • ポインタ型がレシーバの関数も取得できる

185

func run(pass *analysis.Pass) (interface{}, error) {

obj := analysisutil.ObjectOf(pass, "os", "ErrNotExist")

if obj == nil { return nil, nil }

typ, pkg := obj.Type(), obj.Pkg()

m, _, _ := types.LookupFieldOrMethod(typ, false, pkg, "Error")

fmt.Println(m)

return nil, nil

}

186 of 322

analysisutil.MethodOf関数

  • メソッド名を指定してメソッドを取得

186

func run(pass *analysis.Pass) (interface{}, error) {

typ := analysisutil.TypeOf(pass, "bytes", "*Buffer")

if typ == nil { return nil, nil }

m := analysisutil.MethodOf(typ, "Write")

fmt.Println(m)

return nil, nil

}

187 of 322

types.Func型とtypes.Signature

  • *types.Func型はオブジェクト
    • *types.Signature型が型を表す
    • (*types.Func).Typeメソッドは*types.Signature型の値を返す

187

func run(pass *analysis.Pass) (interface{}, error) {

fun := analysisutil.ObjectOf(pass, "fmt", "Println")

if fun == nil { return nil, nil }

// *types.Func *types.Signature

fmt.Printf("%T %T\n", fun, fun.Type())

return nil, nil

}

188 of 322

メソッドセットを取得する

  • types.NewMethodSet関数を用いる
    • *types.MethodSet型の値が取得できる

188

func run(pass *analysis.Pass) (interface{}, error) {

typ := analysisutil.TypeOf(pass, "bytes", "*Buffer")

if typ == nil { return nil, nil }

ms := types.NewMethodSet(typ)

fmt.Println(ms)

return nil, nil

}

189 of 322

types.Struct

  • 構造体型を表す型
    • (*types.Struct).Fieldメソッドでフィールドが取得できる
    • 数はNumFieldsメソッドで取得可能
    • フィールドは*types.Var型で表現される

189

func run(pass *analysis.Pass) (interface{}, error) {

typ := analysisutil.TypeOf(pass, "bytes", "Buffer")

if typ == nil { return nil, nil }

st, _ := typ.Underlying().(*types.Struct)

if st == nil { return nil, nil }

for i := 0; i < st.NumFields(); i++ {

fmt.Printf("%[1]T %[1]v\n", st.Field(i))

}

return nil, nil

}

190 of 322

埋め込まれたフィールドを取得する

  • (*types.Var).Embeddedメソッドで判断する

190

func run(pass *analysis.Pass) (interface{}, error) {

typ := analysisutil.TypeOf(pass, "bytes", "Buffer")

if typ == nil { return nil, nil }

st, _ := typ.Underlying().(*types.Struct)

if st == nil { return nil, nil }

for i := 0; i < st.NumFields(); i++ {

fmt.Println(st.Field(i), st.Field(i).Embedded())

}

return nil, nil

}

191 of 322

analysisutil.Structs関数

  • 定義されている構造体をすべて取得
    • 基底型の*types.Struct型のマップが取得できる
    • *types.Named型ではない

191

func run(pass *analysis.Pass) (interface{}, error) {

for _, st := range analysisutil.Structs(pass.Pkg) {

fmt.Println(st)

}

return nil, nil

}

192 of 322

【TRY】コンテキストを構造体に保持

  • コンテキストを構造体に保持しているものを見つける
    • 定義された構造体をすべて取得する
    • フィールドの型がcontext.Contextインタフェースを実装しているか
      • 単に保持しているだけでも良い

192

package mypkg

import "context"

// NG

type S struct {

ctx context.Context

}

フィールドに保持するのは

好ましくない

193 of 322

【TRY】t.Parallel()の呼び出し

  • t.Parallelメソッドの呼び出し方が非効率なもの
    • サブテストでt.Parallel()を呼び出しているのに親テストで呼び出していないケースを探す
    • https://engineering.mercari.com/blog/entry/how_to_use_t_parallel/
    • テスト関数の一覧を取得する
    • テスト関数でt.Parallelを呼んでいるかチェックする
    • t.Runを呼んでいる箇所を探す
    • t.Runに渡している関数のボディでt.Parallelが呼ばれているかチェックする
      • 無名関数を渡している前提でよい

193

194 of 322

16.5. コード生成

194

195 of 322

コード生成とは

  • ソースコードをプログラムによって生成する
    • 冗長なコードの自動生成
    • テストコードの生成
    • 静的解析の解析結果やデータベースのスキーマなどから生成

静的解析

コード生成

ソースコード

抽象構文木

型情報

DB

スキーマ

196 of 322

コード生成の必要性

  • Goにはジェネリクスがない
    • Draft designは出ている
    • 汎用的な処理を書くために必要
      • リフレクションだとコストがかかる
      • 愚直に書くのは大変
  • 冗長なコードの自動生成
    • 自動生成が可能だけど人力で書くのは手間
    • テストコードの生成
    • 最適化されたテクニカルなコード
      • 可読性が低いがパフォーマンスが良いコード

196

197 of 322

go generate

  • コード生成を行うための仕組み
    • //go:generate コメントディレクティブを使う
    • 実行したいコマンドを書く
    • go generateコマンドにより実行される
    • 直接実行されるのでリダイレクトとかができない

197

//go:generate stringer -type=MyStatus

実行するコマンド

198 of 322

コード生成と静的解析

  • 静的解析した結果を元にコードを生成する
    • 型や構造体タグやコメントを解析

198

package main

//go:generate stringer -type=MyStatus

type MyStatus int

const (

A MyStatus = iota

B

C

)

// Code generated by "stringer -type=MyStatus"; DO NOT EDIT.

package main

import "strconv"

/* 略 */

const _MyStatus_name = "ABC"

var _MyStatus_index = [...]uint8{0, 1, 2, 3}

func (i MyStatus) String() string {

if i < 0 || i >= MyStatus(len(_MyStatus_index)-1) {

return "MyStatus(" + strconv.FormatInt(int64(i), 10) + ")"

}

return _MyStatus_name[_MyStatus_index[i]:_MyStatus_index[i+1]]

}

コード生成

199 of 322

コード生成されたコード

  • コード生成で作られたことを伝えるコメント
    • // Code generated by xxxxx; DO NOT EDIT.
    • 手動で変更しないことを促す
    • スケルトンコードなど変更される前提の場合はつけない

199

// Code generated by "stringer -type=MyStatus"; DO NOT EDIT.

package main

import "strconv"

/* 略 */

200 of 322

テンプレートを使ったコード生成

  • text/templateパッケージを用いる
    • 雛形が読みやすい
      • skeletonでも使用している
    • 生成されるコードを利用者側がコントロールできる
      • コード生成を行うORMなど利用されている
    • 複数のファイルにまたがる場合はtxtarパッケージを使っても良い

200

201 of 322

抽象構文木からソースコードを生成

  • 抽象構文木の構築
    • astutil.Apply関数を用いてASTを変更する
  • フォーマットはgo/format.Node関数を用いる

201

expr, err := parser.ParseExpr(`v+1`)

if err != nil { /* エラー処理 */ }

fset := token.NewFileSet()

var buf bytes.Buffer

err = format.Node(&buf, fset, expr)

if err != nil { /* エラー処理 */ }

// v + 1

fmt.Println(buf.String())

202 of 322

文字列としてフォーマットする

  • go/format.Source関数を用いる
    • bytes.Bufferなどでソースコードを構成していく
    • 最後にフォーマットをかける

202

// func Source(src []byte) ([]byte, error)

src, err := format.Source([]byte(`v+1`))

fmt.Println(string(src), err) // v + 1 <nil>

203 of 322

goimports

  • ライブラリとしてgoimportsを使う
    • golang.org/x/tools/importsパッケージを使う

203

func Process(filename string,

 src []byte, opt *Options) ([]byte, error)

204 of 322

コード生成の難しさ

  • 静的解析の知識が必要
    • golang.org/x/tools/go/packagesパッケージなどを使う
    • 抽象構文木の情報と型情報をいったり来たりする必要がある
  • analysis.Analyzerのようなエコシステムがない
    • 再利用性がない
    • 簡単に始めにくい

204

205 of 322

knife(ナイフ)

  • go listのように型情報を表示できるツール
    • https://github.com/gostaticanalysis/knife
    • アーミーナイフのようにマルチに使えるツール
    • 指定したパッケージ(Goファイル)に対し静的解析を行う
    • 型情報をテンプレートをもとに出力する
      • コード生成にも利用ができる
    • 下の例ではfmtパッケージの関数を表示している
      • grepコマンドでさらにPrintで始まる関数だけ絞っている

205

$ knife -f "{{names .Funcs}}" fmt | grep "^Print"

Print

Printf

Println

206 of 322

knifeを使ったコード生成

  • テンプレートベースのコード生成

206

var tmpl = `{{with index .Types (data "type")}}{{if interface .}}

// Code generated by mockgen; DO NOT EDIT.

package {{(pkg).Name}}

type Mock{{data "type"}} struct {

{{- range $n, $f := methods .}}

{{$n}}Func {{$f.Signature}}

{{- end}}

}

{{range $n, $f := methods .}}

func (m *Mock{{data "type"}}) {{$n}}({{range $f.Signature.Params}}

{{- .Name}} {{.Type}},

{{- end}}) ({{range $f.Signature.Results}}

{{- .Name}} {{.Type}},

{{- end}}) {

{{if $f.Signature.Results}}return {{end}}m.{{$n}}Func({{range $f.Signature.Params}}

{{- .Name}},

{{- end}})

}

{{end}}{{end}}{{end}}`

207 of 322

コード生成ツール:hagane

  • テンプレートベースのコード生成を行うCLIツール

207

package sample

//go:generate hagane -template template.go.tmpl -o sample_mock.go -data {"type":"DB"} sample.go

type DB interface {

Get(id string) int

Set(id string, v int)

}

type db struct {}

func (db) Get(id string) int {

return 0

}

func (db) Set(id string, v int) {}

208 of 322

codegenパッケージ

  • コード生成のエコシステムを提供
    • https://github.com/gostaticanalysis/codegen
    • gostaticanalysisの実験的なプロジェクト
    • analysisパッケージをラップしている
    • Analyzer型に似たGenerator型を提供
      • コード生成に必要な機能のみに制限したAnalyzerに相当する
    • テスト用にcodegentestパッケージも提供
      • analysistestパッケージをラップしている

208

209 of 322

codegen.Generator

  • コード生成器を表す構造体
    • 処理はRunフィールドの関数で記述する
    • 依存するAnalyzerを指定できる

209

type Generator struct {

Name string

Doc string

Flags flag.FlagSet

Run func(*codegen.Pass) error

RunDespiteErrors bool

Requires []*analysis.Analyzer

Output func(pkg *types.Package) io.Writer

}

210 of 322

codegen.Pass

  • コード生成用にanalysis.Pass型をラップ
    • Generator.Runフィールドの関数の引数になる
    • 依存するAnalyzerの解析結果なども使える

210

type Pass struct {

Generator *codegen.Generator

Fset *token.FileSet

Files []*ast.File

OtherFiles []string

Pkg *types.Package

TypesInfo *types.Info

TypesSizes types.Sizes

ResultOf map[*analysis.Analyzer]interface{}

Output io.Writer

ImportObjectFact func(obj types.Object, fact analysis.Fact) bool

ImportPackageFact func(pkg *types.Package, fact analysis.Fact) bool

}

211 of 322

Generatorの出力先の変更

  • Outputフィールドを用いる
    • パッケージごとに出力先を変えられる
    • codegen.Pass構造体のOutputフィールドにセットされる
    • 何も指定しないとos.Stdout(標準出力)が設定される
  • *codegen.Pass型のメソッドを用いると簡単
    • Outputフィールド設定されたio.Writerに出力する

211

func (pass *Pass) Print(a ...interface{}) (n int, err error)

func (pass *Pass) Printf(format string, a ...interface{}) (n int, err error)

func (pass *Pass) Println(a ...interface{}) (n int, err error)

212 of 322

Generatorとknifeの組み合わせ

  • knifeのテンプレート機能を利用する
    • テンプレートを用いたコード生成に利用する
    • knife.NewTemplate関数でテンプレートが生成できる
      • テンプレート内で使える関数は設定済み

212

td := &knife.TempalteData{

Fset: pass.Fset, Files: pass.Files,

TypesInfo: pass.TypesInfo, Pkg: pass.Pkg,

Extra: map[string]interface{}{"type": flagTypeName},

}

t, err := knife.NewTemplate(td).Parse(tmpl)

if err != nil { return err }

var buf bytes.Buffer

err = t.Execute(&buf, knife.NewPackage(pass.Pkg))

if err != nil { return err }

213 of 322

codegentestパッケージ

  • codegenパッケージ用のテストユーティリティを提供
    • https://pkg.go.dev/github.com/gostaticanalysis/codegen/codegentest
    • analysistestパッケージをラップしている
      • testdata/src以下のディレクトリをテスト対象として使用
    • Run関数によって簡単にGeneratorのテストができる

213

func TestGenerator(t *testing.T) {

testdata := codegentest.TestData()

rs := codegentest.Run(t, testdata, example.Generator, "example")

// 結果(rs)を使った処理

}

214 of 322

Golden fileを使ったテスト

  • codegentest.Golden関数を使う
    • すでに生成されたファイルとの差分を見る
      • testdata/src/pkgname/pkgname.goldenというファイル
    • フラグで再生成を行うことも可能
    • cmp.Diffで比較され差分があるとt.Errorでテストが落ちる

214

var flagUpdate bool

func TestMain(m *testing.M) {

flag.BoolVar(&flagUpdate, "update", false, "update the golden files")

flag.Parse()

os.Exit(m.Run())

}

func TestGenerator(t *testing.T) {

testdata := codegentest.TestData()

rs := codegentest.Run(t, testdata, example.Generator, "example")

codegentest.Golden(t, rs, flagUpdate)

}

215 of 322

singlegenerator

  • singlegenerator.Main
    • 単独のGeneratorからなるmain関数を提供する
    • go/analysis/singlecheckerのラッパー

package main

import (

"sample"

"github.com/gostaticanalysis/codegen/singlegenerator"

)

func main() {

singlegenerator.Main(sample.Generator)

}

216 of 322

skeletonを使ったコード生成

  • -typeオプションにcodegenを指定する
    • Generatorを用いたコード生成ツールのスケルトンコードを生成
    • デフォルトではインタフェースのモック生成ツール

216

$ skeleton -kind=codegen pkgname

pkgname

├── cmd

│ └── pkgname

│ └── main.go

├── go.mod

├── pkgname.go

├── pkgname_test.go

└── testdata

└── src

└── a

├── a.go

└── pkgname.golden

217 of 322

jennifer

217

218 of 322

【TRY】Generatorを使ったコード生成

  • 構造体のフィールドのGetterとSetterを作る
    • パッケージで定義されたエクスポートされた構造体型のみ
    • フィールドのうち以下の条件に当てはまるものだけ作る
      • 構造体のタグで`gen:"getset"`がついているもの
      • エクスポートされていないもの
    • タグは(struct .Types.T).Fields.XXXX.Tagで取れる
    • レシーバはポインタにする

218

219 of 322

【TRY】シャローコピー

219

220 of 322

16.6. 静的単一代入形式

220

221 of 322

静的単一代入(SSA)形式

  • 変数への代入を1回だけに制限した形式
    • gc(Goのコンパイラ)の中でも使われている技術
      • 別パッケージで表現方法は異なる
      • 最適化に使われている
    • golang.org/x/tools/go/ssaパッケージが担当
      • 静的解析ツール用のパッケージ
      • gcでは使われていない

221

n := 10

n += 10

n0 := 10

n1 := n0 + 10

222 of 322

GoコンパイラのSSA形式で出力する

  • -d 'ssa/build/dump=xxx'を指定してビルドする
    • SSA形式になった関数が表示される
  • SSA形式を見て分かること
    • deferがインライン展開されているか

222

$ go tool compile -d 'ssa/build/dump=main' main.go

$ grep defer main_01__build.dump

v24 = StaticCall <mem> {runtime.deferreturn} v23

v20 = StaticCall <mem> {runtime.deferprocStack} [8] v19

v22 = StaticCall <mem> {runtime.deferreturn} v21

223 of 322

GoコンパイラのSSA形式を確認する

  • GOSSAFUNCを指定してビルドする
    • ssa.htmlが生成される

223

$ GOSSAFUNC=main go tool compile main.go

dumped SSA to /tmp/ssa_sample/ssa.html

最適化がかかっていく様子

224 of 322

静的解析のSSA形式ビューア

  • Web上でSSAを確認できるサイト

224

225 of 322

godump

  • ASTとSSAのダンプを行うCLIツール
    • ASTのダンプには内部でastreeを使っている

225

# Dump AST

$ godump /tmp/main.go

/tmp/main.go

File

├── Doc

├── Package = /tmp/main.go:1:1

├── Name

│ └── Ident

│ ├── NamePos = /tmp/main.go:1:9

│ ├── Name = main

│ └── Obj

...

# Dump SSA

$ godump -mode=ssa /tmp/main.go

command-line-arguments.main

Block 0

*ssa.Call println("hello, world":string)

*ssa.Return return

226 of 322

静的単一形式の構成

  • x/tools/go/ssaパッケージのSSAの構成
    • 関数が基本ブロックで構成されている
    • 基本ブロックは命令で構成されている
    • 命令は複数のオペランドを持つ

226

Program

Package

Function

BasicBlock

Instruction

オペランド

Value

227 of 322

基本ブロック

  • 関数を構成する単位
    • 条件分岐単位などで基本ブロックに分けられる
    • 関数は基本ブロックをノードとしたコントロールフローグラフを構成する
    • Goのifforswitchなどは全部If命令とJump命令になる

227

func f() {

n := 10

if n < 10 {

println("n < 10")

} else {

println("n >= 10")

}

}

228 of 322

例:エラー処理のミス

  • nil以外を返すべきところでnilを返してるバグを発見
    • https://github.com/gostaticanalysis/nilerr
    • err != nilと比較しているのにnilを返している
    • 基本ブロックのフローを調べることで簡単に見つけられる
      • If命令のジャンプ先がReturn命令でnilを返していたら

228

func f() error {

err := do()

if err != nil {

return nil // ミス

}

}

Return命令(nil)

If命令(err != nil)

Jump命令

・・・

then

else

229 of 322

例:Spannerのセッションリーク検出

  • gcpug/zagane
    • https://github.com/gcpug/zagane
    • *spanner.RowIteratorStopメソッド(またはDoメソッド)が呼ばれているかだけチェックできる
      • 呼ばれていないとセッションリークする可能性がある
    • 参考:Google Cloud Spannerのセッションリークを静的解析で防ぐ

229

iter := client.Single().Query(ctx, stmt)

for {

row, err := iter.Next()

// (略)

}

iter := client.Single().Query(ctx, stmt)

defer iter.Stop()

for {

row, err := iter.Next()

// (略)

}

230 of 322

セッションリークの検出方法 - 1 -

  • コントロールフローグラフを有向グラフとして処理
    • 基本ブロックをノードとする
    • 注目している基本ブロックを始端とする
    • 関数のreturn文を終端とする
    • StopまたはDoメソッドを呼んでいるノードに☆マークをつける

230

始端

終端

終端

Stop()

Do()

231 of 322

セッションリークの検出方法 - 2 -

  • StopまたはDoメソッドを呼んでいるノードを取り除く
    • ☆マークのついたノードをグラフから取り除く
    • 残ったエッジを通って始端から終端までいけるか検証する
    • 終端に行ける場合はセッションリークが発生する可能性がある

231

始端

終端

終端

Stop()

Do()

232 of 322

静的単一代入形式で分かること

  • コントロールフローグラフ
    • 分岐や繰り返しの簡略化
    • 処理の前後関係を追いやすい
    • 有向グラフなのでグラフ理論のアルゴリズムが使える
  • 単一の代入であることが保証されている
    • 同じ値に対する処理を見つけやすい
      • ある値のメソッドを呼び出しているか?

232

233 of 322

静的単一代入形式で分からないこと

  • 公開された変数への代入
    • 外部パッケージから変更される可能性がある
  • ポインタを介した変更
    • どう変更されるか分からない
    • unsafe.Pointerを用いるとポインタ演算されてしまう
  • インタフェースを介した処理
    • どう代入されるか分からない
  • リフレクションを介した動作
    • 動的に決まるので難しい

233

234 of 322

buildssaパッケージ

  • 静的単一代入形式を構築するAnalyzerを提供
    • Analyzer.Requiresフィールドにbuildssa.Analyzer変数を指定
    • SSA.SrcFuncsフィールドからソースコード中の関数を取得

234

func run(pass *analysis.Pass) (interface{}, error) {

s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)

for _, f := range s.SrcFuncs {

fmt.Println(f)

for _, b := range f.Blocks {

fmt.Printf("\tBlock %d\n", b.Index)

for _, instr := range b.Instrs {

fmt.Printf("\t\t%[1]T\t%[1]v(%[1]p)\n", instr)

for _, v := range instr.Operands(nil) {

if v != nil { fmt.Printf("\t\t\t%[1]T\t%[1]v(%[1]p)\n", *v) }

}

}

}

}

return nil, nil

}

type SSA struct {

Pkg *ssa.Package

SrcFuncs []*ssa.Function

}

235 of 322

静的単一形式の構成

  • x/tools/go/ssaパッケージのSSAの構成
    • 関数が基本ブロックで構成されている
    • 基本ブロックは命令で構成されている
    • 命令は複数のオペランドを持つ

235

Program

Package

Function

BasicBlock

Instruction

オペランド

Value

236 of 322

ssa.Program型の定義

  • プログラムを表す構造体
    • SSAを組み立てる単位
    • (*ssa.Program).AllPackagesメソッドから構成するパッケージが取得可能

236

type Program struct {

Fset *token.FileSet

// 型チェッカーのメソッドセットのキャッシュ

MethodSets typeutil.MethodSetCache

}

237 of 322

ssa.Package型の定義

  • パッケージを表す構造体
    • Membersフィールドにメンバを保持
      • パッケージ関数や型、定数、変数など
      • init関数やinit#1関数なども含む

237

type Package struct {

// 所属するプログラム

Prog *Program

// 対応する*types.Package型

Pkg *types.Package

// メンバ(init関数も含む)

Members map[string]Member

}

238 of 322

ssa.Member型の定義

  • パッケージのメンバを表す
    • パッケージに定義された変数、型、定数、関数

238

type Member interface {

Name() string // 名前

String() string // pkgname.XXXのような名前

RelString(*types.Package) string // パッケージ内の名前

Object() types.Object // 対応するtypes.Object

Pos() token.Pos // 対応するノードの位置

Type() types.Type // 対応するtypes.Type

Token() token.Token // token.{VAR,FUNC,CONST,TYPE}

Package() *Package // 親パッケージ

}

239 of 322

ssa.Member型を実装した型

  • パッケージに定義された変数、型、定数、関数
    • ssa.Global型ssa.Type型ssa.NamedConst型ssa.Function型

239

type NamedConst struct {

Value *Const // 対応するssa.Const

}

type Global struct {

Pkg *Package // 親パッケージ

}

type Type struct { /* 公開されたフィールドはない */ }

240 of 322

ssa.Function型の定義

  • 関数を表す構造体
    • Blocksフィールドに基本ブロックを保持する

240

type Function struct {

Signature *types.Signature // 型情報

Synthetic string

Pkg *ssa.Package // パッケージ

Prog *ssa.Program // プログラム

Params []*ssa.Parameter // 引数

FreeVars []*ssa.FreeVar // 自由変数(クロージャの場合)

Locals []*ssa.Alloc // ローカル変数

Blocks []*ssa.BasicBlock // 構成する基本ブロック

Recover *ssa.BasicBlock

AnonFuncs []*ssa.Function // 関数内で定義された無名関数

}

241 of 322

ssa.BasicBlock型の定義

  • 基本ブロックを表す構造体
    • 複数のssa.Instructionを持つ
    • 遷移元の基本ブロックをPredsフィールドに保持
    • 遷移先の基本ブロックをSuccsフィールドに保持

241

type BasicBlock struct {

Index int

Comment string

Instrs []Instruction

Preds, Succs []*BasicBlock

}

242 of 322

ssa.Instruction型とssa.Value

  • 1つの命令は複数のオペランドを持つ
    • 命令はssa.Instructionインタフェースで表す
    • オペランドはssa.Valueインタフェースで表す
    • ssa.Instruction型とssa.Value型をともに実装した型も存在する

242

Instrction

Value

Value

Value

オペランド

243 of 322

ssa.Instructionの定義

  • 命令を表すインタフェース
    • オペランドとしてssa.Valueを持つ
    • Operandsメソッドはオペランドを引数のスライスに追加する
    • 引数にnilを渡すとオペランドを単純に返す

243

type Instruction interface {

String() string

Parent() *ssa.Function

Block() *ssa.BasicBlock

Operands(rands []*ssa.Value) []*ssa.Value

Pos() token.Pos

}

244 of 322

ssa.Value定義

  • 値を表すインタフェース
    • オペランドとして使用される

244

type Value interface {

Name() string

String() string

Type() types.Type

Parent() *ssa.Function

Referrers() *[]Instruction

Pos() token.Pos

}

245 of 322

値を使用している命令を取得する

  • (ssa.Value).Referrersメソッドを用いる
    • 値をオペランドで使用している命令を取得する
    • 重複した命令が含まれる場合もある
      • 同じ命令のオペランドとして複数回出現する場合
    • スライスのポインタなのは呼び出しもとがスライスを変更できるようにするため

245

type Value interface {

Referrers() *[]Instruction

/* 略 */

}

246 of 322

ssa.Nodeの定義

  • 値と命令を表すインタフェース
    • ssa.Value型とssa.Instruction型を一緒に扱うための型
    • 多くの型がこの両方をインタフェースとしては満たしている
      • 必ずしもすべてのメソッドが呼べるわけではない

246

type Node interface {

// 共通のメソッド

String() string

Pos() token.Pos

Parent() *Function

// 個別のメソッド

Operands(rands []*Value) []*Value // 命令ではない場合はnil

Referrers() *[]Instruction // 値ではない場合はnil

}

247 of 322

ssa.Value特徴

  • 値が同じものはssa.Valueも同じ
    • 変数が同じでも再代入されれば値が変わる

247

a.f

Block 0

*ssa.BinOp 0xc000152000 10:int + 20:int

*ssa.Const 0xc00000d000 10:int

*ssa.Const 0xc00000d060 20:int

*ssa.Call 0xc000122180 println(t0)

*ssa.Builtin 0xc00000d0a0 builtin println

*ssa.BinOp 0xc000152000 10:int + 20:int

*ssa.Return 0xc00005f710 return

func f() {

n := 10

n = n + 20

println(n)

}

248 of 322

ssaパッケージの型 −1−

248

型名

説明

Value

Instruction

Member

*ssa.Alloc

メモリ割り当て(変数の定義など)

*ssa.BinOp

2項演算

*ssa.Builtin

組み込み関数

*ssa.Call

関数呼び出し

*ssa.ChangeInterface

別のインタフェースへ変換

*ssa.ChangeType

型のキャスト

*ssa.Const

定数

*ssa.Convert

基本型のキャスト

*ssa.DebugRef

デバッグ情報

*ssa.Defer

defer

*ssa.Extract

複数戻り値からの取り出し

*ssa.Field

フィールド

※ ◯がついていなくてもssa.Valueとssa.Instructionについては実装している場合があるが、それはssa.Nodeを満たすためである

249 of 322

ssaパッケージの型 −2−

249

型名

説明

Value

Instruction

Member

*ssa.FieldAddr

フィールドへのポインタ

*ssa.FreeVar

自由変数

*ssa.Function

関数

*ssa.Global

パッケージ変数

*ssa.Go

ゴールーチン呼び出し

*ssa.If

分岐

*ssa.Index

インデクサ

*ssa.IndexAddr

インデクサへのポインタ

*ssa.Jump

ジャンプ命令(基本ブロック間の移動)

*ssa.Lookup

マップへカンマOK形式でのアクセス

*ssa.MakeChan

チャネルの生成

*ssa.MakeClosure

クロージャの生成

※ ◯がついていなくてもssa.Valueとssa.Instructionについては実装している場合があるが、それはssa.Nodeを満たすためである

250 of 322

ssaパッケージの型 −3−

250

型名

説明

Value

Instruction

Member

*ssa.MakeInterface

インタフェースの生成

*ssa.MakeMap

マップの生成

*ssa.MakeSlice

スライスの生成

*ssa.MapUpdate

マップのキーに対する値の更新

*ssa.NamedConst

名前付き定数

*ssa.Next

string型とマップのrangeによるイテレータ

*ssa.Panic

パニック

*ssa.Parameter

引数

*ssa.Phi

φノード

*ssa.Range

range

*ssa.Return

return文(暗黙的なものも含む)

*ssa.RunDefers

defer文で予約した関数呼び出しの実行

※ ◯がついていなくてもssa.Valueとssa.Instructionについては実装している場合があるが、それはssa.Nodeを満たすためである

251 of 322

ssaパッケージの型 −4−

251

型名

説明

Value

Instruction

Member

*ssa.Select

select

*ssa.Send

チャネルへの送信

*ssa.Slice

スライス演算

*ssa.Store

変数への代入

*ssa.Type

*ssa.TypeAssert

型アサーション

*ssa.UnOp

単項演算

※ ◯がついていなくてもssa.Valueとssa.Instructionについては実装している場合があるが、それはssa.Nodeを満たすためである

252 of 322

【TRY】使われてない値

  • 下のコードのように使われていない引数を探すコードを書いてください
    • nはすぐに再代入されて使われていない

252

func f(n, m int) {

n = 20

println(n, m)

}

253 of 322

割り当て

  • ssa.Alloc構造体が表す
    • メモリの割り当てを表す
    • Heapフィールドがtrueの場合はヒープに確保される
      • エスケープされるような場合に使われる

253

type Alloc struct {

Comment string

Heap bool // ヒープに確保されるかどうか

}

254 of 322

代入

  • ssa.Store構造体が表す
    • アドレスが指す先への代入を表す
      • Addrフィールドがアドレス
      • Valフィールドが代入する値を表す

254

type Store struct {

Addr Value

Val Value

}

255 of 322

ssa.NaiveForm

  • ssa.BuilderMode型の定数
    • すべてのローカル変数へのロードとストアを明示的に表現したもの
    • このモードを指定しないとliftingという処理で余計なストアなどは、代入される直接ssa.Valueに置き換えられる
    • buildssa.AnalyzerでビルドされるSSAでは指定されない

255

256 of 322

ssa.Make*

  • 特定の種類の型の値を生成する命令
    • Alloc命令で確保されたアドレスにStore命令で代入される

256

型名

説明

*ssa.MakeChan

チャネルの生成

*ssa.MakeClosure

クロージャの生成

*ssa.MakeInterface

インタフェースの生成

*ssa.MakeMap

マップの生成

*ssa.MakeSlice

スライスの生成

257 of 322

関数呼び出し

  • ssa.CallInstructionインタフェースを実装した型
    • *ssa.Defer型*ssa.Go型*ssa.Call

257

type CallInstruction interface {

Instruction

// 関数呼び出しの共通部分

Common() *ssa.CallCommon

// *Call型の値またはnil (*Go型または*Defer型の場合)

Value() *ssa.Call

}

258 of 322

ssa.CallCommon構造体

  • 関数呼び出しの共通部分
    • インタフェース経由のメソッド呼び出し: invokeモード
    • それ以外の関数の呼び出し:callモード
      • 組み込み関数はValueフィールドが*ssa.Builtin型になる

258

type CallCommon struct {

// レシーバ(invokeモード)または関数の値(callモード)

Value ssa.Value

// 対応するインタフェースのメソッド(invokeモード)

Method *types.Func

// パラメータ(静的なメソッド呼び出しの場合はレシーバも含む)

Args []ssa.Value

}

259 of 322

【TRY】チャネルを2度closeしない

  • 同じチャネルを2回closeしている場所を探す
    • close関数の引数に2度同じssa.Valueが渡されている箇所を探す
    • 関数の呼び出しはssa.CallInstruction型で表される
    • (*ssa.CallInstruction).Commonメソッドで*ssa.CallCommon型の値が取得できる
    • ssa.CallCommon型のValueフィールドから関数が取得できる
    • 組み込み関数は*ssa.Builtin型で表される
      • 名前がcloseなものを探す

259

260 of 322

複数戻り値からの取り出し

  • ssa.Extract構造体で表される
    • Tupleフィールドが表す値のIndex番目の値を表す
    • 関数呼び出しだけではなく、型アサーションなども表す場合がある
      • *ssa.Call型や*ssa.TypeAssert型などの値がくる

260

type Extract struct {

Tuple Value // 関数呼び出しや型アサーションなど

Index int

}

261 of 322

クロージャの生成と自由変数

  • ssa.FreeVar構造体で自由変数を表す
    • ssa.Function構造体のFreeVarsフィールドに保持される
    • ssa.MakeClosure構造体のBindingsフィールドで束縛された値が取得できる
      • *ssa.Alloc型の値など

261

type MakeClosure struct {

Fn Value // *ssa.Function型の値

Bindings []Value // Fn.FreeVarsに束縛された値

}

262 of 322

【TRY】自由変数の変更

  • 自由変数の値を変更しているコードを見つける
    • クロージャ内でStore命令を探す
    • 自由変数(*ssa.FreeVar)にストアしている部分を見つける
    • 変数を定義している箇所でエラーを出す

262

func f() {

var n int // want "NG"

func() { n = 10 }()

println(n)

}

263 of 322

分岐と基本ブロック

  • ssa.If構造体で表される
    • 条件なしの基本ブロックへの移動はssa.Jump構造体
    • *ssa.If型や*ssa.Jump型の値は基本ブロックの最後の命令になる
    • thenは(*ssa.If).Block().Succs[0]に遷移
    • elseは(*ssa.If).Block().Succs[1]に遷移
    • for文やswitch文などに対応する命令はない
      • すべてssa.If構造体とssa.Jump構造体で表す

263

type If struct {

Cond Value // 条件

}

func (v *If) Block() *BasicBlock

264 of 322

If命令の取得

  • analysisutil.IfInstr関数を用いる
    • 基本ブロック中に存在するIf命令を取得する
    • 存在しない場合にはnilを返す

264

func IfInstr(b *ssa.BasicBlock) *ssa.If

265 of 322

ssa.Return構造体

  • 関数の終了を表す
    • 関数の終了時には必ず基本ブロックの最後の命令として存在する
      • Goのソースコード中で暗黙にreturnしてる場合も含む
    • (*ssa.Return).Block().Succsは必ず空

265

type Return struct {

Results []Value

}

266 of 322

Return命令の取得

  • analysisutil.Returns関数を用いる
    • 関数はクロージャからReturn命令を取得できる
    • 存在しない場合はnilを返す

266

// vは*ssa.Function型または*ssa.MakeClosure型

func Returns(v ssa.Value) []*ssa.Return

267 of 322

ssa.Phi構造体

  • 経路によって変わる値を表す
    • Φノードを表す
    • 分岐などによって値が変わる可能性がある場合に用いる
    • Edgesフィールドは経路ごとの値
    • Edges[i](*ssa.Phi).Block().Preds[i]から来た場合の値
    • 必ずブロックの先頭の命令になる

267

type Phi struct {

Comment string // 目的

Edges []Value // Edges[i]はBlock().Preds[i]に対応する値

}

268 of 322

ssa.Phi構造体の取得

  • analysisutil.Phi関数を用いる
    • 基本ブロックから*ssa.Phi型の値を取得する
    • 存在しない場合はnilが返される

268

func Phi(b *ssa.BasicBlock) (phis []*ssa.Phi)

269 of 322

関数が呼び出されているか

  • analysisutil.Called関数を用いる
    • 関数が呼び出されているかどうかを調べる
    • 第1引数の命令以降の基本ブロック中の命令よりあとを調べる
    • メソッドの場合は第2引数を指定する
    • 呼び出しを確認したい関数は第3引数で指定する

269

func Called(instr ssa.Instruction,

recv ssa.Value, f *types.Func) bool

270 of 322

デバッグ情報の取得

  • ssa.DebugRef型から取得する
    • デバッグ情報なので特に何もしない命令
    • BuilderModeにssa.GlobalDebugを指定する必要がある
      • buildssa.AnalyzerではOFFになっているため注意
    • デバッグ情報があれば(*ssa.Function).ValueForExprから取得可
      • ASTの式から対応するSSAの値が取得できる

270

type DebugRef struct {

Expr ast.Expr // 対応するASTの式(*ast.ParenExprは含まない)

IsAddr bool // アドレスかどうか

X Value // 対応する値

}

271 of 322

【TRY】間違ったエラーの返却

  • if err != nil で比較しているのにnilを返す関数
    • 本来ならエラーを返すべき
    • 逆も検出できる考えてみる

271

var err error = f()

if err != nil {

// 間違え

return nil

}

272 of 322

16.7. パッケージ情報の取得

272

273 of 322

go listコマンド

  • Goのパッケージ情報などを表示するコマンド
    • -fでテンプレートを変更できる(表示する情報を変更できる)
      • text/template形式
    • -mでモジュールの情報を表示
    • -jsonでJSON形式で出力
    • go list -m -u -json all
      • すべてのモジュールのアップデート情報を出す
    • 詳細は go help list コマンドで確認できる

273

$ go list -f '{{range .GoFiles}}{{.}} {{end}}' fmt

doc.go errors.go format.go print.go scan.go

$ cd `go env GOPATH`/src/github.com/gostaticanalysis/knife

$ go list -m -f '{{.GoVersion}}'

1.14

274 of 322

go/buildパッケージ

  • Goのパッケージ情報を集めるパッケージ
    • Go Modulesに対応していない

274

type Context struct {

GOARCH string; GOOS string; GOROOT string; GOPATH string

Dir string

CgoEnabled bool; UseAllFiles bool

Compiler string

BuildTags []string; ReleaseTags []string

InstallSuffix string

JoinPath func(elem ...string) string

SplitPathList func(list string) []string

IsAbsPath func(path string) bool

IsDir func(path string) bool

HasSubdir func(root, dir string) (rel string, ok bool)

ReadDir func(dir string) ([]os.FileInfo, error)

OpenFile func(path string) (io.ReadCloser, error)

}

func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Package, error)

275 of 322

Go Modules

  • 標準のモジュール管理の仕組み
    • https://golang.github.io/dep/
    • Go1.11/Go1.12でテクニカルレビュー
    • Go1.13で正式導入
  • modulesの特徴
    • ビルド時に依存関係を解決する(go toolのように)
    • ベンダリングが不要になる
    • 新しくモジュールという概念単位でバージョン管理する
    • 互換性がなくなる場合はインポートパスを変える
    • 可能な限り古いバージョンが優先される(Minimal Version Selection)

275

276 of 322

x/tools/go/packagesパッケージ

  • パッケージ情報を集めるパッケージ
    • パッケージ名からGoファイルの情報などを取得
    • 構文解析と型チェックまでを自動で行う
      • packages.LoadModeによって不必要な情報は取得しなくできる

276

flag.Parse()

mode := packages.NeedFiles | packages.NeedSyntax // 構文解析まで

cfg := &packages.Config{Mode:mode}

pkgs, err := packages.Load(cfg, flag.Args()...)

if err != nil { /* エラー処理 */ }

if packages.PrintErrors(pkgs) > 0 { /* エラー処理 */ }

for _, pkg := range pkgs {

for _, f := range pkg.Syntax { ast.Print(pkg.Fset, f) }

}

277 of 322

packages.Package構造体

  • パッケージ単位の解析結果を保持する型
    • Syntaxフィールドに抽象構文木
    • Fsetフィールドにノードの位置情報
    • TypeInfoフィールドに型情報

type Package struct {

ID string

Name string

PkgPath string

Errors []Error

GoFiles []string

CompiledGoFiles []string

OtherFiles []string

IgnoredFiles []string

ExportFile string

Imports map[string]*Package

Types *types.Package

Fset *token.FileSet

IllTyped bool

Syntax []*ast.File

TypesInfo *types.Info

TypesSizes types.Sizes

Module *Module

}

278 of 322

14.8. go/analysis詳細

278

279 of 322

ドライバー

  • 静的解析ツールを実行するプログラム
    • analysis.Analyzer型で定義したものが対象
    • unitcheckerパッケージを利用して作ったコマンドを実行する
    • 公式ではgo vetgoplzなどがある

279

ドライバー

go vet, goplz

静的解析ツール

unitchecker.Main

Analyzer-1

Analyzer-2

・・・

実行

Run

xxx.go

yyy.go

280 of 322

singlechecker

  • singlechecker.Main
    • 単独Analyzerからなるmain関数を提供する
    • Analyzerはゴールーチンで1回だけ実行される
    • ドライバなしで単体で動くバイナリを生成できる

package main

import (

"simple"

"golang.org/x/tools/go/analysis/singlechecker"

)

func main() { singlechecker.Main(simple.Analyzer) }

281 of 322

multichecker

  • multichecker.Main
    • 複数Analyzerからなるmain関数を提供する
    • Analyzerはゴールーチンで1回だけ実行される
    • ドライバなしで単体で動くバイナリを生成できる

package main

import (

"xxx"

"yyy"

"golang.org/x/tools/go/analysis/multichecker"

)

func main() { multichecker.Main(xxx.Analyzer, yyy.Analyzer) }

282 of 322

analysis.Diagnostic構造体

  • token.Pos(位置)に関連付けられた静的解析結果
    • 任意の位置へのエラーを表現するために使う
      • 例:〇行目に〇〇というエラーがあります
    • (*analysis.Pass).Reportf(*analysis.Pass).Reportで出力可

282

type Diagnostic struct {

Pos token.Pos

End token.Pos // オプション

Category string // オプション

Message string

SuggestedFixes []SuggestedFix // オプション

Related []RelatedInformation // オプション

}

283 of 322

analysis.SuggestedFix構造体

  • 変更の提案を行うための型
    • analysis.Diagnostic構造体のフィールドとして設定する
    • -fixオプションで提案を反映することができる
      • singlecheckermulticheckerの場合のみ
    • goplsからも使用できる

283

type SuggestedFix struct {

Message string // 変更の概要

TextEdits []TextEdit

}

284 of 322

analysis.Factインタフェース

  • 述語を表現した型
    • types.Objecttypes.Packageが対象
    • 「絶対returnしない」のような述語を表す
    • 「関数F」や「パッケージP」などの主語は表さない

284

type Fact interface {

AFact() // 明示的に実装させるためのメソッド(空でよい)

}

285 of 322

Factの設定

  • analysis.Analyzer型のFactTypesフィールドに設定
    • 型が分かれば十分
    • FactGobでシリアライズされる

285

type Analyzer struct {

/* 略 */

// インポートまたはエクスポートするFact(型情報が必要)

// 要素はポインタである必要がある

FactTypes []Fact

}

286 of 322

Factのエクスポート

  • analysis.Pass型のフィールドからエクスポート

286

type Pass struct {

/* 略 */

// オブジェクトに関連付けられたFactをエクスポートする

// 第2引数に渡す具象型の値はポインタである必要がある

// スレッドセーフではない

ExportObjectFact func(obj types.Object, fact Fact)

// パッケージに関連付けられたFactをエクスポートする

// 他の挙動はExportObjectFactと同じ

ExportPackageFact func(fact Fact)

}

287 of 322

Factのインポート

  • analysis.Pass型のフィールドからインポート

287

type Pass struct {

/* 略 */

// オブジェクトに関連付けられたFactをインポートする

// 第2引数に渡す具象型の値はポインタである必要がある

// Factを満たす場合はインポートした値を� // 第2引数で渡したポインタが指す先に代入

// スレッドセーフではない

ImportObjectFact func(obj types.Object, fact Fact) bool

// パッケージに関連付けられたFactをインポートする

// 他の挙動はImportObjectFactと同じ

ImportPackageFact func(pkg *types.Package, fact Fact) bool

}

288 of 322

Factを使った解析

  • 依存するパッケージもすべて解析対象になる
    • analysis.Analyzer型のFactTypesフィールドがnilじゃない場合
    • importしているパッケージにもRunフィールドの関数を適用
      • Factを使ってない場合はimportしているパッケージは対象外
    • FactはGobでシリアライズされる

288

ドライバー

go vet, goplz

静的解析ツール

unitchecker.Main

パッケージ1

実行

解析

静的解析ツール

unitchecker.Main

静的解析ツール

unitchecker.Main

パッケージ2

解析

パッケージ3

解析

Fact

Fact

Gob

Export

Export

Import

import "pkg1"

import "pkg2"

289 of 322

Factの利用例

  • x/tools/go/analysis/passes/printfパッケージ
    • go vetの中で使われているAnalyzerを定義している
    • printf系の関数をラップしているかどうかを表すFactを定義している
    • importしているパッケージも対象として解析ができる

289

type isWrapper struct{ Kind Kind }

func (f *isWrapper) AFact() {}

func (f *isWrapper) String() string {

switch f.Kind {

case KindPrintf: return "printfWrapper"

case KindPrint: return "printWrapper"

case KindErrorf: return "errorfWrapper"

default: return "unknownWrapper"

}

}

290 of 322

外部パッケージを含むコードの解析

  • サードパーティ製のパッケージを含むコードの解析
    • importしていない場合は早期に解析を終わらせる
      • 利用していないコードで無駄に実行時間を使わないように
    • テストはベンダリングする必要がある
      • 依存するパッケージはvendorディレクトリ以下に置く
      • バージョンごとのテストなどはできないので工夫が必要
      • Go Modulesを使ったテストは現状だとできない
        • gostaticanalysis/testutilを使うとできる

290

291 of 322

【TRY】recoverしていない関数

  • panic関数を呼んでいるのにrecoverしていない関数
    • panic関数を呼んでいる関数を呼んでいても検出こと
    • Factを使ってpanic関数を呼んでいる関数を検出する

291

package a

import "reflect"

func f() {

g()

}

func g() {

v := reflect.ValueOf(10)

v.SetInt(20)

}

292 of 322

16.9. コールグラフとポインタ解析

292

293 of 322

コールグラフ

293

main()

f1()

f2()

f3()

f4()

294 of 322

コールグラフを表す型

  • golang.org/x/tools/go/callgraphで定義される

294

type Graph struct {

Root *Node // ルートノード

Nodes map[*ssa.Function]*Node // 関数との対応

}

type Node struct {

Func *ssa.Function // どの関数に対応するか

ID int // 0ベースの連番

In []*Edge // ソートされてない入力エッジのスライス(n.In[*].Callee == n)

Out []*Edge // ソートされてない出力エッジのスライス(n.Out[*].Caller == n)

}

type Edge struct {

Caller *Node

Site ssa.CallInstruction

Callee *Node

}

295 of 322

アルゴリズムの違い

295

アルゴリズム

説明

パッケージ

静的取得

静的に決まる関数呼び出しのみ取得する

x/tools/go/callgraph/static

Andersenのアルゴリズム

ポインタ解析とともに取得

x/tools/go/pointer

Class Hierarchy Analysis (CHA)

インタフェースを実装しているすべての方のメソッドにエッジを結ぶ

x/tools/go/callgraph/cha

Rapid Type Analysis (RTA)

ポインタ解析のものより精度は低くなるが高速

x/tools/go/callgraph/rta

Variable Type Analysis (VTA)

変数を起点として解析を行う、精度は良いがコストが高い。

x/tools/go/callgraph/vta

296 of 322

静的なコールグラフの取得

  • golang.org/x/tools/go/callgraph/staticを用いる
    • 入力として静的単一代入形式が必要
    • 静的な関数呼び出しのみを取得できる
    • インタフェースやポインタを介した呼び出しは取得できない
      • ポインタ解析が必要

296

func CallGraph(prog *ssa.Program) *callgraph.Graph

297 of 322

VTAによるコールグラフの取得

  • golang.org/x/tools/go/callgraph/vtaを用いる
    • VTA: Variable Type Analysis
    • 初期値となるコールグラフが必用

297

func run(pass *analysis.Pass) (interface{}, error) {

s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)

initcg := cha.CallGraph(s.Pkg.Prog)

cg := vta.CallGraph(ssautil.AllFunctions(s.Pkg.Prog), initcg)

/* (略) */

return nil, nil

}

298 of 322

ポインタ解析

  • ポインタがどこを指しているのか解析する
    • x/tools/go/pointerパッケージを用いる
    • ポインタpがどの変数を指し示すのか
  • エスケープ解析の一部で利用される技術
    • コンパイラ内部で行われる
    • スタックまたはヒープに割り当てるべきか解析する

298

func f() {

var n int

p := &n

g(p)

var m int

g(&m)

}

func g(p *int) { /* 略 */ }

299 of 322

ポインタ解析で分かること

  • どのポインタがどの変数を指すか
    • ポインタから指し示される可能性のある変数を取得できる
  • インタフェースを介したメソッド呼び出しを追える
    • 呼び出されたメソッドの実態を取得できる

299

type C struct{}

func (C) m() { /* 略 */ }

func f() {

var i I = C{}

g(i)

}

func g(i I) {

i.m()

}

実態を辿ることができる

300 of 322

ポインタ解析の流れ

  • 構文解析と型チェックを行う
    • golang.org/x/tools/go/pacakgesパッケージを使う
    • Linterではない静的解析ツールに使われる
  • 静的単一代入形式(SSA)に変換
    • golang.org/x/tools/ssaパッケージを使う
    • buildssa.Analyzerを参考に依存するパッケージも含めて構築する
  • ポインター解析
    • golang.org/x/tools/pointerパッケージを使う
    • 指定位置の抽象構文木のノードから対応するSSAの値を取得する
    • SSAの値を対象にポインタ解析を行う

301 of 322

ポインタ解析の例

301

func pointer(mainPkg *ssa.Package) {

config := &pointer.Config{Mains: []*ssa.Package{mainPkg}}

// Cのメソッドfの引数m(マップ)へのポインタセットを取得するクエリ

C := mainPkg.Type("C").Type()

Cfm := mainPkg.Prog.LookupMethod(C, mainPkg.Pkg, "f").Params[1]

config.AddQuery(Cfm)

result, err := pointer.Analyze(config) // ポインタ解析の実行

if err != nil { /* エラー処理 */ }

// (C).f(m)のポインタセットのラベルを表示する

fmt.Println("m may point to:")

var labels []string

for _, l := range result.Queries[Cfm].PointsTo().Labels() {

pos := prog.Fset.Position(l.Pos())

label := fmt.Sprintf(" %s: %s", pos, l)

labels = append(labels, label)

}

sort.Strings(labels)

for _, label := range labels { fmt.Println(label) }

}

// 解析対象のコード

type I interface { f(map[string]int) }

type C struct{}

func (C) f(m map[string]int) {

fmt.Println("C.f()")

}

func main() {

var i I = C{}

x := map[string]int{"one":1}

i.f(x) // 動的なメソッド呼び出し

}

# 出力結果

m may point to:

main.go:18:21: makemap

302 of 322

ポインタ解析によるコールグラフ生成

  • ポインタ経由の関数呼び出しも取得できる
    • poniter.Config構造体のBuildCallGraphフィールドをtrueにする
    • pointer.Result構造体のCallGraphフィールドから取得できる

302

func callgraph(result *pointer.Result) {

var edges []string

callgraph.GraphVisitEdges(result.CallGraph, func(edge *callgraph.Edge) error {

caller := edge.Caller.Func

if caller.Pkg == mainPkg {

edges = append(edges, fmt.Sprint(caller, " --> ", edge.Callee.Func))

}

return nil

})

sort.Strings(edges)

for _, edge := range edges { fmt.Println(edge) }

fmt.Println()

}

303 of 322

ポインタ解析の例

  • ポインタ解析を行い結果を表示するツール

package main

func main() {

f(map[string]int{})

f(map[string]int{})

}

func f(m map[string]int) {

println(len(m))

}

# a.goのオフセット80(81バイト目)を指定

$ ptrls `pwd`/a.go 80

m� a.go:4:18 map[string]int� a.go:5:18 map[string]int

304 of 322

ptrls:静的単一代入形式への変換

  • 依存するパッケージも変換する
    • (*ssa.Program).CreatePackageメソッドで*ssa.Packageを作る
    • 抽象構文木と型情報をまとめておく
      • packages.Load関数の結果はパッケージごと

// デバッグ情報を付加しながら生成する

prog := ssa.NewProgram(fset, ssa.GlobalDebug)

created := make(map[*types.Package]bool)

var createAll func(pkgs []*types.Package)

createAll = func(pkgs []*types.Package) {

for _, p := range pkgs {

if created[p] { continue }

created[p] = true

prog.CreatePackage(p, nil, nil, true)

createAll(p.Imports())

}

}

var mainPkg *packages.Package

var files []*ast.File

info := &types.Info{ /* 初期化(略) */ }

for _, pkg := range pkgs { // packages.Load関数で得た結果

createAll(pkg.Types.Imports()) // 依存パッケージを処理

mergeTypesInfo(info, pkg.TypesInfo) // 型情報をまとめる

files = append(files, pkg.Syntax...) // 抽象構文木をまとめる

if pkg.Module != nil && pkg.Module.Main { mainPkg = pkg }

}

if mainPkg == nil { /* エラー処理 */ }

ssapkg := prog.CreatePackage(mainPkg.Types, files, info, true)

ssapkg.Build()

305 of 322

ptrls:ポインタ解析の手順

  • 静的単一代入形式に変換
    • go/analysisパッケージの場合はbuildssa.Analyzerを利用する
    • ptrlsの場合は前述の方法で自力で作成
  • クエリの登録
    • pointer.Config構造体に解析用のクエリを登録する
    • ポインタ解析は重たい解析なので解析したいものを明示的に登録する

type Config struct {

Mains []*ssa.Package // 静的単一代入形式

Reflection bool

BuildCallGraph bool // コールグラフを生成するか

Queries map[ssa.Value]struct{} // クエリ: ptr(x)

IndirectQueries map[ssa.Value]struct{} // デリファレンして解析: ptr(*x)

Log io.Writer

}

306 of 322

ptrls:解析対象を取得(位置の取得)

  • ファイル名とoffsetからソースコード上の位置を取得
    • token.FileSet構造体から取得する
    • token.Pos型がソースコード上の位置を表す

func (prog *Program) Pos(filename string, offset int) token.Pos {

var pos token.Pos

prog.Fset.Iterate(func(f *token.File) bool {

if f.Name() == filename { pos = f.Pos(offset); return false }

return true

})

return pos

}

307 of 322

ptrls:解析対象を取得(ノードの取得)

  • 位置情報から抽象構文木のノードを取得
    • x/tools/go/ast/astutilパッケージを用いる
    • astutil.PathEnclosingInterval関数で取得

func (prog *Program) Path(pos token.Pos) (path []ast.Node, exact bool) {

for _, f := range prog.Files {

// token.Pos型は整数なので比較できる

if f.Pos() <= pos && pos <= f.End() {

return astutil.PathEnclosingInterval(f, pos, pos)

}

}

return nil, false

}

308 of 322

ptrls:クエリの追加

  • (*pointer.Config).AddQueryメソッドを使う
    • 抽象構文木のノードに対応する型を調べて解析可能か調べる
    • 抽象構文木のノードに対応する静的単一代入形式の値を取得(後述)
    • クエリに追加する

path, exact := prog.Path(pos)

if !exact { /* 指定位置のノードが取得出来なかった */ }

expr, _ := path[0].(ast.Expr)

typ := prog.TypesInfo.TypeOf(expr) // 対象の式の型情報取得

if !pointer.CanPoint(typ) { continue } // ポインタ解析できない場合はSkip

v := getValue(prog, path, expr) // 対応する静的単一代入の値を取得

if v == nil { continue }

value2node[v] = expr // 静的単一代入の値と式を対応付けておく

config.AddQuery(v) // クエリに追加

309 of 322

ptrls:ast.Expr型からssa.Value型を取得

  • デバッグ情報を持つ静的単一代入形式なら取得可
    • 静的単一代入形式の構築時にssa.GlobalDebugを指定する
      • 抽象構文木の情報が静的単一代入形式に残る
    • (*ssa.Function).ValueForExprメソッドを用いる
    • または、(*ssa.Program).VarValueメソッドを用いる

func getValue(p *Program, path []ast.Node, expr ast.Expr) ssa.Value {

f := ssa.EnclosingFunction(p.Main, path) // 抽象構文木のパスから静的単一代入形式の関数を取得

if f != nil { if v, _ := f.ValueForExpr(expr); v != nil { return v } }

var id *ast.Ident

switch expr := expr.(type) {

case *ast.Ident: id = expr

case *ast.SelectorExpr: id = expr.Sel // x.yのような形式の式(yの部分を取得)

}

o, _ := p.TypesInfo.ObjectOf(id).(*types.Var) // 識別子に対応するオブジェクトを取得(変数)

if o != nil { if v, _ := p.SSA.VarValue(o, p.Main, path); v != nil { return v } }

return nil

}

310 of 322

16.10. 型パラメタを含むコードの解析

310

311 of 322

goパッケージの何が変わるのか

  • GOROOT/api/go1.18.txtを見てみよう

$ grep "go/" `go1.18beta1 env GOROOT`/api/go1.18.txt

pkg go/ast, method (*IndexListExpr) End() token.Pos

pkg go/ast, method (*IndexListExpr) Pos() token.Pos

pkg go/ast, type FuncType struct, TypeParams *FieldList

pkg go/ast, type IndexListExpr struct

pkg go/ast, type IndexListExpr struct, Indices []Expr

pkg go/ast, type IndexListExpr struct, Lbrack token.Pos

pkg go/ast, type IndexListExpr struct, Rbrack token.Pos

pkg go/ast, type IndexListExpr struct, X Expr

pkg go/ast, type TypeSpec struct, TypeParams *FieldList

pkg go/constant, method (Kind) String() string

(略)

312 of 322

抽象構文木をダンプして確かめる

  • ast.Print関数を使う
    • 第2引数のノードをダンプする関数
  • knsh14/astreeを使う(使いたい)
    • treeコマンドっぽく出すツール
    • まだ未対応だけどPRは出てるっぽい

ast.Print(pass.Fset, pass.Files[0])

313 of 322

抽象構文木から型パラメタを得る

  • 関数(ast.FuncType構造体)
    • 関数の型を表すノード
    • *ast.FieldList型のTypeParamsフィールドから取得できる
    • ast.Field構造体で同じ制約の型パラメタのリストを表す
      • [X, Y any, Z fmt.Stringer]だとX, Y anyZ fmt.Stringerの単位
      • Namesフィールドが型パラメタのスライス([]*ast.Ident型
      • Typeフィールドが型制約(ast.Expr型
  • 型(ast.TypeSpec構造体)
    • 型宣言のtypeキーワードより後の部分を表すノード
    • *ast.FieldList型のTypeParamsフィールドから取得できる

func Print[T any](s []T) {...}

type Vector[T any] []T

314 of 322

ノードから型情報の型パラメタを得る

  • 型パラメタの型情報をtypes.TypeParam構造体で表す
    • types.Typeインタフェースを実装
    • Constraintメソッドで制約が取得できる
  • 型パラメタを表す識別子から取得できる
    • 例:ast.FuncType構造体のTypeParamsフィールド
      • ast.Field構造体のNamesフィールドの要素から取得できる
    • (*types.Info).TypeOfメソッドを使う

315 of 322

型情報から型パラメタを取得

  • 関数(types.Signature型)
    • TypeParamsメソッドから取得できる
    • レシーバの型パラメタもRecvTypeParamsメソッドから取得可
  • 型(types.Named型)
    • TypeParamsメソッドから型パラメタを取得
    • TypeArgsメソッドから型引数が取得できる
  • 型パラメタを表すオブジェクト
    • types.TypeName構造体で表される

316 of 322

制約を取得する

  • (*types.TypeParam).Constraintメソッドで取得
    • *types.Interface型で表す
    • IsComparebleメソッドで比較かのうかどうか取得
      • 組み込みのcompareble制約(インタフェース)
      • 型セットがcompareble制約のものの部分集合の制約
    • IsImplicitメソッドで暗黙のインタフェースか取得
      • func f[T ~int]()のようにinterface{ ~int }を省略できる
    • IsMethodSetメソッドでメソッドセットのみか判定
      • 制約以外にも使えるかどうか分かる

317 of 322

型パラメタを持つ関数の呼び出し

  • 関数呼び出しはast.CallExpr構造体が表す
    • Funフィールドに呼び出す関数を表すノードを保持
    • 型引数が指定してあるかどうかで型が変わる
    • 型引数が1つの場合は*ast.IndexExpr型の値が入る
      • ast.IdexExpr構造体はスライスやマップでも使われる既存の型
      • 後方互換性のためにast.IdentListExpr型に統合されなかった
    • 型引数が2つ以上の場合は*ast.IndexListExpr型の値が入る
      • f[string, int]("hoge", 100)のような場合
    • 型引数なしで型推論をする場合は*ast.Ident型の値が入る
      • 関数呼び出しは型引数が省略できる
      • 抽象構文木だけでは実際の型引数が分からない

318 of 322

制約のインスタンス化

  • types.Instantiate関数を用いる
    • types.Context構造体はインスタンス化された型情報などを持つ
      • 型チェック時のtypes.Config構造体で指定できる
    • 第2引数の型は型パラメタを持つ型
    • 第3引数は型引数のスライス
    • 第4引数はインスタンス化の検証を行うかどうか

Print[T]

Print[string]

func([]string)

Print[int]

func([]int)

T => string

T => int

インスタンス化

func Instantiate(ctxt *Context, orig Type, targs []Type, validate bool) (Type, error)

319 of 322

新しく導入されるトークン

  • ~(チルダ):token.TILDE定数
    • インタフェース要素に使える
    • interface{ ~string | int }のように記述できる

InterfaceType = "interface" "{" { InterfaceElem ";" } "}" .

InterfaceElem = MethodElem | TypeElem .

MethodElem = MethodName Signature .

MethodName = identifier .

TypeElem = TypeTerm { "|" TypeTerm } .

TypeTerm = Type | UnderlyingType .

UnderlyingType = "~" Type .

320 of 322

制約のインタフェースを表すノード

  • ast.InterfaceType構造体で表す
    • *ast.FieldList型のMethodsフィールドがインタフェース要素
      • Methodsなのは後方互換のため
    • ~stringなどはast.UnaryExpr型(単項演算式)で表す
    • int | ~stringなどはast.BinaryExpr型(2項演算式)で表す

InterfaceType = "interface" "{" { InterfaceElem ";" } "}" .

InterfaceElem = MethodElem | TypeElem .

MethodElem = MethodName Signature .

MethodName = identifier .

TypeElem = TypeTerm { "|" TypeTerm } .

TypeTerm = Type | UnderlyingType .

UnderlyingType = "~" Type .

321 of 322

16.11. GraphQLの静的解析

321

WIP

322 of 322

16.12. Protocol Buffersの静的解析

322

WIP