[インデックス 12104] ファイルの概要
このコミットは、Go言語のcmd/api
ツールにおける型チェックの改善と、resolveName
メソッドのクリーンアップを目的としています。具体的には、関数の戻り値の型を記録することで、変数宣言時の型チェックをより正確に行えるようにし、go/build
パッケージの宣言における既存の不具合を修正しています。
コミット
commit d75023e1d144793dcf83ba45c3857656134c4fa0
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Tue Feb 21 07:37:25 2012 +0100
cmd/api: record return type of functions for variable typecheck.
Also cleanup the resolveName method.
Fixes failure on go/build declaration:
var ToolDir = filepath.Join(...)
R=golang-dev, bradfitz
CC=golang-dev, remy
https://golang.org/cl/5681043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d75023e1d144793dcf83ba45c3857656134c4fa0
元コミット内容
cmd/api
: 変数型チェックのために関数の戻り値の型を記録する。
また、resolveName
メソッドをクリーンアップする。
go/build
宣言における以下の不具合を修正:
var ToolDir = filepath.Join(...)
変更の背景
Go言語のcmd/api
ツールは、Goの標準ライブラリのAPIサーフェスを分析し、その構造を記述するために使用されます。このツールは、Goのソースコードを抽象構文木(AST)として解析し、APIの公開された要素(関数、型、変数など)を抽出します。
このコミットが行われた背景には、go/build
パッケージ内の特定の変数宣言、具体的には var ToolDir = filepath.Join(...)
のような形式の宣言において、cmd/api
ツールが正確な型推論を行えないという問題がありました。Goは静的型付け言語であり、変数の型はコンパイル時に決定されますが、型推論の機能も持ち合わせています。しかし、関数呼び出しの結果を変数に代入するようなケースでは、その関数の戻り値の型を正確に把握している必要があります。
従来のcmd/api
ツールは、関数呼び出しの戻り値の型を適切に追跡できていなかったため、このような宣言で誤った型チェックの結果を出す可能性がありました。このコミットは、この問題を解決し、cmd/api
ツールがより堅牢なAPI分析を行えるようにするために導入されました。また、resolveName
メソッドのクリーンアップも行われ、コードの可読性と保守性が向上しています。
前提知識の解説
Go言語のcmd/api
ツール
cmd/api
は、Go言語の標準ライブラリの公開APIを分析し、そのAPIサーフェスを定義するための内部ツールです。Goのリリースプロセスにおいて、APIの互換性を維持するために重要な役割を果たします。このツールは、Goのソースコードを解析し、エクスポートされた関数、型、変数などの情報を抽出し、APIの変更を検出するために使用されます。
抽象構文木 (AST)
Goコンパイラやcmd/api
のようなツールは、Goのソースコードを直接テキストとして扱うのではなく、抽象構文木(Abstract Syntax Tree, AST)というデータ構造に変換して処理します。ASTは、プログラムのソースコードの抽象的な構文構造を木構造で表現したものです。各ノードは、変数宣言、関数呼び出し、式などのコードの構成要素に対応します。Go言語では、go/ast
パッケージがASTの表現と操作を提供し、go/parser
パッケージがソースコードをASTに解析する機能を提供します。
Go言語の型チェックと型推論
Goは静的型付け言語であり、すべての変数はコンパイル時に型が決定されます。これにより、実行時エラーの多くをコンパイル時に検出できます。しかし、Goは型推論の機能も備えており、開発者が明示的に型を宣言しなくても、コンパイラが初期値から変数の型を推測できます。例えば、x := 42
と書くと、コンパイラはx
をint
型と推論します。
関数呼び出しの結果を変数に代入する場合、コンパイラはその関数の戻り値の型に基づいて変数の型を推論します。cmd/api
ツールがAPIの型情報を正確に抽出するためには、この型推論のプロセスを正確にシミュレートできる必要があります。
filepath.Join
関数
filepath.Join
はGoの標準ライブラリpath/filepath
パッケージに含まれる関数で、複数のパス要素を結合して1つのパスを生成します。この関数は、オペレーティングシステム固有のパス区切り文字(Windowsでは\
、Unix系では/
)を適切に処理し、余分な区切り文字を削除するなどして、クリーンなパスを生成します。
技術的詳細
このコミットの主要な技術的変更点は、cmd/api
ツールが関数の戻り値の型を明示的に記録するメカニズムを導入したことです。
変更前は、Walker
構造体(ASTを走査してAPI情報を収集する主要な構造体)には、関数の戻り値の型を保存するための専用のマップがありませんでした。そのため、varValueType
のような関数が関数呼び出しの戻り値の型を推論しようとすると、resolveName
メソッドに依存していましたが、このメソッドは関数の宣言自体を解決するだけで、その戻り値の型を直接提供するものではありませんでした。特に、パッケージをまたがる関数呼び出しや、複雑な式の中での関数呼び出しの場合、正確な型推論が困難でした。
このコミットでは、以下の変更が導入されました。
functionTypes
マップの追加:Walker
構造体にfunctionTypes map[pkgSymbol]string
という新しいマップが追加されました。このマップは、pkgSymbol
(パッケージ名とシンボル名からなる構造体)をキーとして、その関数の戻り値の型(文字列形式)を値として保持します。peekFuncDecl
関数の導入:WalkPackage
メソッド内で、ファイル走査の前にすべての関数宣言を事前に処理するためのpeekFuncDecl
関数が導入されました。この関数は、*ast.FuncDecl
(関数宣言のASTノード)を受け取り、その関数の戻り値の型を抽出し、functionTypes
マップに記録します。これにより、後続の型チェック処理で、関数の戻り値の型を事前に参照できるようになります。varValueType
の改善:varValueType
メソッドは、変数に代入される値の型を決定する役割を担っています。このコミットでは、*ast.CallExpr
(関数呼び出しのASTノード)を処理する際に、新しく追加されたfunctionTypes
マップを参照するように変更されました。これにより、関数呼び出しの戻り値の型をより正確に取得できるようになり、var ToolDir = filepath.Join(...)
のような宣言における型チェックの不具合が修正されました。特に、パッケージ修飾された関数呼び出し(例:ptwo.F()
)の場合も、selectorFullPkg
マップと組み合わせて正確なパッケージとシンボル名を解決し、対応する戻り値の型を取得できるようになっています。resolveName
のクリーンアップ:resolveName
メソッドは、与えられた名前がどのASTノード(関数、型、変数など)に対応するかを解決する汎用的なメソッドでした。このコミットでは、varValueType
がfunctionTypes
マップを直接参照するようになったため、resolveName
から関数宣言や型宣言の解決ロジックが削除され、よりシンプルで特化した役割を持つようになりました。これにより、コードの責務が明確になり、保守性が向上しています。
これらの変更により、cmd/api
ツールは、関数呼び出しの戻り値の型をより正確に推論できるようになり、GoのソースコードのAPIサーフェスをより堅牢に分析できるようになりました。
コアとなるコードの変更箇所
src/cmd/api/goapi.go
Walker
構造体にfunctionTypes map[pkgSymbol]string
が追加されました。NewWalker
関数でfunctionTypes
マップが初期化されます。WalkPackage
関数内で、walkFile
の前にpeekFuncDecl
を呼び出してすべての関数宣言を事前に処理するループが追加されました。varValueType
関数内の*ast.CallExpr
の処理ロジックが大幅に変更され、functionTypes
マップを使用して関数の戻り値の型を解決するようになりました。これにより、パッケージ修飾された関数呼び出し(例:pkg.Func()
)の戻り値の型も正確に取得できるようになりました。resolveName
関数から、関数宣言と型宣言を解決するロジックが削除されました。peekFuncDecl
関数が新しく追加されました。この関数は、関数宣言のASTノードを受け取り、その戻り値の型を抽出し、Walker.functionTypes
マップに記録します。
src/cmd/api/testdata/src/pkg/p1/golden.txt
src/cmd/api/testdata/src/pkg/p1/p1.go
src/cmd/api/testdata/src/pkg/p2/golden.txt
src/cmd/api/testdata/src/pkg/p2/p2.go
- テストデータが更新され、新しい型チェックのロジックが正しく機能することを確認するためのケースが追加されました。特に、関数呼び出しの結果を変数に代入するパターン(例:
var V = ptwo.F()
)や、エラーを返す関数(例:BarE()
)のテストが追加されています。
コアとなるコードの解説
Walker
構造体へのfunctionTypes
の追加
type Walker struct {
// ... 既存のフィールド ...
functionTypes map[pkgSymbol]string // symbol => return type
selectorFullPkg map[string]string // "http" => "net/http", updated by imports
wantedPkg map[string]bool // packages requested on the command line
}
func NewWalker() *Walker {
return &Walker{
// ... 既存の初期化 ...
functionTypes: make(map[pkgSymbol]string), // 新しく追加されたマップの初期化
selectorFullPkg: make(map[string]string),
wantedPkg: make(map[string]bool),
prevConstType: make(map[pkgSymbol]string),
}
}
Walker
はASTを走査し、API情報を収集する中心的な構造体です。functionTypes
マップは、pkgSymbol
(パッケージ名とシンボル名)をキーとして、その関数の戻り値の型を文字列として保存します。これにより、後で関数呼び出しの型を推論する際に、このマップを参照できるようになります。
WalkPackage
におけるpeekFuncDecl
の呼び出し
func (w *Walker) WalkPackage(name string) {
// ... 既存の処理 ...
// Register all function declarations first.
for _, afile := range apkg.Files {
for _, di := range afile.Decls {
if d, ok := di.(*ast.FuncDecl); ok {
w.peekFuncDecl(d)
}
}
}
for _, afile := range apkg.Files {
w.walkFile(afile)
}
}
WalkPackage
はパッケージ内のすべてのファイルを走査する前に、まずすべての関数宣言をpeekFuncDecl
を使って事前に処理します。これは、関数呼び出しの型を推論する際に、その関数がまだwalkFile
で処理されていない場合でも、その戻り値の型がfunctionTypes
マップに登録されていることを保証するためです。
peekFuncDecl
関数の実装
func (w *Walker) peekFuncDecl(f *ast.FuncDecl) {
if f.Recv != nil {
return // メソッドはここでは処理しない
}
// Record return type for later use.
if f.Type.Results != nil && len(f.Type.Results.List) == 1 {
retType := w.nodeString(w.namelessType(f.Type.Results.List[0].Type))
w.functionTypes[pkgSymbol{w.curPackageName, f.Name.Name}] = retType
}
}
peekFuncDecl
は、関数宣言(*ast.FuncDecl
)を受け取ります。レシーバを持つメソッドはスキップし、戻り値が1つだけの場合にその戻り値の型を抽出し、現在のパッケージ名と関数名をキーとしてfunctionTypes
マップに保存します。w.namelessType
は、型からパッケージ修飾子などを取り除き、基本的な型名を取得するヘルパー関数です。
varValueType
における*ast.CallExpr
の処理
func (w *Walker) varValueType(vi interface{}) (string, error) {
// ... 既存の処理 ...
case *ast.CallExpr:
var funSym pkgSymbol
if selnode, ok := v.Fun.(*ast.SelectorExpr); ok {
// assume it is not a method.
pkg, ok := w.selectorFullPkg[w.nodeString(selnode.X)]
if !ok {
return "", fmt.Errorf("not a package: %s", w.nodeString(selnode.X))
}
funSym = pkgSymbol{pkg, selnode.Sel.Name}
if retType, ok := w.functionTypes[funSym]; ok {
if ast.IsExported(retType) && pkg != w.curPackageName {
// otherpkg.F returning an exported type from otherpkg.
return pkg + "." + retType, nil
} else {
return retType, nil
}
}
} else {
funSym = pkgSymbol{w.curPackageName, w.nodeString(v.Fun)}
if retType, ok := w.functionTypes[funSym]; ok {
return retType, nil
}
}
// maybe a function call; maybe a conversion. Need to lookup type.
return "", fmt.Errorf("not a known function %q", w.nodeString(v.Fun))
// ... 既存の処理 ...
}
varValueType
は、変数に代入される値の型を決定する重要な関数です。*ast.CallExpr
(関数呼び出し)の場合、呼び出される関数がセレクタ式(例: pkg.Func()
)であるか、単純な関数名(例: Func()
)であるかを判別します。
- セレクタ式の場合:
selnode.X
からパッケージ名を解決し、pkgSymbol
を作成します。その後、functionTypes
マップから戻り値の型を検索します。もし戻り値の型がエクスポートされており、かつ現在のパッケージとは異なるパッケージからのものである場合、pkg.ReturnType
の形式で型を返します。 - 単純な関数名の場合: 現在のパッケージ名と関数名から
pkgSymbol
を作成し、functionTypes
マップから戻り値の型を検索して返します。
これにより、filepath.Join
のような関数呼び出しの戻り値の型(この場合はstring
)が正確に推論され、var ToolDir = filepath.Join(...)
のような変数宣言の型チェックが正しく行われるようになります。
resolveName
からのロジック削除
--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -575,19 +593,8 @@ func (w *Walker) resolveName(name string) (v interface{}, t interface{}, ok bool
for _, file := range w.curPackage.Files {
for _, di := range file.Decls {
switch d := di.(type) {
- case *ast.FuncDecl:
- if d.Name.Name == name {
- return d, d.Type, true
- }
case *ast.GenDecl:
switch d.Tok {
- case token.TYPE:
- for _, sp := range d.Specs {
- ts := sp.(*ast.TypeSpec)
- if ts.Name.Name == name {
- return ts, ts.Type, true
- }
- }
case token.VAR:
for _, sp := range d.Specs {
vs := sp.(*ast.ValueSpec)
resolveName
関数は、以前は関数宣言や型宣言も解決していましたが、このコミットでそのロジックが削除されました。これは、varValueType
がfunctionTypes
マップを直接参照するようになったため、resolveName
がこれらの情報を解決する必要がなくなったためです。これにより、resolveName
の責務がより限定され、コードの依存関係が整理されました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Go言語の
go/ast
パッケージ: https://pkg.go.dev/go/ast - Go言語の
go/parser
パッケージ: https://pkg.go.dev/go/parser - Go言語の
path/filepath
パッケージ: https://pkg.go.dev/path/filepath
参考にした情報源リンク
- Go CL 5681043: https://golang.org/cl/5681043
- Go
cmd/api
ツールの目的に関する情報 (Web検索結果より) - Go ASTパッケージの使用法に関する情報 (Web検索結果より)
- Go変数の型チェックと型推論に関する情報 (Web検索結果より)