Perl Hackers Hub

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

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはmoznionこと川上大喜さんで、テーマは「静的解析」です。

静的解析の背景

静的解析とは何か

静的解析とは、プログラムを実際に実行することなくソフトウェアの解析を実現する手法の一つです。静的コード解析や静的プログラム解析と呼ばれることもあります。

一般に静的解析と言った場合、解析の主体がコンピュータであるか人間であるかは問われませんが(人間が解析する場合は「コードレビュー」などと呼ばれるでしょう⁠⁠、本記事では、特に断りがない限りコンピュータによる解析を指すものとします。

静的解析の利点

静的解析を行うことにより、⁠未使用変数の検出」「コーディングスタイルの不一致の検出」といった人間が行うにはあまりにも単純で退屈な、しかしコードの品質を向上させるためには必要な作業をコンピュータに任せることができます。

また、そうした単純作業とは逆に、クラス間の依存関係の分析注1)といった、人間がやるには複雑で難しい処理についてもコンピュータに肩代わりさせることができます。

加えて、静的解析によって実行時エラーの検出なども行えます。たとえば、Null参照の例外(いわゆるヌルポ)をユニットテストやインテグレーションテストなどのソフトウェアテストで網羅的に検出するのは困難ですが、静的解析の手法を用いることでこれらの問題の検出の一助となる場合があります。

静的解析を実行する方法

静的解析を実現するアプローチとしては、

  • ソースコードをトークナイザによってトークン列に分解してそれを直接評価・解析する方法
  • 分解したトークン列をパーサによって抽象構文木に変換してから解析する方法
  • 言語処理系の出力する中間ファイルを解析する方法

など多岐にわたります。1つ目のトークン列を直接評価および解析する手法は汎用的に使える技術なので、本稿では主にこの方法について述べます。

静的解析の例

実際の静的解析の例を見てみましょう。静的解析の技術やその周辺ツールがほかの言語と比較して豊富なJavaを例に解説します。

未使用のローカル変数の検出

静的解析の代表的な例として、未使用のローカル変数の検出が挙げられます。

検出結果はコンパイラの警告として出力されますが、こうした機能はIDEIntegrated Development Environment統合開発環境)に組み込まれていることが多いでしょう。図1はJavaのIDEであるEclipseにおける未使用ローカル変数の検出例です。使用されていないString型のfooという文字列が検出され、警告とともにハイライトされていることがわかります。

図1 未使用のローカル変数の検出例
図1 未使用のローカル変数の検出例

Checkstyle――コーディングスタイルのチェック

Checkstyleというソフトウェアを利用することでコーディングスタイルのチェックを行えます。CheckstyleはEcpilseをはじめとしたさまざまなIDEのプラグインとしてサポートされているので、簡単に導入できます。

図2では、if文の開き中括弧の直前にホワイトスペースがないことが警告されています。Checkstyleを用いると、このように人間が行うにはつまらない作業を静的解析処理に任せることができます。Checkstyleではこのほかにも、⁠変数の命名規則に則っているか」「修飾子の整合性がとれているか」といった多岐にわたるチェックを実行できることに加え、ユーザが独自のコーディングスタイルのルールを設定することもできます。

図2 Checkstyleによるコーディングスタイルの不一致の検出例
図2 Checkstyleによるコーディングスタイルの不一致の検出例

FindBugs――潜在的なバグの検出

静的解析によって潜在的なバグの検出も実現できます。JavaではFindBugsというソフトウェアが有名です。FindBugsも多くのIDEでプラグインが提供されています。

図3ではFindBugsを使って、Null参照の例外が発生してしまうコードを検出しています。Null参照の例外は実行時エラーですが、静的解析によって実際にプログラムを実行することなく検出できています。Find Bugsはこれ以外にも多種多様な潜在バグを、静的解析を用いることにより検出できます。

図3 FindBugsによる潜在バグの検出例
図3 FindBugsによる潜在バグの検出例

これらはほんの一例にすぎませんが、静的解析の便利さを感じていただけたと思います。

Perlにおける静的解析の背景と課題

Perlの静的解析が抱えていた問題

先述したように、静的解析を実現するためにはソースコードをトークンレベルにまで分割する必要があります。しかしPerlは、⁠Only perl can parse Perl⁠⁠、つまり「Perlをパースできるのはperlだけ!」という格言(?)が示す複雑かつ動的な文法が災いして、Perlで書かれたプログラムを解析してトークンに分解するのは無理、すなわち静的解析は不可能という見方が長らく続いていました。

PPIの登場

しかし、2002年ごろからAdam Kennedyさんを中心としてPPIと呼ばれるPerlの総合的な解析器(ざっくり言うとPerlで記述されたトークナイザとパーサ)が開発され、Perlにおける静的解析の分野に一筋の光が射しました。現在、Perlの静的解析周りの多くのツールがPPIを解析器として利用しています。

最近の静的解析を取り巻く状況

最近では、五嶋壮晃さんが中心となり2012年ごろから開発している、C++製で高速に動作するPerlのトークナイザおよびパーサであるCompiler::LexerCompiler::Parserなども台頭してきており、Perlの静的解析を取り巻く状況は非常におもしろいものとなっています。

これらにまつわる詳細については次節で説明します。

Perlの静的解析を支える技術

Perlの静的解析を実現する方法はいくつかあります。ここではその中からPerlの静的解析を支える代表的な技術を紹介します。

PPIを使う方法

先述したPPIは、2005年に初の安定版がリリースされ、現在ではバージョン1.213がリリースされています。

PPIの特徴

PPIはコードがPerlで記述されているため、多くのプラットフォーム上で安定して動作します。それに加えて、CPANにアップロードされているモジュールの99%以上を正しく解析できたという実績と信頼があるモジュールです。

一方でピュアPerlな実装であるため、先述したCompiler::LexerなどのC++で記述された解析器と比較すると実行速度に劣るというデメリットもあります。ただ、それを補って余りある信頼性の高さから、Perlの静的解析の分野では広く使用されています。

PPIによるソースコードの解析

PPIによるPerlのソースコード解析は大きく2つのステップに分割できます。1つ目のステップはPPI::Tokenizerを使ってソースコードをトークン列に分解する作業です。続く2つ目のステップはPPI::LexerあるいはPPI::Documentを用いて、PDOMと呼ばれる抽象構文木をPerl向け(と言うよりもPPI向け)に拡張したデータ構造に変換する作業です。PPI::LexerはPPI::Tokenizerによって得られるトークン列から、PPI::Documentは対象とするソースコードのファイルパスをからそれぞれPDOMを生成します。以下にコード例を示します。

トークン列を取得するプログラム
use PPI;

# トークナイザのインスタンスを作る
my $tokenizer = PPI::Tokenizer->new('/path/to/file.pl');
my $tokens = $tokenizer->all_tokens(); # トークン列を得る
PPI::LexerでPDOMを取得するプログラム
use PPI;

# トークナイザのインスタンスを作る
my $tokenizer = PPI::Tokenizer->new('/path/to/file.pl');
# PPI::Lexer のインスタンスを作る
my $lexer = PPI::Lexer->new();
my $pdom = $lexer->lex_tokenizer($tokenizer); # PDOM を得る
PPI::DocumentでPDOMを取得するプログラム
use PPI;

my $pdom = PPI::Document->new('/path/to/file.pl');
                                              # PDOM を得る

なお、PDOMに変換する処理は、内部的にはPPI::Tokenizerでトークナイズして得られたトークン列を処理することで実現しています。解析に際してPDOMが必要とならない場合、つまりトークン列のみで十分な場合にPPI::Lexerなどを使うとオーバーヘッドの原因となるので、状況に応じた使い分けが必要となってくるでしょう。詳細に関してはPPIのPerldocを参照してください。

PPI::XSは使わないように!

CPANには、PPI::XSと呼ばれるXSによる高速化版を謳(うた)うモジュールも存在します。しかし、長らくメンテナンスがされていないことや、XSで提供されている部分は実は定数の返却の部分のみで、ほかの部分はピュアPerlの実装と何ら変わりがなく、高速に動作することはほぼありえないという理由から、積極的に採用する理由は皆無でしょう。

Compiler::LexerおよびCompiler::Parserを使う方法

先述したCompiler::LexerとCompiler::Parserはそれぞれ、Perlのソースコードをトークン列に分割する処理と、そのトークン列を抽象構文木に変換する処理を行います。

Compiler::*の特徴

両モジュールはC++で記述されており、前述のPPIと比較して非常に高速に動作します。また、豊富なテストによって機能が担保されていることや、最近ではPPIで書かれていたモジュールをCompiler::Lexerで再実装したモジュールが利用されていることなどから、信頼性についても問題のない水準に達していると言えるでしょう。

Compiler系のモジュールを使うことによる高速化の実例を挙げると、PPIを利用して書かれたPerl::MinumumVersionとCompiler::Lexerにより書かれたPerl::MinimumVersion::Fastの速度差を比較すると後者のほうが30倍程度高速に動作するといったものが挙げられます。これだけでも使う動機としては十分でしょう。

Compiler::*によるソースコードの解析

Compiler::LexerによりPerlで書かれたソースコードをトークン列に変換し、そのトークン列をCompiler::Parserで抽象構文木に変換するという一連の流れを記述したコード例を示します。

use Compiler::Lexer;
use Compiler::Parser;

# ソースコードの文字列を読み込む
open my $fh, "<", '/path/to/file.pl';
my $src = do { local $/; <$fh> };

# トークン列を得る
my $lexer = Compiler::Lexer->new($filename);
my $tokens = $lexer->tokenize($src);

# トークン列から抽象構文木を得る
my $parser = Compiler::Parser->new();
my $ast = $parser->parse($tokens);

詳細については各モジュールに関するPerldocを参照してください。

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

おすすめ記事

記事・ニュース一覧