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

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

このコミットは、Go言語のVimプラグインにおけるインポートの削除(Dropコマンド)に関するバグ修正です。具体的には、複数のインポートグループが存在する場合に、最初のグループ以降のインポートが正しく削除されない問題を解決します。

コミット

commit adcf0a2aa06ec1424b4d51d2b7ce043a60d29361
Author: David Symonds <dsymonds@golang.org>
Date:   Thu Sep 20 08:11:07 2012 +1000

    misc/vim: fix Drop for imports after the first group.
    
    Previously, an import block such as
            import (
                    "net"
    
                    "stack"
            )
    would not permit ":Drop stack" to work because we were aborting
    the scan early, which is only correct when Import is in operation.
    
    R=golang-dev, franciscossouza
    CC=golang-dev
    https://golang.org/cl/6532053

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

https://github.com/golang/go/commit/adcf0a2aa06ec1424b4d51d2b7ce043a60d29361

元コミット内容

このコミットは、Go言語のVimプラグイン(misc/vim/ftplugin/go/以下に存在するファイル群)におけるインポート管理機能の修正を目的としています。特に、Vimのコマンドラインから特定のインポートパスを削除する:Dropコマンドが、Goのソースコード内でインポートが複数のグループに分かれている場合に正しく機能しないという問題に対処しています。

具体的には、以下のようなインポートブロックがあった場合:

import (
        "net"

        "stack"
)

:Drop stackというコマンドを実行しても、「stack」インポートが削除されないという問題がありました。これは、インポートをスキャンする処理が、最初のインポートグループの後に早期に中断されてしまっていたためです。この早期中断のロジックは、インポートを追加する:Importコマンドの場合には適切でしたが、インポートを削除する:Dropコマンドの場合には不適切でした。

変更の背景

Go言語のソースコードでは、import文をグループ化し、グループ間に空行を挿入することで、標準ライブラリ、サードパーティライブラリ、プロジェクト内のローカルパッケージなどを視覚的に区別する慣習があります。VimのGoプラグインは、これらのインポートを効率的に管理するための機能(追加、削除など)を提供しています。

このコミット以前は、:Dropコマンドがこのグループ化されたインポート構造を正しく扱えず、最初のインポートグループ以降に記述されたインポートパスを認識・削除できないというバグが存在していました。これにより、開発者はVimのインポート管理機能の恩恵を十分に受けられず、手動でインポートを削除する必要がありました。この修正は、VimユーザーのGo開発体験を向上させるために不可欠でした。

前提知識の解説

Vimとftplugin

  • Vim: 高機能なテキストエディタであり、プログラミングにおいて広く利用されています。Vimは非常にカスタマイズ性が高く、プラグインによって機能を拡張できます。
  • ftplugin (filetype plugin): Vimのプラグインの一種で、特定のファイルタイプ(例: Go言語のファイル)を開いたときに自動的にロードされるスクリプトです。これにより、そのファイルタイプに特化した機能や設定を提供できます。misc/vim/ftplugin/go/ディレクトリは、Go言語ファイル用のVimプラグインファイルが格納されている場所です。

Go言語のimport文

Go言語では、外部パッケージの機能を利用するためにimport文を使用します。

  • 単一インポート: import "fmt"
  • グループインポート:
    import (
        "fmt"
        "net/http"
    )
    
  • インポートグループ: Goの慣習として、インポートは関連性に基づいてグループ化され、グループ間に空行を挟むことがあります。例えば、標準ライブラリ、サードパーティライブラリ、プロジェクト内のローカルパッケージなどでグループを分けることがあります。

golang.org/cl

golang.org/cl/は、GoプロジェクトのコードレビューシステムであるGerritの変更リスト(Change-List)へのリンクです。Goプロジェクトでは、すべてのコード変更はGerritを通じてレビューされ、承認された後にメインリポジトリにマージされます。https://golang.org/cl/6532053はこのコミットに対応するGerritの変更リストを示しており、詳細な議論やレビューの履歴を確認できます。

技術的詳細

この修正は、misc/vim/ftplugin/go/import.vimファイル内のs:SwitchImport関数に焦点を当てています。この関数は、インポートの追加(Import)と削除(Drop)の両方のロジックを処理する汎用的な関数です。

問題の核心は、インポートブロックをスキャンするループ内で、siteprefix == ""という条件が満たされた場合に早期にループをbreakしていた点にありました。siteprefixが空であることは、通常、標準ライブラリのインポートや、インポートブロックの最初のグループに属するインポートを示します。

  • 既存のロジック(問題点):

    if siteprefix == ""
        " must be in the first group
        break
    endif
    

    このロジックは、インポートを追加する:Importコマンドの場合には、新しいインポートを適切なグループ(通常は最初のグループ)に挿入するために、最初のグループを見つけたらスキャンを中断するという意味で理にかなっていました。しかし、インポートを削除する:Dropコマンドの場合、削除対象のインポートが最初のグループ以降のグループに存在しても、このbreakによってスキャンが中断されてしまい、対象のインポートが見つからずに削除が失敗していました。

  • 修正後のロジック:

    if siteprefix == "" && a:enabled
        " must be in the first group
        break
    endif
    

    修正では、break条件に&& a:enabledが追加されました。ここで、a:enableds:SwitchImport関数に渡される引数であり、この関数がインポートを追加するモード(a:enabledが真)で動作している場合にのみ、早期のbreakが実行されるように制御します。

    • a:enabledが真(Import操作)の場合: 以前と同様に、最初のグループでスキャンが中断され、新しいインポートが挿入されます。
    • a:enabledが偽(Drop操作)の場合: siteprefix == ""の条件が満たされてもbreakは実行されず、スキャンはインポートブロック全体を継続します。これにより、:Dropコマンドはすべてのインポートグループを走査し、対象のインポートパスを正しく見つけて削除できるようになります。

また、misc/vim/ftplugin/go/test.shのテストスクリプトも更新され、test_one関数がより汎用的にVimコマンドを実行できるように変更されました。そして、複数のインポートグループを持つbase.goファイルを作成し、:Dropコマンドが正しく機能するかを検証する新しいテストケースが追加されました。これにより、修正が意図通りに動作し、将来のリグレッションを防ぐことが保証されます。

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

misc/vim/ftplugin/go/import.vim

--- a/misc/vim/ftplugin/go/import.vim
+++ b/misc/vim/ftplugin/go/import.vim
@@ -96,7 +96,7 @@ function! s:SwitchImport(enabled, localname, path)\
                 let linestr = getline(line)
                 let m = matchlist(getline(line), '^\\()\\|\\(\\s\\+\\)\\(\\S*\\s*\\)"\\(.\\+\\)"\\)\'')
                 if empty(m)
-                    if siteprefix == ""
+                    if siteprefix == "" && a:enabled
                         " must be in the first group
                         break
                     endif

misc/vim/ftplugin/go/test.sh

--- a/misc/vim/ftplugin/go/test.sh
+++ b/misc/vim/ftplugin/go/test.sh
@@ -22,36 +22,53 @@ EOF
 
 fail=0
 
-# usage: test_one new_import pattern
+# usage: test_one command pattern
 test_one() {
-  echo 2>&1 -n "Import $1: "
+  echo 2>&1 -n "$1: "
   vim -e -s -u /dev/null -U /dev/null --noplugin -c "source import.vim" \
-    -c "Import $1" -c 'wq! test.go' base.go
+    -c "$1" -c 'wq! test.go' base.go
   # ensure blank lines are treated correctly
   if ! gofmt test.go | cmp test.go; then
     echo 2>&1 "gofmt conflict"
-    gofmt test.go | diff -u test.go - | sed "s/^/\t/" 2>&1
+    gofmt test.go | diff -u test.go - | sed "s/^/\t/" 2>&1
     fail=1
     return
   fi
   if ! grep -P -q "(?s)$2" test.go; then
     echo 2>&1 "$2 did not match"
-    cat test.go | sed "s/^/\t/" 2>&1
+    cat test.go | sed "s/^/\t/" 2>&1
     fail=1
     return
   fi
   echo 2>&1 "ok"
 }
 
-test_one baz '"baz".*"bytes"'
-test_one io/ioutil '"io".*"io/ioutil".*"net"'
-test_one myc '"io".*"myc".*"net"'  # prefix of a site prefix
-test_one nat '"io".*"nat".*"net"'
-test_one net/http '"net".*"net/http".*"mycorp/foo"'
-test_one zoo '"net".*"zoo".*"mycorp/foo"'
-test_one mycorp/bar '"net".*"mycorp/bar".*"mycorp/foo"'
-test_one mycorp/goo '"net".*"mycorp/foo".*"mycorp/goo"'
+# Tests for Import
+
+test_one "Import baz" '"baz".*"bytes"'
+test_one "Import io/ioutil" '"io".*"io/ioutil".*"net"'
+test_one "Import myc" '"io".*"myc".*"net"'  # prefix of a site prefix
+test_one "Import nat" '"io".*"nat".*"net"'
+test_one "Import net/http" '"net".*"net/http".*"mycorp/foo"'
+test_one "Import zoo" '"net".*"zoo".*"mycorp/foo"'
+test_one "Import mycorp/bar" '"net".*"mycorp/bar".*"mycorp/foo"'
+test_one "Import mycorp/goo" '"net".*"mycorp/foo".*"mycorp/goo"'
+
+# Tests for Drop
+
+cat > base.go <<EOF
+package test
+
+import (
+	"foo"
+
+	"something"
+	"zoo"
+)
+EOF
+
+test_one "Drop something" '([^\"]*\"foo\"[^\"]*\"zoo\"[^\"]*)'
 
 rm -f base.go test.go
 if [ $fail -gt 0 ]; then

コアとなるコードの解説

misc/vim/ftplugin/go/import.vimの変更

  • s:SwitchImport関数: この関数は、VimのGoプラグインにおけるインポートの追加(Import)と削除(Drop)の主要なロジックを担っています。
  • if siteprefix == "" && a:enabled:
    • siteprefix == "": これは、現在処理しているインポートパスが、Goの標準ライブラリの一部であるか、またはインポートブロックの最初のグループに属していることを示唆する条件です。
    • a:enabled: これはs:SwitchImport関数に渡されるブール値の引数で、この関数がインポートを追加するモード(true)で呼び出されているか、削除するモード(false)で呼び出されているかを制御します。
    • 変更前はif siteprefix == ""のみでbreakしていました。これにより、Drop操作時でも最初のインポートグループでスキャンが中断され、それ以降のグループにあるインポートが処理されませんでした。
    • 変更後は&& a:enabledが追加されたことで、この早期breakImport操作(a:enabledtrue)の場合にのみ適用されるようになりました。Drop操作(a:enabledfalse)の場合、この条件は満たされず、スキャンはインポートブロック全体を継続するため、すべてのインポートグループから対象のインポートを正しく見つけて削除できるようになります。

misc/vim/ftplugin/go/test.shの変更

  • test_one関数の汎用化:
    • 以前はtest_one new_import patternという形式で、Importコマンド専用でした。
    • 変更後はtest_one command patternとなり、vim -c "$1"のように、任意のVimコマンドを第一引数として受け取れるようになりました。これにより、ImportだけでなくDropコマンドのテストも同じフレームワークで実行できるようになりました。
  • Importテストの更新: test_oneの呼び出しが、新しいtest_one "Import baz"のような形式に更新されました。
  • Dropテストの追加:
    • cat > base.go <<EOF ... EOFブロックで、複数のインポートグループを持つbase.goファイルが動的に作成されます。このファイルには、"foo""something""zoo"の3つのインポートが含まれ、"something""zoo"は空行で区切られた別のグループに配置されています。
    • test_one "Drop something" '([^\"]*\"foo\"[^\"]*\"zoo\"[^\"]*)'という新しいテストが追加されました。
      • "Drop something": Vimに対してsomethingインポートを削除するよう指示します。
      • '([^\"]*\"foo\"[^\"]*\"zoo\"[^\"]*)': これは正規表現パターンです。テスト実行後、test.goファイル(base.goが変更されたもの)の内容がこのパターンにマッチするかどうかをgrep -P -qで確認します。このパターンは、"foo""zoo"がファイル内に存在し、かつ"something"が存在しないことを検証します。これにより、somethingが正しく削除され、他のインポートが影響を受けていないことが確認されます。

これらの変更により、VimのGoプラグインは、Go言語のインポートグループの慣習に沿ったコードに対しても、インポートの追加・削除を正確に実行できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Vimの公式ドキュメント
  • Gerrit Code Reviewのインターフェース
  • コミットメッセージと差分情報
  • Go言語のコーディングスタイルガイド(インポートのグループ化に関する慣習)
  • Vimscriptの構文と関数に関する一般的な知識
  • 正規表現(PCRE)に関する一般的な知識