Perl Hackers Hub

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

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

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

もとのソースコードに戻すために,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.125

2021年10月23日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-12435-9

  • 特集1
    作って学ぶプログラミング言語のしくみ
    インタプリタ,構文解析器,文法
  • 特集2
    GraphQL完全ガイド
    RESTの先へ! フロントエンドに最適化されたAPI
  • 特集3
    速習DynamoDB
    AWSフルマネージドNoSQLの探求

著者プロフィール

谷脇真琴(たにわきまこと)

1989年生まれ。山口県出身。

面白法人カヤックのゲーム事業部でサーバサイドアプリケーションの開発とワークフローの整備に携わってきた。

趣味はゲームと3Dプリンタの製作。