カバレッジ計測中でも `enable_frozen_string_literal` が動作するように

rails/bootsnap

Ruby 4.0.4以降を対象に、Coverage モジュールが有効な状態でも Bootsnap.enable_frozen_string_literal が正しく機能するよう修正されました。旧バージョンのRubyでは警告を出しながらフォールバックする形で既存動作との互換性が維持されます。

背景

これまで enable_frozen_string_literal とコードカバレッジ計測は共存できない問題がありました。Coverage モジュールが実行中の場合、ISeq(InstructionSequence)のダンプ・ロードが妨げられるため、Bootsnapはカバレッジ検出時にキャッシュ処理をスキップする設計になっていました。load_iseq メソッド内の return nil if coverage_on? がその証拠で、frozen string literalコンパイラを選択していても無条件にバイパスされていました。

その結果、test_boot_frozen_string_literal_and_coverage というテストは skip("Need to find a workaround for this...") のままになっており、カバレッジ計測環境での動作は未解決の課題として残っていました。Ruby 4.0.4でISeqのダンプ・ロード周りの制約が解消されたことで、この問題に対処できるようになりました。

技術的な変更

カバレッジ検出ロジックが load_iseq から fetch クラスメソッドに移動され、コンパイラの種別に応じた細かい制御が可能になりました。

変更前のコードでは、load_iseq の冒頭でカバレッジを確認し、有効であれば即座に nil を返していました。これはfrozen string literalコンパイラが選択されている場合でも例外なく適用されていました。

変更前:

module InstructionSequenceMixin
  def load_iseq(path)
    # Having coverage enabled prevents iseq dumping/loading.
    return nil if coverage_on?
    ...
  end
end

変更後は fetch メソッド内でカバレッジを検出し、コンパイラの種別ごとに3通りの分岐を設けています。

変更後:

COVERAGE_SUPPORTED = RUBY_VERSION >= "4.0.4"
@coverage_support_warning_emitted = false

def self.fetch(path, cache_dir: ISeq.cache_dir)
  compiler = compiler_selector&.call(path) || default_compiler

  if coverage_on?
    return nil if compiler.equal?(DEFAULT)

    if COVERAGE_SUPPORTED
      return compiler.input_to_output(File.read(path.to_s), path.to_s, nil)
    elsif !@coverage_support_warning_emitted
      @coverage_support_warning_emitted = true
      warn(<<~MSG)
        Using `Bootsnap.enable_frozen_string_literal` with code coverage enabled is only supported on Ruby 4.0.4+.
        Files loaded while coverage is on, will have mutable string literals.
      MSG
    end

    return nil
  end
  ...
end

カバレッジが有効な場合の挙動を整理すると次のとおりです:

  • DEFAULT コンパイラ選択時: キャッシュをスキップ(nil を返す)。従来と同じ動作。
  • frozen string literal コンパイラ選択時 + Ruby 4.0.4以降: キャッシュを経由せず input_to_output を直接呼び出してコンパイル結果を返す。
  • frozen string literal コンパイラ選択時 + Ruby 4.0.4未満: 一度だけ警告を出力し、nil を返してフォールバック。

また、coverage_on? メソッドの実装もRubyバージョンで分岐するよう整理されました。Ruby 3.1未満では Coverage.running? を、3.1以降では Coverage.state != :idle を使用します。これは Coverage.state がRuby 3.1で追加された新しいAPIであるためです。

if RUBY_VERSION < "3.1."
  def self.coverage_on?
    defined?(Coverage) && Coverage.running?
  end
else
  def self.coverage_on?
    defined?(Coverage) && Coverage.state != :idle
  end
end

テスト面では、test_boot_frozen_string_literal_and_coverage がスキップされていた状態から、test_coverage_working_with_frozen_string_literal として実際に動作を検証するテストに置き換えられました。新たに追加された coverage_accurate.rbcoverage_test.rb フィクスチャは、カバレッジデータが正確に計測されているかを検証するとともに、string literalのfrozen状態も CHECK_STRING_LITERALS 環境変数を通じて確認します。

設計判断

キャッシュをバイパスして直接コンパイルする という方式が、カバレッジ有効時の解決策として採用されています。

カバレッジが有効な状態でISeqをダンプ・ロードできない制約はRuby 4.0.4未満では解消できないため、frozen string literalを適用したいコンパイラについては input_to_output を直接呼び出すことでキャッシュを迂回します。これはパフォーマンス上の最適化(キャッシュ)を犠牲にしつつも、機能の正確性(frozen string literal の適用)を優先する判断です。一方、DEFAULT コンパイラにとってはキャッシュが主目的であるため、カバレッジ有効時はそのまま nil を返すことでRuby本来の処理に委ねています。

警告の重複出力を防ぐ @coverage_support_warning_emitted フラグを設けることで、旧Rubyでの劣化動作を開発者に一度だけ通知する設計も注目に値します。ノイズを最小限に抑えながら問題を可視化する、実用的なアプローチです。

まとめ

この変更は、カバレッジ計測とfrozen string literalの最適化が長らく両立できなかった制約を、Rubyバージョンに応じた細かい分岐によって解消したものです。Ruby 4.0.4以降では両機能を同時に享受できるようになり、旧バージョンでも明示的な警告によって挙動の変化が把握できるようになっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
898d6a73

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)の3部構成が明確に適用されており、記事全体の構成が非常に分かりやすい。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```ruby:ファイルパス)とGitHubのPRリンク記法([PR #550](URL))が正しく使用されている。

対象読者への適合性 ✓ PASS

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

ISeq、frozen string literal、コンパイラといった専門用語を前提としており、対象読者である専門知識を持つエンジニアに適した技術レベルで書かれている。

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

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

各セクション、各パラグラフが要点(トピックセンテンス)から始まり、1段落1トピックの原則が守られているため、非常に読みやすい構造になっている。

Diff内容との照合 ✓ PASS

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

記事内で引用されている変更前後のコードブロック(`lib/bootsnap/compile_cache/iseq.rb`)は、提供されたDiffの内容と完全に一致している。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

ISeq, Coverage, frozen string literal, フォールバックなど、PRの文脈で使われている技術用語を正確に使用している。

説明の技術的正確性 ✓ PASS

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

カバレッジ検出ロジックの移動、Rubyバージョンに応じた3つの分岐、キャッシュのバイパスといった技術的な説明が、Diffの内容と論理的に整合しており正確である。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionやDiffの内容から裏付け可能であり、ハルシネーション(捏造された情報)は検出されなかった。

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

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

PR番号「#550」、Rubyバージョン「4.0.4」「3.1」などの数値や固有名詞が、提供された情報と正確に一致している。

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

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

記事タイトル「カバレッジ計測中でも `enable_frozen_string_literal` が動作するように」は、PRのタイトル「Make `Bootsnap.enable_frozen_string_literal` work even with coverage on」の内容を的確に反映している。

外部知識の正確性 ✓ PASS

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

記事内のRubyバージョンに関する言及(4.0.4, 3.1)は、すべてDiff内のコード(`RUBY_VERSION >= "4.0.4"`, `RUBY_VERSION < "3.1."`)に基づいたものであり、PR外の知識の捏造はない。

時間表現の正確性 ✓ PASS

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

「これまで」「変更後は」といった時間的関係を示す表現が、PRの変更内容と正確に一致している。