[インデックス 11293] ファイルの概要
このコミットは、Go言語の抽象構文木(AST)を扱うgo/ast
パッケージにおいて、ImportSpec.EndPos
の扱いを修正するものです。具体的には、インポート宣言の終了位置を正しく尊重するように変更され、これによりgofix
ツールがインポート文を操作する際のバグが修正されました。
コミット
commit b0360e469cc77d88bfa435d63e319c5518bd8787
Author: Scott Lawrence <bytbox@gmail.com>
Date: Fri Jan 20 13:34:19 2012 -0500
go/ast: respect ImportSpec.EndPos
Fixes #2566.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5541068
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b0360e469cc77d88bfa435d63e319c5518bd8787
元コミット内容
このコミットは、go/ast
パッケージ内のsortSpecs
関数におけるImportSpec
の終了位置の計算方法を修正しています。以前は、ImportSpec
の終了位置をs.Pos() + 1
としていましたが、これをs.End()
を使用するように変更しました。これにより、インポート文の正確な範囲がAST内で表現されるようになり、gofix
ツールがインポート文を追加・削除する際に発生していた問題(Issue 2566)が解決されました。
変更されたファイルは以下の通りです。
src/cmd/gofix/import_test.go
:gofix
ツールのインポート関連のテストが追加・修正されています。特に、addDelImportFn
という新しいテストヘルパー関数が追加され、インポートの追加と削除を同時にテストできるようになっています。src/pkg/go/ast/import.go
:sortSpecs
関数内でImportSpec
の終了位置を決定するロジックが変更されています。
変更の背景
この変更の背景には、Go言語のツールチェインにおけるgofix
ツールの不具合がありました。具体的には、GoのIssue 2566「gofix
import
command adds import
to wrong place」が報告されており、gofix
コマンドがインポート文を誤った位置に追加してしまう問題がありました。
この問題は、go/ast
パッケージがインポート宣言の正確な終了位置を把握できていなかったことに起因します。AST(抽象構文木)を操作する際、各ノードの開始位置(Pos()
)と終了位置(End()
)は非常に重要です。特に、コードの整形や修正を行うツール(gofix
など)にとっては、正確な位置情報が不可欠です。
以前の実装では、ImportSpec
の終了位置を単純に開始位置に1を加えたものとしていましたが、これはインポートパスの文字列の長さが変更された場合に、正確な範囲をカバーできなくなる可能性がありました。この不正確さが、gofix
がインポート文を挿入する際に既存のコードを破壊したり、意図しない場所に挿入したりする原因となっていました。
このコミットは、ImportSpec
が持つ本来のEndPos
情報(Goパーサーが正確に計算した終了位置)を尊重することで、この問題を根本的に解決しようとするものです。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。
-
Go言語のAST (Abstract Syntax Tree):
- Goコンパイラは、ソースコードを解析して抽象構文木(AST)を構築します。ASTは、プログラムの構造を木構造で表現したものです。
go/ast
パッケージは、GoプログラムのASTを表現するためのデータ構造と、それを操作するための関数を提供します。- ASTの各ノード(例えば、関数宣言、変数宣言、インポート宣言など)は、ソースコード内の対応する位置情報(開始位置
Pos()
と終了位置End()
)を持っています。これらの位置情報は、token.Pos
型で表されます。 ast.ImportSpec
は、import "path/to/package"
のような個々のインポート宣言を表すASTノードです。
-
gofix
ツール:gofix
は、古いGoのコードを新しいGoのバージョンや慣習に合わせて自動的に修正するためのコマンドラインツールです。- Go言語の進化に伴い、APIの変更や構文の変更が行われることがありますが、
gofix
はこれらの変更に追従し、既存のコードベースを自動的に更新するのに役立ちます。 gofix
は内部的にgo/ast
パッケージを使用してソースコードのASTを解析し、変更を適用します。
-
token.Pos
とtoken.FileSet
:token.Pos
は、ソースコード内の特定の文字位置を表す型です。token.FileSet
は、複数のソースファイルにわたる位置情報を管理するための構造体です。これにより、異なるファイルや同じファイル内の異なる位置を正確に参照できます。
-
GoのIssueトラッカー:
- Go言語の開発は、GitHubのIssueトラッカー(以前はGoogle CodeのIssueトラッカー)で管理されています。
Fixes #XXXX
というコミットメッセージは、そのコミットが特定のIssue番号(XXXX)を修正したことを示します。
技術的詳細
このコミットの核心は、src/pkg/go/ast/import.go
ファイル内のsortSpecs
関数にあります。この関数は、インポート宣言のリスト(specs
)をソートする際に、各インポート宣言の範囲(posSpan
)を特定するために使用されます。
以前のコードでは、ImportSpec
の範囲を定義するposSpan
構造体のEnd
フィールドに、s.Pos() + 1
という値を使用していました。
// 変更前
// Cannot use s.End(), because it looks at len(s.Path.Value),
// and that string might have gotten longer or shorter.
// Instead, use s.Pos()+1, which is guaranteed to be > s.Pos()
// and still before the original end of the string, since any
// string literal must be at least 2 characters ("" or ``).
pos[i] = posSpan{s.Pos(), s.Pos() + 1}
このコメントは、s.End()
を使用しない理由として、「s.Path.Value
の長さに依存するため、文字列が長くなったり短くなったりした場合に問題が生じる可能性がある」と述べています。そして、「代わりにs.Pos() + 1
を使用する。これはs.Pos()
より大きく、かつ元の文字列の終わりより前にあることが保証される」と説明しています。しかし、このアプローチは、インポートパスの実際の長さや、インポート宣言に付随するコメントなどの要素を考慮していませんでした。結果として、gofix
のようなツールがASTを操作する際に、インポート宣言の正確な範囲を特定できず、誤ったコード生成を引き起こす原因となっていました。
このコミットでは、この行が以下のように変更されました。
// 変更後
pos[i] = posSpan{s.Pos(), s.End()}
この変更により、ImportSpec
ノードが持つ本来のEnd()
メソッドが返す正確な終了位置が使用されるようになりました。s.End()
は、Goパーサーがソースコードを解析する際に計算した、そのASTノードが占める実際の範囲の終了位置を返します。これには、インポートパスの文字列だけでなく、そのインポート宣言に関連する可能性のあるコメントや空白も含まれる場合があります。
この修正により、gofix
ツールはインポート文の正確な範囲を把握できるようになり、インポートの追加や削除、並べ替えといった操作をより安全かつ正確に行えるようになりました。
src/cmd/gofix/import_test.go
の変更は、この修正が正しく機能することを確認するためのテストケースの追加です。特に、addDelImportFn
という新しいテストヘルパー関数が導入され、これは特定のインポートを追加し、別のインポートを削除するという複合的なシナリオをテストします。これにより、インポート操作の堅牢性が向上したことを検証しています。
コアとなるコードの変更箇所
src/pkg/go/ast/import.go
--- a/src/pkg/go/ast/import.go
+++ b/src/pkg/go/ast/import.go
@@ -67,12 +67,7 @@ func sortSpecs(fset *token.FileSet, f *File, specs []Spec) {
// Record positions for specs.
pos := make([]posSpan, len(specs))
for i, s := range specs {
- // Cannot use s.End(), because it looks at len(s.Path.Value),
- // and that string might have gotten longer or shorter.
- // Instead, use s.Pos()+1, which is guaranteed to be > s.Pos()
- // and still before the original end of the string, since any
- // string literal must be at least 2 characters ("" or ``).
- pos[i] = posSpan{s.Pos(), s.Pos() + 1}
+ pos[i] = posSpan{s.Pos(), s.End()}
}
// Identify comments in this range.
src/cmd/gofix/import_test.go
このファイルでは、主にテストケースの追加と、新しいテストヘルパー関数addDelImportFn
の追加が行われています。
--- a/src/cmd/gofix/import_test.go
+++ b/src/cmd/gofix/import_test.go
@@ -351,7 +351,7 @@ var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18
`,
},
{
- Name: "import.3",
+ Name: "import.17",
Fn: addImportFn("x/y/z", "x/a/c"),
In: `package main
@@ -382,6 +382,26 @@ import (
"d/f"
)
+`,
+ },
+ {
+ Name: "import.18",
+ Fn: addDelImportFn("e", "o"),
+ In: `package main
+
+import (
+ "f"
+ "o"
+ "z"
+)
+`,
+ Out: `package main
+
+import (
+ "e"
+ "f"
+ "z"
+)
`,
},
}
@@ -409,6 +429,21 @@ func deleteImportFn(path string) func(*ast.File) bool {
}
}
+func addDelImportFn(p1 string, p2 string) func(*ast.File) bool {
+ return func(f *ast.File) bool {
+ fixed := false
+ if !imports(f, p1) {
+ addImport(f, p1)
+ fixed = true
+ }
+ if imports(f, p2) {
+ deleteImport(f, p2)
+ fixed = true
+ }
+ return fixed
+ }
+}
+
func rewriteImportFn(oldnew ...string) func(*ast.File) bool {
return func(f *ast.File) bool {
fixed := false
コアとなるコードの解説
src/pkg/go/ast/import.go
の変更
sortSpecs
関数は、Goソースファイル内のインポート宣言(import (...)
ブロック内の各行)を処理し、必要に応じてソートするために使用されます。この関数は、各ImportSpec
(個々のインポート宣言を表すASTノード)の開始位置と終了位置をposSpan
構造体として記録します。
変更前のコードでは、pos[i] = posSpan{s.Pos(), s.Pos() + 1}
としていました。これは、インポート宣言の開始位置s.Pos()
から1文字だけを範囲として捉えるという、非常に限定的なアプローチでした。この「+1」は、文字列リテラルが最低2文字(""
や``` `` )であるという仮定に基づいていたようですが、インポートパスの実際の長さや、インポート宣言に付随するコメント、空白文字などを考慮していませんでした。このため、
gofix`のようなツールがインポート宣言の正確な範囲を特定できず、コードの変更時に問題を引き起こしていました。
変更後のpos[i] = posSpan{s.Pos(), s.End()}
は、ImportSpec
ノードが持つ本来のEnd()
メソッドを使用しています。s.End()
は、Goパーサーがソースコードを解析する際に、そのASTノードが実際に占める範囲の正確な終了位置を計算して返します。これにより、インポート宣言の文字列全体、およびそれに付随する可能性のあるコメントや空白も正確に範囲として含まれるようになります。この正確な位置情報の利用が、gofix
がインポート文を正しく操作するための鍵となります。
src/cmd/gofix/import_test.go
の変更
このテストファイルでは、gofix
ツールのインポート修正機能のテストが行われています。
Name: "import.3"
がName: "import.17"
に変更され、新しいテストケースが追加されています。- 最も重要な変更は、
addDelImportFn
という新しいテストヘルパー関数の追加です。
この関数は、指定された2つのパスfunc addDelImportFn(p1 string, p2 string) func(*ast.File) bool { return func(f *ast.File) bool { fixed := false if !imports(f, p1) { // p1がインポートされていなければ追加 addImport(f, p1) fixed = true } if imports(f, p2) { // p2がインポートされていれば削除 deleteImport(f, p2) fixed = true } return fixed } }
p1
とp2
に対して、p1
をインポートに追加し、p2
をインポートから削除するという複合的な操作をテストします。これにより、インポートの追加と削除が同時に行われるシナリオでも、gofix
が正しく動作することを確認できます。 - 新しいテストケース
import.18
では、このaddDelImportFn
を使用して、"e"
を追加し、"o"
を削除するシナリオをテストしています。これにより、gofix
がインポートブロック内で複数の変更を正確に適用できることが検証されます。
これらのテストの追加は、go/ast/import.go
の変更が、gofix
のインポート操作の正確性と堅牢性を向上させたことを裏付けています。
関連リンク
- Go Issue 2566:
gofix
import
command addsimport
to wrong place - https://github.com/golang/go/issues/2566 (このコミットが修正したIssue) - Go CL 5541068:
go/ast
: respectImportSpec.EndPos
- https://golang.org/cl/5541068 (このコミットのGerritレビューページ) - Go
ast
パッケージのドキュメント: https://pkg.go.dev/go/ast - Go
token
パッケージのドキュメント: https://pkg.go.dev/go/token
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコードリポジトリ (GitHub)
- Go言語のIssueトラッカー (GitHub Issues)
- Go言語のGerritコードレビューシステム (golang.org/cl)
- Go言語のASTに関する一般的な解説記事やチュートリアル (Web検索)
gofix
ツールに関する情報 (Web検索)- Go言語の
token.Pos
とtoken.FileSet
に関する情報 (Web検索)