90
CPAN のののののののののののののの ののののののの Kenichi Ishigaki (@charsbar) July 3, 2016 YAP(achimon)C::Asia Hachioji 2016mid

CPANの依存モジュールをもう少し正しく検出したい

Embed Size (px)

Citation preview

Page 1: CPANの依存モジュールをもう少し正しく検出したい

CPAN の依存モジュールをもう少し正しく検出し

たい

Kenichi Ishigaki(@charsbar)July 3, 2016

YAP(achimon)C::AsiaHachioji 2016mid

Page 2: CPANの依存モジュールをもう少し正しく検出したい

Perl QA Hackathon• April 21-24 @ Rugby, UK

http://act.qa-hackathon.org/qa2016/sponsors.html

• PAUSE• CPANTS

https://www.flickr.com/photos/jj_perl/26591371285/in/album-72157667398548526/

Page 3: CPANの依存モジュールをもう少し正しく検出したい

PAUSE• CPAN モジュールの登録・管

理• 簡単な事前審査や静的解析

も• 昨年から進めていた Plack へ

の移行は完了• さらに近代化を進めたい

Page 4: CPANの依存モジュールをもう少し正しく検出したい

CPANTS• 第三者目線のテスト (<-> CPAN::Testers)• 体裁が適切かどうか ( 静的解析 )• 2012 年に管理を引き継ぎ• いろいろ問題が出てきていたので全面

的にリファクタリング• 更新間隔が 1 日から 15 分に

Page 5: CPANの依存モジュールをもう少し正しく検出したい

prereq_matches_use

• 必要な依存モジュールが適切に宣言されているか( インストールには成功するんだけど場合によっては使うときに Can't locate FooBar.pm in @INC エラーが出るようなモジュールを検出 )

• マシンを買い換えたとか Perl のバージョンを上げたらコケるようになったというときに真っ先にチェックしたい項目のひとつ

• 静的インストールの検証にも

Page 6: CPANの依存モジュールをもう少し正しく検出したい

prereq_matches_use

package Archive::Any::Lite; use strict;use warnings;use File::Spec;...package Archive::Any::Lite::Tar;use Archive::Tar;...package Archive::Any::Lite::Zip;use Archive::Zip …

requires: Archive::Tar: '1.76' Archive::Zip: '0' File::Spec: '0' File::Temp: '0.19' IO::Uncompress::Bunzip2: '0'

Page 7: CPANの依存モジュールをもう少し正しく検出したい

prereq_matches_use古くからあるコアモジュールは省略可能

package Archive::Any::Lite; use strict;use warnings;use File::Spec;...package Archive::Any::Lite::Tar;use Archive::Tar;...package Archive::Any::Lite::Zip;use Archive::Zip …

requires: Archive::Tar: '1.76' Archive::Zip: '0' File::Spec: '0' File::Temp: '0.19' IO::Uncompress::Bunzip2: '0'

Page 8: CPANの依存モジュールをもう少し正しく検出したい

prereq_matches_use

現状では 2 割くらいに問題がある

•META の更新漏れ•新しいコアモジュールの指定漏れ•残念ながら CPANTS 側に問題があることも

Page 9: CPANの依存モジュールをもう少し正しく検出したい

Module::ExtractUse

• Thomas Klausner さん作(2003 年~ 2008 年、 2012 年~ )

• 不要なものを除去したあとセミコロン単位で行を分割

• use などのキーワードを見つけたら再帰下降パーサ (Parse::RecDescent) で解析

Page 10: CPANの依存モジュールをもう少し正しく検出したい

たいていの場合はこれで十分

Page 11: CPANの依存モジュールをもう少し正しく検出したい

Moose の仲間には未対応

ちょうど開発中断していた頃に活発化

•Moose (2006 年~ )•Mouse (2008 年~ )•Moo (2010 年~ )

Page 12: CPANの依存モジュールをもう少し正しく検出したい

ヒアドキュメントの中身を見てしまう

my $template = <<'END';use Module;...END

Page 13: CPANの依存モジュールをもう少し正しく検出したい

環境依存のモジュールを無視してくれない

use Test::More;BEGIN { if ($^O ne 'MSWin32') { plan skip_all => 'for Win32 only'; }}use Win32::Module;...

Page 14: CPANの依存モジュールをもう少し正しく検出したい

ヒアドキュメントや plan skip_all の後も除去?

CPANTS 的には取り過ぎるよりは取れない方が害は少ない( 表に出てこない高速化モジュールなどがあるので、 META 側の情報が多い分には問題ない )

Page 15: CPANの依存モジュールをもう少し正しく検出したい

META の自動生成などには使いづらくなる

BEGIN { if ($^O ne 'MSWin32') { plan skip_all => 'for Win32 only'; }}# Win32 環境では入れてほしいuse Win32::Module;

Page 16: CPANの依存モジュールをもう少し正しく検出したい

どこまで正確さを求めるかも悩まし

Page 17: CPANの依存モジュールをもう少し正しく検出したい

厳密に言うならBEGIN の有無も

チェック必要BEGIN { if ($^O ne 'MSWin32') { plan skip_all => 'for Win32 only'; }}use Win32::Module;

Page 18: CPANの依存モジュールをもう少し正しく検出したい

本気で解析するなら、セミコロン分割から直すべ

きBEGIN { use Test::More; if ($^O ne 'MSWin32') { plan skip_all => 'for Win32 only'; }}use Win32::Module;

Page 19: CPANの依存モジュールをもう少し正しく検出したい

ハッカソンの枠では収まりそうにな

い他にもいろいろな課題が残っていたので

ついつい後回しに…

Page 20: CPANの依存モジュールをもう少し正しく検出したい

プロファイルを取ってみた

ファイルまわりの問題が出てくることを期待していた

Page 21: CPANの依存モジュールをもう少し正しく検出したい

Pod::Checker

分析部分だけ取り出してみる

Module::ExtractUse

Parse::LocalDistribuiton

Page 22: CPANの依存モジュールをもう少し正しく検出したい

Pod::Checker

分析部分だけ取り出してみる

Module::ExtractUse

Parse::LocalDistribution

Parse::RecDescent Pod::Strip

Page 23: CPANの依存モジュールをもう少し正しく検出したい

Pod::Checker

Module::ExtractUse

Parse::LocalDistribution

Parse::RecDescent Pod::Strip

どう見ても次のターゲットは…

Page 24: CPANの依存モジュールをもう少し正しく検出したい

高速化だけでお茶を濁す手も

• Pod::Strip を外す• 毎回パーサを生成する必要はないはず• 再帰下降パーサを別のものに切り替え

る (Parse::RecDescent は AUTOLOAD を使っているので遅くなりがち )

• これだけでも新しいテストの検証などは楽になる

Page 25: CPANの依存モジュールをもう少し正しく検出したい

見てしまったからには

仕方ないいまやらなかったら、

次いつ直せるの?

Page 26: CPANの依存モジュールをもう少し正しく検出したい

代わりになりそうな

ものを探すことにした

当然ですよね

Page 27: CPANの依存モジュールをもう少し正しく検出したい

ふたつの異なる要件サーバ側•Module::ExtractUse と同程度の速さはほしい•fork() などに対応していて欲しい•Perl のバージョンは気にしない

ユーザ側•速さは気にしない•Test::Kwalitee 対応するなら Perl 5.8.1 が必須 (Lancaster Consensus)•現状テストには DB が必要ということになっているので、サイト専用のテストに移行する手もある

Page 28: CPANの依存モジュールをもう少し正しく検出したい

Perl::PrereqScanner

• Ricardo Signes さん作 (2010 年~ )• Dist::Zilla から派生• PPIベース (2001 年~ )• Moose 、 if 、 VERSION メソッドなどに

も対応• リポジトリには追加プラグインの PR

Page 29: CPANの依存モジュールをもう少し正しく検出したい

Perl::PrereqScanner

• 事実上の評価基準 : これでできることができないとツライ

• 遅すぎて大量のバッチ処理には向かない( 当初はこれしかなかったから移行を断念 )

Page 30: CPANの依存モジュールをもう少し正しく検出したい

Perl::PrereqScanner::Lite

• moznion さん作 (2013 年~ )• WEB+DB PRESS Vol.81/82

(Perl Hackers Hub 第 27/28 回 )• Compiler::Lexerベース (goccy さん作 )• 非常に高速 (P::PS の 20 ~ 30倍 )• Moose と VERSION メソッドに対応• App::scan_prereqs_cpanfile などで利用

Page 31: CPANの依存モジュールをもう少し正しく検出したい

Perl::PrereqScanner::Lite

I have to redesign and reimplement this

module, and I have some plans. (GH#13)

Page 32: CPANの依存モジュールをもう少し正しく検出したい

Perl::PrereqScanner::Lite

• Compiler::Lexer が不安定(大量処理時に SEGV)

• 拡張性の問題 ( トークンをそのまま処理している )

• 今年に入ってから FAIL が多発 (GH#15)( おそらく version モジュールの変更によるもの )

Page 33: CPANの依存モジュールをもう少し正しく検出したい

Perl::PrereqScanner::NotQuiteLite

• 拙作 (2013 年~ )• Compiler::Lexerベース• 生トークンは隠蔽• Perl::PrereqScanner と Module::ExtractUse

のテストを通るように改造• Plack::Builder にも対応

Page 34: CPANの依存モジュールをもう少し正しく検出したい

Perl::PrereqScanner::NotQuiteLite

• 個人的なモジュール管理には重宝• これも大量処理時には不安定• Compiler::Lexer を差し替えたい…

Page 35: CPANの依存モジュールをもう少し正しく検出したい

Perl::Lexer

• tokuhirom さん作 (2013 年~ )• Perl 本体の解析器を利用• ( 理屈としては ) これ以上正しいものは

ない• 同期が取れなくなる心配もない

Page 36: CPANの依存モジュールをもう少し正しく検出したい

Perl::LexerTHIS LIBRARY IS WRITTEN FOR RESEARCHING PERL5 LEXER API. THIS MODULE

USES PERL5 INTERNAL API. DO NOT USE THIS.

Page 37: CPANの依存モジュールをもう少し正しく検出したい

Perl::Lexer

• 繰り返し使えるようには ( まだ ) なっていない

• Perl 5.18.1 → 5.10.0• Devel::Declare の手法が参考にできるか

Page 38: CPANの依存モジュールをもう少し正しく検出したい

Text::Balanced• Damian Conway さん作 (1997 年~ )• Perl 5.7.3 から標準添付• 文字列、括弧、正規表現などを ( 正規

表現で ) 取り出せる• Parse::RecDescent の中でも使われてい

Page 39: CPANの依存モジュールをもう少し正しく検出したい

Text::Balanced• 開発は停滞

(実質的には 2006 年で止まっている )• 最近の Perl についていけていない箇所

も (Perl 5.14 で導入された修飾子など )

• 括弧の対応がとれていないものに弱い• 補助的なたたき台としては使えそう

Page 40: CPANの依存モジュールをもう少し正しく検出したい

Perl::Tokenizer• スライドを書いている最中に発見• Daniel Șuteu さん作 (2015 年~ )• 正規表現ベース• 内部で状態管理• 非常に高精度• 書き始める前に見つけていたら…

Page 41: CPANの依存モジュールをもう少し正しく検出したい

Perl::Tokenizer

• Perl 5.18• 解釈の部分は自前で実装必要• 効率面でもベンチマークで確認必要

Page 42: CPANの依存モジュールをもう少し正しく検出したい

すぐに代用できるものはなさそう…

Page 43: CPANの依存モジュールをもう少し正しく検出したい

というわけで、作ることにした

Page 44: CPANの依存モジュールをもう少し正しく検出したい

作成方針

• トークンは(基本的には)正規表現で抽出

• 正規表現などの塊の抽出はひとまずText::Balanced のコードを流用

• スコープの情報などは適宜状態変数に格納していく

Page 45: CPANの依存モジュールをもう少し正しく検出したい

頑張りすぎない

• 欲しいのは依存モジュールの解析器• モジュール名とバージョンを取れれば

いい• 変な書き方まで検出できる必要はない

print "@{[require Module]}";s/YA8C/use Module/e;

Page 46: CPANの依存モジュールをもう少し正しく検出したい

困ったときは perlのソースを見る

• perlop などはあくまで人間向けの説明• 正しい仕様を確認したいときは perly.y

や toke.c 、 keywords.h などを見る• あまり読めていませんが…

Page 47: CPANの依存モジュールをもう少し正しく検出したい

最初の目標

• まずは CPAN 全体のトークナイズに挑戦• 中期的には Perl::PrereqScanner::NotQuiteLite

の置き換え( ここまでできれば、 BEGIN の処理などの追加はそれほど苦労せずにできるはず )

Page 48: CPANの依存モジュールをもう少し正しく検出したい

基本的な構造while (...) { # 字句解析 if ($code =~ m/\G($re_whitespace)/gc) { ...; next; } elsif ($code =~ m/\G($re_keyword)/gc) { ... next; } ... elsif ($code =~ m/\G($re_anything_else)/gc) { die; } last;} continue { # 構文解析}

Page 49: CPANの依存モジュールをもう少し正しく検出したい

正規表現を補う道具

my $pos = pos($code);my $c1 = substr($code, $pos, 1);my $c2 = substr($code, $pos, 2);if ($c1 eq '@') { if ($code =~ m/\G($re_array)/gc) { ... next; } else { pos($code) = $pos + 1; ... next; }}

Page 50: CPANの依存モジュールをもう少し正しく検出したい

先読みに失敗したら戻せるように

# 内部で先読みしていくif (match_regexp(\$code)) { ... next; # 正規表現の抽出に成功} else { # 解釈に失敗したら保存してある位置まで戻す pos($code) = $pos;}

Page 51: CPANの依存モジュールをもう少し正しく検出したい

トークンの収集は最小限に

• 関係ない行のトークンは集めても無駄• オブジェクトにするのはもっと無駄• 登録済みのキーワードを見つけたらトークン

収集開始• メソッドの呼び出し元だけは事前に保存して

おくModule->VERSION(1.50)

• ブロックのスタックや条件文の存在は別途保存

Page 52: CPANの依存モジュールをもう少し正しく検出したい

名前空間や数字は扱いやすい形に

• use などの引数を解釈するときに同じような処理は書きたくない

• 一度の正規表現で読み切れるならそれにこしたことはない

• 必要ない字句までまとめる必要はない

Page 53: CPANの依存モジュールをもう少し正しく検出したい

テスト

• きれいなコードをパースできるのは当然

• 仕様を片手にあれこれテストコードを考えるより、何が出てくるかわからない現実のコードでテストする方が確実(頑張りすぎない )

Page 54: CPANの依存モジュールをもう少し正しく検出したい

WorePANmy $worepan = WorePAN->new( use_minicpan => 1,)->walk(sub { my $dir = shift; $dir->recurse(callback => sub { my $file = shift; return unless $file =~ /\.pm$/; scan($file); });});# 最新版をテストし続けるのには便利

Page 55: CPANの依存モジュールをもう少し正しく検出したい

先に解凍しておく

• 毎回解凍するのは時間の無駄• WorePAN では問題があるファイル

を消せない• もともと壊れているモジュール• Perl6 モジュール

Page 56: CPANの依存モジュールをもう少し正しく検出したい

エラーが出たファイルだけを詳細に分析

• テスト環境にあわせてデバッグモードを切り替える

• 問題を解決できたらテストコードに追加

• 解析できないモジュールは対象から消したり、例外に追加したり

Page 57: CPANの依存モジュールをもう少し正しく検出したい

悩まされたコードたち

• アポストロフィ• Unicode• 一見キーワード• 無名サブルーチン• プロトタイプ

• print $fh• // と /• /#/x• qr))• eval ''.''

Page 58: CPANの依存モジュールをもう少し正しく検出したい

1. アポストロフィ

• 文字列をくくるだけではありません• Perl 4時代の名前空間の区切り文字とし

てもおなじみ&jcode'convert($value, 'sjis');

• Acme::Don't (Damian Conway さん作 )

Page 59: CPANの依存モジュールをもう少し正しく検出したい

名前空間を一度で取ろうとしたら…

m/\G(\w+(?:(?:'|::)\w+)*)/gc

Page 60: CPANの依存モジュールをもう少し正しく検出したい

キーワードはあらかじめ避けておく必要

が…if (ref($hashOrInternalId) eq'HASH') {

PHRED/WebService-NetSuite-0.04/lib/WebService/NetSuite.pm

Page 61: CPANの依存モジュールをもう少し正しく検出したい

ヒアドキュメントと…

our $SIGNATURE_GRAMMAR = << '#\'END';...#'END

GAAL/Perl6-Signature-0.04/lib/Perl6/Signature.pm

Page 62: CPANの依存モジュールをもう少し正しく検出したい

特殊変数と…

my $list = shift @'_;my $sep = @$list <= 1 ? '' : do {...

SPROUT/CSS-DOM-0.16/lib/CSS/DOM/PropertyParser.pm

Page 63: CPANの依存モジュールをもう少し正しく検出したい

サブルーチン名にまで…

sub O'o { [ shift,oO( @_ ) ]->[!$[] }

TYEMQ/Acme-ESP-1.002007/ESP.pm

Page 64: CPANの依存モジュールをもう少し正しく検出したい

2. Unicodepackage Acme::Lambda;use 5.008;use warnings;use strict;use utf8;...*λ = \&lambda;

NELHAGE/Acme-Lambda-0.03/lib/Acme/Lambda.pm

Page 65: CPANの依存モジュールをもう少し正しく検出したい

2. Unicodeuse utf8;package Acme::ಠ_ಠ;{ $Acme::ಠ_ಠ::VERSION = '0.006';}BEGIN { $Acme::ಠ_ಠ::AUTHORITY = 'cpan:ETHER';}# ABSTRACT: send warnings with ಠ_ಠ

ETHER/Acme-LookOfDisapproval-0.006/lib/Acme/o_o.pm

Page 66: CPANの依存モジュールをもう少し正しく検出したい

2. Unicode

• マルチバイト文字が泣き別れしないように

• use utf8; を見つけたら全体を decode• 解析前には BOM のチェックも

Page 67: CPANの依存モジュールをもう少し正しく検出したい

3. 一見キーワード

sub thx ($) { my ($str) = @_; $INLINE->use if $INLINE;

SATOH/Text-Xatena-0.18/t/lib/Text/Xatena/Test.pm

Page 68: CPANの依存モジュールをもう少し正しく検出したい

3. 一見キーワードpush @{$data->{stack}}, {

in => (caller(1))[3] || '-', package => (caller(1))[0] || '-', sub => (caller(2))[3] || '-', filename => (caller(1))[1] || '-', line => (caller(1))[2] || '-',};

STEVEB/Devel-Trace-Subs-0.22/lib/Devel/Trace/Subs.pm

Page 69: CPANの依存モジュールをもう少し正しく検出したい

3. 一見キーワード

{ } の中に空白がない場合は特別扱い

$reg->{ y } = ( $reg->{ y } - 1 ) & 0xff;

BRICAS/Games-NES-Emulator-0.03/lib/CPU/Emulator/6502/Op/DEY.pm

Page 70: CPANの依存モジュールをもう少し正しく検出したい

4. 無名サブルーチン

enable_if sub { fingerprinted(@_) }, 'Header', set => ['Expires' => MAX_DATE];

INGY/Cog-0.11/lib/Cog/Runner.pm

map {} ... なども同じ理由で地雷原

Page 71: CPANの依存モジュールをもう少し正しく検出したい

5. プロトタイプとアトリビュート

Perl には $) という特殊変数があるのですが…

return ( .... $>, # uid $), # gid ...);

MSISK/Net-Nmsg-0.15/lib/Net/Nmsg/Layer.pm

Page 72: CPANの依存モジュールをもう少し正しく検出したい

5. プロトタイプとアトリビュート

プロトタイプなどは ( ) の組を最優先

sub run_test ($) {

AGENT/Makefile-DOM-0.008/t/Shell.pm

Page 73: CPANの依存モジュールをもう少し正しく検出したい

6. ファイルハンドル? 変数?

print $dest_fh <<__EOJS__;...__EOJS__

DAMI/Alien-GvaScript-1.44/GvaScript_Builder.pm

Page 74: CPANの依存モジュールをもう少し正しく検出したい

6. ファイルハンドル? 変数?

print $dest_fh << 1;...1

print $dest_fh << 1;

どうやって区別?

Page 75: CPANの依存モジュールをもう少し正しく検出したい

7. // と /srand(((time/$$)^($>*time))/(time/(time^$$)));foreach (1..$length){ $letter = pack("c", rand(128)); redo unless $letter =~ /[a-zA-Z]/; # I just don't like \w, okay? $word .= $letter;}

JONG/Bioinf_V2.0/Bioinf.pm

Page 76: CPANの依存モジュールをもう少し正しく検出したい

7. // と /合わせ技

正規表現は変数の直後には来ませんが…

print $fh /$var/ ? 1 : 0;print $fh // $var;print $var{foo} / 1;map {...} /$var/ ? 1 : 0;

Page 77: CPANの依存モジュールをもう少し正しく検出したい

8. コメントと正規表現

m{# trying to cheat with cpants game ;)use strict;use warnings;}x;

MONS/AnyEvent-SMTP-0.10/lib/AnyEvent/SMTP/Client.pm

Page 78: CPANの依存モジュールをもう少し正しく検出したい

8. コメントと正規表現入れ子がおかしくなる場合

$rest =~ s{ \b # start at word boundary ( # begin $1 { $urls : # need resource and a colon [$any] +? # followed by on or more ... ) # end $1 } ...}{<A HREF="$1">$1</A>}igox;

AUTRIJUS/Pod-HtmlHelp-1.1/WinHtml.pm

Page 79: CPANの依存モジュールをもう少し正しく検出したい

8. コメントと正規表現次の文字がデリミタになるんじゃなかったの?

unless ($$this =~ s# {use (warnings|strict)...)\K} {use warnings...)\K} {$begin_block}s) { die("Could not add a begin block.\n"); }

WINTRU/Carrot-1.1.309/lib/Carrot/Modularity/Package/Source_Code.pm

Page 80: CPANの依存モジュールをもう少し正しく検出したい

8. コメントと正規表現どちらのコメント?

$regexp=qr{ ....# (?{# my $pos=pos;# my $prev=substr($str, $pos-10, 10);# my $post=substr($str, $pos, 10);# print "emitted at position ",pos,...;# }) }xs;

OPI/HTML-YaTmpl-1.8/lib/HTML/YaTmpl/_parse.pm

Page 81: CPANの依存モジュールをもう少し正しく検出したい

8. コメントと正規表現

単一行だけど x の有無で意味が変わる場合

if ($token =~ m/\[ # matches [ /x) {

DYLUNIO/Gwybodaeth-0.02/lib/Gwybodaeth/Parsers/N3.pm

Page 82: CPANの依存モジュールをもう少し正しく検出したい

9. 括弧の数が…if(my $base_elem = $doc->look_down(_tag

=> 'base', target => qr)(?:\)))){ $name = $base_elem->attr('target');}

SPROUT/WWW-Scripter-0.031/lib/WWW/Scripter.pm

Page 83: CPANの依存モジュールをもう少し正しく検出したい

9. 括弧の数が…かっこをひとつ変えるとエラーにUnmatched ) in regex; marked by <-- HERE in m/(?:)) <-- HERE

if(my $base_elem = $doc->look_down(_tag

=> 'base', target => qr((?:\)))){

Page 84: CPANの依存モジュールをもう少し正しく検出したい

10. eval: 文字列の中身は不完全なコードかも

$c->{constraint} = eval 'sub { no strict qw/refs/; return defined &{"match_'.$c->{constraint}.'"}(@_)}';

MARKSTOS/Data-FormValidator-4.66/lib/Data/FormValidator/Results.pm

Page 85: CPANの依存モジュールをもう少し正しく検出したい

3 ヶ月ほどの試行錯誤でトークナイズはほぼできるように

• 例外として登録したモジュール 6個• 109個のテスト• 偶然解析できているものや意味的に間違っているものが残っている可能性はあり

Page 86: CPANの依存モジュールをもう少し正しく検出したい

古い perl の問題

• なぜか $c1 に 2文字入ることがある (初期の 5.8系列 )

• 取る順序をひっくり返すと直る

my $c1 = substr($code, $pos, 1);my $c2 = substr($code, $pos, 2);

Page 87: CPANの依存モジュールをもう少し正しく検出したい

古い perl の問題

巨大で複雑な文字列を正規表現にかけると落ちることがある (5.8系列すべて )

TOKUHIROM/lib/Amon2/Setup/Asset/Bootstrap.pm

Page 88: CPANの依存モジュールをもう少し正しく検出したい

現状と今後の予定

Page 89: CPANの依存モジュールをもう少し正しく検出したい

リポジトリ ( 予定地 )

https://github.com/charsbar/Perl-PrereqScanner-NotQuiteLite

Page 90: CPANの依存モジュールをもう少し正しく検出したい

ご清聴ありがとうございました