Perl Hackers Hub

第63回 PPIとPerl::Tidyを組み合わせて作るコード整形ツール(2)

この記事を読むのに必要な時間:およそ 2.5 分

前回の(1)こちらから。

静的解析ライブラリを組み合わせて部分的にコード整形を行う

(2)では,PPIPerl::Tidyを組み合わせて独自のコード整形ツールを作成します。

例として,開発の初期にはPerl::Tidyが導入されていなかったプロジェクトについて考えます。あとからPerl::Tidyを導入する場合,それまでに書かれたコードが一度に整形されます。そうすると,あとになって該当部分のコードが書かれた意図を履歴から調べる際に,コード整形のコミットログが出てきて,本来知りたかったコミットまでたどり着けません。

この問題を解決するために,新しく追加や編集した行に限定してコード整形を行うツールを作成します。

Git::Repositoryで変更した箇所を検知する

Gitを導入しているプロジェクトであれば,編集した差分はgit diffで知ることができます。この情報を解析して,整形すべき場所を特定します。PerlではgitコマンドのラッパとしてGit::Repositoryモジュールがあるので,これを利用します。

Perlからgitコマンドを扱う

Git::Repositryを使用して変更された行を特定するには,次のようにします。

use Git::Repository;
my $repo = Git::Repository->new(
    work_tree => '/path/to/directory'
);

my @diff_outputs = $repo->run(
    qw/diff --diff-filter -U0 *.pm *.pl/
);

このコードでは,ファイル名と行数がわかる最小限の表示しかしないオプションを渡してgit diffを実行しています。また,Perlのソースコードに限って検出するため,.pmおよび.pl拡張子が付いたファイルのみを指定しています。

git diffの出力を解析する

前項のgit diffの出力から,変更されたファイル名と行番号を抜き出します。以下はパースする正規表現の例です。

my $filename_regexp = qr!\+\+\+\sb/(.*)$!x;
my $line_regexp = qr/
^@@\s-(?\d+)
(?:,(?\d+))?
\s\+(?\d+)
(?:,(?\d+))?\s@@
/x;

これらの情報を組み合わせると,ファイル内の変更された行番号を列挙できます。

PPIで変更箇所を抜き出す

差分が発生した行番号を特定できましたが,この行だけを整形しようとした場合,意図どおりの結果は得られません。その理由と,PPIを用いた解決方法を紹介します。

変更行だけを抜き出しても,うまくいかない

差分が発生した行だけを整形すれば,期待する結果が得られるかどうかを考えてみます。

例として,ハッシュの中にキーを追加した差分を考えます。

my $hash = {
    abc => "ABC",
    def => "DEF",
    ghij => "GHIJ", # この行を追加
};

Perl::Tidyはハッシュの=>の位置を,ハッシュ内の一番長いキーの長さに合わせてほかのキーも調整します。ですが,追加した行だけ整形した場合,そうはなりません。期待した結果を得るためには,my $hash = { ... };全体を整形する必要があります。

自然な整形の単位を考える

Perlのプログラムは文で構成されます。文は式やブロックなどの組み合わせで成り立っています。前項で示したソースコードは,全体が1つの文になっています。このことから,変更された行を含む文を抜き出すことができれば,期待どおりの結果が得られます。

変更された行を含む文を抜き出すためには,次の条件を満たす必要があります。

  • 字句が存在するソースコード上の行番号がわかる
  • 任意の文が特定の字句を含むかを調べられる
  • 任意の文をソースコードに変換できる

PPIは上記すべての要素を満たします。ソースコードから生成したPDOMには,行番号などファイル上の位置が保存されています。PDOMは,プログラム上で意味がある構造を保ったまま木構造として表現されるため,あるPDOMを含む親のPDOMであるかを調べる機能があります。また,PDOMにはもととなったソースコードに戻すメソッドが実装されています。

PPIを使って特定の行を含む文を抽出したうえで,その部分だけにPerl::Tidyを実行すると,目的を達成できます。

変更行を含む文を抜き出す

PPIを使って,指定した行を含む文を抜き出すプログラムを書きます。

my $doc = PPI::Document->new($filename);
my $nodes = $doc->find(
    sub {
        my (undef, $node) = @_;
        return grep {
            $node->line_number == $_
        } @$line_numbers;
    }
);

PPI::Documentfindメソッドを使って,対象の行だけに存在する文や字句をPDOMとして抽出しています。

次に,抜き出したPDOMが所属する文まで親をたどっていきます。

for my $node (@$nodes) {
    my $current_node = $node;
    while (!$current_node->parent->scope) {
        $current_node = $current_node->parent;
    }
    next if $current_node->isa("PPI::Token");
    push @matches_statement, $current_node;
}

scopeは,スコープを作る文だと真になるメソッドです。マップの中の文など,狭すぎる範囲を回避するために用いています。また,この段階で,文ではなく字句単体で存在する場合を取り除いています。この中には空白などが含まれますが,コード整形には不要だからです。

ある文が別の文に含まれている場合,1つに集約します。

my @reduced_statement;
for my $stmt (@matches_statement) {
    my $prev = scalar(@reduced_statement) > 0 ?
        $reduced_statement[-1] : undef;
    next if $prev && $prev->contains($stmt);
    push @reduced_statement, $stmt;
}

これで,コード整形を行いたい文が得られました。

Perl::Tidyで抜き出した文を整形する

得られた文をPerlのソースコードに変換したうえで,erl::Tidyで整形します。

for my $statement (@$reduced_statement) {
    my $content = $statement->content;
    my $dest_content = "";
    my $err = Perl::Tidy::perltidy(
        argv => "",
        source =>     source => \$content,
        destination =>     source => \$dest_content,
    );
    $err and die $err;
}

次に,整形したコードをPDOMに変換します。そして,もともとのPDOMと置き換えます。

my $dest_doc = PPI::Document->new(    source => \$dest_content);
my @dest_elements = $dest_doc->elements;
$stmt->insert_before(@dest_elements);
$stmt->remove;

PDOMの文が継承するPPI::Elementクラスには置換に使うreplaceメソッドが存在しますが,うまく機能しないことがあります。そこで,もともとのPDOMからinsert_beforeしたうえで,removeを行ってPDOMの置換を実現しています。

整形した結果をファイルに書き戻す

PPI::Documentではsaveメソッドでファイルに保存できます。

$doc->save($filename);

これで,変更した部分を含む文だけをコード整形するツールが完成しました。

まとめ

Perlの静的解析モジュールと,その使用例を示しました。また,静的解析モジュールを組み合わせて,現場のプロジェクトに合ったツールを作成する例を紹介しました。

静的解析は一見難しそうなテーマではありますが,リンタやコード整形ツールなど,実は日常的に使うツールで使われている技術です。また,本質は文字列の解析であり,Perlの得意分野でもあります。本稿を通して静的解析ツールの自作に興味を持っていただければ幸いです。

さて,次回の執筆者は佐藤健太さんで,テーマは「少しマニアックなPerlのテクニック」です。お楽しみに。

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.119

2020年10月24日発売
B5判/160ページ
定価(本体1,480円+税)
ISBN978-4-297-11669-9

  • 特集1
    [古い技術,コードの重複,密結合]
    フロントエンド脱レガシー
    長く愛されるプロダクトをさらに改善していくために
  • 特集2
    インフラ障害対応演習
    「避難訓練」でいざに備える!
  • 特集3
    深層学習入門以前
    チュートリアルを動かす前に知っておくこと

著者プロフィール

谷脇真琴(たにわきまこと)

1989年生まれ。山口県出身。

面白法人カヤックのゲーム事業部でサーバサイドアプリケーションの開発とワークフローの整備に携わってきた。

趣味はゲームと3Dプリンタの製作。