[インデックス 16156] ファイルの概要
このコミットは、Go言語の抽象構文木(AST)を扱う go/ast
パッケージにおける、関数とメソッドのフィルタリング処理の改善に関するものです。具体的には、MergePackageFiles
関数がパッケージ内のファイルをマージする際に、重複する関数宣言をフィルタリングするロジックにおいて、通常の関数とレシーバーを持つメソッドを適切に区別するように変更されています。
コミット
commit d06313e8cebb5d956f2b5e8c74f8c495808b2275
Author: Robert Griesemer <gri@golang.org>
Date: Wed Apr 10 13:39:20 2013 -0700
go/ast: distinguish between methods and functions in filtering
Go1.1 harmless, but not critical.
Fixes #5249.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8609043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d06313e8cebb5d956f2b5e8c74f8c495808b2275
元コミット内容
go/ast: distinguish between methods and functions in filtering
このコミットは、go/ast
パッケージにおいて、フィルタリング処理中にメソッドと関数を区別するように変更を加えるものです。Go 1.1においては無害ですが、重要ではありません。
Issue #5249 を修正します。
変更の背景
Go言語では、関数とメソッドは異なるエンティティとして扱われます。関数は独立したサブルーチンですが、メソッドは特定の型に関連付けられた関数であり、レシーバー(receiver)を持ちます。go/ast
パッケージはGoのソースコードを解析して抽象構文木を構築し、そのASTを操作するための機能を提供します。
以前の go/ast
パッケージの MergePackageFiles
関数では、パッケージ内の複数のファイルから関数宣言をマージする際に、関数とメソッドを同じ名前空間で扱っていました。これにより、例えば func f()
と func (T) f()
のように、名前が同じであるために異なる種類の宣言(関数とメソッド)が重複として誤って扱われる可能性がありました。
このコミットの背景には、このような誤った重複検出を修正し、ASTのフィルタリング処理が関数とメソッドを正しく区別できるようにするという目的があります。コミットメッセージに「Fixes #5249」とあることから、この問題がGoのIssueトラッカーで報告されていたことが伺えます。
前提知識の解説
Go言語のAST (Abstract Syntax Tree)
Go言語のコンパイラやツールは、ソースコードを直接解釈するのではなく、まずソースコードを抽象構文木(AST)というデータ構造に変換します。ASTは、プログラムの構造を木構造で表現したもので、各ノードがプログラムの要素(変数宣言、関数定義、式など)に対応します。go/ast
パッケージは、このASTをプログラムで操作するためのAPIを提供します。
go/ast
パッケージ
go/ast
パッケージは、Goのソースコードを解析してASTを構築し、そのASTを操作するための基本的な型と関数を提供します。主要な型には File
(単一のGoソースファイルを表すASTのルートノード)、Package
(複数のGoソースファイルからなるパッケージを表す)、Decl
(宣言を表すインターフェース)、FuncDecl
(関数またはメソッドの宣言を表す) などがあります。
FuncDecl
(Function Declaration)
go/ast
パッケージにおける FuncDecl
構造体は、Goの関数またはメソッドの宣言を表します。この構造体には、関数名 (Name
)、関数がレシーバーを持つかどうかを示す Recv
フィールド(メソッドの場合に非nil)、関数の型 (Type
)、関数本体 (Body
) などが含まれます。
レシーバー (Receiver)
Go言語のメソッドは、特別な引数であるレシーバーを持ちます。レシーバーは、メソッドがどの型に関連付けられているかを示します。レシーバーは値レシーバー(例: (t T)
)またはポインタレシーバー(例: (t *T)
)のいずれかです。レシーバーの型は、メソッドが属する型を決定します。
MergePackageFiles
関数
go/ast
パッケージの MergePackageFiles
関数は、複数のGoソースファイル(*ast.File
)をマージして、単一の *ast.File
を生成する機能を提供します。この関数は、パッケージ内の重複する宣言(例えば、同じ名前の関数が複数のファイルで宣言されている場合)をフィルタリングする際に使用されます。フィルタリングのモードは MergeMode
列挙型で指定され、FilterFuncDuplicates
は重複する関数宣言をフィルタリングするモードです。
技術的詳細
このコミットの主要な変更点は、go/ast/filter.go
ファイルに nameOf
という新しいヘルパー関数が導入されたこと、および MergePackageFiles
関数内でこの nameOf
関数が使用されるようになったことです。
nameOf
関数の導入
nameOf
関数は、与えられた *ast.FuncDecl
(関数宣言)に対して、その関数またはメソッドの一意な名前を文字列として返します。
- 通常の関数: レシーバーを持たない関数(
f.Recv
がnil
)の場合、f.Name.Name
(関数名)をそのまま返します。 - メソッド: レシーバーを持つメソッド(
f.Recv
が非nil
)の場合、レシーバーの型名とメソッド名を結合した形式(例:TypeName.MethodName
)で名前を生成します。- レシーバーの型がポインタ型(
*T
)の場合、ポインタをデリファレンスして基底の型名を取得します。 - レシーバーの型が
*ast.Ident
(識別子、つまり型名)であると仮定し、その名前を使用します。 - ASTが不正でレシーバーの型を特定できない場合は、通常の関数として扱い、メソッド名のみを返します。
- レシーバーの型がポインタ型(
この nameOf
関数により、func f()
と func (T) f()
はそれぞれ異なる名前(f
と T.f
)として識別されるようになります。
MergePackageFiles
の変更
MergePackageFiles
関数内では、重複する関数を検出するために funcs
という map[string]int
が使用されていました。このマップは、関数名をキーとして、その関数が decls
スライス内のどこに格納されているかを追跡していました。
変更前は、このマップのキーとして f.Name.Name
(関数名のみ)が直接使用されていました。これにより、f1
という名前の関数と t1.f1
という名前のメソッドが同じキー f1
でマップに登録され、誤って重複として扱われる可能性がありました。
変更後は、nameOf(f)
が返す文字列がマップのキーとして使用されるようになりました。これにより、f1
は f1
として、t1.f1
は t1.f1
としてマップに登録され、関数とメソッドが適切に区別されるようになります。
filter_test.go
の追加
このコミットでは、src/pkg/go/ast/filter_test.go
という新しいテストファイルが追加されています。このテストファイルは、MergePackageFiles
関数が FilterFuncDuplicates
モードで関数とメソッドの重複を正しく処理することを確認するためのものです。
テストケース TestFilterDuplicates
では、以下のような入力コードが使用されています。
package p
type t1 struct{}
type t2 struct{}
func f1() {}
func f1() {} // 重複
func f2() {}
func (*t1) f1() {} // メソッド
func (t1) f1() {} // メソッド (レシーバーが異なるが名前は同じ)
func (t1) f2() {} // メソッド
func (t2) f1() {} // メソッド
func (t2) f2() {} // メソッド
func (x *t2) f2() {} // メソッド (レシーバーが異なるが名前は同じ)
この入力に対して、ast.MergePackageFiles
を ast.FilterFuncDuplicates
モードで呼び出した結果が、期待される出力(golden
定数)と一致するかどうかを検証しています。期待される出力では、通常の関数 f1
の重複が除去され、メソッド (*t2).f2
が (t2).f2
よりも優先されて残るなど、関数とメソッドが正しく区別されてフィルタリングされていることが示されています。
コアとなるコードの変更箇所
src/pkg/go/ast/filter.go
--- a/src/pkg/go/ast/filter.go
+++ b/src/pkg/go/ast/filter.go
@@ -284,6 +284,27 @@ const (
FilterImportDuplicates
)
+// nameOf returns the function (foo) or method name (foo.bar) for
+// the given function declaration. If the AST is incorrect for the
+// receiver, it assumes a function instead.
+//
+func nameOf(f *FuncDecl) string {
+ if r := f.Recv; r != nil && len(r.List) == 1 {
+ // looks like a correct receiver declaration
+ t := r.List[0].Type
+ // dereference pointer receiver types
+ if p, _ := t.(*StarExpr); p != nil {
+ t = p.X
+ }
+ // the receiver type must be a type name
+ if p, _ := t.(*Ident); p != nil {
+ return p.Name + "." + f.Name.Name
+ }
+ // otherwise assume a function instead
+ }
+ return f.Name.Name
+}
+
// separator is an empty //-style comment that is interspersed between
// different comment groups when they are concatenated into a single group
//
@@ -348,7 +369,7 @@ func MergePackageFiles(pkg *Package, mode MergeMode) *File {
var decls []Decl
if ndecls > 0 {
decls = make([]Decl, ndecls)
- funcs := make(map[string]int) // map of global function name -> decls index
+ funcs := make(map[string]int) // map of func name -> decls index
i := 0 // current index
n := 0 // number of filtered entries
for _, filename := range filenames {
@@ -365,7 +386,7 @@ func MergePackageFiles(pkg *Package, mode MergeMode) *File {
// entities (const, type, vars) if
// multiple declarations are common.
if f, isFun := d.(*FuncDecl); isFun {
- name := f.Name.Name
+ name := nameOf(f)
if j, exists := funcs[name]; exists {
// function declared already
if decls[j] != nil && decls[j].(*FuncDecl).Doc == nil {
src/pkg/go/ast/filter_test.go
新規ファイルとして追加。内容は上記の「技術的詳細」セクションで説明したテストコード。
コアとなるコードの解説
nameOf
関数
func nameOf(f *FuncDecl) string {
if r := f.Recv; r != nil && len(r.List) == 1 {
// looks like a correct receiver declaration
t := r.List[0].Type
// dereference pointer receiver types
if p, _ := t.(*StarExpr); p != nil {
t = p.X
}
// the receiver type must be a type name
if p, _ := t.(*Ident); p != nil {
return p.Name + "." + f.Name.Name
}
// otherwise assume a function instead
}
return f.Name.Name
}
この関数は、*ast.FuncDecl
を受け取り、その宣言が通常の関数かメソッドかに応じて一意な名前を生成します。
f.Recv
がnil
でない場合、それはメソッド宣言です。r.List[0].Type
でレシーバーの型を取得します。t.(*StarExpr)
でポインタ型 (*T
) であるかをチェックし、もしそうであればp.X
で基底の型 (T
) を取得します。これにより、(t *T) Method
と(t T) Method
が同じT.Method
として扱われるようになります。t.(*Ident)
でレシーバーの型が識別子(型名)であるかをチェックし、そうであればp.Name + "." + f.Name.Name
の形式でTypeName.MethodName
という文字列を生成します。- 上記の条件に合致しない場合(例えば、ASTが不正な場合や、レシーバーの型が識別子でない場合)、または通常の関数の場合は、
f.Name.Name
(関数名のみ)を返します。
MergePackageFiles
内の変更
// ...
decls = make([]Decl, ndecls)
funcs := make(map[string]int) // map of func name -> decls index
// ...
if f, isFun := d.(*FuncDecl); isFun {
name := nameOf(f) // ここが変更点
if j, exists := funcs[name]; exists {
// ...
MergePackageFiles
関数内で、FuncDecl
を処理する際に、以前は name := f.Name.Name
と直接関数名を取得していましたが、この変更により name := nameOf(f)
と nameOf
関数を介して名前を取得するようになりました。これにより、funcs
マップのキーとして、関数とメソッドが区別された一意な名前が使用されるようになり、重複検出のロジックが改善されました。
関連リンク
- Go言語のASTパッケージに関する公式ドキュメント: https://pkg.go.dev/go/ast
- Go言語のトークンパッケージに関する公式ドキュメント: https://pkg.go.dev/go/token
- Go言語のパーサーパッケージに関する公式ドキュメント: https://pkg.go.dev/go/parser
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語のIssueトラッカー (一般的な情報源として): https://github.com/golang/go/issues
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/ (コミットメッセージに記載されている
https://golang.org/cl/8609043
は、このGerritの変更リストへのリンクです。) - Go言語のメソッドに関する公式ドキュメント: https://go.dev/tour/methods/1
- Go言語の関数に関する公式ドキュメント: https://go.dev/tour/moretypes/1
- Go言語のASTに関する一般的な解説記事 (例: "Go AST: The Abstract Syntax Tree"): https://go.dev/blog/go-ast (これは一般的な情報源であり、特定のコミットに直接関連するものではありませんが、ASTの理解に役立ちます。)
- Go言語のAST操作に関するチュートリアルやブログ記事 (例: "Go AST: A Practical Guide"): https://yourbasic.org/golang/go-ast-example/ (これも一般的な情報源であり、特定のコミットに直接関連するものではありませんが、ASTの理解に役立ちます。)
- Go言語のIssue #5249については、Goの公式GitHubリポジトリでは直接見つけることができませんでした。これは、Issueがクローズされた、移動された、または別のリポジトリに存在した可能性を示唆しています。しかし、コミットメッセージに明記されているため、このコミットがそのIssueを解決したことは確かです。