strict_locals の非ASCII文字デフォルト値で発生するエンコーディングエラーを修正

rails/rails

File.binread でロードされたテンプレートで strict locals に非ASCII文字のデフォルト値(例: label: "café")を使用した際に発生する Encoding::CompatibilityError を修正しました。原因となっていた @strict_locals のエンコーディングタグを、force_encoding で再設定する1ブロックの追加で解消しています。

背景

File.binread でテンプレートを読み込むと、ソース文字列は ASCII-8BIT(BINARY)タグで生成されます。ActionView::Template はその後 encode! メソッドで Encoding.default_external(通常 UTF-8)に再タグ付けしますが、strict locals の抽出はこの encode! よりも前に行われていました。

strict_locals! は正規表現キャプチャ($1)で locals 宣言を取り出しますが、キャプチャ文字列はソースのエンコーディングタグを引き継ぐため、@strict_locals は ASCII-8BIT タグのままになります。続いて compiled_source のヒアドキュメントがこの ASCII-8BIT 文字列を UTF-8 タグの ERB ハンドラ出力と文字列結合しようとすると、両者に 0x7F を超えるバイトが含まれている場合に Ruby のエンコーディング調停が Encoding::CompatibilityError を送出します。

なお、テンプレート本文に 0x7F を超えるバイトが含まれない場合はエラーは発生しませんが、コンパイル済みソースのエンコーディングタグが UTF-8 ではなく ASCII-8BIT のままになるという別の問題が残ります。

技術的な変更

strict_locals!force_encoding(Encoding.default_external) の呼び出しを追加し、@strict_locals のエンコーディングタグを encode! がソース本体に対して行うものと同じ処理で揃えます。

変更前:

def strict_locals!
  # ...
  return if @strict_locals.nil? # Magic comment not found

  @strict_locals = "**nil" if @strict_locals.blank?
end

変更後:

def strict_locals!
  # ...
  return if @strict_locals.nil? # Magic comment not found

  # Tag with the assumed encoding before encode! runs, same as
  # encode! does for the source itself (see above).
  if @strict_locals.encoding == Encoding::BINARY
    @strict_locals.force_encoding(Encoding.default_external)
  end

  @strict_locals = "**nil" if @strict_locals.blank?
end

これは ラベル変更のみforce_encoding)であり、バイト列の変換(encode)は行いません。File.binread が UTF-8 バイト列を ASCII-8BIT タグで保持しているだけであり、バイト列自体は正しい UTF-8 エンコードのため、タグを付け替えるだけで十分です。

テストでは、\xC3\xA9(U+00E9、é)と \xC3\xBC(U+00FC、ü)を含む ASCII-8BIT 文字列を直接生成し、レンダリング結果のエンコーディングが UTF-8 になること、および内容が正しくレンダリングされることを検証しています。

def test_strict_locals_with_non_ascii_default_values
  # \xC3\xA9 = U+00E9 (e with acute), \xC3\xBC = U+00FC (u with diaeresis)
  source = "<%# locals: (label: \"caf\xC3\xA9\") -%>\n<p><%= label %> \xC3\xBC</p>"
  assert_equal Encoding::ASCII_8BIT, source.encoding

  @template = new_template(source)
  assert_equal Encoding::UTF_8, render.encoding
  assert_equal "<p>caf\u{E9} \u{FC}</p>", render
end

設計判断

encode! と同じ再タグ付けパターンを strict_locals! にも適用するという 一貫性のある修正 が採用されました。

修正箇所は @strict_locals.encoding == Encoding::BINARY の条件ガードを設けた上で force_encoding を呼び出すだけであり、ASCII-8BIT 以外のエンコーディングで読み込まれたテンプレートには影響しません。また encode(バイト変換)ではなく force_encoding(タグ付け替え)を選択したのは、File.binread が保持するバイト列がすでに正しい UTF-8 バイト列であるという前提に基づいています。この前提は encode! がソース本体に対して行う処理と同一であり、既存の設計と整合的です。

まとめ

本修正は、strict_locals!encode! より先に実行されることで生じるエンコーディングタグの不一致を、6行の追加で解消しています。force_encoding という最小限の手術により、バイト列を変換せずにエンコーディングの一貫性を回復する Ruby のエンコーディング設計の活用例として参考になる変更です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
d42507e7

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

「総論→各論→結論」の構成が明確です。リード文、背景、技術的な変更、まとめの必須要素がすべて含まれています。さらに任意である「設計判断」セクションも設けられており、非常に分かりやすい構成です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックのファイル名付きシンタックスハイライトは正しく使用されています。しかし、フッターのPRリンクが `[PR #56906](URL)` となっており、ガイドラインで推奨される `[#56906](URL)` 形式とわずかに異なります。

対象読者への適合性 ✓ PASS

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

Rubyのエンコーディングに関する深い知識(ASCII-8BIT, force_encodingなど)を前提としており、専門のエンジニアという対象読者に適合した内容です。

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

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

各セクション、各パラグラフが「総論→各論」の構造で書かれています。トピックセンテンスが明確で、1段落1トピックの原則が守られており、可読性が非常に高いです。

Diff内容との照合 ✓ PASS

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

記事内のコードブロック(変更前、変更後、テストコード)は、提供されたDiff情報と完全に一致しています。ファイルパスの指定も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`File.binread`、`ASCII-8BIT`、`Encoding::CompatibilityError`、`force_encoding` といった技術用語が、PRの文脈およびRubyの仕様に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

問題の根本原因(`strict_locals!` と `encode!` の実行順序)や、解決策として `force_encoding` を使う理由(バイト列自体は正しいUTF-8であるため)について、技術的に正確かつ論理的に説明されています。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionで裏付けられています。問題の発生条件、原因、解決策について、ハルシネーション(捏造)や根拠のない推測は見られません。

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

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

フッターで参照されているPR番号(#56906)は正確です。コード例で示されているバイト表現(`\xC3\xA9`)も正しく記載されています。

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

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

記事のタイトルは、PRのタイトル「Fix Encoding::CompatibilityError with non-ASCII strict locals defaults」の内容を忠実に日本語で表現しており、記事の主題と完全に一致しています。

外部知識の正確性 ✓ PASS

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

PR情報に記載のない外部知識(LTS、リリース日程など)の追加はなく、記事の信頼性が保たれています。

時間表現の正確性 ✓ PASS

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

時間表現の誤用はなく、PRで行われた修正を過去の事実として正確に記述しています。