Perl Hackers Hub

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

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

(1)こちら(2)こちらから。

静的解析を行うプログラムを書いてみる

静的解析を行うプログラムやツールは世の中にたくさんありますが,特定の機能を満足させるために自分で書きたくなることもあるでしょう。本節では,ソースコード内の変数名(簡単化のため,ローカル変数かつスカラ変数に限定します)がミススペリングしていた場合に警告を出す静的解析プログラムをCompiler::Lexerを使って書いていくことで,静的解析をするプログラムの書き方を俯瞰(ふかん)します。

本節で解析の対象とするソースコードは次に示すspell.plです。

my $name = "foo";
my $namae = "bar";

nameは英語辞書に載っている正しい英単語なので警告を出さず,namae「なまえ」を単純にローマ字にしただけの言葉で英単語ではないので警告を出す,という挙動を実装するのがゴールです。

Compiler::Lexerでトークン列に分割する

まず,ソースコードを解析するためには,ソースコードの文字列をトークン列に分解する必要があります。Compiler::Lexerでは次のようなプログラムを書くだけで,ソースコード文字列をトークン列に分割できます。

use Compiler::Lexer;

my $filename = 'spell.pl';

# ソースコードの文字列を読み込む
open my $fh, '<', $filename
  or die "Cannot open $filename: $!";
my $src = do { local $/; <$fh> };

# トークナイザのインスタンスを作る
my $lexer = Compiler::Lexer->new($filename);

# トークナイザでソースコード文字列をトークン列に分割する
my $tokens = $lexer->tokenize($src);

実際にトークン列を見てみる

上記のプログラムを実行してspell.plを解析すると,次のようなトークン列が得られます。

[
  bless( {
    data => "my",  …①
    has_warnings => 0,
    kind => 3,
    line => 1,  …②
    name => "VarDecl",  …③
    stype => 0,
    type => 59  …④
  }, 'Compiler::Lexer::Token' ),
  bless( {
    data => "\$name",
    has_warnings => 0,
    kind => 24,
    line => 1,
    name => "LocalVar",
    stype => 0,
    type => 187
  }, 'Compiler::Lexer::Token' ),
(中略)
  bless( {
    data => ";",
    has_warnings => 0,
    kind => 21,
    line => 2,
    name => "SemiColon",
    stype => 0,
    type => 103
  }, 'Compiler::Lexer::Token' )
]

静的解析を行うにあたってはこのトークン列を適宜見ながら処理する必要があります。

1つ目のトークンを見ると,data => "my"という記述からソースコードのmyに対応していることがわかります。また,name => "VarDecl"およびtype => 59という記述から変数宣言のトークンであること(typeの持つ数値が何を表しているかはCompiler::Lexer::TokenTypeに記述されています),line=> 1という記述から1行目に位置していることがわかります。以上をまとめると,「1行目に存在する変数宣言のためのmy」という情報をこのトークンから読み取ることができます。同様に2つ目のトークンからは,「1行目に存在するローカル変数$name」という情報を読み取ることができます注3)。

注3)
トークンのそのほかの情報についてはCompiler::Lexer::Tokenのドキュメントを参照してください。

トークンの情報に基づいて機能を実装する

今回のゴールは「ローカル変数かつスカラ変数の変数名がミススペリングしているときに警告を出すプログラム」なので,トークンのタイプがローカル変数であるトークン,つまりnameLocalVarであるトークンに注目すればよいことがわかります。したがって,トークンタイプがLocalVarのトークンのみを処理し,それ以外のトークンについては読み捨てる,といったプログラムを書いていくことになります。

$lexer->tokenize()で取得できるトークン列は配列リファレンスなので,単純にforによって走査できます。したがってトークン列の配列リファレンスを先頭から末尾にかけて走査していき,nameの値がLocalVarだったときだけdataの値に対してスペルチェックを行います注4)。この流れを実際にコードに起こすと次のようになります。

(前のプログラムの続き)
use Lingua::Ispell qw/spellcheck/;
for my $token (@$tokens) {
    if ($token->{name} eq 'LocalVar') {
        my $val = $token->{data};
        my $val_name = substr($val, 1); # シジルの削除
        if (spellcheck($val_name)) {
            print "[Maybe Typo] '$val' at $filename " .
                  "line $token->{line}\n";
        }
    }
}

今回はスペルチェックのためのモジュールとしてLingua::Ispellを利用しました。Lingua::Ispellが提供するspellcheck()という関数は,その名のとおり引数に渡された単語のスペルチェックを行います。

上記のプログラムを実行すると,次の出力が得られます。

[Maybe Typo] '$namae' at spell.pl line 2

2行目の$namaeのミススペリングをうまく検出できていることがわかります。


以上,駆け足でしたが簡単な静的解析プログラムの書き方を紹介しました。今回は簡単な例でしたが,複雑な処理をしたいときでも組み合わせのバリエーションが増えるだけで基本的な流れとしては大差はないはずなので,役に立つでしょう。

注3)
本来であればnameの値とLocalVarとを文字列比較するべきではありません。なぜならCompiler::Lexerの実装が変更されたときにプログラムが壊れてしまうからです。正しく行うには,typeの値とCompiler::Lexer::TokenType::T_LocalVarとを数値比較するべきです。

まとめ

静的解析の利点や,Perlにおける静的解析の背景・現状,および静的解析ツールについて駆け足で解説しました。Perlでもある程度静的解析を実施でき,またその恩恵にあずかれることをおわかりいただけたと思います。紙幅の都合上紹介できなかったPerlのための静的解析ツールはまだまだたくさんあるので,適宜調べてください。

また,簡単な例ですがPerlで静的解析を行うためのプログラムの書き方についても説明しましたので,簡単なツール程度であればこれを参考に書けるのではないかと思います。

次回の執筆者は今回取り上げたCompiler::LexerやCompiler::Parserの作者である五嶋壮晃さんで,テーマは「Perl5の構文解析器」です。今回は触れられなかった,字句解析や構文解析などの静的解析を支える解析器の深層について触れてもらえるのではないかと思います。楽しみですね!

Perl::Lint──新しく,シンプルで,高速なコーディング規約チェッカー

Perl::Lintは前述したPerl::Criticの高速化版モジュールです。The Perl FoundationというPerlのための活動に取り組んでいる財団から助成金を受けて,筆者が開発を進めています。

開発に至った経緯

Perl::Criticは便利なモジュールですが,本文で解説したとおり処理に時間がかかります。Test::Perl::Criticなどを使ってプロジェクトのファイルにあまねくPerl::Criticを適用すると,大きなプロジェクトでは処理が完了するまでに数分かかったりします。また,エディタの保存時にフックしてPerl::Criticを走らせるような設定にしていると,保存のたびにチェックが走り便利なのですが,その都度数秒ないしは数十秒待たねばならなくなってしまいます。

これらは開発のテンポを妨げる原因となり,またストレスの原因ともなっていました。しかしPerl::Criticを使わずにプロジェクトを進めていくのは,言うなればシートベルトを付けずに荒れ地を自動車で走るようなものなので,なかばしかたなく,我慢しながら利用している状態でした。

そこで,このPerl::Criticを高速化すれば価値があるのではないか,と思ったのが開発に至った経緯です。先に紹介したようにPPIを利用しているPerl::Minimum Versionの解析器部分をCompiler::Lexerに乗せ換えることによって30倍程度高速化できた,という情報もあったので,Perl::CriticもCompiler::Lexerに乗せ換えれば高速化できるのではないか,という見込みもありました。

また,The Perl FoundationがPerl関連で広い範囲に有用性が認められるプロジェクトに対し定期的に助成金を交付していること,そしてその交付ルールがちょうど変更され,応募しやすくなったことを知り,良い機会だと思い助成金に応募したところ審査が通ったので,いよいよ開発に踏み切ったという経緯もあります。

Perl::Lintの特徴

Perl::LintはPerl::Criticとは異なり,解析器としてPPIではなくCompiler::Lexerを使用しているため高速な動作が可能です。また基本的な動作に関してはPerl::Criticと互換性を持っていますが,Perl::Criticの持つ複雑なポリシーやルールの機構などは省き,よりシンプルで使いやすい新たな機構を採用する予定となっています。

Perl::Lintはまだ開発途中のモジュールなので,開発を手伝ってくださる方を目下募集しています。

著者プロフィール

川上大喜(かわかみたいき)

北海道函館市出身。高専卒業後,大学に編入。現在,大学院生業務をこなしつつ某企業でパートタイムエンジニアとして活動中。ソフトウェアのテストやCIといった,ソフトウェアの品質担保に関する話題に強い興味を持っている。

Twitter:@moznion
Web:http://moznion.hatenadiary.com/

コメント

コメントの記入