やろうとしていること
長年秘伝のタレ的に開発が続けられているコードがあります。
歴史的には古くて、コードサイズは巨大で、手を入れたいけど何があるかわからないからあまり手を入れたくない、そういったコードです。
この手のコードは内部的にはいろいろあります。
テストがあったりなかったり、命名規則が時代によって違ったり、思想が時期により異なったり、あるいはもともと別プログラムだったり。
これらが統一されていればそれなりの利益はあります。
でも統一されない、なぜなら統一するためのコストが払えないから。
でもなんとかしたい、そう思われている方はいないでしょうか。
しかし統一する必要が出てきました。
autoloadしたいとか命名規則を統一したいとかいろいろあって諸事情により、長年開発が続けられてきたリポジトリのModelのクラス名を統一したいと考えました。
手で置き換えていくのは現実的ではありません。第一やりたくありません。
そこで、静的解析による大量ソースコード内の文字列一括置き換えを行い、命名規則に合わせてクラスの命名規則を統一しました。
統一したい、そういった同じことを考えている方の助けになれば幸いです。
前提
命名規則
パスに基づいて名前は決定されます
Models/Hoge/Fuga/Foo.php
上の場合は名前はclass HogeFugaFoo
となります。
やりたいこと
まず
Models/Hoge/Fuga/Foo.php
このクラス名を書き換えます。もともとそうなっていたら何もしません。
変更前:class Fuga_Foo
変更後:class HogeFugaFoo
今回は違うのでやります。
次にこのクラスを使っている場所のクラス名を書き換えます
変更後:
hoge_fuga_foo = new HogeFugaFoo()
HogeFugaFoo::BAR_VALUE
以上終わり。
初動の失敗
シェルコマンドで一括ですればいいんでしょ?
ファイル名はそのままで、中身の特定の語句を s/src/dist/
してくれるものが求められました。
git管理下のファイル内の文字列を一括置換するワンライナー
http://qiita.com/edvakf@github/items/fdaef326d4ce0e02ac2e
こんな感じでやればいいと思ってました。
最初はそう思っていたがすぐに破綻しました。
誤陽性
たとえば Models/User/Rank.php
をclass User_Rank
からclass UserRank
にしたいと考えます。
すると上の方法で一括置き換えすると、使っている場所が置き換わるのがいいのですが、bar_rate.getUser_Rank
のような部分一致する場所までbar_rate.getUserRank
と置き換えてしまいます。
この誤陽性反応が私を困らせました。
また、ただclass Rank
とある場合もあります、これをclass UserRank
に置き換えねばなりません。するとパス指定している場所や、ただの一般名詞としてRank
としてPlayerRanking
と書いている場所すらPlayerUser_Ranking
と置き換えます。これは非常に困ります。
冷静に考えた
こうなってはファイルの中身の文字列をparseしてなんとかするしかありません。
しかし、どうやって?
先人に学べ
コンパイラやインタプリンタの構造を参考にしました。
単語単位にばらして解釈します。
まず単語単位にばらします。
$race_group_type = $adventurer_manager.get_race_group_type($player_data, Adventurer_Value::RACE_GROUP_TYPES); # 定義からプレイヤーキャラの種族分類を導出、AdventurerValueの定義に準拠。ハイエルフならエルフが返る
これをline
という文字列に格納します。
つぎに分割します。
line_words = line.split('\W') # クラスに使う文字(a-zA-Z0-9_)以外を区切り文字として分割
line_words = line_words.delete_if {|key| key == "" } # 空文字削除
puts line_words
[$race_group_type, $adventurer_manager, get_race_group_type, $player_data, Adventurer_Value, RACE_GROUP_TYPES, Adventurer_Value]
コメント部分まで分割してくれるのがよいですね。
今回はAdventurer_Value
を命名規則に合わせてAdventurerValue
にリネームします。
あとはString#index
を使って、置き換えたい単語の場所を指定して入れ替えます。
置き換え結果がこちらです。
$race_group_type = $adventurer_manager.get_race_group_type($player_data, AdventurerValue::RACE_GROUP_TYPES); # 定義からプレイヤーキャラの種族分類を導出、AdventurerValueの定義に準拠。ハイエルフならエルフが返る
問題発生(誤陽性)
しかし以下のケースで「本来置き換えるべきでない語句」を自動変換するケースが発生しました。
対象のクラスメイト同名のメソッド、定数
const Player_Job # ゲーム内のプレイヤーの職業
function Player_Job # ゲーム内のプレイヤーの職業
なども
Models/Player/Job.php
クラスをclass Player_Job
からclass PlayerJob
にリネームする場合に巻き添えになりました。
この人違いに対策する必要があります。
対策
直前の単語と区切り文字でクラス名か、それ以外かを判断しました。
直前の単語がconst
やfunction
なら定数やメソッド名なので、置換対象から外しました。
またメソッドの呼び出し側のほうも文節で判断しました。
([区切り文字]*(単語))で「文節」1グループを作り、区切り文字の情報を失わないようにしました。
直前の区切り文字が '->', '.', '::'
ならばメソッド名(or定数)を指しているので置換不要です。
直前の区切り文字が '/'
ならばパス名を指しているのでこちらも置換不要です。
教訓
精度重要
精度はきわめて重要です。なぜなら、置換スクリプトの精度が低ければ本番での障害か、ヒューマンチェックのコストがかかります。
ヒューマンチェックをこの量に対してかけるのは現実的でないと判断し、精度向上にかなりの時間をかけました。(ただしテストが壊れていないか、画面からのヒューマンテストも行っています。)
コードと設計
長くなりすぎて体力が切れたので反響があれば書きます。
本当に安全なの?
本番移行前に慎重なテストを行いたいです