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

[インデックス 13620] ファイルの概要

このコミットは、Go言語のパーサー(go/parserパッケージ)における改善で、ソースファイルが有効なGoのテキストを含まない場合に、パーシング処理を早期に終了させるように変更しています。これにより、無効な入力に対するパーサーの堅牢性と効率が向上します。

コミット

commit a9d0ff6ead470b565b832e2af29b564e9ac28e65
Author: Robert Griesemer <gri@golang.org>
Date:   Sat Aug 11 21:06:40 2012 -0700

    go/parser: exit early if source file does not contain text
    
    Partial fix for issue 3943.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/6458115

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a9d0ff6ead470b565b832e2af29b564e9ac28e65

元コミット内容

このコミットの元の内容は、Goパーサーがソースファイルにテキストが含まれていない場合に早期に終了するように変更することです。これは、Issue 3943に対する部分的な修正として行われました。

変更の背景

この変更の背景には、Goパーサーが不正な入力、特にGoのソースコードとして認識できないようなファイル(例えば、空のファイルや、Goの構文とは全く異なる内容のファイル)を処理する際の挙動の改善があります。元のパーサーは、このような無効な入力に対しても最後までパースを試み、その結果として不必要な処理時間やリソースを消費する可能性がありました。

コミットメッセージに記載されている「Partial fix for issue 3943」は、この変更が特定のバグや問題(Issue 3943)を解決するための一部であることを示唆しています。Issue 3943は、Goのツールチェインにおけるパーサーの堅牢性やエラーハンドリングに関する問題であったと推測されます。具体的には、パーサーが不正な入力に対して無限ループに陥ったり、クラッシュしたり、あるいは非常に長い時間をかけてエラーを報告したりするようなケースが考えられます。

このコミットは、パーサーが最初のトークンをスキャンした時点でエラーが発生した場合、またはパッケージ句のパース中にエラーが発生した場合に、それ以上パースを続行せずに早期に処理を中断し、nil(または空の*ast.File)を返すようにすることで、これらの問題を部分的に解決しようとしています。これにより、無効な入力に対するパーサーの応答性が向上し、リソースの無駄遣いを防ぐことができます。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。

  • Go言語のパーサー (go/parser パッケージ): Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成する役割を担うパッケージです。ASTは、プログラムの構造を木構造で表現したもので、コンパイラや各種ツールがコードを理解・操作するために使用します。ParseFile関数は、指定されたGoソースファイルをパースし、そのASTを返します。

  • 抽象構文木 (AST: Abstract Syntax Tree): ソースコードの構文構造を抽象的に表現した木構造のデータ構造です。各ノードはソースコードの構成要素(変数宣言、関数呼び出し、演算子など)を表します。*ast.Fileは、Goのソースファイル全体のASTを表す構造体です。

  • トークン (Token): ソースコードを構成する最小単位の要素です。例えば、キーワード(package, func)、識別子(変数名、関数名)、演算子(+, =)、リテラル(数値、文字列)などがトークンに該当します。パーサーは、まずソースコードをトークンに分解する字句解析(Lexical Analysis)を行い、そのトークンの並びから構文解析(Syntactic Analysis)を行います。

  • エラーハンドリング: パーサーは、不正な構文や無効な入力に遭遇した場合にエラーを報告するメカニズムを持っています。p.errorsは、パーサーが検出したエラーを管理するための内部的な構造であると推測されます。

  • token.FileSet: Goのソースファイル内の位置情報(行番号、列番号など)を管理するための構造体です。パーサーは、ASTノードに正確な位置情報を付与するためにこれを使用します。

  • ast.NewScope(nil): 新しいスコープ(変数の有効範囲)を作成する関数です。nilを渡すことで、親スコープを持たないトップレベルのスコープを作成します。

  • ast.Ident: 識別子(変数名、関数名など)を表すASTノードです。

技術的詳細

このコミットは、src/pkg/go/parser/interface.gosrc/pkg/go/parser/parser.go の2つのファイルにわたる変更を含んでいます。

src/pkg/go/parser/interface.go の変更

ParseFile 関数は、Goソースファイルをパースするための主要なエントリポイントです。このコミットでは、p.parseFile() の呼び出し後に以下のチェックが追加されています。

	if f == nil {
		// source is not a valid Go source file - satisfy
		// ParseFile API and return a valid (but) empty
		// *ast.File
		f = &ast.File{
			Name:  new(ast.Ident),
			Scope: ast.NewScope(nil),
		}
	}

このコードブロックは、p.parseFile()nil を返した場合(つまり、パーシングが早期に失敗した場合)に実行されます。ParseFile APIの要件を満たすために、有効ではあるものの空の *ast.File 構造体を生成して返します。これにより、呼び出し元は常に *ast.File 型のオブジェクトを受け取ることができ、nilチェックの負担を軽減できます。空の *ast.File は、Nameフィールドに新しいast.Identを、Scopeフィールドに新しいトップレベルスコープを設定しています。

src/pkg/go/parser/parser.go の変更

parser 構造体の parseFile メソッドは、実際のファイルパースロジックを含んでいます。このコミットでは、以下の2つの主要な変更が行われています。

  1. 初期エラーチェックの追加: parseFile メソッドの冒頭に、以下の早期終了チェックが追加されました。

    	// Don't bother parsing the rest if we had errors scanning the first token.
    	// Likely not a Go source file at all.
    	if p.errors.Len() != 0 {
    		return nil
    	}
    

    これは、字句解析の段階で最初のトークンをスキャンした時点で既にエラーが検出されている場合(p.errors.Len() != 0)、それ以上パースを続行しても無意味であると判断し、即座に nil を返して処理を中断します。これは、入力がGoソースファイルとして全く認識できないような場合に特に有効です。

  2. パッケージ句パース後のエラーチェックの修正: 既存のエラーチェックのコメントがより正確になり、条件が変更されました。

    変更前:

    	// Don't bother parsing the rest if we had errors already.
    	if p.errors.Len() == 0 && p.mode&PackageClauseOnly == 0 {
    

    変更後:

    	// Don't bother parsing the rest if we had errors parsing the package clause.
    	// Likely not a Go source file at all.
    	if p.errors.Len() != 0 {
    		return nil
    	}
    
        // ... (original code for p.mode&PackageClauseOnly == 0)
    

    この変更により、パッケージ句のパース後にエラーが検出された場合も、早期に nil を返して処理を中断するようになりました。これにより、不正なパッケージ宣言を持つファイルに対しても、不必要な後続のパース処理をスキップできます。また、p.mode&PackageClauseOnly == 0 の条件は、エラーチェックとは独立して、パッケージ句のみをパースするモードでない場合にのみ後続の宣言をパースするというロジックとして残されています。

これらの変更は、パーサーが不正な入力に対してより効率的に、かつ堅牢に振る舞うように設計されています。特に、Goソースファイルではない可能性が高い入力に対して、早期に処理を打ち切ることで、CPUサイクルとメモリ使用量を節約します。

コアとなるコードの変更箇所

src/pkg/go/parser/interface.go

--- a/src/pkg/go/parser/interface.go
+++ b/src/pkg/go/parser/interface.go
@@ -90,6 +90,15 @@ func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode)\
 	var p parser
 	p.init(fset, filename, text, mode)
 	f := p.parseFile()
+	if f == nil {
+		// source is not a valid Go source file - satisfy
+		// ParseFile API and return a valid (but) empty
+		// *ast.File
+		f = &ast.File{
+			Name:  new(ast.Ident),
+			Scope: ast.NewScope(nil),
+		}
+	}
 
 	// sort errors
 	if p.mode&SpuriousErrors == 0 {

src/pkg/go/parser/parser.go

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -2285,6 +2285,12 @@ func (p *parser) parseFile() *ast.File {
 		defer un(trace(p, "File"))
 	}
 
+	// Don't bother parsing the rest if we had errors scanning the first token.
+	// Likely not a Go source file at all.
+	if p.errors.Len() != 0 {
+		return nil
+	}
+
 	// package clause
 	doc := p.leadComment
 	pos := p.expect(token.PACKAGE)
@@ -2296,13 +2302,16 @@ func (p *parser) parseFile() *ast.File {
 	}
 	p.expectSemi()
 
-	// Don't bother parsing the rest if we had errors already.
+	// Don't bother parsing the rest if we had errors parsing the package clause.
 	// Likely not a Go source file at all.
+	if p.errors.Len() != 0 {
+		return nil
+	}
 
 	p.openScope()
 	p.pkgScope = p.topScope
 	var decls []ast.Decl
-	if p.errors.Len() == 0 && p.mode&PackageClauseOnly == 0 {
+	if p.mode&PackageClauseOnly == 0 {
 		// import decls
 		for p.tok == token.IMPORT {
 			decls = append(decls, p.parseGenDecl(token.IMPORT, parseImportSpec))\

コアとなるコードの解説

このコミットの核となる変更は、GoパーサーのparseFileメソッドにおける早期終了ロジックの導入です。

  1. parser.go における早期終了: parseFileメソッドの開始直後にif p.errors.Len() != 0 { return nil }というチェックが追加されました。これは、字句解析の初期段階で既にエラーが検出されている場合(例えば、ファイルが空であるか、Goの有効なトークンで始まっていない場合)に、それ以上のパース処理をスキップし、即座にnilを返します。これにより、無効な入力に対するパーサーの応答性が大幅に向上し、不必要な計算を避けることができます。

  2. parser.go におけるパッケージ句後の早期終了: パッケージ句のパース後にも同様のif p.errors.Len() != 0 { return nil }チェックが追加されました。これは、パッケージ宣言が不正であるなど、Goソースファイルとして基本的な構造が満たされていない場合に、後続の複雑なパース処理に進むことなく、早期にエラーを検出して終了することを可能にします。これにより、パーサーはより効率的にエラーを処理し、リソースの消費を抑えることができます。

  3. interface.go におけるAPI互換性の維持: ParseFile関数は、p.parseFile()nilを返した場合に、APIの要件を満たすために空の*ast.File構造体を生成して返します。これは、パーサーの内部的な早期終了が、外部からParseFileを呼び出すコードに影響を与えないようにするための配慮です。呼び出し元は常に有効な*ast.Fileオブジェクトを受け取ることが期待されるため、この変換は重要です。空の*ast.Fileは、Nameフィールドに新しいast.Identを、Scopeフィールドに新しいトップレベルスコープを設定することで、最小限の有効なAST構造を提供します。

これらの変更は、Goパーサーの堅牢性とパフォーマンスを向上させるための重要なステップであり、特に大規模なコードベースや、自動生成されたコード、あるいはユーザーからの多様な入力ファイルを扱うシステムにおいて、その効果を発揮します。

関連リンク

参考にした情報源リンク