| name | description |
|---|---|
find-unused-pm |
到達可能性分析を使用してプロジェクト内の未使用のPerlパッケージ(.pmファイル)を見つける |
このコマンドは以下の手順でプロジェクト内の未使用のPerlモジュールファイル(.pm)を見つけます:
- テンプレートをもとに、検出スクリプトを作成
- スクリプトを実行して未使用パッケージを検出
- 必要に応じて設定を調整して再実行
まず find-unused-pm.pl を作成して実行する。必ずスクリプト実行してから対処という形で、経験的に進める。先回りしたプロジェクト構成の調査は禁止。
実行コマンド:
perl find-unused-pm.pl 2>/dev/null結果が納得できるもので説明可能となるまで、調整と再実行を繰り返す。
- Controller::やWeb::などフレームワーク側でディスパッチするもの
- プロジェクト構成を確認して、
@ENTRY_POINTS,@ENTRY_DIRS,@IGNORE_PATTERNSを追加、調整
- プロジェクト構成を確認して、
- Modelなどコード内に直接の呼び出しがある場合は、メソッド呼び出しをもって依存とみなす(たとえば
$package名->を正規表現で探すなど) - 少量でもみつかった場合
- package名を
::で区切った末端部分(ファイル名に該当する部分)でGrepして、実際の利用状況を調査 - 例:
lib/MyApp/Model/UserProfile.pm→UserProfileでGrep - 例:
lib/Plack/Middleware/PostErrorMessageToSlack.pm→PostErrorMessageToSlackでGrep - 他のファイルから参照されている場合は、検出パターンの追加が必要
- package名を
- 未使用パッケージありと判断された場合:
- 各ファイルを簡単な説明とともにリストアップ
- gitヒストリーでコンテキストを確認(作成日時、最終更新日時)
- 削除しても安全かどうかを提案
- 未使用パッケージがなにもない場合
- 問題なしと報告して終了
このスクリプトは到達可能性分析を使用します:
- エントリーポイント(app.psgi、スクリプトなど)から開始
- 依存関係を反復的にたどる:パッケージAが到達可能でパッケージBを使用している場合、Bも到達可能
- 新しいパッケージが見つからなくなるまで継続(不動点)
- lib/内で到達されなかったパッケージは未使用
ほとんどのプロジェクトでは通常2〜3回の反復で収束します。
重要: Catalystは多くのコンポーネントを動的にロードするため、静的解析では偽陽性が多発します。
-
Controllerの扱い
- Catalystはlib/App/Controller/配下を自動検出・ロードする
@IGNORE_PATTERNSに'YourApp::Controller::'を追加(天下り的に使用中とみなす)- 重要: ただし、Controllerからuseされているパッケージは到達可能として追跡する
- IGNORE_PATTERNSのパッケージも依存関係グラフには含め、そこからの参照を辿る
-
Model/Viewの検出パターン追加
$c->model('API::Foo')のような文字列リテラルでの呼び出しを検出extract_used_packagesに以下を追加:
# Catalyst動的ロード: ->model('API::Changes') if (/->(?:model|view)\s*\(\s*['"]([^'"]+)['"]/) { my $name = $1; # 'API::Changes' -> 'YourApp::Model::API::Changes' if ($name =~ /::/ || $name =~ /^(?:API|その他のModel名)/) { push @packages, "YourApp::Model::$name"; } else { push @packages, "YourApp::View::$name"; } }
-
設定ファイルからの参照
default_view => 'Xslate'などの設定も検出対象- 設定ファイル(*.conf, *.yml)もエントリーポイントに追加を検討
-
その他の動的パターン
with::roles('Role::Name')- Moose/Mooロールの動的適用=> 'API::Foo'- 属性でのモデル名指定__PACKAGE__->request_class_traits([qw( ... )])- トレイト指定
Catalystアプリケーションでは、到達可能性分析よりも以下の方法が効果的:
- 実行トレース: 実際のHTTPリクエストでどのコードが実行されたか記録
- カバレッジツール: Devel::CoverなどでテストカバレッジからMissing領域を特定
- gitヒストリー: 長期間更新されていないファイルを調査
- チームへの確認: 実装者に直接確認
静的解析は「明らかに未使用」なファイルの検出に留め、最終判断は人間が行うこと。
このテンプレートをコピーして実行します。コアモジュールのみで構成するよう注意。 リポジトリがexample-comなら、find-unsed-pm-for-example-com.pl のような形でsuffixをつけてください。
#!/usr/bin/env perl
# 未使用Perlパッケージ検出スクリプト
use strict;
use warnings;
use File::Find;
use File::Spec;
# 設定(プロジェクトに合わせてカスタマイズ)
my $BASE_DIR = '.';
my @ENTRY_POINTS = (
'app.psgi', # PSGIエントリーポイント
# 他のエントリーポイントをここに追加
);
my @ENTRY_DIRS = (
'script', # script/内の全ファイルをエントリーポイントとして扱う
# 他のディレクトリをここに追加
);
my @IGNORE_PATTERNS = (
# 'Example::DynamicLoad::', # 動的ロードされるパッケージ
# 除外するパッケージのパターンをここに追加
);
sub find_perl_files {
my $dir = shift;
my @files;
return () unless -d $dir;
File::Find::find({
wanted => sub { push @files, $File::Find::name if /\.pm$/; },
no_chdir => 1, # File::Findがchdirしないようにする(重要)
}, $dir);
return @files;
}
sub extract_package_name {
my $file = shift;
open my $fh, '<', $file or return;
while (<$fh>) {
if (/^\s*package\s+([\w:]+)/) {
close $fh;
return $1;
}
}
close $fh;
return;
}
sub extract_used_packages {
my $file = shift;
my @packages;
open my $fh, '<', $file or return ();
while (<$fh>) {
# use Package
if (/^\s*use\s+([\w:]+)/) {
push @packages, $1;
}
# use parent qw/Package1 Package2/
if (/^\s*use\s+parent\s+qw[\/\(]([^\/\)]+)[\)\/]/) {
push @packages, split /\s+/, $1;
}
# use parent 'Package'
if (/^\s*use\s+parent\s+['"]([^'"]+)['"]/) {
push @packages, $1;
}
# with/extends (Mojo/Moose)
if (/^\s*(?:with|extends)\s+['"]([^'"]+)['"]/) {
push @packages, $1;
}
# Package->method
if (/([\w:]+)->\w+/) {
push @packages, $1 if $1 =~ /::/;
}
# require Package
if (/^\s*require\s+([\w:]+)/) {
push @packages, $1;
}
}
close $fh;
return @packages;
}
sub should_ignore {
my $pkg = shift;
return 1 unless defined $pkg;
for my $pattern (@IGNORE_PATTERNS) {
return 1 if $pkg =~ /^\Q$pattern\E/;
}
return 0;
}
# lib/内の全パッケージを収集
my @lib_files = find_perl_files('lib');
my %all_packages; # 未使用チェック対象のパッケージ
my %ignored_packages; # 除外するパッケージ(依存関係は追跡するが未使用として報告しない)
my %file_to_package;
for my $file (@lib_files) {
my $pkg = extract_package_name($file);
next unless $pkg;
$file_to_package{$file} = $pkg;
if (should_ignore($pkg)) {
$ignored_packages{$pkg} = $file;
} else {
$all_packages{$pkg} = $file;
}
}
# エントリーポイントから直接使用されているパッケージを収集
my %directly_used;
# エントリーポイントを処理
for my $entry (@ENTRY_POINTS) {
my $file = File::Spec->catfile($BASE_DIR, $entry);
if (-f $file) {
warn "エントリーポイントを処理中: $entry\n";
for my $pkg (extract_used_packages($file)) {
# all_packagesとignored_packagesの両方をチェック
$directly_used{$pkg} = 1 if exists $all_packages{$pkg} || exists $ignored_packages{$pkg};
}
}
}
# エントリーディレクトリを処理(全ファイルをエントリーポイントとして扱う)
for my $dir (@ENTRY_DIRS) {
next unless -d $dir;
File::Find::find({
wanted => sub {
return unless -f $_;
for my $pkg (extract_used_packages($File::Find::name)) {
# all_packagesとignored_packagesの両方をチェック
$directly_used{$pkg} = 1 if exists $all_packages{$pkg} || exists $ignored_packages{$pkg};
}
},
no_chdir => 1, # File::Findがchdirしないようにする(重要)
}, $dir);
}
# 依存関係グラフを構築(ignored_packagesも含む)
my %deps;
for my $file (@lib_files) {
my $pkg = $file_to_package{$file};
next unless $pkg;
my @used = extract_used_packages($file);
# all_packagesとignored_packagesの両方をチェック
$deps{$pkg} = [grep { defined $_ && (exists $all_packages{$_} || exists $ignored_packages{$_}) } @used];
}
# 到達可能性分析(不動点計算)
my %reachable = %directly_used;
# ignored_packagesは全て到達可能とみなす(動的ロードされるため)
for my $pkg (keys %ignored_packages) {
$reachable{$pkg} = 1;
}
my $changed = 1;
my $iteration = 0;
while ($changed) {
$iteration++;
$changed = 0;
my $old_size = scalar(keys %reachable);
for my $pkg (keys %reachable) {
if ($deps{$pkg}) {
for my $dep (@{$deps{$pkg}}) {
unless ($reachable{$dep}) {
$reachable{$dep} = 1;
$changed = 1;
}
}
}
}
warn "反復 $iteration: " . scalar(keys %reachable) . " 個の到達可能なパッケージ (+". (scalar(keys %reachable) - $old_size) .")\n";
last if $iteration > 100; # 無限ループ防止
}
# 未使用パッケージを出力
for my $pkg (sort keys %all_packages) {
unless ($reachable{$pkg}) {
print "$all_packages{$pkg}\n";
}
}
warn "未使用パッケージ " . scalar(grep { !$reachable{$_} } keys %all_packages) . " 個を検出\n";