[インデックス 16878] ファイルの概要
このコミットは、Go言語のパーサーパッケージ (go/parser
) における ParseDir
関数の挙動を修正し、ディレクトリ内の .go
拡張子を持つファイルのみを解析対象とするように変更するものです。これにより、意図しないファイルが解析されることを防ぎ、パーサーの堅牢性と予測可能性を向上させます。
コミット
commit 4a695d2c18713ea85029922c0e0758423dcd99ab
Author: Robert Griesemer <gri@golang.org>
Date: Thu Jul 25 09:36:22 2013 -0700
go/parser: restrict ParseDir to files with suffix ".go"
Fixes #5956.
R=rsc, r
CC=golang-dev
https://golang.org/cl/11813043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4a695d2c18713ea85029922c0e0758423dcd99ab
元コミット内容
go/parser: restrict ParseDir to files with suffix ".go"
このコミットは、go/parser
パッケージの ParseDir
関数が、ディレクトリ内のファイルのうち、.go
というサフィックス(拡張子)を持つファイルのみを解析対象とするように制限するものです。
Fixes #5956.
これは、GoのIssueトラッカーにおけるIssue 5956を修正するものであることを示しています。
変更の背景
この変更の背景には、go/parser.ParseDir
関数が、ディレクトリ内のすべてのファイルを無条件に解析しようとする既存の挙動がありました。これは、Goのソースコードではないファイル(例えば、README.md
、Makefile
、あるいは一時ファイルなど)が同じディレクトリに存在する場合に問題を引き起こす可能性がありました。
具体的には、以下のような問題が考えられます。
- 誤った解析結果: GoのソースコードではないファイルをGoのパーサーが解析しようとすると、構文エラーが発生したり、予期しないAST(抽象構文木)が生成されたりする可能性があります。これは、ツールがGoのコードベースを正確に理解する上で障害となります。
- パフォーマンスの低下: 大量の非Goファイルを含むディレクトリを解析する場合、不要なファイルの読み込みと解析処理に時間がかかり、パフォーマンスが低下する可能性があります。
- 予期しないエラー: パーサーがGoの構文規則に従わないファイルを処理しようとすると、パニックやその他のランタイムエラーが発生するリスクがありました。
- Issue 5956の修正: コミットメッセージに「Fixes #5956」とあることから、この問題はGoコミュニティで認識されており、特定のシナリオで実際に問題が発生していたことが示唆されます。Issue 5956の内容を確認すると、
go/parser.ParseDir
が.go
以外のファイルを誤って解析しようとすることによる問題が報告されていたことがわかります。例えば、foo.go.orig
のようなバックアップファイルがGoのソースファイルとして扱われてしまうケースなどです。
この変更は、ParseDir
の動作をより厳密にし、Goのソースファイルのみを対象とすることで、これらの問題を解決し、パーサーの堅牢性と信頼性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連ツールの基本的な知識が必要です。
- Go言語のパッケージとモジュール:
- Goのコードは「パッケージ」という単位で整理されます。関連するGoソースファイルは同じパッケージに属し、通常は同じディレクトリに配置されます。
- Go 1.11以降では「モジュール」が導入され、複数のパッケージをまとめる単位として機能します。モジュールは依存関係管理にも使用されます。
go/parser
パッケージ:- Go標準ライブラリの一部であり、Goのソースコードを解析してAST(抽象構文木)を生成するための機能を提供します。
ParseFile
: 単一のGoソースファイルを解析します。ParseDir
: 指定されたディレクトリ内のGoソースファイルを解析し、それらが属するパッケージのASTを構築します。ast
(Abstract Syntax Tree): Goのソースコードの構造を木構造で表現したものです。コンパイラ、リンター、コードフォーマッターなどのツールがGoコードを理解し、操作するために使用します。token
(Tokenization): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに分解するプロセスに関連する機能を提供します。FileSet
は、ソースファイル内の位置情報を管理するために使用されます。
- ファイルシステム操作:
os
パッケージ: オペレーティングシステムとのインタラクション(ファイルやディレクトリの操作など)を提供します。os.FileInfo
はファイルやディレクトリのメタデータ(名前、サイズ、変更時刻など)を表します。path/filepath
パッケージ: ファイルパスを操作するためのユーティリティを提供します。filepath.Join
は、プラットフォームに依存しない形でパスの要素を結合するために使用されます。
- 文字列操作:
strings
パッケージ: 文字列操作のためのユーティリティを提供します。このコミットではstrings.HasSuffix
が使用されており、文字列が特定のサフィックスで終わるかどうかをチェックします。
- Issueトラッカー:
- Goプロジェクトでは、バグ報告や機能要望を管理するためにIssueトラッカー(GitHub Issuesなど)が使用されます。コミットメッセージの「Fixes #XXXX」は、そのコミットが特定のIssueを修正したことを示します。
これらの知識があることで、コミットがGoのパーサーのどの部分に影響を与え、どのような問題を解決しようとしているのかを深く理解することができます。
技術的詳細
このコミットの技術的な核心は、go/parser
パッケージの ParseDir
関数がディレクトリ内のファイルを走査するロジックに、.go
拡張子のチェックを追加することです。
変更前は、ParseDir
は filter
関数が提供されている場合にのみ、そのフィルターを通過したファイルを考慮していました。filter
が nil
の場合、またはフィルターを通過した場合、そのファイルは ParseFile
に渡され、Goのソースファイルとして解析が試みられました。この挙動は、.go
拡張子を持たないファイル(例: main.txt
、config.json
、README
)がディレクトリ内に存在する場合でも、それらをGoのソースファイルとして解析しようとする問題を引き起こしていました。
変更後は、ParseDir
はまずファイルの Name()
が .go
で終わるかどうかを strings.HasSuffix(d.Name(), ".go")
を使ってチェックします。このチェックが true
であり、かつ既存の filter
関数(もしあれば)も true
を返した場合にのみ、そのファイルは ParseFile
に渡されます。これにより、ParseDir
は明示的にGoのソースファイルのみを対象とするようになります。
また、ParseDir
のドキュメンテーションコメントも更新され、この新しい挙動が反映されています。変更前は「files in the directory specified by path」と漠然と記述されていましたが、変更後は「all files with names ending in ".go" in the directory specified by path」と明確に記述されています。これにより、関数の意図がより明確になります。
テストコード (parser_test.go
) も更新され、この新しい挙動が正しく機能することを確認しています。特に、nameFilter
関数が変更され、parser.go.orig
のようなファイルが ParseDir
によって無視されるべきであることを示すテストケースが追加されています。これは、.go
拡張子を持つが実際にはGoのソースファイルではない(または解析対象とすべきではない)ファイルが正しく除外されることを保証します。
この変更は、Goのツールチェインの堅牢性を高める上で重要です。パーサーはGoのコードを扱う多くのツール(コンパイラ、リンター、IDEなど)の基盤となるため、その入力処理が正確であることは非常に重要です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下の2つのファイルです。
src/pkg/go/parser/interface.go
src/pkg/go/parser/parser_test.go
src/pkg/go/parser/interface.go
の変更
--- a/src/pkg/go/parser/interface.go
+++ b/src/pkg/go/parser/interface.go
@@ -15,6 +15,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
+ "strings"
)
// If src != nil, readSource converts src to a []byte if possible;
@@ -115,11 +116,13 @@ func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode)\n return\n }\n \n-// ParseDir calls ParseFile for the files in the directory specified by path and\n-// returns a map of package name -> package AST with all the packages found. If\n-// filter != nil, only the files with os.FileInfo entries passing through the filter\n-// are considered. The mode bits are passed to ParseFile unchanged. Position\n-// information is recorded in the file set fset.\n+// ParseDir calls ParseFile for all files with names ending in ".go" in the\n+// directory specified by path and returns a map of package name -> package\n+// AST with all the packages found.\n+//\n+// If filter != nil, only the files with os.FileInfo entries passing through\n+// the filter (and ending in ".go") are considered. The mode bits are passed\n+// to ParseFile unchanged. Position information is recorded in fset.\n //\n // If the directory couldn't be read, a nil map and the respective error are\n // returned. If a parse error occurred, a non-nil but incomplete map and the\n@@ -139,7 +142,7 @@ func ParseDir(fset *token.FileSet, path string, filter func(os.FileInfo) bool, m\n \n pkgs = make(map[string]*ast.Package)\n for _, d := range list {\n-\t\tif filter == nil || filter(d) {\n+\t\tif strings.HasSuffix(d.Name(), ".go") && (filter == nil || filter(d)) {\n \t\t\tfilename := filepath.Join(path, d.Name())\n \t\t\tif src, err := ParseFile(fset, filename, nil, mode); err == nil {\n \t\t\t\tname := src.Name.Name
src/pkg/go/parser/parser_test.go
の変更
--- a/src/pkg/go/parser/parser_test.go
+++ b/src/pkg/go/parser/parser_test.go
@@ -34,13 +34,12 @@ func TestParse(t *testing.T) {
func nameFilter(filename string) bool {
switch filename {
- case "parser.go":
- case "interface.go":
- case "parser_test.go":
- default:
- return false
+ case "parser.go", "interface.go", "parser_test.go":
+ return true
+ case "parser.go.orig":
+ return true // permit but should be ignored by ParseDir
}
- return true
+ return false
}
func dirFilter(f os.FileInfo) bool { return nameFilter(f.Name()) }
@@ -51,14 +50,17 @@ func TestParseDir(t *testing.T) {
if err != nil {
t.Fatalf("ParseDir(%s): %v", path, err)
}
- if len(pkgs) != 1 {
- t.Errorf("incorrect number of packages: %d", len(pkgs))
+ if n := len(pkgs); n != 1 {
+ t.Errorf("got %d packages; want 1", n)
}
pkg := pkgs["parser"]
if pkg == nil {
t.Errorf(`package "parser" not found`)
return
}
+ if n := len(pkg.Files); n != 3 {
+ t.Errorf("got %d package files; want 3", n)
+ }
for filename := range pkg.Files {
if !nameFilter(filename) {
t.Errorf("unexpected package file: %s", filename)
コアとなるコードの解説
src/pkg/go/parser/interface.go
の変更点
-
import "strings"
の追加:- ファイルの拡張子をチェックするために、
strings
パッケージが新しくインポートされました。strings.HasSuffix
関数がこの目的で使用されます。
- ファイルの拡張子をチェックするために、
-
ParseDir
関数のドキュメンテーションコメントの更新:- 変更前: 「ParseDir calls ParseFile for the files in the directory specified by path...」
- 変更後: 「ParseDir calls ParseFile for all files with names ending in ".go" in the directory specified by path...」
- この変更により、
ParseDir
が.go
拡張子を持つファイルのみを対象とすることが明確に示されています。また、filter
関数が指定された場合でも、.go
拡張子のチェックが優先されることが追記されています。
-
ParseDir
関数のファイルフィルタリングロジックの変更:- 変更前:
if filter == nil || filter(d) { // ... ParseFileを呼び出す ... }
- 変更後:
if strings.HasSuffix(d.Name(), ".go") && (filter == nil || filter(d)) { // ... ParseFileを呼び出す ... }
- これがこのコミットの最も重要な変更点です。ディレクトリ内の各ファイル
d
について、まずd.Name()
(ファイル名)が.go
で終わるかどうかをstrings.HasSuffix
でチェックします。 - この条件が
true
であり、かつ 既存のfilter
関数(もしあれば)がnil
であるか、またはfilter(d)
がtrue
を返す場合にのみ、そのファイルはGoのソースファイルとしてParseFile
に渡され、解析されます。 - これにより、
ParseDir
は明示的に.go
拡張子を持つファイルのみを処理するようになり、Issue 5956で報告されたような、意図しないファイルの解析を防ぎます。
- 変更前:
src/pkg/go/parser/parser_test.go
の変更点
-
nameFilter
関数の変更:- この関数は、テストにおいてどのファイルを解析対象とするかを決定するフィルターとして使用されます。
- 変更前は、
parser.go
,interface.go
,parser_test.go
以外のファイルはfalse
を返していました。 - 変更後、
parser.go.orig
という新しいケースが追加され、これに対してtrue
を返すように変更されました。コメントには「permit but should be ignored by ParseDir」とあり、これはnameFilter
自体はparser.go.orig
を許可するが、ParseDir
の新しいロジックによって.go
拡張子がないために無視されるべきであることをテストしています。これにより、ParseDir
の新しいフィルタリングロジックが正しく機能するかを確認できます。
-
TestParseDir
関数のアサーションの強化:- パッケージの数とファイル数のチェックがより厳密になりました。
- 変更前は
len(pkgs) != 1
と単純なチェックでしたが、変更後はif n := len(pkgs); n != 1
のように、よりGoらしい慣用的な書き方で変数を宣言し、エラーメッセージも詳細になりました。 - 最も重要な追加は、
pkg.Files
の数に対するアサーションです。
このアサーションは、if n := len(pkg.Files); n != 3 { t.Errorf("got %d package files; want 3", n) }
ParseDir
がparser.go
,interface.go
,parser_test.go
の3つのファイルのみを正しく解析し、parser.go.orig
のようなファイルが除外されたことを確認します。もしparser.go.orig
が誤って解析されていれば、pkg.Files
の数は4になり、テストが失敗します。
これらの変更は、ParseDir
の動作をより正確に制御し、テストによってその新しい挙動が保証されることを示しています。
関連リンク
- Go Issue 5956: https://github.com/golang/go/issues/5956
- Go CL 11813043: https://golang.org/cl/11813043 (Gerrit Code Review)
参考にした情報源リンク
- Go言語公式ドキュメント:
go/parser
パッケージ (https://pkg.go.dev/go/parser) - Go言語公式ドキュメント:
os
パッケージ (https://pkg.go.dev/os) - Go言語公式ドキュメント:
path/filepath
パッケージ (https://pkg.go.dev/path/filepath) - Go言語公式ドキュメント:
strings
パッケージ (https://pkg.go.dev/strings) - Go言語のASTについて (Go公式ブログなど、一般的なGoのASTに関する解説記事)
- Go言語のテストについて (Go公式ドキュメントのTesting Go Codeなど)
- Gerrit Code Review System (Goプロジェクトがコードレビューに使用しているシステム)