解説 Ruby Refactoring Browser - Ruby Refactoring Browser の組み込み
初稿:2005-11-16
文章: 大林一平
はじめに
この文章では Ruby Refactoring Browser を様々な IDE、エディタに組み込み、
利用できるようにするために必要なことを解説します。
Ruby Refactoring Browser に関する基本的な事柄や Emacs での使いかたなどについて
知りたい方は、前回の記事 (解説 Ruby Refactoring Browser - Emacs でリファクタリング) を参照してください。
この文章が対象とする読者
この文章の読者には、少なくともリファクタリングについて一定の知識を持って
いることと、Ruby についてそれなりの知識があることを仮定します。
また、例としては Emacs と Vim での実装を挙げるので、それらについて
知識があったほうが理解しやすいでしょう。
なぜ組み込むのか
なぜリファクタリングブラウザを組み込むのでしょうか。
仮に、リファクタリングブラウザが IDE とは独立したツールであった場合を考えましょう。
そのときには、以下の手順でこのツールを利用することになります。
- IDE 上のファイルをすべてセーブする。
- リファクタリングブラウザにそのファイルを読み込む。
- リファクタリングする。
- リファクタリングした後のファイルを IDE へロードする。
リファクタリングをしようとするとこれを何度も繰り返す必要があります。
これは非常に面倒です。
よって、快適にリファクタリングするためには、自分の開発環境
になんらかの形でリファクタリングブラウザを組込み、手軽に使えるよう
にする必要があるのです。
準備
あらかじめ Ruby Refactoring Browser をインストールし、正しく動作するかテストして
おいてください。
内部構造
組み込み方を説明する前に内部構造を解説します。
まず全体像を図示しましょう。
図の上の方が EmacsLisp で、下のほうが Ruby で実装されています。
下部から解説していきましょう。
まず、リファクタンリング機能を実現するコアの部分です。
ソースコードとリファクタリングに必要なパラメータ
(例えば古い変数名と新しい変数名など)
をうけとり、リファクタリング可能かを判定し、
可能ならばその結果を返す、という機能を実現しています。
ここは Ruby で書かれています。
次に、 Emacs とのやりとりをする部分の Ruby 側の部分です。
これは bin/rrb というコマンドとして実現されています。
その実体は lib/rrb/emacs_interface.rb です。
最後に EmacsLisp で書かれた部分です。基本的にユーザからの入力を受けとり、
bin/rrb を呼びだしている (call-process、call-process-region) だけです。
実装の方針
実際に組み込む方法を解説していきます。
まずは、どのようにして対象のエディタに組込むのかを考えていきます。
以下の 3 つの方針を考えましょう。
- コアライブラリの部分だけ利用する。
- bin/rrb コマンド部も利用する。
- 新たなプロセス間インターフェースを作る。
1 は対象の IDE / エディタに Ruby が組込まれている場合、
もしくはそれが Ruby で書かれている場合に有効です。
この方針は面倒なプロセス間通信をしなくてよい分プログラムが簡潔に
なることが期待できます。
2 は対象の IDE / エディタが Ruby を直接利用できない場合につかいます。
3 は COM や CORBA のインターフェースを作ってしまおうというアイデアです。
面倒なので今のところやっていませんが、Windows などでは有用かもしれません。
Emacs での例
まずは上の方針 2 に従った実装例として Emacs での例をあげましょう。
Ruby Refactoring Browser の配布物には Emacs 用インターフェースとして
rrb.el が付属していますが、これはエラー処理や便利な機能の実現のため複雑
になっています。そのためここでは
それを非常に簡略化したものを例として挙げます。
Emacs や Emacs Lisp に詳しくない人にとっては少々わかりにくいかもしれませんが
elisp の解説 (info など) を読んで補完してください。
また、以下の例は Debian testing 上の Emacs のバージョン 21.4.1 で動作することを
確認しています。
実現する仕様
まずどんな仕様にするかを考えましょう。簡単のため、以下のようにします。
- リファクタリングの対象となるファイルはカレントバッファのファイルのみ
- Rename local variable のみ実装する
- ユーザに「どのクラス」の「どのメソッド」に含まれる「どのローカル変数」を「どのような名前」に変更するかを指定させる
注: カレントバッファとは現在編集しているファイルのこと
bin/rrb コマンド
まずは rrb コマンドの仕様について解説します。コマンドライン上で
とだけ打つと、以下のようなヘルプメッセージが得られます。
まずどのリファクタリングをするかを refactoring-type オプション群から選び、
そのリファクタリングに必要なパラメータ (例えば新しい変数名など) を
二番目以降の引数 (refactoring-parameter) に指定します。
最後にリファクタリングの対象となるファイルのやりとりの方法を
io-type オプション群から選んで指定します。
Heke::Hoge クラスの f というメソッドの中の i というローカル変数を index に変更する
リファクタリングをさせる場合の例を下に挙げましょう。
また、/home/ohai/test.rb の 10 行目から 17 行目までを foo というメソッドとして
抽出する場合は以下のようにします。
「–stdin-stdout」というオプションは、リファクタリング対象の
ファイルを標準入力に渡し、リファクタリングした結果を標準出力から得る、
ということを指定しています。
そして受け渡しのフォーマットは以下の通りです。
ここで \C-a はアスキーコード 1 の文字を表しています。
実装
まずはインターフェースから実装していきましょう。
入力部分はこれで OK です。interactive を使って 4 つの文字列を入力させ、
それぞれ class, method, old-var, new-var という変数に束縛します。
ちなみに関数名のプリフィクスになっている rrbs は「rrb simple」の省略形です。この後も使います。
つぎに入力データの準備をしましょう。
(buffer-file-name (current-buffer)) でカレントバッファのファイル名を、
(buffer-string) でカレントバッファの中身を得ています。
また、入出力用のバッファを別に “ rrbs-input” 、 “ rrbs-output” という
名前で用意します。そして (insert 文字列) で入力用バッファに文字列
を書き込みます。
これで
というソース (/home/ohai/test.rb) に対し
という内容のデータが” rrbs-input “に用意されます。
次に rrb コマンドを呼びだします。
call-process-region でプロセスを起動し、rrb コマンドを呼びだします。
引数の詳しい意味は info を見てください。
この結果、リファクタリングされたソースコードが “ rrbs-output” に出力されます。
例えば上の例で Heke#f の x を u にリネームすると
という内容となります。
最後に出力結果をもとのバッファに反映します。
(setq result (cadr (split-string (buffer-string) “\C-a”)))
で必要な部分を取りだし、
(erase-buffer) でもとの内容を全消去、 (insert result) で新たな内容を反映
して終わりです。
rrb コマンドに関する注意
「–pushdown-method」「–pullup-method」「–extract-superclass」
の 3 つのオプションは、
どのファイルのどの行に新しいメソッド/クラスを生成するのかを
引数 path と lineno で指定します。
「–extract-superclass」オプションのように複数のクラスを指定するときは、
一つのコマンドライン引数でまとめて渡す必要があります。
したがってコマンドラインから使うときは “Class1 Class2 Class3”
のようにクオートでくくらなければいけません。
上の例では一切エラー処理をしていませんが、rrb コマンドの返り値が 0 でない場合は
エラーで、エラーの原因は stderr から得られます。ちゃんとした実装をしようとし
た場合はきちんとエラー処理する必要があるでしょう。
「rrb_なんとか」コマンド
Ruby Refactoring Browser の配布物のなかには、rrb コマンドの他にも、
rrb_marshal, rrb_compinfo, rrb_default_value というコマンドがあります。
それぞれ引数無しで呼べば引数の種類等がわかります。
- rrb_compinfo INFOTYPE IOTYPE
- Ruby スクリプトをパースし、含まれる識別子のリストを得るコマンドです。クラス名のリスト、ローカル変数名のリストなどが得られます。この情報を補完をするためなどに利用します。
- rrb_default_value PATH LINENO INFOTYPE IOTYPE
- Ruby スクリプトをパースし、指定したファイルの指定した行のところのメソッド名/クラス名は何か、を得るためのコマンドです。得た文字列をユーザに入力させる値のデフォルト値として利用します。
- rrb_marshal
- 上で挙げたコマンドを利用すると、スクリプトのパースと構文木の構築を何度も実行することになります。これは実行コストが高いので、あらかじめスクリプトをパースし、生成した構文木を Marshal を利用してファイルに出力することで高速化できる場合があります。rrb_marshal はそのためのコマンドです。rrb コマンドの –marshalin-stdout などはこれを利用するためにあります。
以上のコマンドで、出力される内容やその形式など詳しいことは
それぞれのソースを読んでください。
Vim での例
Emacs の次は Vim で実装してみましょう。Vim には独自のスクリプト言語があり、
そこから ruby のインタプリタとやりとりすることができます。
これを利用しましょう。
仕様は以下の通りにしましょう。
- リファクタリングの対象となるファイルはカレントバッファのファイルのみ
- Rename local variable のみ実装する
- リネームする変数はカーソルの下にあるものとする
- 新しい変数名はユーザに入力させる
以下での例を実行するためには、
Vim のコンパイル時に ruby インターフェースを有効にしておく必要があります。
この例は Debian testing 上で ruby インターフェースを有効にした Vim 6.3 で
動作を確認しています。
Vim スクリプトや Ruby インターフェースについては詳しく解説
しませんので、Vim のヘルプを参考にしてください。
コアライブラリの仕様
まず、
でライブラリをロードします。
コアライブラリの中で中心的なクラスは RRB::Script と RRB::ScriptFile です。
RRB::ScriptFile が個々のファイルで、それを集約したものが RRB::Script です。
- RRB::ScriptFile.new(str, path)
- 新しい ScriptFile のインスンタンスを生成し返す。str はそのファイルの中身の文字列、path はファイルパス文字列。
- RRB::Script.new(files)
- 新しい Script のインスンタンスを生成し返す。files は ScriptFile のインスタンスの配列。
- RRB::Script#files
- 含まれる RRB::ScriptFile のインスタンス全ての配列。
- RRB::Script#rename_local_var?(method, old_var, new_var)
- そのファイル群が Rename Local variable 可能かどうか判定する。method は RRB::Method のインスタンスでどのメソッドのローカル変数を変更するかを指定する。old_var は古い変数名、new_var は新しい変数名。
- RRB::Script#rename_local_var(method, old_var, new_var)
- 実際に変換する。引数の意味は RRB::Script#rename_local_var? と同じ。
- RRB::ScriptFile#new_script
- 変換した結果の文字列。nil ならば変更点がないことを示している。
- class RRB::Method
- クラス名とメソッド名の組を表すクラス。
- RRB::Method[str]
- 新しいインスタンスを作成する。str は “Foo::Bar#baz” という形式の文字列で与える。
この他にも様々なクラス、メソッドがあります。
Ruby Refactoring Browser の配布物の doc/dev/rrb_embed.ja.rd に解説が書かれています。
また、これだけでは不十分な場合はすいませんが直接ソースを見てください。
Vim での実装
全体の枠組みを作っていきます。
これで vim に RRBRenameLocalVariable というコマンドが登録され、
とすることで foo という文字列が ruby の rename_local_var というメソッドに
渡されるようになりました。
カーソルのある位置にある識別子を取る処理を書きます
VIM::Window#cursor でカーソルの位置を得、VIM::Buffer#[] で一行分の文字列
を得ます。
次にカーソルのある行のクラスの名前およびメソッドの名前を作ります。
上で説明したように RRB::Script のインスタンスを作ります。そして
RRB::Script#get_method_on_cursor(path, lineno) でその行がどのメソッド
に含まれているかを得ます。
これで変数 methodname に “Hoge#f” という形の文字列が代入されます。
今度はリファクタリングが可能かどうかの判定をします。
RRB#script#rename_local_var? で判定します。真なら可能、偽なら不可能です。
ローカル変数のリネームの場合、同じ名前の新しい名前の変数がすでに存在
する場合などに偽が帰ってきます。
そして失敗した場合は VIM.message で Vim にメッセージを表示させ終了するように
しました。
最後に実際に変換して、その結果を反映させます。
RRB::Script#rename_local_var で変換をします。変換した結果は
RRB::ScriptFile#new_script から取りだせます。
さらなる開発のために
ここまで説明してきたことはごく基本的な内容です。さらにきちんとしようと
考えると以下のような問題が考えられます。
UI の問題
上で挙げた Emacs の例のような UI を作るのであれば補完入力は必須でしょう。
また、Rename Method では任意個のクラスを指定できるので、これに対応するた
めの UI を作るのは面倒になるかもしれません。各開発環境に合わせて適切な
UI を考えてください。
エラー処理
上に挙げた例はエラー処理等を殆どしていません。実用的なものに仕上げるには
きちんとエラー処理をする必要があるでしょう。
複数のファイルを対象にする
ある程度大規模なプログラムであればプログラムが複数のファイルに
分かれている場合が考えられるでしょう。その場合どのファイルをリファクタリング
の対象とし、どれを対象としないかを判別するのは結構難しい問題です。
RDT のようにプロジェクトという単位でファイルを管理している場合は
簡単ですが、そうでない場合はどうすれば良いのでしょうか。
Emacs インターフェースでは、その
Emacs プロセスで開いているファイルをすべてひとかたまりのものとして
扱っています。複数のファイルが開けるエディタならこの方法が良いかも
しれません。Vim ではそのファイルと同じディレクトリにある Ruby スクリプト
全体を対象にしてしまっても良いかもしれません。
これに対する一般的な回答はありません。そのエディタや IDE の性質に
合わせて適宜決めてください。
最後に
参考資料としてはやはり Ruby Refactoring Browser のソースが一番でしょう。
lib/rrb/cui_interface.rb、lib/rrb/emacs_interface.rb は Ruby 側の例として
参考になるでしょう。 FreeRIDE に付属しているリファクタリングプラグイン
や、elisp/rrb.el も見てください。
わからないことがあれば遠慮なく筆者 (ohai@kmc.gr.jp) に質問してください。
筆者の個人的願望としては RDT や RDE、xyzzy などが対応してくれないかなあ、
と思っています。
様々な環境で Ruby Refactoring Browser が使えるようになることを
願っています。
著者について
私 (大林) は京都で大学院生をしています。専門は数学です。
また KMC (京大マイコンクラブ) 部員でもあります。
Ruby Refactoring Browser の主要開発者です。
Ruby Refactoring Browser は最初 KMC 内部のプロジェクトとして開発を開始しました。
解説 Ruby Refactoring Browser 連載一覧