Perl Hackers Hub

第27回Perlにおける静的解析(2)

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

Perlの静的解析ツール

(2)では、Perlを用いた開発で広く利用されている静的解析ツールについて解説します。前述したように、現在使われている静的解析ツールの多くはPPIを解析器として使用しています。ここで取り上げるツールもPPIで実装されているものが主ですが、中には高速なCompiler::*モジュールで再実装されているものも存在します。本項ではそうした高速なバージョンについても紹介します。

Perl::Critic─⁠─ コーディング規約のチェッカー

Perl::Criticは、Perlコード用のコーディング規約のチェッカーです。このモジュールは静的なソースコード分析エンジンとして働き、主にDamian Conwayさんの著書『Perlベストプラクティス』注2に書かれているコーディングポリシーに則っているかどうかをチェックします(一部例外もあります⁠⁠。

使い方

Perl::Criticの基本的な使い方は次のとおりです。

use Perl::Critic;

my $critic = Perl::Critic->new();
print $critic->critique('/path/to/file.pl');

Perl::Criticのインスタンスメソッドであるcritiqueの引数にチェックしたいファイルを指定すると、そのファイルのコーディング規約をチェックしてくれます。

たとえば次のようなソースコードに対してPerl::Criticを適用すると警告が出ます。

open my $fh, "<$filename";
# => Two-argument "open" used at line 8, column 1. See page
207 of PBP.

Perl::Criticを用いると、このようなコーディングポリシーに則っていないコード(今回の場合はセキュリティに問題のある2引数のopen)を検出できます。

応用例

このようにPerl::Criticで警告が出るコードは品質に劣るケースがあります。これを効率的に検出するために、Perl::Criticによるチェック機構をユニットテストに組み込み、criticの警告を検出したらテストをfailさせる働きをするTest::Perl::Criticというモジュールも存在しており、こちらも広く使われています。また、Perl::Criticのチェック機構を組み込んだプラグインがVimやEmacsなどのさまざまなエディタで提供されており、たとえばファイルの保存時にフックしてそのたびにチェックを走らせるといった処理が可能となっています。

Perl::CriticはPPIで実装されていることに加え、ソースコードを評価するという複雑な処理を行っていることから、チェック処理に時間がかかります。しかし、非推奨な書き方をしてしまったばかりにうっかりバグを作りこんでしまうなどといったミスを未然に防ぐことができるので、ぜひ走らせておきたい静的解析処理の一つと言えるでしょう。

Perl::PrereqScanner(::Lite)─⁠─ 依存モジュールの抽出

Perl::PrereqScannerは、プロジェクトが依存しているモジュールの一覧を抽出するモジュールです。

登場の背景

プロジェクトの開発を進めていくうえで外部のCPANライブラリを利用することは多々あると思いますが、自由気ままにモジュールをプロジェクトに取り込んで開発していくと、そのプロジェクトがどのモジュールに依存しているか(つまりuserequireしているか)が簡単にはわからなくなってしまいます。ローカルの開発環境ではそれでも問題ないかもしれませんが、たとえばステージング環境や本番環境にデプロイする際に、プロジェクトがどのモジュールに依存しているかがわからないと、どの外部モジュールを環境にインストールすればよいのかがわからなくなってしまうため、その解決には非常に苦労します。

新しいモジュールをプロジェクトに取り入れた段階で、まめに依存モジュールをファイルに記述して更新すればよいという意見もありますが、うっかり更新し忘れてしまったり、モジュール名を書き間違えてしまったりする恐れがあります。さらに最近ではcpanmcpanfileCartonなどの台頭によってプロジェクト単位で利用するモジュールのバージョンを固定したいという要求も増えてきており、そのためにはモジュール名だけでなくそのバージョンまで管理する必要があります。こうした人間には厳しい仕事は機械にやらせたほうがよいというモチベーションの高まりから生まれたのがPerl::PrereqScannerです。

使い方

Perl::PrereqScannerの基本的な使い方は次のとおりです。

use Perl::PrereqScanner;

my $scanner = Perl::PrereqScanner->new();
my $prereqs = $scanner->scan_file('/path/to/file.pl');

たとえば次のようなファイルを上記のプログラムで解析すると、ファイルの依存関係を抜き出すことができます。

依存関係を抜き出したいファイル
use Acme;
require Acme::Buffy;
use Moose;
extends 'Acme::Colour'; # Moose における継承の記法
no Moose;
プログラムを実行すると得られる依存の情報
bless( {
  bad_version_hook => undef,
  requirements => {
    Acme => bless( {
      minimum => bless( {
        original => 0,
        version => [
          0
        ]
      }, 'version' )
    }, 'CPAN::Meta::Requirements::_Range::Range' ),
    "Acme::Buffy" => bless( {
      minimum => bless( {
        original => 0,
        version => [
          0
        ]
      }, 'version' )
    }, 'CPAN::Meta::Requirements::_Range::Range' ),
    "Acme::Colour" => bless( {
      minimum => bless( {
        original => 0,
        version => [
          0
        ]
      }, 'version' )
    }, 'CPAN::Meta::Requirements::_Range::Range' ),
    Moose => bless( {
      minimum => bless( {
        original => 0,
        version => [
          0
        ]
      }, 'version' )
    }, 'CPAN::Meta::Requirements::_Range::Range' )
  }
}, 'CPAN::Meta::Requirements' )

Perl::PrereqScannerのインスタンスメソッドであるscan_*()は上記のようにCPAN::Meta::Requirementsのオブジェクトを返却してくるので、好きなフォーマットで表示したい場合は適宜オブジェクトを操作する必要があります。そうした処理を書くのが煩わしい場合は、Perl::PrereqScannerに付属しているスクリプトscan_prereqsなど)や、App::scan_prereqscpanfileなどのスクリプトを使うとよいでしょう。以下にscanprereqs-cpanfileの実行結果を示します。

$ scan-prereqs-cpanfile
requires 'Acme';
requires 'Acme::Buffy';
requires 'Acme::Colour';
requires 'Moose';

Perl::PrereqScanner::Lite─⁠─高速化版Perl::PrereqScanner

Perl::PrereqScannerは便利ですが、PPIで記述されているため処理に時間がかかります。これをPPIからCompiler::Lexerに乗せ換え、高速化したのが拙作のPerl::PrereqScanner::Liteです。Perl::PrereqScanner::LiteはオリジナルのPerl::PrereqScannerと比較して20~30 倍程度の高速化が実現されています。前述のApp::scan_prereqs_cpanfileでは内部のスキャナとしてPerl::PrereqScanner::Liteが採用されています。

App::PRT─⁠─ リファクタリングツール

App::PRTはPerlコードのためのリファクタリングツールです。PRTは「Perl Refactoring Tool」の頭字語です。

sedを用いたリファクタリング

Perlのリファクタリングと言えばsedなどのツールを使って正規表現で一括置換といった方法が一般的でしたが、コンテキストを考慮せずに単純に置換処理を行うと、プログラムが壊れて動かなくなったり、壊れないまでも予期せぬ動作をしたりする場合があります。

簡単な例ですが次のようなプログラムがあったとして、$blogというハッシュリファレンスのidというキーをarticle_idにリネームしたいとしましょう。

my $blog = shift;
print $blog->{id};
print $blog->{user_id};

sedで単純にリネームすると次のようになると思います。

$ sed -e "s/id/article_id/g" script.pl

しかしこれではidというキーだけではなくuser_idというキーに含まれるidという部分にも影響が及んでしまい、次のように意図しない結果が出力されてしまいます。

my $blog = shift;
print $blog->{article_id};
print $blog->{user_article_id};

prtを用いたリファクタリング

そこで登場するのがApp::PRTです。App::PRTをインストールすると一緒に入ってくるコマンドラインツールprtを利用することで、さまざまな処理を行えます。今回の例だと、prtのサブコマンドであるreplace_tokenを使うと望みの処理を実現できます。

$ prt replace_token id article_id script.pl

第一引数のreplace_token「トークンレベルでの置換を行う」というサブコマンドです。第二引数には置き換えたいトークンの名前、第三引数には置き換えるトークンの名前を指定します。最後の引数にはリファクタリングの対象とするファイルを与えます。このコマンドを実行することによって、idというトークンにのみ置換が働いてarticle_idに変更できます。以下は、上記のコマンドによって置換を実行した結果です。

my $blog = shift;
print $blog->{article_id};
print $blog->{user_id};

App::PRTは内部的にPPIを使っており、ソースコードの解析・置換時にPPIのPDOMおよびTokenクラスを見ているため、このような賢い置換が可能となっています。App::PRTでは、トークンの置換だけではなく、クラスのリネームやメソッドの削除といった処理もインテリジェントに実行できます。

App::PRTはまだ若いプロジェクトなので、今後の発展が注目されるところです。

<続きの(3)こちら。>

おすすめ記事

記事・ニュース一覧