Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

perlimportsから探るPPIの世界

AnaTofuZ
March 05, 2022

 perlimportsから探るPPIの世界

YAPC::Japan::Online 2022の登壇資料です
https://yapcjapan.org/2022online/

AnaTofuZ

March 05, 2022
Tweet

More Decks by AnaTofuZ

Other Decks in Technology

Transcript

  1. perlimportsとは The Perl and Raku Conference (In the Cloud) 2021で発表されたツール

    https://metacpan.org/dist/App-perlimports 作者はOALDERS(Olaf Alders)さん 最近静的解析ツールにハマっているっぽい App::perlimports 今日の内容 App::perlvars コード上で未使用の変数検出ツール
  2. ロードタイミングの比較 require の場合 #!/usr/bin/env perl use strict; use warnings; print

    "before require\n"; require Hoge; print "after require\n"; require の直前のprint文が実行されている 逐次的に処理される ❯ perl hoge.pl before require Can't locate Hoge.pm in @INC (you may need to install the Hoge module) (@INC contains: ...) at hoge.pl line 8.
  3. ロードタイミングの比較 use の場合 #!/usr/bin/env perl use strict; use warnings; print

    "before use\n"; use Hoge; print "after use\n"; 何もprintされずに終了する スクリプトの実行前にuseが処理される ❯ perl hoge.pl Can't locate Hoge.pm in @INC (you may need to install the Hoge module) (@INC contains: ... ) at hoge.pl line 8. BEGIN failed--compilation aborted at hoge.pl line 8.
  4. useの実態 実際は次の構文と等価 use Hoge; BEGIN { require Hoge; Hoge->import(); }

    BEGINブロック Perlでコンパイルタイム(最初)に実行したい処理を書けるブロック BEGINブロック内で require して import を実行する
  5. 関数エクスポートをするimport 様々なやり方があるが、最近はExporterを使うのが主流 Exporterモジュールを継承、もしくはuseすると自動でエクスポート処理を import とし て登録する package Hoge::First; use strict;

    use warnings; use feature qw/say/; use Exporter 'import'; our @EXPORT = (qw/hoge/);  # サブルーチンの名前を文字列で配列の中にいれるとEXPORT される sub hoge { say 'this is first!'; } 1;
  6. 関数エクスポートをするimport @EXPORT に登録されているシンボル(関数名)をエクスポートする このコードでは hoge がエクスポートされる package Hoge::First; use strict;

    use warnings; use feature qw/say/; use Exporter 'import'; our @EXPORT = (qw/hoge/);  # サブルーチンの名前を文字列で配列の中にいれるとEXPORT される sub hoge { say 'this is first!'; } 1;
  7. EXPORT_OK @EXPORT に詰めたシンボルは無条件でエクスポートされてしまう 対して @EXPORT_OK はデフォルトではすべてをロードせず、use時にインポートしたい 関数名を書くことを強制させられる use JSON qw(encode_json);

    # encode_json がインポートされる @EXPORT の場合も関数名を use モジュール名 の後ろに列挙すると指定したもののみイ ンポートできる 空を指定すると何もインポートしない use JSON qw(); # encode_json はインポートされないので使えない
  8. 混沌としたコード use MyPkg::One; use MyPkg::Two; use MyPkg::Three; use MyPkg::Four; f1();

    f2(); MyPkg::Three->f3(); f1はMyPkg::OneがEXPORTしているものを使っている f2はMyPkg::TwoがEXPORT_OKしているが、コード上は指定を忘れている→
  9. useしているモジュールの実装 package MyPkg::Two; use strict; use warnings; use Exporter 'import';

    our @EXPORT_OK = qw(f2); sub f2 { print "f2\n"; } 1; f2はMyPkg::TwoがEXPORT_OKで宣言している useするものを書かないとf2がインポートされない 先程のコードではインポートしきれていないのでf2の実行で処理が止まってしまう
  10. perlimportsを使えば混沌としたコードも use MyPkg::One; use MyPkg::Two; use MyPkg::Three; use MyPkg::Four; f1();

    f2(); MyPkg::Three->f3(); f1はMyPkg::OneがEXPORTしているものを使っている f2はMyPkg::TwoがEXPORT_OKしているが、コード上は指定を忘れている f3はフルパスで呼び出している MyPkg::Four はuseしているが特に使っていない
  11. perlimportsを使えば混沌としたコードも $ perlimports --libs lib --no-preserve-unused main.pl use MyPkg::One qw(

    f1 ); use MyPkg::Two qw( f2 ); use MyPkg::Three (); f1(); f2(); MyPkg::Three->f3(); f1 , f2 が適切にインポートされた 特に関数をインポートしない MyPkg::Three は () を指定するようになった 使用していない MyPkg::Four は use が消えた
  12. ちなみに何でPerlの解析が難しいのか PPIのドキュメントにある例を見ると以下の状況 @result = (dothis $foo, $bar); Perlは関数呼び出しの () を省略できるので、

    dothis は何かしらの関数呼び出しである しかしこの記述だと次のどちらかに相当するのかパット見で判断つかない @result = (dothis($foo), $bar); @result = dothis($foo, $bar);
  13. なぜ判断つかないのか @result = (dothis($foo), $bar); @result = dothis($foo, $bar); 通常は関数呼び出しは後ろの引数をすべて取る

    Perlは関数プロトタイプ機能があり、引数を1つだけに成約することができる BEGIN ブロック内で何かしらの成約が追加されている可能性がある 例えば特定の状況で dothis の実態を切り替えるなど はたまたまだ dothis が宣言されていない可能性がある ...むずい!!!
  14. 静的解析とPerl Perlの静的解析はめちゃくちゃ難しいとされている Nothing but perl can parse Perl Perlのコードはperl(処理系)以外はパースできない 静的解析するツールを作成するのが極めて困難

    perl処理系が何かしらの静的解析で使いやすいAPIを提供している訳でもない それでも先人がツールを作成しているので巨人の肩に乗るのが良い 今回はperlimportsが使っているPPIを見ていきます 他にもPerlの静的解析ツールは色々生み出されています ライブラリの特性などを見る場合はYAPC::Tokyoの発表がおすすめです Perl5の静的解析入門
  15. PPIの特徴 Round Trip Safe(往復安全) PPI <-> Perlの変換を何回繰り返しても同じ結果になる 空白やコメントも含めてPPIがPerlスクリプトを解析した結果を保持する 大体のPerlプログラムを解析できる At

    time of writing there are only 28 non-Acme Perl modules in CPAN that PPI is incapable of parsing. 解析出来なかったものはそもそもぶっ壊れているモジュールだったとのこと
  16. PPIの構成 大きくPPI::TokenizerとPPI::Lexerで構成されている 解析した結果はPerl Document Object Model(PDOM)として返る PDOM ソースコードを表現した木構造のPerlのオブジェクト いわゆる抽象構文木に相当するもの Perlコードに対応する要素は

    PPI::Element として表現され、PDOMはその集合を持つ PDOMはあくまでPPIが作った解析用のオブジェクトモデル perl処理系のオブジェクトモデルとは異なっている PPIを活用する場合基本はこのPDOMを取り回すことになる
  17. PDOMを眺める PPIにはPDOMを出力できるDumperが付随している my $hoge = "hello"; ソースコードの左側に表示されているのが対応する PPI::Element まとまりがインデントで表現されていると捉えればわかりやすい PPI::Document

    PPI::Statement::Variable PPI::Token::Word 'my' PPI::Token::Whitespace ' ' PPI::Token::Symbol '$hoge' PPI::Token::Whitespace ' ' PPI::Token::Operator '=' PPI::Token::Whitespace ' ' PPI::Token::Quote::Double '"hello"' PPI::Token::Structure ';' PPI::Token::Whitespace '\n'
  18. 例えばPerlコード上の "hoge" に対応するElementを見てみる PPI::Token::Quote::Double->string で中身を感じに文字列化することができ る my $quote_tokens = $doc->find(

    sub { $_[1]->isa('PPI::Token::Quote::Double') } ); my $quote = $quote_tokens->[0]; say $quote->string; # hello PerlコードとPPI::Elementの対応がわかれば、 PPIのインターフェイスを通じてPerlのコ ード情報をPerlで操作することができる 難しそうな静的解析も普段のPerlプログラミングのレイヤーに
  19. useに相当するPPI::Element perlimports は use をいい感じにするモジュール use がPPI::Elementでどう表現されるかを把握しておくと何をやっているかがわか りやすい PPI::Statement::Include があり、以下の例はすべて

    Statement::Include として 解釈される use 5.006; use strict; use My::Module; use constant FOO => 'Foo'; require Foo::Bar; require "Foo/Bar.pm"; require $foo if 1; no strict 'refs';
  20. use JSON;をPPI::Elementに 例えば次の特に何もインポートするシンボルを指定していないuseをPPI::Elementに変換 してみる use JSON; 1つめの PPI::Token::Word に use

    が格納されている 2つめの PPI::Token::Word にモジュール名が格納されている PPI::Document PPI::Statement::Include PPI::Token::Word 'use' PPI::Token::Whitespace ' ' PPI::Token::Word 'JSON' PPI::Token::Structure ';' PPI::Token::Whitespace '\n'
  21. use JSON qw(encode_json);をPPI::Elementに encode_json を指定した状態でPPI::Elementに変換する use JSON qw(encode_json); 2つめのWordの後ろに QuoteLike::Words

    がある ここにインポートしたい関数の名前がある PPI::Document PPI::Statement::Include PPI::Token::Word 'use' PPI::Token::Whitespace ' ' PPI::Token::Word 'JSON' PPI::Token::Whitespace ' ' PPI::Token::QuoteLike::Words 'qw(encode_json)' PPI::Token::Structure ';' PPI::Token::Whitespace '\n'
  22. use JSON qw(encode_json);をPPI::Elementに 何もインポートしない () を指定した状態でPPI::Elementに変換する use JSON (); 2つめのWordの後ろに

    Structure::List がある 今回のケースではその中身は空文字 PPI::Document PPI::Statement::Include PPI::Token::Word 'use' PPI::Token::Whitespace ' ' PPI::Token::Word 'JSON' PPI::Token::Whitespace ' ' PPI::Structure::List ( ... ) PPI::Token::Structure ';' PPI::Token::Whitespace '\n'
  23. PPI::Statement::Includeからたぐるモジュールuseの構成 以下の状況を満たすPDOMは use しているものであると判断できる PPI::Statement::Includeのブロックであること 最初の PPI::Token::Word が use であること

    次の PPI::Token::Word が呼び出すモジュール名であること 指定がある場合で qw を使っていた場合は次に PPI::Token::QuoteLike::Words があり、なにか入っている 素朴な () の場合は PPI::Structure::List が来ており、空白の場合は中身 がないこと ※ use Exporter 'import'; など、 () を使わないケースもあるので必ずしもこ の条件が全てではない
  24. perlimportsの処理を眺める 実際に use を解析している箇所を眺めてみます findメソッドで指定した条件に一致するPDOMを取り出す事ができる これは PPI::Statement::Include でかつ use のものを指定している

    先程まで見ていたケースのPDOMを取り出す sub _build_original_imports { #... my $found = $self->ppi_document->find( sub { $_[1]->isa('PPI::Statement::Include') && !$_[1]->pragma # no pragmas && !$_[1]->version # Perl version requirement && $_[1]->type && $_[1]->type eq 'use'; } ) || [];
  25. useする際に指定しているシンボル名 PDOMでの表現を再度確認 use JSON qw(encode_json); 2つめのWordの後ろに QuoteLike::Words の中にシンボルがある Wordsの中を見ていけばよい PPI::Document

    PPI::Statement::Include PPI::Token::Word 'use' PPI::Token::Whitespace ' ' PPI::Token::Word 'JSON' PPI::Token::Whitespace ' ' PPI::Token::QuoteLike::Words 'qw(encode_json)' PPI::Token::Structure ';' PPI::Token::Whitespace '\n'
  26. useする際に指定しているシンボル名 気合でループしてシンボルを探している よいですね for my $child ( $include->schildren ) {

    if ( $child->isa('PPI::Structure::List') && !defined $imports ) { $imports = []; } if ( !$child->isa('PPI::Token::QuoteLike::Words') && !$child->isa('PPI::Token::Quote::Single') ) { next; } my @imports = $child->literal; if ( defined $imports ) { push( @{$imports}, $child->literal ); } else { $imports = [ $child->literal ]; } } return $imports;