Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Composerオートロードと重複する冗長なrequire_onceを静的解析で自動検出する

 Composerオートロードと重複する冗長なrequire_onceを静的解析で自動検出する

Avatar for しゅんぺい

しゅんぺい

October 03, 2026

Other Decks in Programming

Transcript

  1. ⼤量のrequire_onceが残っていた require_once __DIR__ . "/../../setting.php"; require_once __DIR__ . "/../environment.php"; require_once

    __DIR__ . "/../config/constants.php"; require_once __DIR__ . "/../../vendor/autoload.php"; if (!defined('CLASS_DIR_PATH')) { define("CLASS_DIR_PATH", BASE_DIR_PATH . "classes/"); } require_once CLASS_DIR_PATH . "Example.php";
  2. こんな⼿作業コードジャンプはやってられない require_once CLASS_DIR_PATH . 'Example.php'; CLASS_DIR_PATH にコードジャンプする ↓ define("CLASS_DIR_PATH", BASE_DIR_PATH

    . "classes/"); CLASS_DIR_PATH を定義するために値の定数にさらにコードジャンプ する ↓ define("BASE_DIR_PATH", "/main/");
  3. こんな読み込み先を特定する作業もやってられない front/init.php require_once '../web.php'; $example = new Example(); ↘ 読み込み先を開く

    web.php require_once __DIR__ . '/common.php'; ↘ さらに読み込み先を開く common.php require_once __DIR__ . '/../vendor/autoload.php'; require_once CLASS_DIR_PATH . 'Example.php';
  4. require_onceとComposerの読み込み先を確認する クラス名からComposerの読み込み先と元のファイルを⽐較する $resolved = $resolver->resolve($requireOnceClassName); // Composerの読み込み先を問い合わせる if ( $resolved

    !== null //パス表記を揃え⽐較する && PathHelper::normalize($resolved) !== $normalizedTarget // 同じファイルか確認 && !$this->sameRealFile($resolved, $normalizedTarget) ) { // 別ファイルなら削除候補外とする }
  5. 全てのクラスが⼀致してもrequire_onceはまだ消せない Composerのオートロード対象は関数呼び出しでは発⽕しない // ReportGenerator.php class ReportGenerator {} function format_report_title() {}

    // クラスがまだ読み込まれていない状態で実⾏。 // 他からの読み込みはない前提 format_report_title(); // 未定義エラー
  6. クラス定義以外のトップレベル構⽂を確認する クラス外の関数宣⾔や実⾏処理を含むファイルを候補から外す foreach ($this->topLevelStmts($stmts) as $stmt) { $allowed = $stmt

    instanceof ClassLike // オートロードで読み込みたい対象 || $stmt instanceof Use_ // 単体ではファイル読み込み、関数実⾏は起きない || $stmt instanceof GroupUse // use App\{Foo, Bar}; のようなまとめた use宣言 } || $stmt instanceof Declare_ // コード設定のため || $stmt instanceof Nop; // 何も実⾏しないから if (!$allowed) { return false; }
  7. 読み込み元をたどりエントリーポイント候補を探す foreach ($edges as $edge) { $this->callers[$edge['to']][] = $edge['from']; }

    private function trace(string $file): void { $callers = $this->callers[$file] ?? []; if ($callers === []) { $this->entrypoints[] = $file; return; } foreach ($callers as $caller) { $this->trace($caller); } } $edge にはrequire_once の 静的解析で得た「どのファイル が、どのファイルを読み込むか」 という情報が入っている 他のファイルからの読み込みがな い場合はエントリーポイントとする 読み込み元をさらにたどる
  8. 静的に解決できなかった箇所の理由を出⼒する // require_once の後ろにあるパスの式を取り出す $exprTokens = $this->includeExprParser ->readIncludeExprTokens($tokens, $i); //

    require_once の式を静的に評価し、パス⽂字列を求める $raw = $this->includeExprParser ->evalStaticExpr($exprTokens, $consts, $absolutePath); // パス⽂字列を求められなければ、該当箇所‧ // パスを静的に解析できない理由(実⾏時に決まる値など)‧元の式を記録する if ($raw === null) { $unresolved[] = [ 'file' => $relativePath, 'line' => $line, 'reason' => $this->includeExprParser ->classifyUnresolvableReason($exprTokens), 'expr' => TokenHelper::tokensToString($exprTokens), ]; continue; }