hitode909の日記

以前はプログラミング日記でしたが、今は子育て日記です

Perlでメソッドが呼ばれたときに呼出元のソースコードを書き換えるやつ

普段Perl書いてて,メソッドのシグニチャ変えたときプロジェクト中の呼び出しを手で書き換えてた.
たくさんあると疲れるので,ソースコード中の,メソッドの呼び出しと引数を置き換えるプログラムを作りたい.
これができれば,非推奨なメソッドの呼び出しを新しいメソッドに自動で置き換えたり,メソッドのシグニチャ変えたときに機械的に置換できる.


以前,トークンを置き換えるのを作った.これを使えば,ifとunlessを置き換えられる.1トークンだけ置き換えできても,あまり役に立たない.トークン単位は以前やったので,今回は文単位で置き換えしてみる.

静的解析して置き換える

PPI::Transformを使って,ソースコードを静的解析して置き換えることを考えた.
PPIでトークン単位に分割して,指定したトークン列にマッチしたら,マッチした箇所のトークン列がやってきて,それを文字列操作で書き換える,というインターフェイス.


たとえば,二つの引数を取るaddという関数への呼び出しを,reverse_addという関数への呼び出しに置き換えて,引数の順番を逆にしたいとき,こういう風に書ける.'add'はaddというシンボルにマッチ,qr/.*/はなんでもいい.ここは引数が入る.マッチしたトークンたちが引数の渡ってくるので,それを使って,文字列連結して置き換え後のソースコードを返す.

my $transform = ReplaceStatement->new(
    rules => [
        {
            pattern => ['add', '(', qr/.*/, ',', qr/.*/, ')'],
            apply => sub {
                my ($tokens) = @_;
                my $method = $tokens->[0];
                my $arg1 = $tokens->[2];
                my $arg2 = $tokens->[4];

                return "reverse_$method($arg2, $arg1)";
            },
        },
    ],
);

$transform->file('in.pl' => 'out.pl');


実行前

add(1, 2);
add(1, 2, 3);


実行後

reverse_add(2, 1);
add(1, 2, 3);


1つめの呼び出しのトークン列は['add', '(', qr/.*/, ',', qr/.*/, ')']にマッチするので置き換えられる.2つめの呼び出しは,引数が3つあって,パターンにマッチしないので,置き換えられない.


これでは素朴すぎて,実用的でない.たとえば,以下のように,addの引数をハッシュで受け取るような場合に困る.

sub add {
    my (%args) = @_;
    $args{a} + $args{b};
}


これの呼び出し方法はいろいろある.以下のどれも,1+2を計算する.末尾にカンマがあるときとか,ハッシュのキーの順番がちがうときとか,いろいろある.上の方法でこれら全てに対処するのは難しい.正規表現みたいにする必要があるけど,正規表現で書くと職人みたいな技が要求される.字句解析したところで,ハッシュのキーと値の組をもらえればいいけど,そうはいかなくて,途中に3引数のリストが入ってるとキーと値がずれたりする.

add(a => 1, b => 2);
add(b => 2, a => 1);
add(a => 1, b => 2,);
add(a, 1, b, 2);

実行時に書き換える

さっきのハッシュの例では,実行すれば,%argsに呼ばれた結果が入ってるので,表現上の違いを気にする必要がない.途中までPerlのインタプリタが解釈してくれる.
以下の手順で置き換える.

  1. 置き換え対象のメソッドへの呼び出しをフックする
  2. 実際に呼び出された引数から,新しい形式の呼び出しのソースコードを作る
  3. 呼び出し元のソースコードを新しい形式のソースコードで置き換える


というのを作ってみた.


ハッシュで引数を受け取るAdderクラスのlegacy_addメソッドへの呼び出しを2つの引数で受け取るaddメソッドへの呼び出しに書き換える例.Adderのlegacy_addにコールバックを設定する.コールバックに,legacy_addを呼ぶさいに設定された%argsが渡ってくるので,そのハッシュのaとbを使って,addへの呼び出しを作る.

my $rewrite_on_called = RewriteOnCalled->new;
$rewrite_on_called->register('Adder', 'legacy_add', sub {
    my ($self, $adder, %args) = @_;
    my $a = $args{a};
    my $b = $args{b};
    my $args_string = $self->dump([$a, $b]);
    "add($args_string)";
});

my $adder = Adder->new;
warn $adder->legacy_add(a => 1, b => 2);


実行すると,legacy_addがaddに変わる.置き換え元のソースコードはコメントアウトして置いておかれる.

my $adder = Adder->new;
# warn $adder->legacy_add(a => 1, b => 2);
warn $adder->add(1, 2);


これも,簡単な例ではうまくいく.l上では1と2を足してたけど,$xと$yを足すような場合にうまくいかない.

my $x = 1;
my $y = 2;
$adder->legacy_add(a => $x, b => $y);

これを置き換えると,実行時に%argsに渡ってくるのは$xと$yではなく,1と2なので,add(1, 2)に置き換えられてしまう.もとのソースコードもコメントアウトされて残されるので,見比べながら変数名を元に戻す必要があり,あまり役に立たない.


感想

  • 実行時に書き換える方法では,カバレッジの高いテストがなければ適用できない(呼ばれるまで書き換えられないため)
  • 1と2から$xと$yを決めるときに,呼び出し元のソースコードをDeparseして,正規化してから処理すれば良いかもしれないが,行番号がずれて対応関係を取るのが難しくなるかもしれない
  • 正規表現でなんとかするほうがやりやすい?
  • PPIじゃなくて別のを使う? Bとか使えばもうちょっとなんとかなる?


なんか知見ある人いたら教えてください.