[インデックス 16069] ファイルの概要
このコミットは、Go言語のパーサーライブラリである go/parser の ParseExpr 関数が型表現(type expressions)を正しく解析できるように修正し、gofmt -r コマンドのエラーメッセージのフォーマットを改善するものです。特に、interface{} のような型表現が gofmt -r のパターンとして扱えるようになることが主な変更点です。
コミット
commit 2ba6ecb3e2bec6b30667c1969772d7e5063f384b
Author: Robert Griesemer <gri@golang.org>
Date: Wed Apr 3 07:41:26 2013 -0700
go/parser: ParseExpr must accept type expressions
My old code was trying to be too smart.
Also: Slightly better error message format
for gofmt -r pattern errors.
Fixes #4406.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/8267045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2ba6ecb3e2bec6b30667c1969772d7e5063f384b
元コミット内容
go/parser: ParseExpr must accept type expressions
私の古いコードは賢すぎようとしていました。
また、gofmt -r パターンエラーのためのエラーメッセージのフォーマットを少し改善しました。
Issue #4406 を修正します。
変更の背景
この変更の背景には、Go言語のツールである gofmt の rewrite 機能(-r フラグ)が、型表現をパターンとして正しく扱えないという問題がありました。具体的には、Issue #4406 で報告されたように、gofmt -r "interface{}->int" のようなリライトルールが機能しないというバグが存在しました。
gofmt -r は、Goのコードを抽象構文木(AST)レベルでパターンマッチングし、指定されたルールに基づいてコードを書き換える強力なツールです。この機能が型表現を認識できないということは、Goの型システムに関連するリファクタリングやコード変換を行う際に大きな制約となっていました。
コミットメッセージにある「My old code was trying to be too smart.」という記述は、go/parser の ParseExpr 関数が、式(expression)のみを解析することを想定しており、型表現を解析する際に不必要な制約や複雑なロジックを持っていたことを示唆しています。この「賢すぎた」ロジックが、型表現の正しい解析を妨げていたと考えられます。
このコミットは、gofmt -r の実用性を高め、Go開発者がより柔軟にコードを操作できるようにするために、ParseExpr が型表現も適切に扱えるように修正することを目的としています。また、エラーメッセージの改善も、ユーザーが gofmt -r のパターンエラーを理解しやすくするための重要な改善点です。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連ツールの概念を理解しておく必要があります。
-
Go言語の抽象構文木 (AST: Abstract Syntax Tree): Goコンパイラやツールは、Goのソースコードを直接処理するのではなく、まずそのコードを抽象構文木(AST)というデータ構造に変換します。ASTは、プログラムの構造を木構造で表現したもので、各ノードがコードの要素(変数、関数、式、型など)に対応します。Goの標準ライブラリには、
go/astパッケージがあり、ASTの構造を定義しています。 -
go/parserパッケージ:go/parserパッケージは、Goのソースコードを解析し、ASTを生成するためのGo標準ライブラリです。このパッケージは、ファイル、ディレクトリ、または文字列からGoのコードを読み込み、対応するASTを構築する機能を提供します。parser.ParseFile: Goのソースファイル全体を解析し、*ast.Fileを返します。parser.ParseExpr: 与えられた文字列をGoの式(expression)として解析し、ast.Exprを返します。このコミットの主要な変更点はこの関数の挙動に関わります。
-
ast.Exprインターフェース:go/astパッケージで定義されているExprインターフェースは、Goのプログラムにおけるあらゆる種類の式(リテラル、変数参照、関数呼び出し、演算など)を表すための共通の型です。Goの型表現(例:int,string,struct{},interface{}) も、AST上ではast.Exprの一種として扱われることがあります。 -
gofmtツール:gofmtは、Go言語の公式なフォーマッタツールです。Goのソースコードを標準的なスタイルに自動的に整形します。これにより、Goコミュニティ全体で一貫したコードスタイルが保たれ、コードの可読性が向上します。 -
gofmt -r(Rewrite) 機能:gofmtには-rフラグがあり、これを使用すると、指定されたパターンに基づいてコードを書き換えることができます。これは、ASTレベルでのパターンマッチングと置換を行う強力なリファクタリングツールです。-r "pattern -> replacement"の形式で指定します。patternとreplacementはGoの式として解釈されます。- このコミット以前は、
patternやreplacementに型表現(例:interface{})を指定すると、ParseExprがそれを正しく解析できず、エラーになる問題がありました。
-
interface{}(空インターフェース): Go言語におけるinterface{}は、メソッドを一切持たないインターフェース型です。Goのすべての型は、少なくとも0個のメソッドを持つため、interface{}はGoのあらゆる型の値を保持できる「任意の型」を表すことができます。これは、他の言語におけるObjectやanyに近い概念です。
これらの概念を理解することで、ParseExpr が型表現を扱えるようになることの重要性、そしてそれが gofmt -r の機能拡張にどのように貢献するかが明確になります。
技術的詳細
このコミットの技術的詳細は、主に go/parser パッケージ内の ParseExpr 関数の実装変更に集約されます。
変更前の ParseExpr の問題点:
変更前の ParseExpr 関数は、与えられた文字列を式として解析するために、非常に回りくどい方法を採用していました。具体的には、以下のような手法を用いていました。
// 変更前の ParseExpr の擬似コード
func ParseExpr(x string) (ast.Expr, error) {
// x を完全なGoパッケージのコンテキスト内で解析しようとする
// エラーメッセージの正しい位置情報のために //line ディレクティブを使用
// 式 x を単独の行に配置し、その後に ';' を付けて、式が不完全な場合にエラーを強制する
file, err := ParseFile(token.NewFileSet(), "", "package p;func _(){_=\n//line :1\n"+x+"\n;}", 0)
if err != nil {
return nil, err
}
// 生成されたASTから、割り当てステートメントの右辺にある式を抽出
return file.Decls[0].(*ast.FuncDecl).Body.List[0].(*ast.AssignStmt).Rhs[0], nil
}
このアプローチは、x が単なる式であると仮定し、それを _ = x のような割り当てステートメントの一部として、ダミーの関数とパッケージのコンテキスト内に埋め込んで ParseFile で解析するというものでした。その後、生成されたASTから目的の式を抽出していました。
この「賢すぎた」アプローチの問題点は、ParseExpr が純粋な式だけでなく、interface{} や struct{} のような型表現も解析する必要がある場合に破綻することでした。型表現は、Goの文法上、必ずしも「式」として扱われるわけではありません。特に、_ = interface{} のように割り当てステートメントの右辺に直接型表現を置くことは、Goの文法として不正な場合があります。そのため、この間接的な解析方法では、型表現を正しくASTに変換できませんでした。
変更後の ParseExpr の改善点:
新しい ParseExpr の実装は、より直接的かつ堅牢な方法で式(および型表現)を解析します。
// 変更後の ParseExpr の擬似コード
func ParseExpr(x string) (ast.Expr, error) {
var p parser // 新しいパーサーインスタンスを作成
p.init(token.NewFileSet(), "", []byte(x), 0) // 入力文字列 x でパーサーを初期化
// パッケージレベルのスコープを設定し、nilポインタエラーを回避
// 正しい式 x の場合、nil topScope でも問題ないが、エラーのある x の場合に備えて慎重に
p.openScope()
p.pkgScope = p.topScope
e := p.parseRhsOrType() // 右辺の式または型を解析する新しい内部メソッドを呼び出す
p.closeScope()
assert(p.topScope == nil, "unbalanced scopes")
if p.errors.Len() > 0 {
p.errors.Sort()
return nil, p.errors.Err()
}
return e, nil
}
主な変更点は以下の通りです。
- 直接的なパーサーの利用:
ParseFileを介した間接的な解析ではなく、go/parserパッケージ内部のparser構造体を直接インスタンス化し、初期化しています。これにより、より低レベルで柔軟な解析が可能になります。 parseRhsOrType()の導入:parser構造体にparseRhsOrType()という新しい内部メソッドが導入されました。このメソッドは、Goの文法において右辺の式として現れる可能性のあるもの、または型表現として現れる可能性のあるものを解析するように設計されています。これにより、interface{}やstruct{}のような型表現も、ParseExprが直接かつ正しく解析できるようになりました。- スコープ管理の明示化:
p.openScope()とp.closeScope()を明示的に呼び出し、pkgScopeを設定することで、解析中のスコープ管理をより堅牢にしています。これは、エラーのある入力に対しても安定した動作を保証するためです。 - エラーメッセージの改善:
gofmt/rewrite.goのparseExpr関数では、エラーメッセージのフォーマットがparsing %s %s: %s\nからparsing %s %s at %s\nに変更されました。これにより、エラーが発生した具体的な位置情報がメッセージに含まれるようになり、デバッグが容易になります。
これらの変更により、ParseExpr はGoの式だけでなく、型表現も正確にASTに変換できるようになり、gofmt -r のようなツールがより広範なコード変換ルールをサポートできるようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下の2つのファイルに集中しています。
-
src/pkg/go/parser/interface.go:ParseExpr関数の実装が全面的に書き換えられました。- 変更前は、入力文字列を
package p;func _(){_=\n//line :1\n"+x+"\n;}のような完全なGoコードのコンテキストに埋め込み、ParseFileを呼び出して解析していました。その後、生成されたASTから目的の式を抽出していました。 - 変更後は、
parser構造体を直接初期化し、p.parseRhsOrType()という内部メソッドを呼び出すことで、より直接的に式や型表現を解析するように変更されました。スコープ管理も明示的に行われています。
- 変更前は、入力文字列を
-
src/cmd/gofmt/rewrite.go:parseExpr関数のエラーメッセージのフォーマットが変更されました。fmt.Fprintf(os.Stderr, "parsing %s %s: %s\\n", what, s, err)- から
fmt.Fprintf(os.Stderr, "parsing %s %s at %s\\n", what, s, err)- へ変更され、エラー発生箇所がより明確に示されるようになりました。
その他、テストファイルも変更されています。
src/cmd/gofmt/gofmt_test.go:gofmt -rのテストケースにinterface{}->intのような型表現のリライトが追加されました。src/cmd/gofmt/testdata/rewrite8.goldenとsrc/cmd/gofmt/testdata/rewrite8.input: 新しいテストケースのための入力ファイルと期待される出力ファイルが追加されました。src/pkg/go/parser/parser_test.go:ParseExprがstruct{x *int}のような型表現を正しく解析できることを確認するテストケースが追加されました。
コアとなるコードの解説
src/pkg/go/parser/interface.go の変更
ParseExpr 関数の変更は、このコミットの最も重要な部分です。
変更前(簡略化された説明):
func ParseExpr(x string) (ast.Expr, error) {
// x をダミーの関数とパッケージのコンテキストに埋め込み、ParseFile で解析
file, err := ParseFile(token.NewFileSet(), "", "package p;func _(){_=\\n//line :1\\n"+x+"\\n;}", 0)
if err != nil {
return nil, err
}
// ASTから割り当てステートメントの右辺を抽出
return file.Decls[0].(*ast.FuncDecl).Body.List[0].(*ast.AssignStmt).Rhs[0], nil
}
この古い実装は、x が常に有効な「式」として _ = x の形式で埋め込めることを前提としていました。しかし、interface{} のような型表現は、Goの文法上、必ずしも直接「式」として割り当ての右辺に置けるわけではありません。このため、型表現を解析しようとすると、文法エラーが発生したり、意図しないASTが生成されたりする問題がありました。
変更後:
func ParseExpr(x string) (ast.Expr, error) {
var p parser // 新しいパーサーインスタンス
p.init(token.NewFileSet(), "", []byte(x), 0) // 入力文字列 x でパーサーを初期化
// パッケージレベルのスコープを設定
p.openScope()
p.pkgScope = p.topScope
e := p.parseRhsOrType() // 右辺の式または型を解析する内部メソッドを呼び出す
p.closeScope()
assert(p.topScope == nil, "unbalanced scopes")
if p.errors.Len() > 0 {
p.errors.Sort()
return nil, p.errors.Err()
}
return e, nil
}
新しい実装では、parser 構造体を直接利用し、その内部メソッドである parseRhsOrType() を呼び出しています。
parser構造体は、Goのソースコードを解析するための状態(トークンストリーム、エラーリスト、スコープ情報など)を保持します。p.init()は、新しいファイルセット、ファイル名(ここでは空文字列)、入力バイト列(x)、およびモードフラグでパーサーを初期化します。p.openScope()とp.closeScope()は、パーサーの内部的なスコープ管理メカニズムです。pkgScopeの設定は、解析中にシンボル解決などが必要になった場合に備えて、適切なコンテキストを提供します。p.parseRhsOrType()は、Goの文法において「右辺の式」または「型」として現れる可能性のある構文要素を解析するために特化されたメソッドです。これにより、interface{}やstruct{}のような型表現も、ParseExprが直接、かつ文法的に正しい方法でASTに変換できるようになりました。- エラー処理も改善され、
p.errors.Err()を通じて、より詳細なエラー情報が返されるようになりました。
この変更により、ParseExpr は、Goの式と型表現の両方を、その文法的な特性に応じて適切に解析できるようになり、gofmt -r のようなツールがより柔軟なパターンマッチングとリライトを可能にしました。
src/cmd/gofmt/rewrite.go の変更
parseExpr 関数のエラーメッセージの変更は、ユーザーエクスペリエンスの改善を目的としています。
変更前:
func parseExpr(s string, what string) ast.Expr {
x, err := parser.ParseExpr(s)
if err != nil {
fmt.Fprintf(os.Stderr, "parsing %s %s: %s\n", what, s, err)
os.Exit(2)
}
return x
}
エラーメッセージは parsing [what] [s]: [err] の形式でした。例えば、parsing pattern interface{}: expected expression のようなメッセージが出力される可能性がありました。
変更後:
func parseExpr(s, what string) ast.Expr {
x, err := parser.ParseExpr(s)
if err != nil {
fmt.Fprintf(os.Stderr, "parsing %s %s at %s\n", what, s, err)
os.Exit(2)
}
return x
}
エラーメッセージは parsing [what] [s] at [err] の形式に変更されました。at %s の部分には、go/parser から返されるエラーオブジェクトが持つ詳細な位置情報(例: 1:1: expected expression)が含まれるようになります。これにより、ユーザーはエラーが入力文字列のどの部分で発生したかをより正確に把握できるようになり、デバッグが容易になります。
関連リンク
- Go Issue #4406:
gofmt -r "interface{}->int"fails- このコミットが修正した具体的な問題に関するGoのIssueトラッカーのエントリです。
- https://github.com/golang/go/issues/4406
- Go CL 8267045:
go/parser: ParseExpr must accept type expressions- このコミットに対応するGoのコードレビューシステム(Gerrit)のチェンジリストです。詳細な議論や変更履歴を確認できます。
- https://golang.org/cl/8267045
参考にした情報源リンク
- Go言語の公式ドキュメント:
go/astパッケージ: https://pkg.go.dev/go/astgo/parserパッケージ: https://pkg.go.dev/go/parsergofmtコマンド: https://pkg.go.dev/cmd/gofmt
- Go言語のASTに関する解説記事:
- "Go AST: The Abstract Syntax Tree" (Go by Example): https://gobyexample.com/go-ast (一般的なASTの概念理解に役立ちます)
- "Understanding and Using Go's AST" (Dave Cheney): https://dave.cheney.net/2019/01/08/understanding-and-using-gos-ast (より深い技術的理解に役立ちます)
- Go言語の
interface{}に関する解説:- "The Empty Interface" (Go Blog): https://go.dev/blog/empty-interface (Goにおける
interface{}の役割と使い方を理解するのに役立ちます)
- "The Empty Interface" (Go Blog): https://go.dev/blog/empty-interface (Goにおける
gofmt -rの利用例:- "Go: gofmt -r for refactoring" (Stack Overflow): https://stackoverflow.com/questions/20070000/go-gofmt-r-for-refactoring (
gofmt -rの実用的な利用方法の例が示されています)
- "Go: gofmt -r for refactoring" (Stack Overflow): https://stackoverflow.com/questions/20070000/go-gofmt-r-for-refactoring (
[インデックス 16069] ファイルの概要
このコミットは、Go言語のパーサーライブラリである go/parser の ParseExpr 関数が型表現(type expressions)を正しく解析できるように修正し、gofmt -r コマンドのエラーメッセージのフォーマットを改善するものです。特に、interface{} のような型表現が gofmt -r のパターンとして扱えるようになることが主な変更点です。
コミット
commit 2ba6ecb3e2bec6b30667c1969772d7e5063f384b
Author: Robert Griesemer <gri@golang.org>
Date: Wed Apr 3 07:41:26 2013 -0700
go/parser: ParseExpr must accept type expressions
My old code was trying to be too smart.
Also: Slightly better error message format
for gofmt -r pattern errors.
Fixes #4406.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/8267045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2ba6ecb3e2bec6b30667c1969772d7e5063f384b
元コミット内容
go/parser: ParseExpr must accept type expressions
私の古いコードは賢すぎようとしていました。
また、gofmt -r パターンエラーのためのエラーメッセージのフォーマットを少し改善しました。
Issue #4406 を修正します。
変更の背景
この変更の背景には、Go言語のツールである gofmt の rewrite 機能(-r フラグ)が、型表現をパターンとして正しく扱えないという問題がありました。具体的には、Issue #4406 で報告されたように、gofmt -r "interface{}->int" のようなリライトルールが機能しないというバグが存在しました。
gofmt -r は、Goのコードを抽象構文木(AST)レベルでパターンマッチングし、指定されたルールに基づいてコードを書き換える強力なツールです。この機能が型表現を認識できないということは、Goの型システムに関連するリファクタリングやコード変換を行う際に大きな制約となっていました。
コミットメッセージにある「My old code was trying to be too smart.」という記述は、go/parser の ParseExpr 関数が、式(expression)のみを解析することを想定しており、型表現を解析する際に不必要な制約や複雑なロジックを持っていたことを示唆しています。この「賢すぎた」ロジックが、型表現の正しい解析を妨げていたと考えられます。
このコミットは、gofmt -r の実用性を高め、Go開発者がより柔軟にコードを操作できるようにするために、ParseExpr が型表現も適切に扱えるように修正することを目的としています。また、エラーメッセージの改善も、ユーザーが gofmt -r のパターンエラーを理解しやすくするための重要な改善点です。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連ツールの概念を理解しておく必要があります。
-
Go言語の抽象構文木 (AST: Abstract Syntax Tree): Goコンパイラやツールは、Goのソースコードを直接処理するのではなく、まずそのコードを抽象構文木(AST)というデータ構造に変換します。ASTは、プログラムの構造を木構造で表現したもので、各ノードがコードの要素(変数、関数、式、型など)に対応します。Goの標準ライブラリには、
go/astパッケージがあり、ASTの構造を定義しています。 -
go/parserパッケージ:go/parserパッケージは、Goのソースコードを解析し、ASTを生成するためのGo標準ライブラリです。このパッケージは、ファイル、ディレクトリ、または文字列からGoのコードを読み込み、対応するASTを構築する機能を提供します。parser.ParseFile: Goのソースファイル全体を解析し、*ast.Fileを返します。parser.ParseExpr: 与えられた文字列をGoの式(expression)として解析し、ast.Exprを返します。このコミットの主要な変更点はこの関数の挙動に関わります。
-
ast.Exprインターフェース:go/astパッケージで定義されているExprインターフェースは、Goのプログラムにおけるあらゆる種類の式(リテラル、変数参照、関数呼び出し、演算など)を表すための共通の型です。Goの型表現(例:int,string,struct{},interface{}) も、AST上ではast.Exprの一種として扱われることがあります。 -
gofmtツール:gofmtは、Go言語の公式なフォーマッタツールです。Goのソースコードを標準的なスタイルに自動的に整形します。これにより、Goコミュニティ全体で一貫したコードスタイルが保たれ、コードの可読性が向上します。 -
gofmt -r(Rewrite) 機能:gofmtには-rフラグがあり、これを使用すると、指定されたパターンに基づいてコードを書き換えることができます。これは、ASTレベルでのパターンマッチングと置換を行う強力なリファクタリングツールです。-r "pattern -> replacement"の形式で指定します。patternとreplacementはGoの式として解釈されます。- このコミット以前は、
patternやreplacementに型表現(例:interface{})を指定すると、ParseExprがそれを正しく解析できず、エラーになる問題がありました。
-
interface{}(空インターフェース): Go言語におけるinterface{}は、メソッドを一切持たないインターフェース型です。Goのすべての型は、少なくとも0個のメソッドを持つため、interface{}はGoのあらゆる型の値を保持できる「任意の型」を表すことができます。これは、他の言語におけるObjectやanyに近い概念です。
これらの概念を理解することで、ParseExpr が型表現を扱えるようになることの重要性、そしてそれが gofmt -r の機能拡張にどのように貢献するかが明確になります。
技術的詳細
このコミットの技術的詳細は、主に go/parser パッケージ内の ParseExpr 関数の実装変更に集約されます。
変更前の ParseExpr の問題点:
変更前の ParseExpr 関数は、与えられた文字列を式として解析するために、非常に回りくどい方法を採用していました。具体的には、以下のような手法を用いていました。
// 変更前の ParseExpr の擬似コード
func ParseExpr(x string) (ast.Expr, error) {
// x を完全なGoパッケージのコンテキスト内で解析しようとする
// エラーメッセージの正しい位置情報のために //line ディレクティブを使用
// 式 x を単独の行に配置し、その後に ';' を付けて、式が不完全な場合にエラーを強制する
file, err := ParseFile(token.NewFileSet(), "", "package p;func _(){_=\n//line :1\n"+x+"\n;}", 0)
if err != nil {
return nil, err
}
// 生成されたASTから、割り当てステートメントの右辺にある式を抽出
return file.Decls[0].(*ast.FuncDecl).Body.List[0].(*ast.AssignStmt).Rhs[0], nil
}
このアプローチは、x が単なる式であると仮定し、それを _ = x のような割り当てステートメントの一部として、ダミーの関数とパッケージのコンテキスト内に埋め込んで ParseFile で解析するというものでした。その後、生成されたASTから目的の式を抽出していました。
この「賢すぎた」アプローチの問題点は、ParseExpr が純粋な式だけでなく、interface{} や struct{} のような型表現も解析する必要がある場合に破綻することでした。型表現は、Goの文法上、必ずしも「式」として扱われるわけではありません。特に、_ = interface{} のように割り当てステートメントの右辺に直接型表現を置くことは、Goの文法として不正な場合があります。そのため、この間接的な解析方法では、型表現を正しくASTに変換できませんでした。
変更後の ParseExpr の改善点:
新しい ParseExpr の実装は、より直接的かつ堅牢な方法で式(および型表現)を解析します。
// 変更後の ParseExpr の擬似コード
func ParseExpr(x string) (ast.Expr, error) {
var p parser // 新しいパーサーインスタンスを作成
p.init(token.NewFileSet(), "", []byte(x), 0) // 入力文字列 x でパーサーを初期化
// パッケージレベルのスコープを設定し、nilポインタエラーを回避
// 正しい式 x の場合、nil topScope でも問題ないが、エラーのある x の場合に備えて慎重に
p.openScope()
p.pkgScope = p.topScope
e := p.parseRhsOrType() // 右辺の式または型を解析する新しい内部メソッドを呼び出す
p.closeScope()
assert(p.topScope == nil, "unbalanced scopes")
if p.errors.Len() > 0 {
p.errors.Sort()
return nil, p.errors.Err()
}
return e, nil
}
主な変更点は以下の通りです。
- 直接的なパーサーの利用:
ParseFileを介した間接的な解析ではなく、go/parserパッケージ内部のparser構造体を直接インスタンス化し、初期化しています。これにより、より低レベルで柔軟な解析が可能になります。 parseRhsOrType()の導入:parser構造体にparseRhsOrType()という新しい内部メソッドが導入されました。このメソッドは、Goの文法において右辺の式として現れる可能性のあるもの、または型表現として現れる可能性のあるものを解析するように設計されています。これにより、interface{}やstruct{}のような型表現も、ParseExprが直接かつ正しく解析できるようになりました。- スコープ管理の明示化:
p.openScope()とp.closeScope()を明示的に呼び出し、pkgScopeを設定することで、解析中のスコープ管理をより堅牢にしています。これは、エラーのある入力に対しても安定した動作を保証するためです。 - エラーメッセージの改善:
gofmt/rewrite.goのparseExpr関数では、エラーメッセージのフォーマットがparsing %s %s: %s\nからparsing %s %s at %s\nに変更されました。これにより、エラーが発生した具体的な位置情報がメッセージに含まれるようになり、デバッグが容易になります。
これらの変更により、ParseExpr はGoの式だけでなく、型表現も正確にASTに変換できるようになり、gofmt -r のようなツールがより広範なコード変換ルールをサポートできるようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下の2つのファイルに集中しています。
-
src/pkg/go/parser/interface.go:ParseExpr関数の実装が全面的に書き換えられました。- 変更前は、入力文字列を
package p;func _(){_=\n//line :1\n"+x+"\n;}のような完全なGoコードのコンテキストに埋め込み、ParseFileを呼び出して解析していました。その後、生成されたASTから目的の式を抽出していました。 - 変更後は、
parser構造体を直接初期化し、p.parseRhsOrType()という内部メソッドを呼び出すことで、より直接的に式や型表現を解析するように変更されました。スコープ管理も明示的に行われています。
- 変更前は、入力文字列を
-
src/cmd/gofmt/rewrite.go:parseExpr関数のエラーメッセージのフォーマットが変更されました。fmt.Fprintf(os.Stderr, "parsing %s %s: %s\\n", what, s, err)- から
fmt.Fprintf(os.Stderr, "parsing %s %s at %s\\n", what, s, err)- へ変更され、エラー発生箇所がより明確に示されるようになりました。
その他、テストファイルも変更されています。
src/cmd/gofmt/gofmt_test.go:gofmt -rのテストケースにinterface{}->intのような型表現のリライトが追加されました。src/cmd/gofmt/testdata/rewrite8.goldenとsrc/cmd/gofmt/testdata/rewrite8.input: 新しいテストケースのための入力ファイルと期待される出力ファイルが追加されました。src/pkg/go/parser/parser_test.go:ParseExprがstruct{x *int}のような型表現を正しく解析できることを確認するテストケースが追加されました。
コアとなるコードの解説
src/pkg/go/parser/interface.go の変更
ParseExpr 関数の変更は、このコミットの最も重要な部分です。
変更前(簡略化された説明):
func ParseExpr(x string) (ast.Expr, error) {
// x をダミーの関数とパッケージのコンテキストに埋め込み、ParseFile で解析
file, err := ParseFile(token.NewFileSet(), "", "package p;func _(){_=\n//line :1\\n"+x+"\\n;}", 0)
if err != nil {
return nil, err
}
// ASTから割り当てステートメントの右辺を抽出
return file.Decls[0].(*ast.FuncDecl).Body.List[0].(*ast.AssignStmt).Rhs[0], nil
}
この古い実装は、x が常に有効な「式」として _ = x の形式で埋め込めることを前提としていました。しかし、interface{} のような型表現は、Goの文法上、必ずしも直接「式」として割り当ての右辺に置けるわけではありません。このため、型表現を解析しようとすると、文法エラーが発生したり、意図しないASTが生成されたりする問題がありました。
変更後:
func ParseExpr(x string) (ast.Expr, error) {
var p parser // 新しいパーサーインスタンス
p.init(token.NewFileSet(), "", []byte(x), 0) // 入力文字列 x でパーサーを初期化
// パッケージレベルのスコープを設定
p.openScope()
p.pkgScope = p.topScope
e := p.parseRhsOrType() // 右辺の式または型を解析する内部メソッドを呼び出す
p.closeScope()
assert(p.topScope == nil, "unbalanced scopes")
if p.errors.Len() > 0 {
p.errors.Sort()
return nil, p.errors.Err()
}
return e, nil
}
新しい実装では、parser 構造体を直接利用し、その内部メソッドである parseRhsOrType() を呼び出しています。
parser構造体は、Goのソースコードを解析するための状態(トークンストリーム、エラーリスト、スコープ情報など)を保持します。p.init()は、新しいファイルセット、ファイル名(ここでは空文字列)、入力バイト列(x)、およびモードフラグでパーサーを初期化します。p.openScope()とp.closeScope()は、パーサーの内部的なスコープ管理メカニズムです。pkgScopeの設定は、解析中にシンボル解決などが必要になった場合に備えて、適切なコンテキストを提供します。p.parseRhsOrType()は、Goの文法において「右辺の式」または「型」として現れる可能性のある構文要素を解析するために特化されたメソッドです。これにより、interface{}やstruct{}のような型表現も、ParseExprが直接、かつ文法的に正しい方法でASTに変換できるようになりました。- エラー処理も改善され、
p.errors.Err()を通じて、より詳細なエラー情報が返されるようになりました。
この変更により、ParseExpr は、Goの式と型表現の両方を、その文法的な特性に応じて適切に解析できるようになり、gofmt -r のようなツールがより柔軟なパターンマッチングとリライトを可能にしました。
src/cmd/gofmt/rewrite.go の変更
parseExpr 関数のエラーメッセージの変更は、ユーザーエクスペリエンスの改善を目的としています。
変更前:
func parseExpr(s string, what string) ast.Expr {
x, err := parser.ParseExpr(s)
if err != nil {
fmt.Fprintf(os.Stderr, "parsing %s %s: %s\n", what, s, err)
os.Exit(2)
}
return x
}
エラーメッセージは parsing [what] [s]: [err] の形式でした。例えば、parsing pattern interface{}: expected expression のようなメッセージが出力される可能性がありました。
変更後:
func parseExpr(s, what string) ast.Expr {
x, err := parser.ParseExpr(s)
if err != nil {
fmt.Fprintf(os.Stderr, "parsing %s %s at %s\n", what, s, err)
os.Exit(2)
}
return x
}
エラーメッセージは parsing [what] [s] at [err] の形式に変更されました。at %s の部分には、go/parser から返されるエラーオブジェクトが持つ詳細な位置情報(例: 1:1: expected expression)が含まれるようになります。これにより、ユーザーはエラーが入力文字列のどの部分で発生したかをより正確に把握できるようになり、デバッグが容易になります。
関連リンク
- Go Issue #4406:
gofmt -r "interface{}->int"fails- このコミットが修正した具体的な問題に関するGoのIssueトラッカーのエントリです。
- https://github.com/golang/go/issues/4406
- Go CL 8267045:
go/parser: ParseExpr must accept type expressions- このコミットに対応するGoのコードレビューシステム(Gerrit)のチェンジリストです。詳細な議論や変更履歴を確認できます。
- https://golang.org/cl/8267045
参考にした情報源リンク
- Go言語の公式ドキュメント:
go/astパッケージ: https://pkg.go.dev/go/astgo/parserパッケージ: https://pkg.go.dev/go/parsergofmtコマンド: https://pkg.go.dev/cmd/gofmt
- Go言語のASTに関する解説記事:
- "Go AST: The Abstract Syntax Tree" (Go by Example): https://gobyexample.com/go-ast (一般的なASTの概念理解に役立ちます)
- "Understanding and Using Go's AST" (Dave Cheney): https://dave.cheney.net/2019/01/08/understanding-and-using-gos-ast (より深い技術的理解に役立ちます)
- Go言語の
interface{}に関する解説:- "The Empty Interface" (Go Blog): https://go.dev/blog/empty-interface (Goにおける
interface{}の役割と使い方を理解するのに役立ちます)
- "The Empty Interface" (Go Blog): https://go.dev/blog/empty-interface (Goにおける
gofmt -rの利用例:- "Go: gofmt -r for refactoring" (Stack Overflow): https://stackoverflow.com/questions/20070000/go-gofmt-r-for-refactoring (
gofmt -rの実用的な利用方法の例が示されています)
- "Go: gofmt -r for refactoring" (Stack Overflow): https://stackoverflow.com/questions/20070000/go-gofmt-r-for-refactoring (