Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.Recvnil)の場合、f.Name.Name(関数名)をそのまま返します。
  • メソッド: レシーバーを持つメソッド(f.Recv が非 nil)の場合、レシーバーの型名とメソッド名を結合した形式(例: TypeName.MethodName)で名前を生成します。
    • レシーバーの型がポインタ型(*T)の場合、ポインタをデリファレンスして基底の型名を取得します。
    • レシーバーの型が *ast.Ident(識別子、つまり型名)であると仮定し、その名前を使用します。
    • ASTが不正でレシーバーの型を特定できない場合は、通常の関数として扱い、メソッド名のみを返します。

この nameOf 関数により、func f()func (T) f() はそれぞれ異なる名前(fT.f)として識別されるようになります。

MergePackageFiles の変更

MergePackageFiles 関数内では、重複する関数を検出するために funcs という map[string]int が使用されていました。このマップは、関数名をキーとして、その関数が decls スライス内のどこに格納されているかを追跡していました。

変更前は、このマップのキーとして f.Name.Name(関数名のみ)が直接使用されていました。これにより、f1 という名前の関数と t1.f1 という名前のメソッドが同じキー f1 でマップに登録され、誤って重複として扱われる可能性がありました。

変更後は、nameOf(f) が返す文字列がマップのキーとして使用されるようになりました。これにより、f1f1 として、t1.f1t1.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.MergePackageFilesast.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.Recvnil でない場合、それはメソッド宣言です。
  • 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言語の公式リポジトリ: 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を解決したことは確かです。