Perl Hackers Hub

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

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

本連載では第一線の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メソッドが定義されており,部分的にソースコードとして出力できます。

著者プロフィール

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

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

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

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