Perl Hackers Hub

第27回Perlにおける静的解析(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]⁠。

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

今回のゴールは「ローカル変数かつスカラ変数の変数名がミススペリングしているときに警告を出すプログラム」なので、トークンのタイプがローカル変数であるトークン、つまり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のミススペリングをうまく検出できていることがわかります。


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

まとめ

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

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

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

おすすめ記事

記事・ニュース一覧