[インデックス 10471] ファイルの概要
このコミットは、Go言語のコード自動修正ツールである gofix
における2つの主要な改善を含んでいます。一つは、gofix
がパッケージのリネームを処理する際に発生していたソート順の誤りを修正することです。もう一つは、新しい import
パスを既存の import
宣言ブロックに挿入する際のヒューリスティックを改善し、より自然で整理された import
リストを生成するように変更することです。具体的には、新しい import
パスと最も長い共通プレフィックスを持つ既存の import
パスの隣に挿入されるようになります。
コミット
2e9d7a6d1cf16f80fec288cad0af03601f00e331
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2e9d7a6d1cf16f80fec288cad0af03601f00e331
元コミット内容
gofix: test and fix missorted renames
Also introduce a new insertion heuristic:
insert new import next to existing import
with the longest matching prefix.
R=golang-dev, adg, gri
CC=golang-dev
https://golang.org/cl/5412053
変更の背景
gofix
は、Go言語のバージョンアップやAPI変更に伴うコードの自動修正を支援するツールです。しかし、既存の実装にはいくつかの課題がありました。
- リネーム処理におけるソートの誤り:
gofix
がパッケージのリネーム(例:http
からnet/http
への変更)を処理する際に、import
宣言のソート順が崩れる問題がありました。これは、コードの可読性を損ない、開発者が手動で修正する必要がある場合がありました。 import
挿入の非効率性: 新しいimport
パスを追加する際、gofix
は単にimport
ブロックの末尾に追加するか、アルファベット順に挿入しようとしていました。しかし、これにより関連性の低いimport
パスが隣接して配置されたり、既存のimport
グループが分断されたりすることがありました。例えば、"net/http"
と"net/url"
のような関連性の高いパッケージが、間に別のパッケージが挿入されることで離れてしまう可能性がありました。
これらの問題は、gofix
の出力が期待通りに整理されず、開発者の手作業を増やしてしまう原因となっていました。このコミットは、これらの問題を解決し、gofix
の利便性と出力品質を向上させることを目的としています。特に、import
挿入のヒューリスティックの改善は、大規模なコードベースで import
宣言が多数存在する際に、より論理的で読みやすい import
ブロックを維持するために重要です。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連ツールの知識が必要です。
-
gofix
:gofix
は、Go言語のソースコードを自動的に修正するためのコマンドラインツールです。Go言語のバージョンアップに伴うAPIの変更や、一般的なコードの慣習に合わせるための修正(例:go fmt
では修正されないような、より意味的な変更)を行います。例えば、Go 1.0リリース前のAPI変更に対応するために広く使われました。gofix
はGoのast
(Abstract Syntax Tree) パッケージを利用してソースコードを解析し、ASTを操作することでコードを修正します。 -
Go言語の
import
宣言: Go言語では、他のパッケージの機能を利用するためにimport
宣言を使用します。import
宣言は、単一のパスを記述することもできますし、複数のパスを括弧で囲んでグループ化することもできます。import "fmt" // 単一インポート import ( "fmt" "net/http" "os" ) // グループインポート
慣習として、
import
パスはアルファベット順にソートされ、標準ライブラリ、サードパーティライブラリ、プロジェクト内のパッケージといったグループに分けられることが多いです。 -
Go言語の
ast
(Abstract Syntax Tree) パッケージ:go/ast
パッケージは、Go言語のソースコードを抽象構文木(AST)として表現するためのデータ構造と関数を提供します。ASTは、プログラムの構造を木構造で表現したもので、コンパイラやコード分析ツールがソースコードを理解し、操作するために使用します。gofix
はこのASTを読み込み、必要な変更をAST上で行い、最終的に修正されたASTから新しいソースコードを生成します。ast.File
: Goのソースファイル全体を表すASTのルートノード。ast.GenDecl
:import
,const
,var
,type
などの一般的な宣言を表すノード。ast.ImportSpec
: 個々のimport
宣言(例:"fmt"
)を表すノード。token.IMPORT
:import
キーワードを表すトークン。
-
go/token
パッケージ:go/token
パッケージは、Go言語のソースコードを構成するトークン(キーワード、識別子、演算子など)を定義します。ASTノードは、ソースコード内の位置情報(行番号、列番号など)をtoken.Pos
型で保持しており、これにより元のソースコードとの対応付けや、正確なコード生成が可能になります。
技術的詳細
このコミットの技術的詳細は、主に gofix
の addImport
関数における import
挿入ロジックの改善と、リネーム処理のテスト強化にあります。
1. import
挿入の新しいヒューリスティック (matchLen
関数)
以前の addImport
関数は、新しい import
パスを既存の import
ブロックに挿入する際に、単純にアルファベット順に挿入しようとしていました。しかし、これは必ずしも最適な配置とは限りませんでした。例えば、"net/http"
と "net/url"
が既に存在する場合に "net/rpc"
を追加する際、アルファベット順では http
と url
の間に rpc
が挿入されるかもしれませんが、より関連性の高い net
パッケージ群の近くに配置される方が望ましい場合があります。
このコミットでは、新しい matchLen
関数が導入されました。
// matchLen returns the length of the longest prefix shared by x and y.
func matchLen(x, y string) int {
i := 0
for i < len(x) && i < len(y) && x[i] == y[i] {
i++
}
return i
}
この関数は、2つの文字列(ここでは import
パス)の最長共通プレフィックスの長さを返します。例えば、matchLen("net/http", "net/url")
は 4
("net/"
の長さ) を返します。
addImport
関数は、この matchLen
関数を使用して、新しい import
パス (ipath
) を挿入する最適な位置を決定します。具体的には、既存の import
宣言ブロック内の各 import
パスと ipath
との matchLen
を計算し、最も長い共通プレフィックスを持つ import
パスの直後に新しい import
を挿入します。これにより、関連性の高い import
パスがグループ化され、コードの可読性が向上します。
もし、最適な挿入位置が見つからない場合(例えば、既存の import
が全くない場合や、共通プレフィックスが全くない場合)、新しい import
は import
ブロックの末尾に追加されます。
2. import
宣言ブロックの処理の改善
addImport
関数は、import "C"
のような特殊な import
宣言をスキップするようになりました。これは、import "C"
がCgo(GoとC言語の相互運用)において特別な意味を持ち、そのドキュメントコメントとの関連性を壊さないようにするためです。
また、import
ブロックが単一の import
宣言から複数の import
宣言に変わる際に、括弧 (()
) が適切に追加されるように修正されました。これは、ast.GenDecl.Lparen
フィールドを適切に設定することで実現されます。
3. リネーム処理のテスト強化
src/cmd/gofix/go1pkgrename_test.go
に新しいテストケースが追加されました。これは、http
から net/http
、url
から net/url
へのリネームが、既存の import
ブロック内で正しく処理され、ソート順が維持されることを確認するためのものです。これにより、「missorted renames」の問題が修正されたことが検証されます。
4. import_test.go
の改善
src/cmd/gofix/import_test.go
では、addImportFn
ヘルパー関数が複数の import
パスを受け取れるように変更され、複数の import
を追加するテストケースが追加されました。特に、import.3
という新しいテストケースは、"x/y/z"
と "x/a/c"
という2つの import
を、既存の import
ブロック("a"
, "b"
, "x/w"
, "d/f"
を含む)に挿入するシナリオをテストしています。このテストは、新しい matchLen
ヒューリスティックが正しく機能し、"x/a/c"
と "x/y/z"
が "x/w"
の近くに、かつアルファベット順に挿入されることを検証します。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/cmd/gofix/fix.go
ファイルの addImport
関数とその周辺に集中しています。
-
src/cmd/gofix/fix.go
:matchLen
関数の追加 (L554-L560)addImport
関数のロジック変更 (L572-L629):bestMatch
,impDecl
,impIndex
変数の導入。- 既存の
import
宣言ブロックを走査し、matchLen
を使って最適な挿入位置 (impIndex
) を見つけるロジックの追加。 import "C"
を含むGenDecl
をスキップするロジックの追加。- 新しい
import
をimpDecl.Specs
に挿入する際のinsertAt
の計算方法の変更。 - 挿入された
newImport
のValuePos
とEndPos
を、前のimport
と同じ位置に設定することで、ソーターが同じブロック内にあると認識するように修正。
rewriteImport
関数内でimp.EndPos = imp.End()
を追加 (L685): これは、リネームされたimport
のEndPos
を正しく更新し、gofix
がimport
パスの長さに応じてEndPos
を再計算するデフォルトの動作を上書きするためのものです。
-
src/cmd/gofix/go1pkgrename_test.go
:go1rename.2
という新しいテストケースの追加 (L93-L116)。これは、http
とurl
のリネームがimport
ブロック内で正しく処理されることをテストします。
-
src/cmd/gofix/import_test.go
:addImportFn
関数が可変長引数 (path ...string
) を受け取るように変更 (L348)。import.3
という新しいテストケースの追加 (L350-L379)。これは、複数のimport
パスが新しいヒューリスティックに従って挿入されることをテストします。
コアとなるコードの解説
matchLen
関数
// matchLen returns the length of the longest prefix shared by x and y.
func matchLen(x, y string) int {
i := 0
for i < len(x) && i < len(y) && x[i] == y[i] {
i++
}
return i
}
この関数は非常にシンプルですが、新しい import
挿入ヒューリスティックの基盤となります。2つの文字列 x
と y
を文字ごとに比較し、一致する文字が続く限りインデックス i
をインクリメントします。最初に一致しなくなった文字のインデックスが、最長共通プレフィックスの長さとなります。
addImport
関数の変更点
addImport
関数は、Goソースファイル (*ast.File
) に新しい import
パス (ipath
) を追加する主要な関数です。変更後のロジックの核心は、最適な import
宣言ブロック (impDecl
) とその中の挿入位置 (impIndex
) を見つける部分です。
var (
bestMatch = -1 // 最長共通プレフィックスの長さ
lastImport = -1 // 最後に発見されたimport宣言のインデックス
impDecl *ast.GenDecl // 最適なimport宣言ブロック
impIndex = -1 // 最適な挿入位置(impDecl.Specs内のインデックス)
)
for i, decl := range f.Decls { // ファイル内の全ての宣言を走査
gen, ok := decl.(*ast.GenDecl)
if ok && gen.Tok == token.IMPORT { // import宣言ブロックであるかチェック
lastImport = i // 最後に発見されたimport宣言のインデックスを更新
// "C" importはスキップ(Cgoとの関連性を壊さないため)
if declImports(gen, "C") {
continue
}
// このブロック内の各importと新しいipathの最長共通プレフィックスを計算
for j, spec := range gen.Specs {
impspec := spec.(*ast.ImportSpec)
n := matchLen(importPath(impspec), ipath) // matchLenで共通プレフィックス長を計算
if n > bestMatch { // より長い共通プレフィックスが見つかった場合
bestMatch = n // bestMatchを更新
impDecl = gen // impDeclをこのGenDeclに設定
impIndex = j // impIndexをこのimportSpecのインデックスに設定
}
}
}
}
// import宣言ブロックが見つからなかった場合、新しいブロックを作成
if impDecl == nil {
impDecl = &ast.GenDecl{
Tok: token.IMPORT,
}
f.Decls = append(f.Decls, nil)
copy(f.Decls[lastImport+2:], f.Decls[lastImport+1:])
f.Decls[lastImport+1] = impDecl
}
// 必要であれば、import宣言ブロックに括弧を追加
if len(impDecl.Specs) > 0 && !impDecl.Lparen.IsValid() {
impDecl.Lparen = impDecl.Pos()
}
// 新しいimportを挿入する位置を決定
insertAt := impIndex + 1 // 最長共通プレフィックスを持つimportの直後
if insertAt == 0 { // 共通プレフィックスが見つからなかった場合(impIndexが-1のまま)
insertAt = len(impDecl.Specs) // ブロックの末尾に挿入
}
impDecl.Specs = append(impDecl.Specs, nil)
copy(impDecl.Specs[insertAt+1:], impDecl.Specs[insertAt:])
impDecl.Specs[insertAt] = newImport // 新しいimportを挿入
// 挿入されたimportのPosを前のimportと同じに設定
// これにより、gofmtなどのツールが同じブロックとして認識し、ソートが正しく行われる
if insertAt > 0 {
prev := impDecl.Specs[insertAt-1]
newImport.Path.ValuePos = prev.Pos()
newImport.EndPos = prev.Pos()
}
このロジックにより、gofix
は新しい import
を追加する際に、単なるアルファベット順ではなく、既存の import
パスとの関連性(共通プレフィックスの長さ)を考慮して、より論理的な位置に挿入できるようになりました。これにより、生成されるコードの可読性と保守性が向上します。
関連リンク
- Gerrit Change-ID:
https://golang.org/cl/5412053
(GoプロジェクトのコードレビューシステムであるGerritの変更セットへのリンク)
参考にした情報源リンク
- Go言語の
go/ast
パッケージ: https://pkg.go.dev/go/ast - Go言語の
go/token
パッケージ: https://pkg.go.dev/go/token gofix
の概要 (Go Wiki): https://go.dev/wiki/Gofix- Go言語の
import
宣言: https://go.dev/doc/effective_go#imports - Go言語のソースコード解析とAST (参考記事): (一般的なGo AST解析に関する記事を検索し、適切なものを選択)
- 例: https://yourbasic.org/golang/ast-parser-source-code/ (これは一般的な例であり、特定のコミットに直接関連するものではありませんが、ASTの概念を理解するのに役立ちます)
- Go言語の
gofix
コマンド: https://pkg.go.dev/cmd/gofix (Go 1.0以降はgo tool fix
に統合されていますが、基本的な機能は同じです)# [インデックス 10471] ファイルの概要
このコミットは、Go言語のコード自動修正ツールである gofix
における2つの主要な改善を含んでいます。一つは、gofix
がパッケージのリネームを処理する際に発生していたソート順の誤りを修正することです。もう一つは、新しい import
パスを既存の import
宣言ブロックに挿入する際のヒューリスティックを改善し、より自然で整理された import
リストを生成するように変更することです。具体的には、新しい import
パスと最も長い共通プレフィックスを持つ既存の import
パスの隣に挿入されるようになります。
コミット
2e9d7a6d1cf16f80fec288cad0af03601f00e331
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2e9d7a6d1cf16f80fec288cad0af03601f00e331
元コミット内容
gofix: test and fix missorted renames
Also introduce a new insertion heuristic:
insert new import next to existing import
with the longest matching prefix.
R=golang-dev, adg, gri
CC=golang-dev
https://golang.org/cl/5412053
変更の背景
gofix
は、Go言語のバージョンアップやAPI変更に伴うコードの自動修正を支援するツールです。しかし、既存の実装にはいくつかの課題がありました。
- リネーム処理におけるソートの誤り:
gofix
がパッケージのリネーム(例:http
からnet/http
への変更)を処理する際に、import
宣言のソート順が崩れる問題がありました。これは、コードの可読性を損ない、開発者が手動で修正する必要がある場合がありました。 import
挿入の非効率性: 新しいimport
パスを追加する際、gofix
は単にimport
ブロックの末尾に追加するか、アルファベット順に挿入しようとしていました。しかし、これにより関連性の低いimport
パスが隣接して配置されたり、既存のimport
グループが分断されたりすることがありました。例えば、"net/http"
と"net/url"
のような関連性の高いパッケージが、間に別のパッケージが挿入されることで離れてしまう可能性がありました。
これらの問題は、gofix
の出力が期待通りに整理されず、開発者の手作業を増やしてしまう原因となっていました。このコミットは、これらの問題を解決し、gofix
の利便性と出力品質を向上させることを目的としています。特に、import
挿入のヒューリスティックの改善は、大規模なコードベースで import
宣言が多数存在する際に、より論理的で読みやすい import
ブロックを維持するために重要です。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連ツールの知識が必要です。
-
gofix
:gofix
は、Go言語のソースコードを自動的に修正するためのコマンドラインツールです。Go言語のバージョンアップに伴うAPIの変更や、一般的なコードの慣習に合わせるための修正(例:go fmt
では修正されないような、より意味的な変更)を行います。例えば、Go 1.0リリース前のAPI変更に対応するために広く使われました。gofix
はGoのast
(Abstract Syntax Tree) パッケージを利用してソースコードを解析し、ASTを操作することでコードを修正します。 -
Go言語の
import
宣言: Go言語では、他のパッケージの機能を利用するためにimport
宣言を使用します。import
宣言は、単一のパスを記述することもできますし、複数のパスを括弧で囲んでグループ化することもできます。import "fmt" // 単一インポート import ( "fmt" "net/http" "os" ) // グループインポート
慣習として、
import
パスはアルファベット順にソートされ、標準ライブラリ、サードパーティライブラリ、プロジェクト内のパッケージといったグループに分けられることが多いです。 -
Go言語の
ast
(Abstract Syntax Tree) パッケージ:go/ast
パッケージは、Go言語のソースコードを抽象構文木(AST)として表現するためのデータ構造と関数を提供します。ASTは、プログラムの構造を木構造で表現したもので、コンパイラやコード分析ツールがソースコードを理解し、操作するために使用します。gofix
はこのASTを読み込み、必要な変更をAST上で行い、最終的に修正されたASTから新しいソースコードを生成します。ast.File
: Goのソースファイル全体を表すASTのルートノード。ast.GenDecl
:import
,const
,var
,type
などの一般的な宣言を表すノード。ast.ImportSpec
: 個々のimport
宣言(例:"fmt"
)を表すノード。token.IMPORT
:import
キーワードを表すトークン。
-
go/token
パッケージ:go/token
パッケージは、Go言語のソースコードを構成するトークン(キーワード、識別子、演算子など)を定義します。ASTノードは、ソースコード内の位置情報(行番号、列番号など)をtoken.Pos
型で保持しており、これにより元のソースコードとの対応付けや、正確なコード生成が可能になります。
技術的詳細
このコミットの技術的詳細は、主に gofix
の addImport
関数における import
挿入ロジックの改善と、リनेーム処理のテスト強化にあります。
1. import
挿入の新しいヒューリスティック (matchLen
関数)
以前の addImport
関数は、新しい import
パスを既存の import
ブロックに挿入する際に、単純にアルファベット順に挿入しようとしていました。しかし、これは必ずしも最適な配置とは限りませんでした。例えば、"net/http"
と "net/url"
が既に存在する場合に "net/rpc"
を追加する際、アルファベット順では http
と url
の間に rpc
が挿入されるかもしれませんが、より関連性の高い net
パッケージ群の近くに配置される方が望ましい場合があります。
このコミットでは、新しい matchLen
関数が導入されました。
// matchLen returns the length of the longest prefix shared by x and y.
func matchLen(x, y string) int {
i := 0
for i < len(x) && i < len(y) && x[i] == y[i] {
i++
}
return i
}
この関数は、2つの文字列(ここでは import
パス)の最長共通プレフィックスの長さを返します。例えば、matchLen("net/http", "net/url")
は 4
("net/"
の長さ) を返します。
addImport
関数は、この matchLen
関数を使用して、新しい import
パス (ipath
) を挿入する最適な位置を決定します。具体的には、既存の import
宣言ブロック内の各 import
パスと ipath
との matchLen
を計算し、最も長い共通プレフィックスを持つ import
パスの直後に新しい import
を挿入します。これにより、関連性の高い import
パスがグループ化され、コードの可読性が向上します。
もし、最適な挿入位置が見つからない場合(例えば、既存の import
が全くない場合や、共通プレフィックスが全くない場合)、新しい import
は import
ブロックの末尾に追加されます。
2. import
宣言ブロックの処理の改善
addImport
関数は、import "C"
のような特殊な import
宣言をスキップするようになりました。これは、import "C"
がCgo(GoとC言語の相互運用)において特別な意味を持ち、そのドキュメントコメントとの関連性を壊さないようにするためです。
また、import
ブロックが単一の import
宣言から複数の import
宣言に変わる際に、括弧 (()
) が適切に追加されるように修正されました。これは、ast.GenDecl.Lparen
フィールドを適切に設定することで実現されます。
3. リネーム処理のテスト強化
src/cmd/gofix/go1pkgrename_test.go
に新しいテストケースが追加されました。これは、http
から net/http
、url
から net/url
へのリネームが、既存の import
ブロック内で正しく処理され、ソート順が維持されることを確認するためのものです。これにより、「missorted renames」の問題が修正されたことが検証されます。
4. import_test.go
の改善
src/cmd/gofix/import_test.go
では、addImportFn
ヘルパー関数が複数の import
パスを受け取れるように変更され、複数の import
を追加するテストケースが追加されました。特に、import.3
という新しいテストケースは、"x/y/z"
と "x/a/c"
という2つの import
を、既存の import
ブロック("a"
, "b"
, "x/w"
, "d/f"
を含む)に挿入するシナリオをテストしています。このテストは、新しい matchLen
ヒューリスティックが正しく機能し、"x/a/c"
と "x/y/z"
が "x/w"
の近くに、かつアルファベット順に挿入されることを検証します。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/cmd/gofix/fix.go
ファイルの addImport
関数とその周辺に集中しています。
-
src/cmd/gofix/fix.go
:matchLen
関数の追加 (L554-L560)addImport
関数のロジック変更 (L572-L629):bestMatch
,impDecl
,impIndex
変数の導入。- 既存の
import
宣言ブロックを走査し、matchLen
を使って最適な挿入位置 (impIndex
) を見つけるロジックの追加。 import "C"
を含むGenDecl
をスキップするロジックの追加。- 新しい
import
をimpDecl.Specs
に挿入する際のinsertAt
の計算方法の変更。 - 挿入された
newImport
のValuePos
とEndPos
を、前のimport
と同じ位置に設定することで、ソーターが同じブロック内にあると認識するように修正。
rewriteImport
関数内でimp.EndPos = imp.End()
を追加 (L685): これは、リネームされたimport
のEndPos
を正しく更新し、gofix
がimport
パスの長さに応じてEndPos
を再計算するデフォルトの動作を上書きするためのものです。
-
src/cmd/gofix/go1pkgrename_test.go
:go1rename.2
という新しいテストケースの追加 (L93-L116)。これは、http
とurl
のリネームがimport
ブロック内で正しく処理されることをテストします。
-
src/cmd/gofix/import_test.go
:addImportFn
関数が可変長引数 (path ...string
) を受け取るように変更 (L348)。import.3
という新しいテストケースの追加 (L350-L379)。これは、複数のimport
パスが新しいヒューリスティックに従って挿入されることをテストします。
コアとなるコードの解説
matchLen
関数
// matchLen returns the length of the longest prefix shared by x and y.
func matchLen(x, y string) int {
i := 0
for i < len(x) && i < len(y) && x[i] == y[i] {
i++
}
return i
}
この関数は非常にシンプルですが、新しい import
挿入ヒューリスティックの基盤となります。2つの文字列 x
と y
を文字ごとに比較し、一致する文字が続く限りインデックス i
をインクリメントします。最初に一致しなくなった文字のインデックスが、最長共通プレフィックスの長さとなります。
addImport
関数の変更点
addImport
関数は、Goソースファイル (*ast.File
) に新しい import
パス (ipath
) を追加する主要な関数です。変更後のロジックの核心は、最適な import
宣言ブロック (impDecl
) とその中の挿入位置 (impIndex
) を見つける部分です。
var (
bestMatch = -1 // 最長共通プレフィックスの長さ
lastImport = -1 // 最後に発見されたimport宣言のインデックス
impDecl *ast.GenDecl // 最適なimport宣言ブロック
impIndex = -1 // 最適な挿入位置(impDecl.Specs内のインデックス)
)
for i, decl := range f.Decls { // ファイル内の全ての宣言を走査
gen, ok := decl.(*ast.GenDecl)
if ok && gen.Tok == token.IMPORT { // import宣言ブロックであるかチェック
lastImport = i // 最後に発見されたimport宣言のインデックスを更新
// "C" importはスキップ(Cgoとの関連性を壊さないため)
if declImports(gen, "C") {
continue
}
// このブロック内の各importと新しいipathの最長共通プレフィックスを計算
for j, spec := range gen.Specs {
impspec := spec.(*ast.ImportSpec)
n := matchLen(importPath(impspec), ipath) // matchLenで共通プレフィックス長を計算
if n > bestMatch { // より長い共通プレフィックスが見つかった場合
bestMatch = n // bestMatchを更新
impDecl = gen // impDeclをこのGenDeclに設定
impIndex = j // impIndexをこのimportSpecのインデックスに設定
}
}
}
}
// import宣言ブロックが見つからなかった場合、新しいブロックを作成
if impDecl == nil {
impDecl = &ast.GenDecl{
Tok: token.IMPORT,
}
f.Decls = append(f.Decls, nil)
copy(f.Decls[lastImport+2:], f.Decls[lastImport+1:])
f.Decls[lastImport+1] = impDecl
}
// 必要であれば、import宣言ブロックに括弧を追加
if len(impDecl.Specs) > 0 && !impDecl.Lparen.IsValid() {
impDecl.Lparen = impDecl.Pos()
}
// 新しいimportを挿入する位置を決定
insertAt := impIndex + 1 // 最長共通プレフィックスを持つimportの直後
if insertAt == 0 { // 共通プレフィックスが見つからなかった場合(impIndexが-1のまま)
insertAt = len(impDecl.Specs) // ブロックの末尾に挿入
}
impDecl.Specs = append(impDecl.Specs, nil)
copy(impDecl.Specs[insertAt+1:], impDecl.Specs[insertAt:])
impDecl.Specs[insertAt] = newImport // 新しいimportを挿入
// 挿入されたimportのPosを前のimportと同じに設定
// これにより、gofmtなどのツールが同じブロックとして認識し、ソートが正しく行われる
if insertAt > 0 {
prev := impDecl.Specs[insertAt-1]
newImport.Path.ValuePos = prev.Pos()
newImport.EndPos = prev.Pos()
}
このロジックにより、gofix
は新しい import
を追加する際に、単なるアルファベット順ではなく、既存の import
パスとの関連性(共通プレフィックスの長さ)を考慮して、より論理的な位置に挿入できるようになりました。これにより、生成されるコードの可読性と保守性が向上します。
関連リンク
- Gerrit Change-ID:
https://golang.org/cl/5412053
(GoプロジェクトのコードレビューシステムであるGerritの変更セットへのリンク)
参考にした情報源リンク
- Go言語の
go/ast
パッケージ: https://pkg.go.dev/go/ast - Go言語の
go/token
パッケージ: https://pkg.go.dev/go/token gofix
の概要 (Go Wiki): https://go.dev/wiki/Gofix- Go言語の
import
宣言: https://go.dev/doc/effective_go#imports - Go言語のソースコード解析とAST (参考記事): (一般的なGo AST解析に関する記事を検索し、適切なものを選択)
- 例: https://yourbasic.org/golang/ast-parser-source-code/ (これは一般的な例であり、特定のコミットに直接関連するものではありませんが、ASTの概念を理解するのに役立ちます)
- Go言語の
gofix
コマンド: https://pkg.go.dev/cmd/gofix (Go 1.0以降はgo tool fix
に統合されていますが、基本的な機能は同じです)