Slide 1

Slide 1 text

perlimportsから探るPPIの世界 YAPC::Japan::Online2022 03/05 八雲アナグラ(AnaTofuZ)

Slide 2

Slide 2 text

自己紹介 八雲アナグラ(@AnaTofuZ) Okinawa.pm -> ??? 最近は京都にいます 株式会社はてなノベルチーム

Slide 3

Slide 3 text

※ 今日のトークについて 途中でピザが来ます 来たら受け取りに行きます

Slide 4

Slide 4 text

今日の内容 perlimportsの雰囲気とPPIの雰囲気がわかる

Slide 5

Slide 5 text

今日の内容 perlimportsとは perlimportsの紹介 perlのuse問題 使い方 perlimportsとPPI 静的解析とは PPIの紹介 perlimportsの処理の一部分を眺める

Slide 6

Slide 6 text

perlimportsとは The Perl and Raku Conference (In the Cloud) 2021で発表されたツール https://metacpan.org/dist/App-perlimports 作者はOALDERS(Olaf Alders)さん 最近静的解析ツールにハマっているっぽい App::perlimports 今日の内容 App::perlvars コード上で未使用の変数検出ツール

Slide 7

Slide 7 text

perlimportsとは perlのuse(モジュールロード)をいい感じにするのを助けてくれるツール 具体的にはどの関数がどのモジュールから持ってきたかを明示的に作る規約のレー ルになるもの ソースコードの情報を解析するのに静的解析を使用している goimportsインスパイア Perlで実装されている

Slide 8

Slide 8 text

静的解析 静的(コード実行することなしに)、コード情報を解析する手法 コードに問題がないかを検出するlinterや、見た目をいい感じに整形するformatterな どはおおよそ何かしらの形で静的解析をしている 逆にコード実行してコード情報を解析する商法に「動的解析」がある マルチスレッド環境におけるバグの発見などは動的解析のほうが望ましい Perlにおいても動的解析を使った課題解決はよく利用される https://techblog.cartaholdings.co.jp/entry/2020/05/07/120000 Perlにおける静的解析の現状はこの後お話します

Slide 9

Slide 9 text

perlimportsが解決したい問題

Slide 10

Slide 10 text

Q. encode_json はどこから来ているでしょう use strict; use warnings; use JSON; print encode_json({hoge => 'hello', isBool => \0});

Slide 11

Slide 11 text

Q. encode_json はどこから来ているでしょう A. 標準関数? use strict; use warnings; use JSON; print encode_json({hoge => 'hello', isBool => \0});

Slide 12

Slide 12 text

Q. encode_json はどこから来ているでしょう A. 標準関数? use strict; use warnings; use JSON; print encode_json({hoge => 'hello', isBool => \0});

Slide 13

Slide 13 text

Q. encode_json はどこから来ているでしょう A. 標準関数? A. JSON がエクスポートしている use strict; use warnings; use JSON; print encode_json({hoge => 'hello', isBool => \0});

Slide 14

Slide 14 text

Perlのモジュールロードの方法 大きく分けて2種類 require use それぞれ次の違いがある ロード方法 ロードタイミング 副作用 require require文実行時(逐次) ない use コンパイルタイム importを実行する

Slide 15

Slide 15 text

ロードタイミングの比較 require は文実行時 use はコンパイルタイム 具体的にどういう挙動の差を見せるか確認する 存在しない Hoge モジュールをロードする例題を考える

Slide 16

Slide 16 text

ロードタイミングの比較 require の場合 #!/usr/bin/env perl use strict; use warnings; print "before require\n"; require Hoge; print "after require\n";

Slide 17

Slide 17 text

ロードタイミングの比較 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.

Slide 18

Slide 18 text

ロードタイミングの比較 use の場合 #!/usr/bin/env perl use strict; use warnings; print "before use\n"; use Hoge; print "after use\n";

Slide 19

Slide 19 text

ロードタイミングの比較 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.

Slide 20

Slide 20 text

useの実態 実際は次の構文と等価 use Hoge; BEGIN { require Hoge; Hoge->import(); } BEGINブロック Perlでコンパイルタイム(最初)に実行したい処理を書けるブロック BEGINブロック内で require して import を実行する

Slide 21

Slide 21 text

import Perlの規約的にuseしたタイミングで実行される関数 何を書くかはオブジェクトを書く人に委ねられている よく use した先で通常の関数の様に使いたいモジュール定義の関数をエクスポート(輸 出)する処理が書かれている エクスポートしない場合はJSONの例では JSON->encode_json の様に扱わなけれ ばならない この煩わしさを解消したいケースなどで関数エクスポートが使われる エクスポート以外にも使うことができる

Slide 22

Slide 22 text

エクスポート以外のimport Class::Accessor::Liteではオブジェクトのアクセサなどの定義にimportを活用している use時に () の中身に文字列以外を渡しているケースはだいたい独自にimportを書いてい る use Class::Accessor::Lite ( new => 1, ro => [ qw(baz) ], wo => [ qw(hoge) ], ); sub import { # 一部 shift; my %args = @_; my $pkg = caller(0); my %key_ctor = ( rw => \&_mk_accessors, ro => \&_mk_ro_accessors, ...

Slide 23

Slide 23 text

関数エクスポートをする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;

Slide 24

Slide 24 text

関数エクスポートをする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;

Slide 25

Slide 25 text

関数エクスポートしたものをuseする hoge メソッドをHoge::First, Hoge::Secondでそれぞれ定義、エクスポートする #!/usr/bin/env perl use strict; use warnings; use Hoge::First; use Hoge::Second; hoge();

Slide 26

Slide 26 text

関数エクスポートしたものをuseする hoge メソッドをHoge::First, Hoge::Secondでそれぞれ定義、エクスポートする この場合呼び出される hoge は最後にロードした Hoge::Second のものになる #!/usr/bin/env perl use strict; use warnings; use Hoge::First; use Hoge::Second; hoge(); ❯ perl -Ilib hoge.pl this is second!

Slide 27

Slide 27 text

関数エクスポートが起こしがちな問題 先程の例の様に、同名の関数をエクスポートしていた場合、ロード順によって呼び出さ れる関数の実態が変わってしまう そもそもどこで定義している関数なのか探しにくい

Slide 28

Slide 28 text

関数エクスポートが起こしがちな問題 先程の例の様に、同名の関数をエクスポートしていた場合、ロード順によって呼び出さ れる関数の実態が変わってしまう そもそもどこで定義している関数なのか探しにくい → 何をどこから持ってきたのか明示的に書けばわかりやすい!

Slide 29

Slide 29 text

EXPORT_OK @EXPORT に詰めたシンボルは無条件でエクスポートされてしまう 対して @EXPORT_OK はデフォルトではすべてをロードせず、use時にインポートしたい 関数名を書くことを強制させられる use JSON qw(encode_json); # encode_json がインポートされる @EXPORT の場合も関数名を use モジュール名 の後ろに列挙すると指定したもののみイ ンポートできる 空を指定すると何もインポートしない use JSON qw(); # encode_json はインポートされないので使えない

Slide 30

Slide 30 text

EXPORTのまとめ Perlはモジュールを use する時に、モジュールにimportがあれば実行する importには関数エクスポートがしばしば定義されていて、関数をインポートすると、モ ジュール側で定義した関数を呼び出したスクリプトファイル内で定義した様に使える しばしばどこで関数を定義したかわからなくなるので、 use の際にインポートしたい関 数名だけ列挙することができる

Slide 31

Slide 31 text

perlimportsが解決したい課題 どの関数がどのモジュールによってエクスポートされてるかを明示的に書きたい!! これをシステマティックにやってほしい!!

Slide 32

Slide 32 text

perlimportsとは perlのuse(モジュールロード)をいい感じにするのを助けてくれるツール CLIツールなので編集したいPerlファイルにたいして実行する 実行するとuseにコード中に使用している関数に対応する qw() 指定を自動でつけてくれ る

Slide 33

Slide 33 text

混沌としたコード 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しているが、コード上は指定を忘れている→

Slide 34

Slide 34 text

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の実行で処理が止まってしまう

Slide 35

Slide 35 text

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しているが特に使っていない

Slide 36

Slide 36 text

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 が消えた

Slide 37

Slide 37 text

perlimportsのツラミポイント Class::Accessor::Liteなど、 import を関数エクスポート以外に使っているケースのもの は、明示的に無視するようにperlimportsに教える必要がある 関数エクスポートしていないとみなされて () になってしまうケースが有る 最近修正されつつあるので発生しないものもある use Class::Accessor::Lite ( rw => [qw/ hoge/], ); use Class::Accessor::Lite ();

Slide 38

Slide 38 text

特定のモジュールを無視する方法 色々な方法で無視することが可能だが、最近出た正規表現で無視するモジュール名を指 定する方法が便利 無視するモジュール名の規則を書いたファイルをperlimportsにわたす $ perlimports --ignore-modules-pattern-filename ignore.txt hoge.pl ^Class ^Acme::AnaTofuZ

Slide 39

Slide 39 text

perlimportsの思想 基本的にすべての関数エクスポートは明示的に行いたい 関数エクスポートしないuseであるならば、絶対にエクスポートにしないように強 制してくる use Acme::AnaTofuZ; これだとAcme::AnaTofuZ側が @EXPORT で宣言されていた場合何かをエクスポートして くる可能性がある ==> こんな感じに書き換えてくる use Acme::AnaTofuZ (); やろうとしてることはわかるけどなんか見た目がアレ...

Slide 40

Slide 40 text

perlimportsまとめ いい感じにuseするのを助けてくれるツール どこから来たかわからない関数を実行しなくて良いので使うのがおすすめ 若干思想が強い癖はある 静的解析をして使用しているモジュールを特定している 静的解析について見ていくぞ!!!

Slide 41

Slide 41 text

静的解析(おさらい) 静的(コード実行することなしに)、コード情報を解析する手法 コードに問題がないかを検出するlinterや、見た目をいい感じに整形するformatterな どはおおよそ何かしらの形で静的解析をしている 逆にコード実行してコード情報を解析する商法に「動的解析」がある マルチスレッド環境におけるバグの発見などは動的解析のほうが望ましい

Slide 42

Slide 42 text

静的解析とPerl Perlの静的解析はめちゃくちゃ難しいとされている Nothing but perl can parse Perl Perlのコードはperl(処理系)以外はパースできない 静的解析するツールを作成するのが極めて困難 perl処理系が何かしらの静的解析で使いやすいAPIを提供している訳でもない

Slide 43

Slide 43 text

ちなみに何でPerlの解析が難しいのか PPIのドキュメントにある例を見ると以下の状況 @result = (dothis $foo, $bar); Perlは関数呼び出しの () を省略できるので、 dothis は何かしらの関数呼び出しである しかしこの記述だと次のどちらかに相当するのかパット見で判断つかない @result = (dothis($foo), $bar); @result = dothis($foo, $bar);

Slide 44

Slide 44 text

なぜ判断つかないのか @result = (dothis($foo), $bar); @result = dothis($foo, $bar); 通常は関数呼び出しは後ろの引数をすべて取る Perlは関数プロトタイプ機能があり、引数を1つだけに成約することができる BEGIN ブロック内で何かしらの成約が追加されている可能性がある 例えば特定の状況で dothis の実態を切り替えるなど はたまたまだ dothis が宣言されていない可能性がある ...むずい!!!

Slide 45

Slide 45 text

静的解析とPerl Perlの静的解析はめちゃくちゃ難しいとされている Nothing but perl can parse Perl Perlのコードはperl(処理系)以外はパースできない 静的解析するツールを作成するのが極めて困難 perl処理系が何かしらの静的解析で使いやすいAPIを提供している訳でもない それでも先人がツールを作成しているので巨人の肩に乗るのが良い 今回はperlimportsが使っているPPIを見ていきます 他にもPerlの静的解析ツールは色々生み出されています ライブラリの特性などを見る場合はYAPC::Tokyoの発表がおすすめです Perl5の静的解析入門

Slide 46

Slide 46 text

PPI Perlで書かれたperlインタプリタの助けなしに動作するPerlの静的解析ツール 2022/02/27現在の最新バージョンは1.272 ソースコードはGitHubのPerl::Criticチームが持っている https://github.com/Perl-Critic/PPI

Slide 47

Slide 47 text

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. 解析出来なかったものはそもそもぶっ壊れているモジュールだったとのこと

Slide 48

Slide 48 text

PPIの構成 大きくPPI::TokenizerとPPI::Lexerで構成されている 解析した結果はPerl Document Object Model(PDOM)として返る PDOM ソースコードを表現した木構造のPerlのオブジェクト いわゆる抽象構文木に相当するもの Perlコードに対応する要素は PPI::Element として表現され、PDOMはその集合を持つ PDOMはあくまでPPIが作った解析用のオブジェクトモデル perl処理系のオブジェクトモデルとは異なっている PPIを活用する場合基本はこのPDOMを取り回すことになる

Slide 49

Slide 49 text

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'

Slide 50

Slide 50 text

PPI::Element Perlのコードに対応するPPI上の表現 なんかいいかんじにコード情報にアクセスするインターフェイスが整っている 例えば "hoge" の様なダブルクォーテーションで囲われたなにかに対応する PPI::Token::Quote::Double がある

Slide 51

Slide 51 text

例えば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プログラミングのレイヤーに

Slide 52

Slide 52 text

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';

Slide 53

Slide 53 text

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'

Slide 54

Slide 54 text

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'

Slide 55

Slide 55 text

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'

Slide 56

Slide 56 text

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'; など、 () を使わないケースもあるので必ずしもこ の条件が全てではない

Slide 57

Slide 57 text

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'; } ) || [];

Slide 58

Slide 58 text

useを取り出してきた後は perlimportsは今現在どのモジュールが何をインポートしているかを把握する必要がある 現状は、 モジュール名 => [ インポートしているシンボル] のhashrefを作る 入力されたコード use Carp; use Data::Dumper qw( Dumper ); use POSIX (); perlimportsが作る内部データ構造 { Carp => undef, 'Data::Dumper' => ['Dumper'], POSIX => [], }

Slide 59

Slide 59 text

useするモジュール名 PPI::Statement::Documentのmoduleメソッドを使うと、useしているモジュール名が取 得できる use Carp; であれば Carp が取得できる 実際のperlimportsでモジュール名を取得している箇所 for my $include ( @{$found} ) { my $pkg = $include->module;

Slide 60

Slide 60 text

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'

Slide 61

Slide 61 text

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;

Slide 62

Slide 62 text

読める気になってきましたね? PPI::Documentとさえ仲良くなればいい感じにコードが読める...!! あとは気合

Slide 63

Slide 63 text

まとめ use をいい感じにするperlimportsがある perlimportsは静的解析ツールのPPIを使っている PPIはむずそうだが、PPI::Elementと仲良くなれば普通のPerlコード PPIと仲良くなると最近のいいツールのコールドリーディングができる