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

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

このドキュメントは、Go言語のツールであるgofmtgofixにおけるインポートのソート機能追加に関するコミット(インデックス 10213)について、その背景、技術的詳細、および関連するコード変更を包括的に解説します。

コミット

commit 4a9ebb18f1ff90cbc182648e65cc9071c8920e3c
Author: Russ Cox <rsc@golang.org>
Date:   Wed Nov 2 15:53:57 2011 -0400

    gofmt, gofix: sort imports
    
    Add ast.SortImports(fset, file) to go/ast, for use by both programs.
    
    Fixes #346.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5330069

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

https://github.com/golang/go/commit/4a9ebb18f1ff90cbc182648e65cc9071c8920e3c

元コミット内容

gofmtgofixツールにインポートのソート機能を追加する。 この機能は、go/astパッケージにast.SortImports(fset, file)として実装され、両方のプログラムで利用される。 この変更は、Issue #346 を修正するものである。

変更の背景

Go言語のコードフォーマッタであるgofmtは、コードのスタイルを統一し、可読性を向上させることを目的としています。しかし、このコミット以前のgofmtには、インポート宣言の順序を自動的にソートする機能がありませんでした。これにより、開発者間でインポートの順序が異なり、コードレビューの際に不必要な差分が生じたり、コードの統一性が損なわれたりする問題がありました。

Issue #346 は、まさにこのインポートのソートに関する要望を扱っています。このIssueでは、gofmtがインポートパスをアルファベット順にソートすべきであるという提案がなされていました。このコミットは、その要望に応える形で、gofmtgofix(Goコードの自動修正ツール)の両方でインポートをソートする機能を追加するものです。これにより、Goコードのフォーマットがさらに統一され、開発者の負担が軽減されることが期待されます。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。

  • Go言語のパッケージとインポート: Go言語では、コードはパッケージに分割され、他のパッケージの機能を利用するにはimport宣言を使用します。import宣言は、通常、ファイルの先頭に記述されます。
    import (
        "fmt"
        "net/http"
        "os"
    )
    
  • go/astパッケージ: Go言語の抽象構文木(AST: Abstract Syntax Tree)を扱うための標準ライブラリです。Goのソースコードを解析し、その構造をASTとして表現することで、プログラムによるコードの分析や変換を可能にします。ast.Fileは、Goのソースファイル全体のASTを表す構造体です。
  • go/tokenパッケージ: Goのソースコード内のトークン(キーワード、識別子、演算子など)や、それらの位置情報(ファイル名、行番号、列番号)を扱うためのパッケージです。token.FileSetは、複数のファイルにわたる位置情報を管理します。
  • gofmt: Go言語の公式なコードフォーマッタです。Goのソースコードを標準的なスタイルに自動的に整形します。これにより、開発者間でコードスタイルに関する議論を減らし、コードの可読性を高めます。
  • gofix: Go言語のコード自動修正ツールです。Go言語のバージョンアップに伴うAPIの変更など、互換性のない変更があった場合に、古いコードを新しいAPIに合わせて自動的に修正します。
  • GenDecl (General Declaration): GoのASTにおける一般的な宣言(import, const, type, var)を表すノードです。インポート宣言もGenDeclの一種として扱われます。
  • ImportSpec: GenDecl内の個々のインポート宣言(例: "fmt")を表すノードです。
  • sortパッケージ: Go言語の標準ライブラリで、スライスやカスタム型をソートするための機能を提供します。このコミットでは、インポートパスをソートするために利用されています。

技術的詳細

このコミットの主要な技術的変更点は、go/astパッケージにSortImports関数が追加されたことです。この関数は、GoソースファイルのASTを受け取り、その中のインポート宣言をソートします。

SortImports関数は、以下のロジックで動作します。

  1. インポート宣言の特定: ast.FileDecls(宣言リスト)を走査し、token.IMPORT型のGenDecl(インポート宣言)を探します。インポート宣言は通常、ファイルの先頭に位置するため、最初に見つかったインポート宣言以降は処理を中断します。
  2. ブロックインポートの識別: 丸括弧で囲まれたブロック形式のインポート(例: import ("fmt"; "os"))のみを対象とします。単一のインポート宣言(例: import "fmt")は、すでにソートされていると見なされます。
  3. 連続するインポート行のソート: ブロックインポート内で、連続するインポート行のグループを識別し、それぞれのグループを個別にソートします。これは、インポート宣言の間に空行やコメントが挟まっている場合でも、それぞれのグループ内でソートを適用できるようにするためです。
  4. インポートパスによるソート: 各インポート宣言(ImportSpec)からインポートパス(例: "fmt"からfmt)を抽出し、その文字列に基づいてアルファベット順にソートします。
  5. コメントの保持: インポート宣言に付随するコメント(行コメントやブロックコメント)も、ソート後も元のインポート宣言に紐付けられたままになるように処理されます。これは、go/astCommentGroupComment構造体、およびtoken.Pos(位置情報)を適切に操作することで実現されます。ソート後、コメントの位置情報も更新され、対応するインポート宣言の末尾に付随するように調整されます。

gofmtgofixは、それぞれファイル処理のロジック内でast.SortImportsを呼び出すように変更されています。これにより、これらのツールがGoソースコードを処理する際に、自動的にインポートがソートされるようになります。

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

このコミットの最も重要な変更は、src/pkg/go/ast/import.goという新しいファイルにSortImports関数が追加されたことです。

// src/pkg/go/ast/import.go (新規ファイル)

// SortImports sorts runs of consecutive import lines in import blocks in f.
func SortImports(fset *token.FileSet, f *File) {
	for _, d := range f.Decls {
		d, ok := d.(*GenDecl)
		if !ok || d.Tok != token.IMPORT {
			// Not an import declaration, so we're done.
			// Imports are always first.
			break
		}

		if d.Lparen == token.NoPos {
			// Not a block: sorted by default.
			continue
		}

		// Identify and sort runs of specs on successive lines.
		i := 0
		for j, s := range d.Specs {
			if j > i && fset.Position(s.Pos()).Line > 1+fset.Position(d.Specs[j-1].End()).Line {
				// j begins a new run.  End this one.
				sortSpecs(fset, f, d.Specs[i:j])
				i = j
			}
		}
		sortSpecs(fset, f, d.Specs[i:])
	}
}

func sortSpecs(fset *token.FileSet, f *File, specs []Spec) {
	// ... (ソートロジックとコメント処理) ...
	sort.Sort(byImportPath(specs))
	// ... (位置情報の更新とコメントの再配置) ...
}

type byImportPath []Spec // slice of *ImportSpec

func (x byImportPath) Len() int           { return len(x) }
func (x byImportPath) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
func (x byImportPath) Less(i, j int) bool { return importPath(x[i]) < importPath(x[j]) }

また、src/cmd/gofmt/gofmt.gosrc/cmd/gofix/main.goにおいて、ファイル処理の際にast.SortImportsが呼び出されるように変更されています。

// src/cmd/gofmt/gofmt.go
func processFile(filename string, in io.Reader, out io.Writer, stdin bool) error {
	// ...
	ast.SortImports(fset, file) // ここで呼び出される
	// ...
}

// src/cmd/gofix/main.go
func gofmtFile(f *ast.File) ([]byte, error) {
	var buf bytes.Buffer
	ast.SortImports(fset, f) // ここで呼び出される
	_, err := printConfig.Fprint(&buf, fset, f)
	if err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

コアとなるコードの解説

src/pkg/go/ast/import.goに新しく追加されたSortImports関数は、GoのASTを操作してインポート宣言をソートする中心的なロジックを担っています。

  • SortImports(fset *token.FileSet, f *File):

    • fset: token.FileSetは、ソースコード内の位置情報を管理するためのオブジェクトです。インポート宣言の行番号などを取得するために使用されます。
    • f: *ast.Fileは、解析されたGoソースファイルの抽象構文木(AST)のルートノードです。このASTを直接変更することで、インポートの順序を並べ替えます。
    • この関数は、ファイルの宣言(f.Decls)をループし、インポート宣言(GenDeclかつTok == token.IMPORT)を見つけます。
    • 単一のインポート(例: import "fmt")ではなく、ブロックインポート(例: import (...))のみを対象とします。これはd.Lparen == token.NoPosで判定されます。
    • sortSpecs関数を呼び出すことで、実際のソート処理が行われます。インポートブロック内の連続するインポート行のグループごとにソートを適用するために、j > i && fset.Position(s.Pos()).Line > 1+fset.Position(d.Specs[j-1].End()).Lineという条件でグループの区切りを検出しています。これは、インポート宣言の間に空行がある場合に、その空行を挟んでソートのグループを分けるためのものです。
  • sortSpecs(fset *token.FileSet, f *File, specs []Spec):

    • この関数は、実際にインポート宣言のリスト(specs)をソートします。
    • まず、すでにソートされている場合は何もしません。
    • インポート宣言に付随するコメントを特定し、ソート後もコメントが正しいインポート宣言に紐付けられるように、コメントの位置情報を管理します。
    • sort.Sort(byImportPath(specs))を呼び出して、byImportPath型で定義されたLessメソッドに基づいてインポートパスをアルファベット順にソートします。
    • ソート後、元の位置情報(posSpan)を使って、ソートされたインポート宣言のPath.ValuePosEndPosを更新します。これにより、ASTが正しく再構築されます。
    • コメントについても、ソートされたインポート宣言の末尾に付随するようにc.Slash(コメントの開始位置)を更新し、コメント自体も位置情報に基づいてソートし直します。
  • byImportPath:

    • sort.Interfaceインターフェースを実装しており、Len, Swap, Lessメソッドを提供します。
    • Lessメソッドは、2つのImportSpecのインポートパスを比較し、アルファベット順にソートするためのロジックを提供します。

これらの変更により、gofmtgofixがGoソースコードを処理する際に、インポート宣言が自動的に整理され、統一されたスタイルが適用されるようになります。

関連リンク

参考にした情報源リンク