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

[インデックス 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.mdMakefile、あるいは一時ファイルなど)が同じディレクトリに存在する場合に問題を引き起こす可能性がありました。

具体的には、以下のような問題が考えられます。

  1. 誤った解析結果: GoのソースコードではないファイルをGoのパーサーが解析しようとすると、構文エラーが発生したり、予期しないAST(抽象構文木)が生成されたりする可能性があります。これは、ツールがGoのコードベースを正確に理解する上で障害となります。
  2. パフォーマンスの低下: 大量の非Goファイルを含むディレクトリを解析する場合、不要なファイルの読み込みと解析処理に時間がかかり、パフォーマンスが低下する可能性があります。
  3. 予期しないエラー: パーサーがGoの構文規則に従わないファイルを処理しようとすると、パニックやその他のランタイムエラーが発生するリスクがありました。
  4. Issue 5956の修正: コミットメッセージに「Fixes #5956」とあることから、この問題はGoコミュニティで認識されており、特定のシナリオで実際に問題が発生していたことが示唆されます。Issue 5956の内容を確認すると、go/parser.ParseDir.go 以外のファイルを誤って解析しようとすることによる問題が報告されていたことがわかります。例えば、foo.go.orig のようなバックアップファイルがGoのソースファイルとして扱われてしまうケースなどです。

この変更は、ParseDir の動作をより厳密にし、Goのソースファイルのみを対象とすることで、これらの問題を解決し、パーサーの堅牢性と信頼性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語および関連ツールの基本的な知識が必要です。

  1. Go言語のパッケージとモジュール:
    • Goのコードは「パッケージ」という単位で整理されます。関連するGoソースファイルは同じパッケージに属し、通常は同じディレクトリに配置されます。
    • Go 1.11以降では「モジュール」が導入され、複数のパッケージをまとめる単位として機能します。モジュールは依存関係管理にも使用されます。
  2. go/parser パッケージ:
    • Go標準ライブラリの一部であり、Goのソースコードを解析してAST(抽象構文木)を生成するための機能を提供します。
    • ParseFile: 単一のGoソースファイルを解析します。
    • ParseDir: 指定されたディレクトリ内のGoソースファイルを解析し、それらが属するパッケージのASTを構築します。
    • ast (Abstract Syntax Tree): Goのソースコードの構造を木構造で表現したものです。コンパイラ、リンター、コードフォーマッターなどのツールがGoコードを理解し、操作するために使用します。
    • token (Tokenization): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに分解するプロセスに関連する機能を提供します。FileSet は、ソースファイル内の位置情報を管理するために使用されます。
  3. ファイルシステム操作:
    • os パッケージ: オペレーティングシステムとのインタラクション(ファイルやディレクトリの操作など)を提供します。os.FileInfo はファイルやディレクトリのメタデータ(名前、サイズ、変更時刻など)を表します。
    • path/filepath パッケージ: ファイルパスを操作するためのユーティリティを提供します。filepath.Join は、プラットフォームに依存しない形でパスの要素を結合するために使用されます。
  4. 文字列操作:
    • strings パッケージ: 文字列操作のためのユーティリティを提供します。このコミットでは strings.HasSuffix が使用されており、文字列が特定のサフィックスで終わるかどうかをチェックします。
  5. Issueトラッカー:
    • Goプロジェクトでは、バグ報告や機能要望を管理するためにIssueトラッカー(GitHub Issuesなど)が使用されます。コミットメッセージの「Fixes #XXXX」は、そのコミットが特定のIssueを修正したことを示します。

これらの知識があることで、コミットがGoのパーサーのどの部分に影響を与え、どのような問題を解決しようとしているのかを深く理解することができます。

技術的詳細

このコミットの技術的な核心は、go/parser パッケージの ParseDir 関数がディレクトリ内のファイルを走査するロジックに、.go 拡張子のチェックを追加することです。

変更前は、ParseDirfilter 関数が提供されている場合にのみ、そのフィルターを通過したファイルを考慮していました。filternil の場合、またはフィルターを通過した場合、そのファイルは ParseFile に渡され、Goのソースファイルとして解析が試みられました。この挙動は、.go 拡張子を持たないファイル(例: main.txtconfig.jsonREADME)がディレクトリ内に存在する場合でも、それらを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つのファイルです。

  1. src/pkg/go/parser/interface.go
  2. 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 の変更点

  1. import "strings" の追加:

    • ファイルの拡張子をチェックするために、strings パッケージが新しくインポートされました。strings.HasSuffix 関数がこの目的で使用されます。
  2. 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 拡張子のチェックが優先されることが追記されています。
  3. 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 の変更点

  1. 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 の新しいフィルタリングロジックが正しく機能するかを確認できます。
  2. 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)
      }
      
      このアサーションは、ParseDirparser.go, interface.go, parser_test.go の3つのファイルのみを正しく解析し、parser.go.orig のようなファイルが除外されたことを確認します。もし parser.go.orig が誤って解析されていれば、pkg.Files の数は4になり、テストが失敗します。

これらの変更は、ParseDir の動作をより正確に制御し、テストによってその新しい挙動が保証されることを示しています。

関連リンク

参考にした情報源リンク

  • 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プロジェクトがコードレビューに使用しているシステム)