[インデックス 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検索結果より)