Perl Hackers Hub

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

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはPerlで長年Webサービス開発に携わっているマコピーこと谷脇真琴さんで、テーマはPPIPerl::Tidyを組み合わせて作るコード整形ツール」です。

本稿のサンプルコードは、WEB+DB PRESS Vol.118のサポートサイトから入手できます。

日常化するプログラムの静的解析

昨今のプログラミング現場では、プログラミング作業を手助けする、さまざまなツールが用いられています。開発チーム内で独自のツールを作成して用いる事例も多く見られます。昔と違う点としては、本格的な静的解析が広く使われていることが挙げられます。

静的解析とは、ソースコードをプログラムとして実行せずに、プログラムの構造や記述を解析する手法です。一方で、プログラムとして実行したうえで解析する手法を動的解析と言います。動的解析に比べて、静的解析には次のメリットがあります。

ソースコード内の情報を多く使うことができる
コメントや関数の位置、ステートメントの前後関係を解析に使える
一般的に動的解析に比べて安全かつ高速
プログラムの実行には必須のリソースが必要ない
プログラミング言語の機能を超えた解析が可能
コメントを用いたアノテーションなどを用いてコード生成に用いる

本稿では、代表的なPerlの静的解析モジュールであるPPIと、コード整形ツールとして有名なPerl::Tidyの使い方を紹介したあと、この2つのモジュールを組み合わせて独自のコード整形ツールを作成する方法を解説します。

本稿は、次のバージョンのPerlとモジュールを用いました。

  • Perl 5.30.0
  • PPI 1.270
  • Perl::Tidy 20200619
  • Git::Repository 1.324
  • Git 2.26.2

Perlの静的解析モジュールの使い方

Perlには、⁠Perlで書かれたソースコードを、perlコマンド以外で解析することは困難である」Only perl can parse Perlという趣旨の言葉があります。Perlは文脈依存の構文を多く持つため、素朴なテキスト解析ではプログラムの構造をうまく解析できないことを表しています。しかし、多くの知見と努力によって、perlコマンド以外でも実用的に静的解析を行えるモジュールがCPANには存在します。

本節ではその中でも、PPIPerl::Tidyを紹介します。

抽象構文木が利用できるPPI

PPIは、Perlのソースコードに対して静的解析を行うモジュールです。PerlのリンタであるPerl::Criticで使われています。

PPIでソースコードを解析すると、一般的な抽象構文木に近い構造が得られます。抽象構文木とは、ソースコードを意味がある単位で分解したうえで、プログラムの入れ子構造に準じたデータ構造に変換したものです。PPIの用語では、この木構造を構成するオブジェクトをPDOMと呼んでいます。PDOMは、字句の種類や文の種別など、クラスが細かく分かれています。

PDOMに変換すると、ソースコード上の文字列に紐付く情報を利用できます。また、木構造になっているため、ブロックスコープなどのソースコード内の構造もより解析しやすくなります。

ソースコードを解析する

PPIを用いてPerlのソースコードを解析する方法を解説します。

今回は、次のソースコードをPDOMに変換します。次のコードでは、$var変数にテキストを挿入しています。

my $var = "Lorem ipsum dolor sit amet";

PPIでソースコードを読み込むにはいくつかの方法がありますが、今回は文字列としてソースコードを記述します。

use PPI;

my $source = <<'EOL';
my $var = "Lorem ipsum dolor sit amet";
EOL

my $doc = PPI::Document->new(\$source);

PPIで文字列のソースコードを読み込むには、上記のようにスカラリファレンスとしてコンストラクタに渡します。ただの文字列を渡した場合は、ファイル名として解釈されます。

このソースコードのPDOMを見てみます。人の目で見てわかりやすく出力するために、PDOMの構造を表示するPPI::Dumperを使用します。

use PPI::Dumper;

my $dumper = PPI::Dumper->new($doc);
$dumper->print;

1つ前のコードでPPI::Documentを用いて解析したPDOM$docに入れて、PPI::Dumperに渡しています。そして、printメソッド用いて標準出力へ出力しています。

このコードを実行すると、標準出力に次の結果が出力されます。

PPI::Document
  PPI::Statement::Variable
    PPI::Token::Word           'my'
    PPI::Token::Whitespace     ' '
    PPI::Token::Symbol         '$var'
    PPI::Token::Whitespace     ' '
    PPI::Token::Operator       '='
    PPI::Token::Whitespace     ' '
    PPI::Token::Quote::Double  '"Lorem ipsum dolor sit
amet"'
    PPI::Token::Structure      ';'
  PPI::Token::Whitespace       '\n'

PPI::Dumperは木構造をインデントで表現します。PPI::Documentが、ソースコード全体を表します。PPI::Documentは、ファイル内のPerlプログラムが解析された結果のPPI::Elementを持ちます。PPI::Statement::VariablePPI::Token::WordいったPDOMは、PPI::Elementを継承しています。

上記の出力を見ると、このPPI::Documentは、PPI::Statement::VariableクラスのPDOMを1個だけ持ちます。変数の宣言は、このPPI::Statement::Variableクラスで表現されます。PPI::Statement::Variableを含むPPI::Statementから始まるクラスは、文を表します。

PPI::Statement::Variableも複数のPDOMを持って入れ子構造を作っています。PPI::Statement::Variableが持つPDOMは、名前がPPI::Tokenから始まるクラスです。PPI::Tokenから始まるクラスは、字句を表します。myキーワードはPPI::Token::Word空白はPPI::Token::Whitespace変数名などのソースコード上のシンボルはPPI::Token::Symbolです。

解析した結果をソースコードに戻す

先ほど見たように、PDOMには、空白などのプログラムの動作に関係しないものも保持されています。これは、PPIのユースケースとして、リファクタリングなどのソースコードの書き換えやコード生成も含まれているためです。つまり、もともとのソースコードへ戻せる形式になっています。

生成したPDOMから、ソースコードに戻してみます。

say $doc->content;

すると、一番初めに例として挙げたソースコードが出力されます。

PPI::Elementクラスには、このcontentメソッドが定義されています。また、配下のPPI::Statement::Variableなどのクラスにもcontentメソッドが定義されており、部分的にソースコードとして出力できます。

ソースコード上の位置を出力する

もとのソースコードに戻すために、PDOMにはファイル内での行と列の情報も入っています。

ここでは、PPI::Statement::Variableのファイル上の位置を取り出してみます。先ほどのPPI::Dumperの結果から、PPI::Statement::VariablePPI::Documentの先頭にあるので、first_elementメソッドで取り出します。そのうえで、PPI::Elementのメソッドであるline_numberおよびcolumn_numberメソッドで、行番号と列番号を表示します。

my $statement = $doc->first_element;
say $statement->line_number;
say $statement->column_number;

実行すると、両方1と表示されます。

では、この文の中の字句についても取り出して表示してみます。PPI::Statement::Variableelementsメソッドで字句のリストを取得して、line_numbercolumn_numberメソッドで行番号と列番号を表示し、contentメソッドで内容を表示します。

for my $elem ($statement->elements) {
    say "line=" . $elem->line_number .
        ", column=" . $elem->column_number .
        ", content=\"" . $elem->content . "\"";
}

実行結果を示します。

line=1, column=1, content="my"
line=1, column=3, content=" "
line=1, column=4, content="$var"
line=1, column=8, content=" "
line=1, column=9, content="="
line=1, column=10, content=" "
line=1, column=11, content=""Lorem ipsum dolor sit amet""
line=1, column=39, content=";"

lineはすべて1のままですが、columnは文字列内の位置によって変わっています。

ソースコード内で宣言されている変数を列挙する

PPIの簡単な使用例として、与えられたソースコード内で宣言されている変数を抽出します。

次のソースコードを解析します。if文で構造が作られている複雑なソースコードです。

my $v1 = "Lorem ipsum dolor sit amet";
if ($var) {
    my $v2 = " consectetur adipiscing elit";
    $v1 .= $v2;
}
say $v1;

上記のソースコード内で宣言されている変数名を列挙します。$sourceに上記のソースコードが格納されているとすると、次のコードで変数名を列挙できます。

my $doc = PPI::Document->new(\$source);
my $vars = $doc->find("PPI::Statement::Variable");
my @vnames = map { $_->variables } @$vars;
say "@vnames";

このプログラムを実行すると、$v1 $v2と出力されます。ソースコード中で宣言された変数の名前が抜き出せました。

PPI::Documentに定義されているfindメソッドは、指定したPPIのクラス名を持っている木構造から検索して列挙します。上のコードでは、変数の宣言の文が表現されるPPI::Statement::Variableクラスを指定しています。

PPI::Statement::Variableクラスにはvariablesメソッドが定義されていて、文の中で定義された変数名を返します。もし、my ($v1, $v2) = ...と一度に複数の変数名が宣言された場合は、複数個の名前が返ってきます。

このプログラムを応用すると、次のようなことが確認できます。

  • ソースコード中の変数がプロジェクトの命名規則にのっとったものになっているか
  • 未使用の変数がないか
  • Perl 5.24.0で廃止された、keysなどの組込み関数にリファレンスを渡せる機能が使われていないか

コード整形ツールPerl::Tidy

Perl::Tidyは、Perlのソースコードを整形するためのモジュールです。Perl::Tidyは静的解析を行い、その情報から整形前後のコードの意味が等しいコード整形を行います。

Perl::Tidyをモジュールとして使う

多くの場合Perl::Tidyperltidyコマンドで使われますが、モジュールとしても使用できます。モジュールとして使用する場合には、コマンドとして使う場合にはできないことを行えます。モジュールとして使用する場合は、Perl::Tidyuseしたうえで、Perl::Tidy::perltidy関数を使います。

次の例では、コード整形を行う前のソースコードに対して加工を行うprefilterと、整形後のソースコードに対して加工を行うpostfilterを指定しています。

use Perl::Tidy;

my $source = '...';
my $destination = '';

Perl::Tidy::perltidy(
    argv => undef,
    source => \$source,
    destination => \$destination,
    prefilter => sub { ... },
    postfilter => sub { ... },
);

特定のコメントが現れた次の行は整形しない

Perl::Tidyの静的解析の結果は、Perl::Tidyの挙動をカスタマイズする際に使用できます。ここでは、Perl::Tidyの具体的なカスタマイズ例を紹介します。

perltidyコマンドを通常どおり使用していると、渡したファイル全体がコード整形されます。しかし、まれにコードを整形されたくない箇所があります。

たとえば、次のソースコードを整形することを考えます。

my $map = {
    key => [{
        "bar" => "bazz",
    }, {
        "foo" => "boo",
    }],
};

Perl::Tidyで整形すると、次に示す形に変化します。

my $map = {
    key => [
        {
            "bar" => "bazz",
        },
        {
            "foo" => "boo",
        }
    ],
};

ハッシュリファレンスの入れ子構造の場合、Perl::Tidyのデフォルト設定だと、このように整形によって大きく書き換えられてしまいます。かえって読みにくくなったり、コードの意図が損なわれる整形がなされる場合もあります。そこで、Perl::Tidyをカスタマイズし、特殊なコメントを入れると整形されない機能を付加します。

前項で述べたPerl::Tidy::perltidy関数には、formatterオプションで独自の整形を行うオブジェクトを渡せます。オブジェクトには、デフォルトではPerl::Tidy::Formatterが用いられます。通常の動作から少しだけ変えたい場合は、このPerl::Tidy::Formatterの挙動を少しだけ変えるとよさそうです。

しかし、Perl::Tidy::Formatterは、初期化する際にいくつかロガーオブジェクトを渡さないとならず、ゼロからオブジェクトを作って初期化をすると、かなりの手間です。そこで、perltidy関数内部で初期化されるPerl::Tidy::Formatterが持つ、整形を行う関数write_lineの挙動を直接上書きして手間を省きます。

Perl::Tidyのドキュメントには、write_line関数は1行ごとに呼び出されると記述されています。引数として、フォーマッタが呼び出される前段階で行った静的解析の結果が渡されます。具体的にはハッシュリファレンスの形で特定の行の中の字句の並びなどが渡されるので、そこから特殊なコメントを検知し、整形処理をスキップします。

*Perl::Tidy::Formatter::write_line = sub {
    my ($obj, $line_of_tokens) = @_;
    if ($ignore_lines > 0) {
        $ignore_lines--;
        $line_of_tokens->{_line_type} = "POD";
    }
    if (
        $line_of_tokens->{_line_type} eq "CODE" &&
        $line_of_tokens->{_rtoken_type}[0] eq "#" &&
        $line_of_tokens->{_rtokens}[0] =~
            /# ignore (\d+) lines?/
    ) {
        $ignore_lines = $1;
    }
}

このコードでは、# ignore <数字> lines形式のコメントを検知して、フォーマットをスキップする行数を$ignore_lines変数に保存しています。$ignore_lines変数は関数の外にあるため、行をまたいでスキップすべき行数が保持されます。

$line_of_tokens->{_line_type}は渡された行の種別を示します。通常のPerlソースコードであればCODEが入りますが、PODPlain Old Documentationの中であればPODファイル中の__END__より下の行であればENDが入ります。Perl::Tidy::FormatterCODE以外ではコードの整形を行わないので、種別をPODに上書きして整形をスキップします。

Perl::Tidyはとても複雑なモジュールで、とっつきにくい面もありますが、このようにカスタマイズの手段が用意されています。プロジェクトに合ったコード整形を行いたい場合は、Perl::Tidyのドキュメントを参照してカスタマイズするとよいでしょう。

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

WEB+DB PRESS

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

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧