16. 静的解析とコード生成
@tenntenn
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
質問について
3
上田拓也
Go ビギナーズ
Go Conference
@tenntenn
Google Developer Expert (Go)
一般社団法人 Gophers Japan 代表理事
バックエンドエンジニアとして日々Goを書いている。Google Developer Expert (Go)。一般社団法人Gophers Japan代表。Go Conference主催者。大学時代にGoに出会い、それ以来のめり込む。人類をGopherにしたいと考え、Goの普及に取り組んでいる。複数社でGoに関する技術アドバイザーをしている。マスコットのGopherの絵を描くのも好き。
作者
【PR】企業向け研修や技術アドバイザー
目次
6
16.1. 静的解析を行う理由
7
Goのプログラムが実行されるまで
8
コーディング
コンパイル
実行
01010010100010100011001000101001
ソースコード
オブジェクトコード
$ go build main.go
Hello, 世界
コードが正しい場合
コーディング
コンパイル
問題のあるソースコード
問題の修正
$ go build main.go
Error: ...
コンパイルエラー
コードに誤りがある場合
コンパイルエラーにならないバグ
9
実装・考慮漏れ
実装ミス
実行時エラー
仕様を満たしていない
仕様にバグがある
考慮が漏れている
文法エラーではないミス
設定ミス
ライブラリの使い方ミス
メモリリーク
ゴルーチンリーク
パニック
コンパイル以外でバグを見つける
10
コーディング
010100101000101000
コンパイル
010100101000101000
デプロイ
リリース
影響小
影響大
テスト
監視
QA
単体テストや結合テスト
機械的に動作確認
人の手による確認
実装漏れや品質を確認
人の手による確認
実装漏れや品質を確認
静的解析
リリース後の本番環境で問題の原因を
見つけるのは非常に困難になる
静的解析と動的解析
11
ソースコードを文字列の塊として
解析せず静的解析を行う理由とは?
"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
}
みんな大好きgrepコマンド
13
$ grep Gopher main.go
Gopher string `json:"gopher"`
gogopher := Gopher()
gogopher.Gopher = gopher
func Gopher() (gopher *GOPHER) {
gopher = &GOPHER{Gopher: "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
}
Gopher関数を探すには?
15
ソースコードとして理解するために
静的解析が必要となる
Goでよく使われる静的解析ツール
16
go vet | バグといえるレベルの誤りを検出 |
errcheck | エラー処理のミスを検出 |
statickcheck | サードパーティ製の静的解析ツールのセット |
golangci-lint | サードパーティ製のLinter Runner |
gosec | セキュリティチェック |
go vet
17
package main
import "fmt"
func main() {
fmt.Printf("%s\n", 100) // %sに数値は指定できない
}
ルール違反はツールで検出する
18
静的解析を行う理由
19
16.2. ast-grepによる静的解析
20
ast-grepとは?
公式ページ
抽象構文木(AST)
AST: Abstract Syntax Tree
v + 1
+
v
1
二項演算式
識別子
整数リテラル
ソースコード
抽象構文木(AST)
構文解析
Playground
インストール不要
視覚的に確認
シェアリンク
ルールの記述方法
# YAML Rule is more powerful! # https://ast-grep.github.io/guide/rule-config.html#rule language: Go severity: error rule: ## ここにルールを記述していく |
kind:ノードの種類を指定
# YAML Rule is more powerful! # https://ast-grep.github.io/guide/rule-config.html#rule language: Go severity: error rule: kind: call_expression # 関数呼び出し式 |
regex:正規表現のマッチ
# 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]* # 整数型かどうか |
【TRY】log.Fatalの呼び出し
package main import "log" func main() { log.Fatal("NG") // NG log.Print("OK") // OK } |
has:ルールを満たすノードを持つ
# 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]" # パッケージ名に大文字を使っている |
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パッケージ以外 |
【TRY】フィールド名のない構造体リテラル
package main type T struct { N int } func main() { var _ = T{N:100} // OK var _ = T{100} // NG } |
inside:親ノードがルールを満たすか
# 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 |
【TRY】関数内でのregexp.MustCompileの呼び出し①
package main import "regexp" var _ = regexp.MustCompile("OK") // OK func main() { var _ = regexp.MustCompile("NG") // NG } |
any:どれかにマッチする
# 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 |
【TRY】関数内でのregexp.MustCompileの呼び出し②
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 } |
【TRY】関数内でのregexp.MustCompileの呼び出し③
package main import "regexp" var _ = regexp.MustCompile("OK") // OK func main() { var _ = regexp.MustCompile("NG") // NG } func init() { var _ = regexp.MustCompile("OK") // OK } |
CLIツールのインストールと実行
$ brew install ast-grep |
$ cargo install ast-grep --locked |
Homebrew (macOS)
cargo
※ cargoはRustのパッケージマネージャ
$ ast-grep scan --rule rule.yml main.go |
実行
16.3. 静的解析クイックスタート
37
静的解析ツールを自作する
静的解析ツールを作ってみよう!
39
package main
func main() {
var v interface{} = "hello"
n := v.(int) // パニックする
println(n)
}
どうやったらこのバグを発見できる?
ソースコードの構造を解析する
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)
構文解析
構文解析
※ 完全に見つけるにはもうちょっと複雑
静的解析ツール開発の流れ
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のバージョンをパースする機能を提供 |
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
Goにおける静的解析のフェーズ
45
構文解析
型チェック
静的単一代入形式
ポインタ解析
字句解析- go/scanner,go/token
46
IDENT
ADD
INT
トークン
ソースコード:
v + 10
構文解析 - go/parser,go/ast
47
v + 10
IDENT
ADD
INT
ソースコード:
+
v
10
BinaryExpr
Ident
BasicLit
トークン:
抽象構文木(AST):
例:import文の重複
48
package main
import fmt1 "fmt"
import fmt2 "fmt"
func main() {
fmt1.Println("Hello")
fmt2.Println("World")
}
*File
[]Decl
*GenDecl
*FuncDecl
*ImportSpec
構文解析で分からないこと
49
BinaryExpr
100 + "hello"
BasicLit
100
BasicLit
"hello"
100 + "hello"
型が合わなくても文法上は問題ない
抽象構文木(AST)を使った解析
50
型チェック - go/types,go/constant
51
n := 100 + 200
m := n + 300
定数の評価
= 300
型の推論
-> int
識別子の解決
型チェックで分かること
52
例:不要な識別子の判別
53
package mypkg
// 使用されていない
func f() { println("f") }
// エクスポートされている
func G() { println("G") }
削除しても問題ない
例:コンテキストを構造体に保持
54
package mypkg
import "context"
// NG
type S struct {
ctx context.Context
}
フィールドに保持するのは
好ましくない
型情報を使った解析
55
静的解析ツールのモジュール化
go/analysisを使う利点
57
analysis.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
}
analysis.Pass
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
}
analysis.Diagnostic
func (pass *Pass) Reportf(pos token.Pos, format string, args ...any)
簡単な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
}
簡単な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
}
analysistestパッケージ
func Test(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, fourcetypeassert.Analyzer, "a")
}
テストデータ
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)
}
}
unitchecker
package main
import (
"example.com/sample"
"golang.org/x/tools/go/analysis/unitchecker"
)
func main() {
unitchecker.Main(sample.Analyzer)
}
go vetからの実行
66
$ go vet -vettool=$(which myvet) pkgname
自作のAnalyzerコレクションを作る
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,
)...)
}
【TRY】自作のAnalyzerコレクション
68
skeleton
$ skeleton example.com/myanalyzer
myanalyzer
├── cmd
│ └── myanalyzer
│ └── main.go
├── myanalyzer.go
├── myanalyzer_test.go
└── testdata
└── src
└── a
└── a.go
skeletonを使えば簡単につくれる
簡単なものは1時間もかからず
開発が可能
skeletonを使った開発の流れ
71
雛形を作る
テストデータを作る
テストを動かす
Analyzerを修正
package a
func f() {
var a any
_ = a.(int) // want "NG"
}
$ skeleton example.com/sample
$ go test
【TRY】skeletonのインストール
72
$ go install github.com/gostaticanalysis/skeleton/v2@latest
$ skeleton example.com/sample
$ cd sample
$ go mod tidy
$ go test
golangci-lintのプラグイン
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
skeletonでgolangci-lintプラグイン作成
74
$ go build -buildmode=plugin \
-ldflags "-X 'main.flags=-funcs=log.Fatal'" \
-o path_to_plugin_dir myanalyzer/plugin/myanalyzer
go/analysis/passesパッケージ
75
パッケージ名 | 説明 |
buildssaパッケージ | 静的単一代入(SSA)形式を生成する |
ctrlflowパッケージ | Control Flow Graph(CGF)を生成する |
inspectパッケージ | 抽象構文木の探索を行う |
gostaticanalysis
【TRY】静的解析ツールを作ろう!
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
16.3. 構文解析
79
構文解析 - go/parser,go/ast
80
v + 10
IDENT
ADD
INT
ソースコード:
+
v
10
BinaryExpr
Ident
BasicLit
トークン:
抽象構文木(AST):
構文解析で分からないこと
81
BinaryExpr
100 + "hello"
BasicLit
100
BasicLit
"hello"
100 + "hello"
型が合わなくても文法上は問題ない
言語仕様を読もう
82
抽象構文木(AST)の取得
83
非推奨
go/analysisパッケージが自動で行う
手動で構文解析を行う理由
84
token.Pos型
85
// tokenパッケージにおける定義
type Pos int
const NoPos Pos = 0
token.FileSet型
86
1
ファイルA
ファイルB
100バイト
100
token.Pos型の値
200バイト
式単位の構文解析
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
parser.Mode型
89
定数名 | 説明 |
parser.PackageClauseOnly | package句で構文解析を止める |
parser.ImportsOnly | import定義で構文解析を止める |
parser.ParseComments | コメントも解析をしASTに加える |
parser.Trace | 構文解析のトレース情報を表示する |
parser.DeclarationErrors | 定義エラー(多重定義など)を報告する |
parser.SpuriousErrors | parser.AllErrorsと同じで後方互換のため |
parser.AllErrors | 最初の10個だけではなくすべてのエラーを報告する |
ディレクトリ単位の構文解析
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
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 { /* エラー処理 */ }
GoAst Viewer
92
astree
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)
}
【TRY】抽象構文木の確認
94
package main
import "fmt"
var msg string
func main() {
msg = "Hello"
name := "Gopher"
fmt.Println(msg, name)
}
抽象構文木のノード
95
type Node interface {
Pos() token.Pos
End() token.Pos
}
+
v
10
BinaryExpr
Ident
BasicLit
ノードの種類 −1−
96
ノードの種類 | 説明 |
パッケージを表すノード | ast.Package型 |
ファイルを表すノード | ast.File型 |
宣言を表すノード | ast.Declインタフェースを実装した型 |
宣言の詳細を表すノード | ast.Specインタフェースを実装した型 |
文を表すノード | ast.Stmtインタフェースを実装した型 |
式を表すノード | ast.Exprインタフェースを実装した型 |
型リテラルを表すノード | ast.ArrayType型やast.FuncType型など |
非推奨
ノードの種類 −2−
97
ノードの種類 | 説明 |
リテラルを表すノード | ast.BasicLit型やast.FuncLit型など |
識別子を表すノード | ast.Ident型(ast.Expr型を実装※) |
コメントを表すノード | ast.Comment型とast.CommentGroup型 |
case句を表すノード | ast.CaseClause型とast.CommClause型 |
フィールドを表すノード | ast.Field型とast.FieldList型 |
...を表すノード | ast.Ellipsis型 |
※ 実際に実装しているのは*ast.Ident型
パッケージを表すノード
98
type Package struct {
Name string
Scope *Scope
Imports map[string]*Object
Files map[string]*File
}
非推奨
ファイルを表すノード
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
型名 | 説明 |
*ast.BadDecl | エラーを含む不完全な宣言 |
*ast.GenDecl | インポート、型、変数、定数などの宣言 宣言の詳細はSpecインタフェースを実装した型で定義 |
*ast.FuncDecl | 関数宣言 |
ast.GenDecl型
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
型名 | 説明 |
*ast.ImportSpec | パッケージのインポート定義 |
*ast.ValueSpec | 定数定義と変数定義 |
*ast.TypeSpec | 型定義 |
【TRY】パッケージ変数の数
103
ast.FuncDecl型
104
type FuncDecl struct {
Doc *CommentGroup // 関連付けられたコメント
Recv *FieldList // レシーバ(nilの場合は関数)
Name *Ident // 関数/メソッド名
Type *FuncType // 関数のシグニチャ(引数、戻り値など)
Body *BlockStmt // 関数のボディでGoの関数じゃない場合はnil
}
【TRY】パッケージ関数の一覧
105
文を表すノード −1−
106
型名 | 説明 |
*ast.BadStmt | エラーを含む不完全な文 |
*ast.DeclStmt | const、var、typeで定数、変数、型などの宣言文 |
*ast.EmptyStmt | 空の文。文が書ける場所で省略した場合など |
*ast.LabeledStmt | ラベルのついた文 |
*ast.ExprStmt | 単一の式からなる文 |
*ast.SendStmt | チャネルの送信文(式ではないことに注意) |
文を表すノード −2−
107
型名 | 説明 |
*ast.IncDecStmt | ++や--での増減を行う文(式ではないことに注意) |
*ast.AssignStmt | =を使った代入文と:=での宣言+代入も含む |
*ast.GoStmt | goキーワードによる新規ゴールーチンでの関数呼び出し |
*ast.DeferStmt | deferキーワードによる関数呼び出し |
*ast.ReturnStmt | return文 |
*ast.BranchStmt | break、continue、fallthrough、gotoなどによる分岐文 |
*ast.BlockStmt | ブロック文(複文) |
*ast.IfStmt | if文 |
文を表すノード −3−
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文 |
【TRY】for文の中のdefer文
109
func f() {
defer func() {}() // OK
for range 10 {
defer func() {}() // want "NG"
if false {
// 本当は見つけてほしいけど難しい
defer func() {}() // want "NG"
}
}
}
【TRY】循環複雑度を求めよう
110
func f2(x, y int) int { if x == 2 { if y == 2 { if x+y == 4 { return x + y } } } return 0 } |
式を表すノード −1−
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 | 関数呼び出しと型変換(キャスト)の式 |
式を表すノード −2−
112
型名 | 説明 |
*ast.StarExpr | ポインタのデリファレンスを行う式とポインタ型 |
*ast.UnaryExpr | 単項演算式(-100など) |
*ast.BinaryExpr | 2項演算式(v + 1など) |
*ast.KeyValueExpr | キーとバリューを:で区切ったような表現の式 |
*ast.BasicLitなど | リテラル |
*ast.Ident | 識別子 |
*ast.Ellipsis | 可変長引数や配列の長さの... |
型リテラルを表すノード
113
型名 | 説明 |
ast.ArrayType | 配列型またはスライス型の型リテラル |
ast.StructType | 構造体型の型リテラル |
ast.FuncType | 関数型の型リテラル |
ast.InterfaceType | インタフェース型の型リテラル |
ast.MapType | マップ型の型リテラル |
ast.ChanType | チャネル型の型リテラル |
// スライスの型リテラル
var ns []int
リテラルを表すノード
114
型名 | 説明 |
*ast.BasicLit | 数値リテラルや文字列リテラル |
*ast.FuncLit | 関数リテラル(無名関数、クロージャ) |
*ast.CompositeLit | 配列、スライス、構造体、マップのコンポジットリテラル |
【TRY】空文字を返す関数
115
識別子を表すノード
116
type Ident struct {
NamePos token.Pos // 識別子の位置
Name string // 識別子名
Obj *Object // 対応する*ast.Object型の値
}
【TRY】非公開なパッケージ関数
117
【TRY】大きな関数
118
ast.Object型
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
type Scope struct {
Outer *Scope // このスコープを内包しているスコープ
Objects map[string]*Object // 名前とオブジェクトのマップ
}
※ ast.Objectとast.Scopeは非推奨になる予定(#52463)
非推奨
ノードの種類で処理を分ける
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
0
1
4
2
3
抽象構文木の走査
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
【TRY】ast.Inspect関数で探索
124
※ast.Objectは非推奨になる予定
抽象構文木の走査
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)
}
抽象構文木の走査 Preorderメソッド
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)
})
抽象構文木の走査 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
})
抽象構文木の走査 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
})
抽象構文木の走査 Cursor型
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
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
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
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
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
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())
【TRY】四則演算式の評価
135
v, err := eval("1 + 2 * 3")
if err != nil { /* エラー処理 */ }
// 7
fmt.Println(v)
【TRY】タグ付きの非公開フィールド
136
type Person struct {
// タグが付いているのに非公開なのでNG
name string `json:"name"`
}
【TRY】引数の多い関数
137
16.4. 型チェック
138
型チェック - go/types,go/constant
139
n := 100 + 200
m := n + 300
定数の評価
= 300
型の推論
-> int
識別子の解決
型チェックで分かること
140
型チェック
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を使う処理 */
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
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)
}
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
}
types.Type型とtypes.Object型
145
var n int
オブジェクト: *types.TypeName
型: *types.Basic
オブジェクト: *types.Var
型: *types.Basic
types.Type型
146
type Type interface {
// underlying typeの取得
Underlying() Type
// fmt.Stringerインタフェースを実装するため
String() string
}
Underlying type
147
int
[]string
struct {N int}
X
type X struct {N int}
Y
type Y X
types.Typeを実装した型 −1−
148
型名 | 説明 |
*types.Basic | int型などの基本型 |
*types.Array | 配列 |
*types.Slice | スライス |
*types.Struct | 構造体 |
*types.Pointer | ポインタ |
*types.Tuple | 変数や引数など変数のタプルを表す。Goの型にはない |
types.Typeを実装した型 −2−
149
型名 | 説明 |
*types.Signature | 関数型。メソッドも含む |
*types.Interface | インタフェース |
*types.Map | マップ |
*types.Chan | チャネル |
*types.Named | typeキーワードで定義した名前付き型(ユーザ定義型) |
types.Object型
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インタフェースを実装するため
}
types.Objectを実装した型
151
型名 | 説明 |
*types.PkgName | パッケージ名 |
*types.Const | 定数 |
*types.TypeName | 型名 |
*types.Var | 変数(フィールドや引数、名前付き戻り値も含む) |
*types.Func | 関数 |
*types.Label | ラベル |
*types.Builtin | 組み込み関数(組み込み型は含まない) |
*types.Nil | nil |
識別子の定義箇所と使用箇所
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
関数名 | 説明 |
types.Identical | 同じ型かどうか |
types.IdenticalIgnoreTags | 構造体タグを無視して比較する |
types.AssertableTo | 型アサーションでキャスト可能かどうか |
types.AssignableTo | 代入可能かどうか |
types.Comparable | 比較可能かどうか |
types.ConvertibleTo | キャスト可能かどうか |
types.Implements | インタフェースを実装しているかどうか |
types.IsInterface | インタフェースかどうか |
型の比較
154
func Identical(x, y types.Type) bool
式から型と値を取得する
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
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
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
func run(pass *analysis.Pass) (interface{}, error) {
initOrder := pass.TypesInfo.InitOrder
for _, initializer := range initOrder {
fmt.Println(initializer)
}
return nil, nil
}
式から型を取得する
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
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
}
【TRY】不要なパッケージ関数の判別
161
package mypkg
// 使用されていない
func f() { println("f") }
// エクスポートされている
func G() { println("G") }
削除しても問題ない
基本型を取得する
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
値 | 説明 |
types.Typ[types.UntypedBool] | trueやfalseなどの型 |
types.Typ[types.UntypedInt] | 100や200などの型 |
types.Typ[types.UntypedRune] | '世'や'A'などの型 |
types.Typ[types.UntypedFloat] | 1.5などの型 |
types.Typ[types.UntypedComplex] | 5iや1+5iなどの型 |
types.Typ[types.UntypedString] | "Hello"などの型 |
types.Typ[types.UntypedNil] | nilの型 |
基本型のプロパティを取得する
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
)
【TRY】int型の式を見つける
165
スコープ
166
ユニバース
パッケージ
ファイル
ブロック
ユニバーススコープの取得
167
// println関数を指すオブジェクトを取得する
obj := types.Universe.Lookup("println")
fmt.Println(obj)
ノードからスコープを取得する
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
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
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
func run(pass *analysis.Pass) (interface{}, error) {
for _, n := range pass.Pkg.Scope().Names() {
fmt.Println(n)
}
return nil, nil
}
【TRY】名前の短いパッケージ変数
172
一番内側のスコープを取得する
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
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
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
}
analysisutil.LookupFromImports関数
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
}
analysisutil.TypeOf関数
177
func run(pass *analysis.Pass) (interface{}, error) {
// fmt.Stringerを探す
typ := analysisutil.TypeOf(pass, "fmt", "Stringer")
fmt.Println(typ)
return nil, nil
}
analysisutil.ObjectOf関数
178
func run(pass *analysis.Pass) (interface{}, error) {
// fmt.Printlnを探す
obj := analysisutil.ObjectOf(pass, "fmt", "Println")
fmt.Println(obj)
return nil, nil
}
名前付きの型とUnderlying type
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
関数名 | 説明 |
types.AssertableTo | 型アサーションでキャスト可能かどうか |
types.Implements | インタフェースを実装しているかどうか |
types.IsInterface | インタフェースかどうか |
インタフェースの埋め込みを取得する
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
}
【TRY】errorインタフェースの実装
182
analysisutil.ImplementsError関数
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
}
analysisutil.Interfaces関数
184
func run(pass *analysis.Pass) (interface{}, error) {
for _, iface := range analysisutil.Interfaces(pass.Pkg) {
fmt.Println(iface)
}
return nil, nil
}
フィールドまたはメソッドを取得する
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
}
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
}
types.Func型と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
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
}
types.Struct型
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
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
}
analysisutil.Structs関数
191
func run(pass *analysis.Pass) (interface{}, error) {
for _, st := range analysisutil.Structs(pass.Pkg) {
fmt.Println(st)
}
return nil, nil
}
【TRY】コンテキストを構造体に保持
192
package mypkg
import "context"
// NG
type S struct {
ctx context.Context
}
フィールドに保持するのは
好ましくない
【TRY】t.Parallel()の呼び出し
193
16.5. コード生成
194
コード生成とは
静的解析
コード生成
ソースコード
抽象構文木
型情報
DB
スキーマ
コード生成の必要性
196
go generate
197
//go:generate stringer -type=MyStatus
実行するコマンド
コード生成と静的解析
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
// Code generated by "stringer -type=MyStatus"; DO NOT EDIT.
package main
import "strconv"
/* 略 */
テンプレートを使ったコード生成
200
抽象構文木からソースコードを生成
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
// func Source(src []byte) ([]byte, error)
src, err := format.Source([]byte(`v+1`))
fmt.Println(string(src), err) // v + 1 <nil>
goimports
203
func Process(filename string,
src []byte, opt *Options) ([]byte, error)
コード生成の難しさ
204
knife(ナイフ)
205
$ knife -f "{{names .Funcs}}" fmt | grep "^Print"
Printf
Println
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}}`
コード生成ツール:hagane
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) {}
codegenパッケージ
208
codegen.Generator型
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
}
codegen.Pass型
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
}
Generatorの出力先の変更
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)
Generatorとknifeの組み合わせ
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 }
codegentestパッケージ
213
func TestGenerator(t *testing.T) {
testdata := codegentest.TestData()
rs := codegentest.Run(t, testdata, example.Generator, "example")
// 結果(rs)を使った処理
}
Golden fileを使ったテスト
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)
}
singlegenerator
package main
import (
"sample"
"github.com/gostaticanalysis/codegen/singlegenerator"
)
func main() {
singlegenerator.Main(sample.Generator)
}
skeletonを使ったコード生成
216
$ skeleton -kind=codegen pkgname
pkgname
├── cmd
│ └── pkgname
│ └── main.go
├── go.mod
├── pkgname.go
├── pkgname_test.go
└── testdata
└── src
└── a
├── a.go
└── pkgname.golden
jennifer
217
【TRY】Generatorを使ったコード生成
218
【TRY】シャローコピー
219
16.6. 静的単一代入形式
220
静的単一代入(SSA)形式
221
n := 10
n += 10
n0 := 10
n1 := n0 + 10
GoコンパイラのSSA形式で出力する
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
GoコンパイラのSSA形式を確認する
223
$ GOSSAFUNC=main go tool compile main.go
dumped SSA to /tmp/ssa_sample/ssa.html
最適化がかかっていく様子
静的解析のSSA形式ビューア
224
godump
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
Program
Package
Function
︙
︙
BasicBlock
︙
︙
Instruction
オペランド
Value
︙
基本ブロック
227
func f() {
n := 10
if n < 10 {
println("n < 10")
} else {
println("n >= 10")
}
}
例:エラー処理のミス
228
func f() error {
err := do()
if err != nil {
return nil // ミス
}
}
Return命令(nil)
If命令(err != nil)
Jump命令
・・・
then
else
例: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()
// (略)
}
セッションリークの検出方法 - 1 -
230
始端
終端
終端
Stop()
Do()
セッションリークの検出方法 - 2 -
231
始端
終端
終端
Stop()
Do()
静的単一代入形式で分かること
232
静的単一代入形式で分からないこと
233
buildssaパッケージ
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
Program
Package
Function
︙
︙
BasicBlock
︙
︙
Instruction
オペランド
Value
︙
ssa.Program型の定義
236
type Program struct {
Fset *token.FileSet
// 型チェッカーのメソッドセットのキャッシュ
MethodSets typeutil.MethodSetCache
}
ssa.Package型の定義
237
type Package struct {
// 所属するプログラム
Prog *Program
// 対応する*types.Package型
Pkg *types.Package
// メンバ(init関数も含む)
Members map[string]Member
}
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 // 親パッケージ
}
ssa.Member型を実装した型
239
type NamedConst struct {
Value *Const // 対応するssa.Const
}
type Global struct {
Pkg *Package // 親パッケージ
}
type Type struct { /* 公開されたフィールドはない */ }
ssa.Function型の定義
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 // 関数内で定義された無名関数
}
ssa.BasicBlock型の定義
241
type BasicBlock struct {
Index int
Comment string
Instrs []Instruction
Preds, Succs []*BasicBlock
}
ssa.Instruction型とssa.Value型
242
Instrction
Value
︙
Value
Value
オペランド
ssa.Instructionの定義
243
type Instruction interface {
String() string
Parent() *ssa.Function
Block() *ssa.BasicBlock
Operands(rands []*ssa.Value) []*ssa.Value
Pos() token.Pos
}
ssa.Valueの定義
244
type Value interface {
Name() string
String() string
Type() types.Type
Parent() *ssa.Function
Referrers() *[]Instruction
Pos() token.Pos
}
値を使用している命令を取得する
245
type Value interface {
Referrers() *[]Instruction
/* 略 */
}
ssa.Nodeの定義
246
type Node interface {
// 共通のメソッド
String() string
Pos() token.Pos
Parent() *Function
// 個別のメソッド
Operands(rands []*Value) []*Value // 命令ではない場合はnil
Referrers() *[]Instruction // 値ではない場合はnil
}
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)
}
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を満たすためである
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を満たすためである
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を満たすためである
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を満たすためである
【TRY】使われてない値
252
func f(n, m int) {
n = 20
println(n, m)
}
割り当て
253
type Alloc struct {
Comment string
Heap bool // ヒープに確保されるかどうか
}
代入
254
type Store struct {
Addr Value
Val Value
}
ssa.NaiveForm
255
ssa.Make*
256
型名 | 説明 |
*ssa.MakeChan | チャネルの生成 |
*ssa.MakeClosure | クロージャの生成 |
*ssa.MakeInterface | インタフェースの生成 |
*ssa.MakeMap | マップの生成 |
*ssa.MakeSlice | スライスの生成 |
関数呼び出し
257
type CallInstruction interface {
Instruction
// 関数呼び出しの共通部分
Common() *ssa.CallCommon
// *Call型の値またはnil (*Go型または*Defer型の場合)
Value() *ssa.Call
}
ssa.CallCommon構造体
258
type CallCommon struct {
// レシーバ(invokeモード)または関数の値(callモード)
Value ssa.Value
// 対応するインタフェースのメソッド(invokeモード)
Method *types.Func
// パラメータ(静的なメソッド呼び出しの場合はレシーバも含む)
Args []ssa.Value
}
【TRY】チャネルを2度closeしない
259
複数戻り値からの取り出し
260
type Extract struct {
Tuple Value // 関数呼び出しや型アサーションなど
Index int
}
クロージャの生成と自由変数
261
type MakeClosure struct {
Fn Value // *ssa.Function型の値
Bindings []Value // Fn.FreeVarsに束縛された値
}
【TRY】自由変数の変更
262
func f() {
var n int // want "NG"
func() { n = 10 }()
println(n)
}
分岐と基本ブロック
263
type If struct {
Cond Value // 条件
}
func (v *If) Block() *BasicBlock
If命令の取得
264
func IfInstr(b *ssa.BasicBlock) *ssa.If
ssa.Return構造体
265
type Return struct {
Results []Value
}
Return命令の取得
266
// vは*ssa.Function型または*ssa.MakeClosure型
func Returns(v ssa.Value) []*ssa.Return
ssa.Phi構造体
267
type Phi struct {
Comment string // 目的
Edges []Value // Edges[i]はBlock().Preds[i]に対応する値
}
ssa.Phi構造体の取得
268
func Phi(b *ssa.BasicBlock) (phis []*ssa.Phi)
関数が呼び出されているか
269
func Called(instr ssa.Instruction,
recv ssa.Value, f *types.Func) bool
デバッグ情報の取得
270
type DebugRef struct {
Expr ast.Expr // 対応するASTの式(*ast.ParenExprは含まない)
IsAddr bool // アドレスかどうか
X Value // 対応する値
}
【TRY】間違ったエラーの返却
271
var err error = f()
if err != nil {
// 間違え
return nil
}
16.7. パッケージ情報の取得
272
go 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
go/buildパッケージ
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)
Go Modules
275
詳しくは 4.4.ライブラリのバージョン管理を参照
x/tools/go/packagesパッケージ
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) }
}
packages.Package構造体
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
}
14.8. go/analysis詳細
278
ドライバー
279
ドライバー
go vet, goplz
静的解析ツール
unitchecker.Main
Analyzer-1
Analyzer-2
・・・
実行
Run
xxx.go
yyy.go
singlechecker
package main
import (
"simple"
"golang.org/x/tools/go/analysis/singlechecker"
)
func main() { singlechecker.Main(simple.Analyzer) }
multichecker
package main
import (
"xxx"
"yyy"
"golang.org/x/tools/go/analysis/multichecker"
)
func main() { multichecker.Main(xxx.Analyzer, yyy.Analyzer) }
analysis.Diagnostic構造体
282
type Diagnostic struct {
Pos token.Pos
End token.Pos // オプション
Category string // オプション
Message string
SuggestedFixes []SuggestedFix // オプション
Related []RelatedInformation // オプション
}
analysis.SuggestedFix構造体
283
type SuggestedFix struct {
Message string // 変更の概要
TextEdits []TextEdit
}
analysis.Factインタフェース
284
type Fact interface {
AFact() // 明示的に実装させるためのメソッド(空でよい)
}
Factの設定
285
type Analyzer struct {
/* 略 */
// インポートまたはエクスポートするFact(型情報が必要)
// 要素はポインタである必要がある
FactTypes []Fact
}
Factのエクスポート
286
type Pass struct {
/* 略 */
// オブジェクトに関連付けられたFactをエクスポートする
// 第2引数に渡す具象型の値はポインタである必要がある
// スレッドセーフではない
ExportObjectFact func(obj types.Object, fact Fact)
// パッケージに関連付けられたFactをエクスポートする
// 他の挙動はExportObjectFactと同じ
ExportPackageFact func(fact Fact)
}
Factのインポート
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
}
Factを使った解析
288
ドライバー
go vet, goplz
静的解析ツール
unitchecker.Main
パッケージ1
実行
解析
静的解析ツール
unitchecker.Main
静的解析ツール
unitchecker.Main
パッケージ2
解析
パッケージ3
解析
Fact
Fact
Gob
Export
Export
Import
import "pkg1"
import "pkg2"
Factの利用例
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
【TRY】recoverしていない関数
291
package a
import "reflect"
func f() {
g()
}
func g() {
v := reflect.ValueOf(10)
v.SetInt(20)
}
16.9. コールグラフとポインタ解析
292
コールグラフ
293
main()
f1()
f2()
f3()
f4()
コールグラフを表す型
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
アルゴリズム | 説明 | パッケージ |
静的取得 | 静的に決まる関数呼び出しのみ取得する | 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
func CallGraph(prog *ssa.Program) *callgraph.Graph
VTAによるコールグラフの取得
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
func f() {
var n int
p := &n
g(p)
var m int
g(&m)
}
func g(p *int) { /* 略 */ }
ポインタ解析で分かること
299
type C struct{}
func (C) m() { /* 略 */ }
func f() {
var i I = C{}
g(i)
}
func g(i I) {
i.m()
}
実態を辿ることができる
ポインタ解析の流れ
ポインタ解析の例
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
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()
}
ポインタ解析の例
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 |
ptrls:静的単一代入形式への変換
// デバッグ情報を付加しながら生成する
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()
ptrls:ポインタ解析の手順
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
}
ptrls:解析対象を取得(位置の取得)
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
}
ptrls:解析対象を取得(ノードの取得)
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
}
ptrls:クエリの追加
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) // クエリに追加
ptrls:ast.Expr型からssa.Value型を取得
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
}
16.10. 型パラメタを含むコードの解析
310
goパッケージの何が変わるのか
$ 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 (略) |
抽象構文木をダンプして確かめる
ast.Print(pass.Fset, pass.Files[0])
抽象構文木から型パラメタを得る
func Print[T any](s []T) {...} |
type Vector[T any] []T |
ノードから型情報の型パラメタを得る
型情報から型パラメタを取得
制約を取得する
型パラメタを持つ関数の呼び出し
制約のインスタンス化
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) |
新しく導入されるトークン
InterfaceType = "interface" "{" { InterfaceElem ";" } "}" . InterfaceElem = MethodElem | TypeElem . MethodElem = MethodName Signature . MethodName = identifier . TypeElem = TypeTerm { "|" TypeTerm } . TypeTerm = Type | UnderlyingType . UnderlyingType = "~" Type . |
制約のインタフェースを表すノード
InterfaceType = "interface" "{" { InterfaceElem ";" } "}" . InterfaceElem = MethodElem | TypeElem . MethodElem = MethodName Signature . MethodName = identifier . TypeElem = TypeTerm { "|" TypeTerm } . TypeTerm = Type | UnderlyingType . UnderlyingType = "~" Type . |
16.11. GraphQLの静的解析
321
WIP
16.12. Protocol Buffersの静的解析
322
WIP