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

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

このコミットは、Go言語の公式フォーマッタであるgofmtのインポート文のソートと重複排除のロジックを改善するものです。具体的には、インポートパスだけでなく、インポート名やコメントも考慮したより詳細なソート順を導入し、コメントのない重複インポートを削除する機能が追加されました。

コミット

  • コミットハッシュ: 08925ce6ee16be5a3b937c0d55c2548bf30c5776
  • 作者: Josh Bleecher Snyder josharian@gmail.com
  • コミット日時: 2013年9月6日 金曜日 16:25:15 -0400

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

https://github.com/golang/go/commit/08925ce6ee16be5a3b937c0d55c2548bf30c5776

元コミット内容

cmd/gofmt: sort more, remove some duplicate imports

* Sort imports by import path, then import name, then comment. Currently, gofmt sorts only by import path.
* If two imports have the same import path and import name, and one of them has no comment, remove the import with no comment. (See the discussion at issue 4414.)

Based on @rsc's https://golang.org/cl/7231070/

Fixes #4414.

R=gri, rsc
CC=golang-dev
https://golang.org/cl/12837044

変更の背景

Go言語では、コードの可読性と一貫性を保つために、gofmtというツールが広く利用されています。gofmtは、Goのソースコードを自動的にフォーマットし、Goコミュニティ全体で統一されたコーディングスタイルを強制します。このツールは、インデント、スペース、改行などの基本的なフォーマットだけでなく、インポート文の整理も行います。

このコミット以前のgofmtは、インポート文をインポートパス(例: "fmt", "net/http")に基づいてソートしていました。しかし、これだけでは不十分なケースがありました。例えば、同じインポートパスを持つ複数のインポート文が存在する場合(エイリアス付きインポートやブランクインポートなど)、それらの相対的な順序は未定義であり、gofmtを実行するたびに順序が変わる可能性がありました。これは、バージョン管理システムでの不要な差分を生み出し、コードレビューを複雑にする原因となっていました。

また、開発者が誤って同じパッケージを複数回インポートしてしまうケースも考えられます。特に、エイリアス付きインポートと通常のインポートが混在する場合などです。このような重複は、コードの冗長性を高め、可読性を損ないます。

このコミットは、これらの問題を解決するために提案されました。コミットメッセージで言及されているissue 4414は、おそらくこれらのインポートのソートと重複排除に関する議論が行われたGoのバグトラッキングシステム上の課題を指していると考えられます。Goのissueトラッカーは進化しており、古いissue番号は直接検索できない場合がありますが、コミットメッセージにその内容が明確に記述されています。

前提知識の解説

gofmt

gofmtは、Go言語のソースコードを自動的にフォーマットするツールです。Goの標準ライブラリの一部として提供されており、Go開発者にとって不可欠なツールとなっています。gofmtは、Goのコードベース全体で一貫したスタイルを維持し、コードレビューの負担を軽減し、可読性を向上させることを目的としています。

Goのインポート文

Goのインポート文は、他のパッケージの機能を利用するために使用されます。基本的な形式は以下の通りです。

import "path/to/package"

インポート文には、いくつかのバリエーションがあります。

  • エイリアス付きインポート: パッケージ名を変更してインポートします。
    import a "path/to/package" // package.Func() ではなく a.Func() でアクセス
    
  • ブランクインポート: パッケージの初期化処理のみを実行し、パッケージ内の識別子を直接使用しない場合に用います。
    import _ "path/to/package" // パッケージのinit関数が実行される
    
  • ドットインポート: パッケージの識別子を修飾なしで直接使用できるようにします。非推奨とされることが多いです。
    import . "path/to/package" // package.Func() ではなく Func() でアクセス
    
  • コメント付きインポート: インポート文にコメントを付加することができます。
    import "fmt" // for printing
    

go/astパッケージ

go/astパッケージは、Goのソースコードの抽象構文木(AST: Abstract Syntax Tree)を表現するためのデータ構造と関数を提供します。Goのコンパイラやツール(gofmtなど)は、ソースコードをASTにパースし、ASTを操作することでコードの解析や変換を行います。このコミットでは、インポート文を表すASTノード(ast.ImportSpec)を操作しています。

go/tokenパッケージ

go/tokenパッケージは、Goのソースコード内のトークン(キーワード、識別子、演算子など)と、それらのソースコード上の位置(ファイル名、行番号、列番号、オフセット)を管理するためのデータ構造を提供します。gofmtのようなツールは、コードのフォーマット時に正確な位置情報を必要とします。このコミットでは、token.FileSetを使用してファイル内の行を削除する機能が追加されています。

技術的詳細

このコミットの主要な変更点は、gofmtがインポート文をソートおよび重複排除する方法にあります。

インポートのソート順の拡張

以前のgofmtは、インポートパスのみに基づいてインポート文をソートしていました。このコミットでは、ソートの優先順位が以下のように拡張されました。

  1. インポートパス (import path): 最も優先されるソートキーです。例えば、"fmt""net/http"より前に来ます。
  2. インポート名 (import name): 同じインポートパスを持つインポート文がある場合、エイリアス名(例: a "path/to/package"a)やブランクインポート(_)、ドットインポート(.)の順でソートされます。エイリアスがない場合は空文字列として扱われます。
  3. コメント (comment): インポートパスとインポート名が同じ場合、コメントの内容に基づいてソートされます。コメントがない場合は空文字列として扱われます。

この新しいソートロジックは、src/pkg/go/ast/import.go内のbyImportSpec型に実装されています。Lessメソッドがこの新しい比較ロジックを定義しています。

重複インポートの削除

このコミットでは、特定の条件を満たす重複インポート文を削除する機能も追加されました。削除の条件は以下の通りです。

  • 2つのインポート文が同じインポートパスを持ち、かつ同じインポート名を持つ。
  • そのうちの一方のインポート文にコメントがない

この条件が満たされる場合、コメントのない方のインポート文が削除されます。これは、コメントがインポートの意図や使用方法に関する重要な情報を含んでいる可能性があるため、コメント付きのインポートを優先するという設計思想に基づいています。このロジックは、collapse関数によって実装されています。

重複排除は、sortSpecs関数内でソート後に行われます。ソートによって同じインポートパスとインポート名を持つインポート文が隣接するため、効率的に重複を検出できます。

token.Fileへの行削除機能の追加

重複インポートを削除する際、単にASTからノードを削除するだけでは、ソースコード上の対応する行が残ってしまう可能性があります。これを解決するため、src/pkg/go/token/position.goRemoveLineという新しいメソッドがtoken.Fileに追加されました。このメソッドは、指定された行番号の行をファイルから削除し、後続の行を詰めることで、ソースコードの正確な表現を維持します。

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

このコミットによって以下のファイルが変更されました。

  • src/cmd/gofmt/testdata/import.golden: gofmtのテストデータのうち、期待される出力(ゴールデンファイル)が更新されました。新しいソート順と重複排除の結果が反映されています。
  • src/cmd/gofmt/testdata/import.input: gofmtのテストデータのうち、入力ファイルが更新されました。新しいソートと重複排除のテストケースが追加されています。
  • src/pkg/go/ast/import.go: インポートのソートと重複排除の主要なロジックが実装されているファイルです。SortImports関数が大幅に修正され、sortSpecs関数が新しいソートロジックと重複排除ロジックを含むように変更されました。また、インポート名とコメントを取得するためのヘルパー関数importNameimportComment、そして重複を判定するcollapse関数が追加されました。
  • src/pkg/go/token/position.go: token.File型にRemoveLineメソッドが追加されました。このメソッドは、ソースコードから特定の行を削除するために使用されます。

コアとなるコードの解説

src/pkg/go/ast/import.go

SortImports関数

この関数は、ファイルのASTを受け取り、その中のすべてのインポート宣言ブロックを走査します。各インポート宣言ブロック(GenDecl)に対して、連続するインポート行のまとまり("run")を識別し、それぞれをsortSpecs関数に渡してソートと重複排除を行います。

変更点として、sortSpecsの戻り値が[]Specとなり、重複排除されたスペックのリストを返すようになりました。これにより、d.Specsが更新され、重複が取り除かれたインポート文のリストが設定されます。

また、重複排除によってインポートブロックの最後に空白行ができてしまう場合があるため、Rparenの位置を調整して空白行を削除するロジックが追加されました。これは、fset.File(d.Rparen).RemoveLine(rParenLine - 1)によって行われます。

importName関数とimportComment関数

これらのヘルパー関数は、ast.ImportSpecからそれぞれインポート名(エイリアス名、ブランクインポート、ドットインポート)とコメントのテキストを安全に取得するために追加されました。nilチェックを行い、存在しない場合は空文字列を返します。

collapse関数

この関数は、2つのSpec(インポート文)を受け取り、prevnextによって削除可能かどうかを判定します。以下の条件がすべて真の場合にtrueを返します。

  1. nextprevのインポートパスが同じである。
  2. nextprevのインポート名が同じである。
  3. prevのインポート文にコメントがない。

この関数は、重複排除ロジックの核心部分を担っています。

sortSpecs関数

この関数は、インポート文のSpecのスライスを受け取り、ソートと重複排除を行います。

  1. ソート: sort.Sort(byImportSpec(specs))を呼び出し、byImportSpec型で定義された新しいソート順(インポートパス、インポート名、コメントの順)でインポート文をソートします。
  2. 重複排除: ソートされたspecsスライスを走査し、隣接するインポート文に対してcollapse関数を適用します。collapse(s, specs[i+1])trueの場合、つまりsspecs[i+1]によって削除可能である場合、sに対応するソースコード上の行をfset.File(p).RemoveLine(...)で削除し、sdedupedスライスに追加しません。これにより、重複するインポート文が取り除かれます。
  3. コメント位置の修正: インポート文のソートや削除によって、コメントの位置がずれる可能性があるため、コメントのASTノードの開始位置と終了位置を再調整します。

この関数は、ソートと重複排除の両方を担当する重要な部分です。

byImportSpec

sort.Interfaceインターフェースを実装しており、Len, Swap, Lessメソッドを提供します。Lessメソッドが、インポートパス、インポート名、コメントの順でインポート文を比較する新しいロジックを定義しています。

src/pkg/go/token/position.go

RemoveLineメソッド

File型に新しく追加されたメソッドです。指定されたline番号(1-based)の行をファイルから削除します。内部的には、f.linesスライス(各行の開始オフセットを格納)から該当するオフセットを削除し、後続のオフセットをシフトすることで、行の削除を実現しています。このメソッドは、SortImports関数内で重複するインポート文の行をソースコードから物理的に削除するために使用されます。

関連リンク

参考にした情報源リンク

  • コミットメッセージの内容
  • Go言語のgo/astパッケージとgo/tokenパッケージのドキュメント(Goの公式ドキュメント)
  • Go言語のgofmtツールの一般的な動作に関する知識

(注: コミットメッセージで言及されているissue 4414については、直接的なWeb検索では詳細な情報を見つけることができませんでした。これは、Goのissueトラッカーのシステム変更や、非常に古いissueであるため公開されていない可能性があるためです。しかし、コミットメッセージ自体にその内容が明確に記述されているため、その情報に基づいて解説を行いました。)