PPIの簡単な使い方

PPIはなんだかとっつきにくかったが,使ってみると意外といける。パースするコードは読まなくても,PDOM(Perl Document Object Model)が分かればとりあえず使うことができる。

ppidump.pl

さて,まず実際にPPI使う前に,ダンプユーティリティを用意しておくと作業が楽になる*1

#!perl -w
# Uasge: ppidump.pl "say(q{Hello, world!})"
use strict;
use PPI::Document;
use PPI::Dumper;
my $document = PPI::Document->new(\"@ARGV");
PPI::Dumper->new($document, whitespace => 0, comments => 0)->print();

これでPerlソースコードから生成されたPDOMを見ることができる。

PPIを使ってみる

基本的には以下のような流れ。

  • $document = PPI::Document->new(\$src) か PPI::Document->new($file)でPPI::Documentオブジェクトを生成
    • $elems_ref = $document->find(\&want)で特定のオブジェクトを探す
      • $documentの要素$elemからは,$elem->parent(), $elem->schild(0), $elem->snext_sibling(), $elem->sprevious_sibling()なので周囲のエレメントにアクセス可能。なお,メソッドのプレフィクス"s"は"significant"の略で,コメントやpodなどの重要ではない要素を除いた「次」やら「前」やらの要素を返す。
      • $elem->__inseret_before($elem2)*2や$elem->__insert_after($elem2)で要素の追加
      • $elem->remove()で要素$elemの削除
    • $document->prune(\&want)で特定のオブジェクトを削除
  • 必要なら$document->serialize()で変更したソースコードを復元

大体において,まずfind()で特定の要素を探して,その特定の要素に何かをするというのがパターンだと思う。
find()に渡す関数は,たとえば以下のようになる。これは'use macro ...;'の行の,更にそのuse文に引数が与えられているケースを探すための関数である。

#macro/0.2より
package macro;
sub _want_use_macro{
	my(undef, $it) = @_; # 第一引数は$document
	my $elem;

	# use ...;文のクラスはStatement::Include
	return $it->isa('PPI::Statement::Include')

		# 子要素のうち,空白でない最初の要素
		&& ($elem = $it->schild(0))
		# $elemはおそらくToken::Word
		# その中身が'use'か否か
		&& ($elem->content eq 'use')

		# 'use'の次の空白でない要素
		# これはToken::Word(モジュール名)
		# またはToken::Number (バージョン番号)
		&& ($elem = $elem->snext_sibling)
		# モジュール名が目的のものか否か
		&& ($elem->content eq __PACKAGE__ or $elem->content eq 'macro')

		# 更にその次の要素を見て,引数の有無を確認
		&& _has_args($it, $elem->snext_sibling);
}

このように,単純なことならそれほど難しくない。少なくとも,自分でパーサーを書いたり正規表現でがんばるよりは楽だと思われる。

*1:もう少し多機能のものがmacroPerl::Criticのディストリビューション内にある。

*2:非公開APIだが,公開されているinsert_before()は特定のクラスのインスタンスしか受け付けないので使い物にならないためこれに頼らなければならないことが多い。