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

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

このコミットは、Go言語のコード自動修正ツールである gofix の機能改善に関するものです。具体的には、gofix がGoソースコード内の import 文を削除した際に残ってしまう余分な空白行を適切に処理できるように、Goの抽象構文木 (AST) を扱う go/astgo/parsergo/printer パッケージに小さな変更を加えています。また、import 文の挿入と削除に関するテストケースが追加されています。

コミット

  • コミットハッシュ: 6323a40f31adbb810f79bac557552f96240a5e1f
  • Author: Russ Cox rsc@golang.org
  • Date: Wed Oct 26 14:04:07 2011 -0700

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

https://github.com/golang/go/commit/6323a40f31adbb810f79bac557552f96240a5e1f

元コミット内容

gofix: test import insertion, deletion

Small change to go/ast, go/parser, go/printer so that
gofix can delete the blank line left from deleting an import.

R=golang-dev, bradfitz, adg
CC=golang-dev
https://golang.org/cl/5321046

変更の背景

Go言語の gofix ツールは、Go言語のバージョンアップに伴うAPI変更や慣習の変更に対応するために、既存のGoソースコードを自動的に修正する役割を担っています。このコミット以前の gofix には、import 文を削除した際に、その import 文があった場所に空白行が残ってしまうという問題がありました。これは、コードの整形(フォーマット)の観点から望ましくなく、手動での修正が必要になる場合がありました。

この変更の背景には、gofix がより「賢く」コードを修正し、開発者が手動で修正する手間を減らすという目的があります。特に、import 文の削除は頻繁に行われる操作であり、その際に不要な空白行が残ることは、開発体験を損なう要因となっていました。このコミットは、go/astgo/parsergo/printer といったGoコンパイラの内部ツール群を連携させることで、この問題を解決しようとしています。

前提知識の解説

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

1. gofix ツール

gofix は、Go言語のソースコードを自動的に修正するためのコマンドラインツールです。Go言語の進化に伴い、古いAPIが非推奨になったり、新しい慣習が導入されたりすることがあります。gofix は、これらの変更に自動的に対応し、既存のコードベースを最新のGo言語の仕様に準拠させることを目的としています。例えば、Go 1からGo 2への移行期には、多くのコード修正が gofix によって行われました。

2. go/ast パッケージ (Abstract Syntax Tree)

go/ast パッケージは、Go言語のソースコードを抽象構文木 (AST) として表現するためのデータ構造を提供します。ASTは、プログラムの構造を木構造で表現したもので、コンパイラやコード分析ツールがソースコードを理解し、操作するために使用します。

  • ast.File: Goのソースファイル全体を表すASTのルートノード。
  • ast.Decl: 宣言(変数宣言、関数宣言、型宣言など)を表すインターフェース。
  • ast.GenDecl: 一般的な宣言(import, const, var, type)を表す構造体。import 文のグループもこれに含まれます。
  • ast.ImportSpec: 個々の import 文(例: import "fmt"import . "os")を表す構造体。
  • token.Pos: ソースコード内の位置(行番号、列番号、オフセット)を表す型。ASTノードは、対応するソースコードの開始位置と終了位置を token.Pos で保持します。

3. go/parser パッケージ

go/parser パッケージは、Go言語のソースコードを解析し、go/ast パッケージで定義されたASTを生成するための機能を提供します。ソースコードの文字列やファイルパスを入力として受け取り、対応するASTを返します。

4. go/printer パッケージ

go/printer パッケージは、go/ast パッケージで生成されたASTを、整形されたGoソースコードとして出力するための機能を提供します。ASTを操作した後、このパッケージを使って修正されたコードをファイルに書き戻すことができます。

5. import 文の構造と空白行

Go言語の import 文は、単一のパスをインポートする場合 (import "fmt") と、複数のパスをグループ化してインポートする場合 (import ("fmt"; "os")) があります。gofiximport 文を削除する際、ASTの構造上、削除された import 文が占めていた行が空白行として残ってしまうことがありました。これは、ASTノードがソースコード上の正確な位置情報(token.Pos)を持っているため、その位置情報に基づいてコードを再生成する際に、削除されたノードの「痕跡」が空白として現れるためです。

技術的詳細

このコミットの技術的な核心は、gofiximport 文を削除した際に、その import 文が占めていたソースコード上の領域を適切に「埋める」ことで、不要な空白行が生成されないようにすることです。これは、go/ast パッケージの ImportSpec 構造体に新しいフィールドを追加し、go/parsergo/printer がそのフィールドを適切に利用するように変更することで実現されています。

go/ast パッケージの変更 (src/pkg/go/ast/ast.go)

  • ast.ImportSpec 構造体に EndPos token.Pos フィールドが追加されました。

    type (
        // ...
        ImportSpec struct {
            Name    *Ident        // local package name (including "."); or nil
            Path    *BasicLit     // import path
            Comment *CommentGroup // line comments; or nil
            EndPos  token.Pos     // end of spec (overrides Path.Pos if nonzero)
        }
        // ...
    )
    

    この EndPos フィールドは、ImportSpec がソースコード上で占める範囲の終了位置を明示的に指定するために使用されます。これにより、go/printer がコードを再生成する際に、import 文の実際の終了位置を正確に把握できるようになります。特に、import 文が削除された場合、この EndPos を調整することで、削除された import 文の領域を前の import 文の領域に「吸収」させることが可能になります。

  • ImportSpecEnd() メソッドが変更されました。

    func (s *ImportSpec) End() token.Pos {
        if s.EndPos != 0 {
            return s.EndPos
        }
        return s.Path.End()
    }
    

    この変更により、ImportSpec の終了位置を取得する際に、新しく追加された EndPos フィールドが優先的に使用されるようになりました。EndPos が設定されていない場合(0 の場合)は、これまで通り Path.End() が使用されます。これにより、go/printerimport 文の正確な終了位置を判断し、空白行の生成を抑制できるようになります。

go/parser パッケージの変更 (src/pkg/go/parser/parser.go)

  • parseImportSpec 関数で ImportSpec を初期化する際に、新しく追加された EndPos フィールドが token.NoPos (ゼロ値) で初期化されるようになりました。
    func parseImportSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
        // ...
        spec := &ast.ImportSpec{doc, ident, path, p.lineComment, token.NoPos}
        // ...
    }
    
    これは、パーサーがASTを生成する時点では EndPos の具体的な値は不明であるため、デフォルト値で初期化しておくことを意味します。この値は、gofix のようなツールがASTを操作する際に、必要に応じて更新されます。

go/printer パッケージの変更 (src/pkg/go/printer/nodes.go)

  • printerspec メソッド内で、ImportSpec を処理する際に s.EndPosprint されるようになりました。
    func (p *printer) spec(spec ast.Spec, n int, doIndent bool, multiLine *bool) {
        // ...
        case *ast.ImportSpec:
            // ...
            p.print(s.EndPos)
        // ...
    }
    
    この変更は、go/printerImportSpecEndPos を考慮してコードを整形することを示唆しています。EndPos が設定されている場合、プリンターはその位置までを ImportSpec の範囲として認識し、その後の空白行の生成を抑制するロジックが内部的に働くようになります。

gofix ツール (src/cmd/gofix/fix.go) の変更

gofix ツール自体も、これらのASTの変更を利用して import 文の削除時の空白行問題を解決しています。

  • deleteImport 関数内で、import 文を削除する際に、削除された import 文の EndPos を前の import 文の EndPos にコピーするロジックが追加されました。

    // ...
    if j > 0 {
        // We deleted an entry but now there will be
        // a blank line-sized hole where the import was.
        // Close the hole by making the previous
        // import appear to "end" where this one did.
        gen.Specs[j-1].(*ast.ImportSpec).EndPos = impspec.End()
    }
    // ...
    

    このコードは、import 文が削除された際に、その直前の import 文の EndPos を、削除された import 文の End() 位置に設定しています。これにより、go/printer がコードを再生成する際に、前の import 文が削除された import 文の領域までを「占有」していると認識し、間に空白行が挿入されるのを防ぎます。

  • addImport 関数内で、import "C" のインポート宣言を避けて新しいインポートを追加するロジックが追加されました。

    // ...
    if ok && gen.Tok == token.IMPORT {
        lastImport = i
        // Do not add to import "C", to avoid disrupting the
        // association with its doc comment, breaking cgo.
        if !declImports(gen, "C") {
            impdecl = gen
            break
        }
    }
    // ...
    

    これは、cgo (GoとC言語の相互運用) で使用される import "C" の特殊な性質を考慮したものです。import "C" は通常、その直前のコメントと密接に関連しており、その構造を崩すと cgo の動作に影響を与える可能性があります。そのため、新しい import 文を追加する際には、既存の import "C" のブロックには追加せず、別の場所に挿入するように変更されています。

import_test.go の追加

このコミットでは、src/cmd/gofix/import_test.go という新しいテストファイルが追加されています。このファイルには、import 文の挿入と削除に関する多数のテストケースが含まれています。これらのテストケースは、gofiximport 文を正しく追加・削除し、特に削除後に不要な空白行が残らないことを検証するために書かれています。

例えば、import.7 から import.9 のテストケースは、複数行の import グループから特定の import を削除した際に、コメントが適切に残り、空白行が挿入されないことを確認しています。また、import.10 から import.12 は、複数行の import グループから import を削除した際に、残りの import が適切に整形されることを確認しています。

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

src/pkg/go/ast/ast.go の変更

--- a/src/pkg/go/ast/ast.go
+++ b/src/pkg/go/ast/ast.go
@@ -752,6 +752,7 @@ type (
 		Name    *Ident        // local package name (including "."); or nil
 		Path    *BasicLit     // import path
 		Comment *CommentGroup // line comments; or nil
+		EndPos  token.Pos     // end of spec (overrides Path.Pos if nonzero)
 	}
 
 	// A ValueSpec node represents a constant or variable declaration
@@ -785,7 +786,13 @@ func (s *ImportSpec) Pos() token.Pos {
 func (s *ValueSpec) Pos() token.Pos { return s.Names[0].Pos() }
 func (s *TypeSpec) Pos() token.Pos  { return s.Name.Pos() }
 
-func (s *ImportSpec) End() token.Pos { return s.Path.End() }
+func (s *ImportSpec) End() token.Pos {
+	if s.EndPos != 0 {
+		return s.EndPos
+	}
+	return s.Path.End()
+}
+
 func (s *ValueSpec) End() token.Pos {
 	if n := len(s.Values); n > 0 {
 		return s.Values[n-1].End()

src/cmd/gofix/fix.godeleteImport 関数内の変更

--- a/src/cmd/gofix/fix.go
+++ b/src/cmd/gofix/fix.go
@@ -540,7 +560,6 @@ func deleteImport(f *ast.File, path string) {
 		}
 		for j, spec := range gen.Specs {
 			impspec := spec.(*ast.ImportSpec)
-\
 			if oldImport != impspec {
 				continue
 			}
@@ -558,7 +577,13 @@ func deleteImport(f *ast.File, path string) {
 			} else if len(gen.Specs) == 1 {
 				gen.Lparen = token.NoPos // drop parens
 			}
-\
+			if j > 0 {
+				// We deleted an entry but now there will be
+				// a blank line-sized hole where the import was.
+				// Close the hole by making the previous
+				// import appear to "end" where this one did.
+				gen.Specs[j-1].(*ast.ImportSpec).EndPos = impspec.End()
+			}
 			break
 		}
 	}

コアとなるコードの解説

ast.go の変更点

  • ImportSpec 構造体への EndPos フィールド追加: これは、import 文がソースコード上でどこまでを占めるかを示すための新しい情報です。従来の Path.End() だけでは、import 文の後に続くコメントや空白行の正確な位置を把握しきれない場合がありました。EndPos を導入することで、gofiximport 文を削除した際に、その import 文が占めていた領域全体を正確に指定し、その領域を「埋める」ことが可能になります。

  • ImportSpec.End() メソッドの変更: この変更により、ImportSpec の終了位置を取得する際に、EndPos が設定されていればそれが優先されます。これにより、go/printer がコードを再生成する際に、gofix が設定した EndPos を参照し、不要な空白行を生成しないように動作します。

fix.godeleteImport 関数内の変更点

  • EndPos の調整ロジック: if j > 0 { ... } ブロック内のコードが、このコミットの核心的な修正です。 import 文が削除された際、その直前の import 文 (gen.Specs[j-1]) の EndPos を、削除された import 文 (impspec) の End() 位置に設定しています。 これにより、go/printer がコードを整形する際、削除された import 文の領域が、その前の import 文の領域の一部として扱われるようになります。結果として、削除された import 文によって生じるはずだった空白行が生成されなくなり、コードの整形がより自然になります。

これらの変更により、gofiximport 文の削除をよりスマートに行えるようになり、開発者が手動で空白行を修正する手間が省けるようになりました。

関連リンク

参考にした情報源リンク