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

[インデックス 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言語の概念とツールに関する知識が必要です。

  1. Go言語のAST (Abstract Syntax Tree):

    • Goコンパイラは、ソースコードを解析して抽象構文木(AST)を構築します。ASTは、プログラムの構造を木構造で表現したものです。
    • go/astパッケージは、GoプログラムのASTを表現するためのデータ構造と、それを操作するための関数を提供します。
    • ASTの各ノード(例えば、関数宣言、変数宣言、インポート宣言など)は、ソースコード内の対応する位置情報(開始位置Pos()と終了位置End())を持っています。これらの位置情報は、token.Pos型で表されます。
    • ast.ImportSpecは、import "path/to/package"のような個々のインポート宣言を表すASTノードです。
  2. gofixツール:

    • gofixは、古いGoのコードを新しいGoのバージョンや慣習に合わせて自動的に修正するためのコマンドラインツールです。
    • Go言語の進化に伴い、APIの変更や構文の変更が行われることがありますが、gofixはこれらの変更に追従し、既存のコードベースを自動的に更新するのに役立ちます。
    • gofixは内部的にgo/astパッケージを使用してソースコードのASTを解析し、変更を適用します。
  3. token.Postoken.FileSet:

    • token.Posは、ソースコード内の特定の文字位置を表す型です。
    • token.FileSetは、複数のソースファイルにわたる位置情報を管理するための構造体です。これにより、異なるファイルや同じファイル内の異なる位置を正確に参照できます。
  4. 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という新しいテストヘルパー関数の追加です。
    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
        }
    }
    
    この関数は、指定された2つのパスp1p2に対して、p1をインポートに追加し、p2をインポートから削除するという複合的な操作をテストします。これにより、インポートの追加と削除が同時に行われるシナリオでも、gofixが正しく動作することを確認できます。
  • 新しいテストケースimport.18では、このaddDelImportFnを使用して、"e"を追加し、"o"を削除するシナリオをテストしています。これにより、gofixがインポートブロック内で複数の変更を正確に適用できることが検証されます。

これらのテストの追加は、go/ast/import.goの変更が、gofixのインポート操作の正確性と堅牢性を向上させたことを裏付けています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコードリポジトリ (GitHub)
  • Go言語のIssueトラッカー (GitHub Issues)
  • Go言語のGerritコードレビューシステム (golang.org/cl)
  • Go言語のASTに関する一般的な解説記事やチュートリアル (Web検索)
  • gofixツールに関する情報 (Web検索)
  • Go言語のtoken.Postoken.FileSetに関する情報 (Web検索)