Skip to content

Instantly share code, notes, and snippets.

@hitode909
Created November 11, 2025 02:20
Show Gist options
  • Select an option

  • Save hitode909/7c520e39c1b61202d336c09cdaf4a7c1 to your computer and use it in GitHub Desktop.

Select an option

Save hitode909/7c520e39c1b61202d336c09cdaf4a7c1 to your computer and use it in GitHub Desktop.
~/.claude/commands/find-unused-pm.md
name description
find-unused-pm
到達可能性分析を使用してプロジェクト内の未使用のPerlパッケージ(.pmファイル)を見つける

未使用Perlパッケージの検出

このコマンドは以下の手順でプロジェクト内の未使用のPerlモジュールファイル(.pm)を見つけます:

  1. テンプレートをもとに、検出スクリプトを作成
  2. スクリプトを実行して未使用パッケージを検出
  3. 必要に応じて設定を調整して再実行

実行手順

1. スクリプトの作成と実行

まず find-unused-pm.pl を作成して実行する。必ずスクリプト実行してから対処という形で、経験的に進める。先回りしたプロジェクト構成の調査は禁止。

実行コマンド:

perl find-unused-pm.pl 2>/dev/null

2. 設定の調整

結果が納得できるもので説明可能となるまで、調整と再実行を繰り返す。

  • Controller::やWeb::などフレームワーク側でディスパッチするもの
    • プロジェクト構成を確認して、@ENTRY_POINTS, @ENTRY_DIRS, @IGNORE_PATTERNS を追加、調整
  • Modelなどコード内に直接の呼び出しがある場合は、メソッド呼び出しをもって依存とみなす(たとえば$package名-> を正規表現で探すなど)
  • 少量でもみつかった場合
    • package名を::で区切った末端部分(ファイル名に該当する部分)でGrepして、実際の利用状況を調査
    • 例: lib/MyApp/Model/UserProfile.pmUserProfile でGrep
    • 例: lib/Plack/Middleware/PostErrorMessageToSlack.pmPostErrorMessageToSlack でGrep
    • 他のファイルから参照されている場合は、検出パターンの追加が必要

3.結果の表示

  • 未使用パッケージありと判断された場合:
    • 各ファイルを簡単な説明とともにリストアップ
    • gitヒストリーでコンテキストを確認(作成日時、最終更新日時)
    • 削除しても安全かどうかを提案
  • 未使用パッケージがなにもない場合
    • 問題なしと報告して終了

仕組み

このスクリプトは到達可能性分析を使用します:

  1. エントリーポイント(app.psgi、スクリプトなど)から開始
  2. 依存関係を反復的にたどる:パッケージAが到達可能でパッケージBを使用している場合、Bも到達可能
  3. 新しいパッケージが見つからなくなるまで継続(不動点)
  4. lib/内で到達されなかったパッケージは未使用

ほとんどのプロジェクトでは通常2〜3回の反復で収束します。

フレームワーク別の調整ポイント

Catalystアプリケーションの場合

重要: Catalystは多くのコンポーネントを動的にロードするため、静的解析では偽陽性が多発します。

必須の調整

  1. Controllerの扱い

    • Catalystはlib/App/Controller/配下を自動検出・ロードする
    • @IGNORE_PATTERNS'YourApp::Controller::'を追加(天下り的に使用中とみなす)
    • 重要: ただし、Controllerからuseされているパッケージは到達可能として追跡する
    • IGNORE_PATTERNSのパッケージも依存関係グラフには含め、そこからの参照を辿る
  2. 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";
        }
    }
  3. 設定ファイルからの参照

    • default_view => 'Xslate'などの設定も検出対象
    • 設定ファイル(*.conf, *.yml)もエントリーポイントに追加を検討
  4. その他の動的パターン

    • with::roles('Role::Name') - Moose/Mooロールの動的適用
    • => 'API::Foo' - 属性でのモデル名指定
    • __PACKAGE__->request_class_traits([qw( ... )]) - トレイト指定

推奨アプローチ

Catalystアプリケーションでは、到達可能性分析よりも以下の方法が効果的:

  • 実行トレース: 実際のHTTPリクエストでどのコードが実行されたか記録
  • カバレッジツール: Devel::CoverなどでテストカバレッジからMissing領域を特定
  • gitヒストリー: 長期間更新されていないファイルを調査
  • チームへの確認: 実装者に直接確認

静的解析は「明らかに未使用」なファイルの検出に留め、最終判断は人間が行うこと。

find-unsed-pm.pl

このテンプレートをコピーして実行します。コアモジュールのみで構成するよう注意。 リポジトリが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";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment