PathScannerのバンドルパス判定とコード統合の改善

rails/bootsnap

PathScannerのnative_callにおけるバンドルパス判定のバグを修正し、ruby_callnative_callの共通処理をprepare_scanヘルパーメソッドに抽出することで、両実装間の動作差異を最小化しました。

背景

#526のレビュー中に、PathScannerruby_callnative_callの間に複数の動作差異が発見されました。特にnative_callは、ネストしたディレクトリスキャンにおいて相対パスと絶対パスのBUNDLE_PATHを比較するバグを含んでいました。

バンドルパスのチェックは、ロードパスに.が含まれ、バンドルパスが.bundleである場合など、スキャン対象パスがバンドルパスの祖先ディレクトリになるケースで重要です。この状況では、バンドルパス内への再帰を防ぐ必要があります。バンドルパス内のコードは他のロードパス項目に存在するため、スキャンする必要がないためです。

技術的な変更

バンドルパス判定のバグ修正

native_calldirs.map!処理後、dir変数は相対パスになりますが、BUNDLE_PATHは絶対パスのままでした。このため、パス比較が常に失敗していました。

変更前:

dirs.map! do |dir|
  next if ignored_dir_names&.include?(dir)
  next if contains_bundle_path && File.join(root_path, dir).start_with?(BUNDLE_PATH)
  absolute_dir = File.join(current_dir, dir)
  next if ignored_abs_paths&.any? { |p| absolute_dir.start_with?(p) }
  dir
end

変更後:

dirs.map! do |dir|
  next if ignored_dir_names&.include?(dir)
  absolute_dir = File.join(current_dir, dir)
  next if contains_bundle_path && absolute_dir.start_with?(BUNDLE_PATH)
  next if ignored_abs_paths&.any? { |p| absolute_dir.start_with?(p) }
  dir
end

absolute_dirの計算を前に移動し、絶対パス同士での比較に変更しました。.bundleのようなドット始まりのディレクトリは別の条件でスキップされるため、このバグは多くのケースで顕在化しませんでしたが、BUNDLE_PATH/path/to/vendor/bundleのようなドットプレフィックスを持たないパスに設定されている場合は失敗していました。

共通処理の抽出

ruby_callnative_callの初期化処理をprepare_scanヘルパーメソッドに統合しました。

def prepare_scan(root_path)
  root_path = File.expand_path(root_path.to_s).freeze
  contains_bundle_path = BUNDLE_PATH.start_with?(root_path)
  ignored_abs_paths, ignored_dir_names = ignored_directories.partition { |p| File.absolute_path?(p) }
  ignored_abs_paths = nil if ignored_abs_paths.empty?
  ignored_dir_names = nil if ignored_dir_names.empty?
  [root_path, contains_bundle_path, ignored_abs_paths, ignored_dir_names]
end

このメソッドは以下の処理を一元化します:

  • パスの正規化: File.expand_pathによる絶対パスへの変換とfreeze
  • バンドルパス判定: BUNDLE_PATHがスキャン対象の子孫かどうかの確認
  • 無視ディレクトリの分類: 絶対パスと相対パス(ディレクトリ名)への分割

walkメソッドの最適化

ruby_callから呼ばれるwalkメソッドが、再帰呼び出しごとに無視ディレクトリリストを再計算していた処理を改善しました。

変更前:

def walk(absolute_dir_path, relative_dir_path, &block)
  # ...
  ignored_abs_paths, ignored_dir_names = ignored_directories.partition { |p| File.absolute_path?(p) }
  # ...
end

変更後:

def walk(absolute_dir_path, relative_dir_path, ignored_abs_paths, ignored_dir_names, &block)
  # ...
end

prepare_scanで事前に分割したignored_abs_pathsignored_dir_namesを引数として受け取ることで、再帰呼び出しごとのpartition処理を排除しました。

テストの改善

test/load_path_cache/path_scanner_test.rbに、バンドルパスがドットプレフィックスを持たない場合のテストケースを追加しました。また、stub_constヘルパーをtest/test_helper.rbに移動し、テスト全体で再利用可能にしています。

def test_ignores_bundle_path_without_dot_prefix
  Dir.mktmpdir do |dir|
    bundle_dir = "#{dir}/vendor/bundle"
    stub_const(Bootsnap::LoadPathCache::PathScanner, :BUNDLE_PATH, bundle_dir) do
      with_ignored_directories([]) do
        FileUtils.mkdir_p(bundle_dir)
        FileUtils.mkdir_p("#{dir}/lib")
        FileUtils.touch("#{bundle_dir}/a.rb")
        FileUtils.touch("#{dir}/lib/b.rb")

        entries = PathScanner.call(dir)
        assert_equal ["lib/b.rb"], entries.sort
      end
    end
  end
end

このテストは、元のコードでは失敗するケースを捕捉します。

設計判断

共通処理の抽出により、ruby_callnative_callの実装差異を最小化する方針が採られました。両メソッドは異なる実装戦略(Pure Rubyとネイティブ拡張)を取りますが、スキャン前の準備処理は同一であるべきです。prepare_scanへの統合により、一方にのみ適用される修正や、変数名の不一致による混乱を防ぎます。

変数名の統一も重要な改善点です。ruby_callpathパラメータをroot_pathにリネームし、両メソッドで一貫した命名規則を使用することで、実装の対比が容易になりました。

walkメソッドへの引数追加は、パフォーマンスと設計の明確さのトレードオフです。引数が増えることでシグネチャは複雑になりますが、再帰呼び出しごとの無駄な計算を排除し、無視ディレクトリリストが呼び出し全体で不変であることを明示しています。

まとめ

本PRは、PathScannerのnative_callにおけるバンドルパス判定のバグを修正し、ruby_callnative_callの共通処理を統合することで、両実装の保守性と一貫性を向上させています。prepare_scanの導入により、今後の機能追加や修正が両実装に確実に反映される基盤が整いました。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細(各論)→まとめ(結論)という理想的な構成が実現できています。設計判断のセクションも設けられており、読者の深い理解を促す構成になっています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きのシンタックスハイライト(```ruby:path/to/file.rb)やPR番号のリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「PathScanner」「BUNDLE_PATH」「ネイティブ拡張」といった専門用語を適切に用い、Rails/Rubyのライブラリ内部構造に関心のあるエンジニアという対象読者に適合した内容になっています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構造を持ち、各パラグラフがトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に読みやすいです。

Diff内容との照合 ⚠ WARNING

コードブロックとDiff内容の一致

記事内のコード引用は概ね正確ですが、「変更前」のコードブロックで、実際のコードの一部(`dir.start_with?('.')`のチェックなど)が省略されています。バグの要点を伝えるための簡略化と解釈できますが、完全な一致ではありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

PRで使われている「relative path」「absolute path」「pre-partitioned」といった技術用語を正確に日本語に反映させ、文脈に沿って適切に使用しています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

相対パスと絶対パスの比較がバグの原因であること、共通処理をヘルパーメソッドに抽出したことなど、技術的な説明はPRの内容と一致しており、論理的で正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのTitle, Description, Diffから裏付けが取れ、ハルシネーション(創作)は見られません。「設計判断」セクションもPRの意図を正確に汲み取っています。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#528, #526)やファイルパスが正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「PathScannerのバンドルパス判定とコード統合の改善」は、PRのタイトル「Fix bundle path check and consolidate common code in PathScanner」の内容を的確に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容は提供されたPR情報の範囲内に留まっており、バージョン情報やリリース予定など、PR外の知識の追記はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「#526のレビュー中に発見された」といった時間的な前後関係がPR Descriptionと一致しており、時間表現は正確です。