Inflector#transliterate が呼び出し元文字列を変更しないように修正

rails/rails

ActiveSupport::Inflector.transliterate が、非 UTF-8 かつ凍結されていない文字列を受け取ったときに内部で force_encoding / encode! を実行し、呼び出し元オブジェクトを副作用的に書き換えていた問題を解消し、常にコピー上で変換を行うようになりました。これにより、parameterize などの上位メソッドでも同様の予期しないミューテーションが起きなくなります。

背景

ActiveSupport::Inflector.transliterate は文字列のローマ字化を行うユーティリティで、以前は文字列が 凍結 (frozen) されている場合にだけ dup を行い、非凍結かつ非 UTF-8 の文字列に対しては直接エンコーディングを変更していました。その結果、次のようなコードで元の文字列が変化してしまいます。

s = "中文".encode(Encoding::GB18030)
ActiveSupport::Inflector.transliterate(s)
puts s.encoding # => UTF-8 (期待は GB18030)

このミューテーションは parameterize が内部で transliterate を呼び出す際にも伝搬し、呼び出し側が文字列オブジェクトの状態を保てないという予期しない副作用を引き起こしていました。実際に非凍結の GB18030 や US‑ASCII(非 ASCII バイトを含む)文字列でテストが失敗していたことが報告されています。

技術的な変更

string を常にコピーしてからエンコーディング変換を行うように変更しました。具体的には、return string.dup if string.ascii_only? の直後に string = string.dup を挿入し、凍結状態に関わらず新しいオブジェクトで処理を進めます。

@@
-      return string.dup if string.ascii_only?
-      string = string.dup if string.frozen?
+      return string.dup if string.ascii_only?
+
+      # Operate on a copy so the in-place force_encoding/encode! below never
+      # mutate the caller's (possibly non-frozen) string.
+      string = string.dup

早期リターンで ASCII のみの文字列は従来通り string.dup したオブジェクトを返すため、パフォーマンスへの影響は最小限に留められています。また、凍結文字列に対しても同様にコピーを作成するため、既存の API 互換性は保持されています。

テストスイートには 3 件の回帰テストが追加され、非凍結の GB18030、US‑ASCII(非 ASCII バイト)、および parameterize 呼び出し時に文字列が変更されないことを確認しています。これらのテストはすべて修正前は失敗し、修正後は緑になることが保証されています。

設計判断

string を無条件で dup する 方針は、条件分岐を減らしコードの可読性と安全性を高めることを目的としています。凍結チェックのみで dup を行う実装は、エンコーディング変換が副作用を持つことを見落としやすく、今回のバグの根本原因となっていました。

無条件 dup にしたことで、呼び出し元文字列が凍結かどうかに関わらずミューテーションが起きない保証が得られます。さらに、API のシグネチャや既存の動作は変わらず、string.dup のコストは文字列長に比例するものの、文字列変換自体が行われる前提条件であるため実質的な性能低下はほとんどありません。

この変更は Inflector 系メソッド全体に波及し、parameterize などの上位ユーティリティでも同様に安全な振る舞いが保証されます。結果として、Rails アプリケーションの文字列操作における予期せぬ副作用が排除され、開発者が文字列オブジェクトの状態を安心して扱えるようになりました。

まとめ

今回の PR は、ActiveSupport::Inflector.transliterate が呼び出し元文字列を変更しないようにするため、入力文字列を常にコピーしてからエンコーディング変換を行うというシンプルかつ安全な修正を導入しました。テストの追加により回帰が防止され、既存 API 互換性を保ちつつ Rails 全体の文字列処理の堅牢性が向上しています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
af544e11

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文があり、背景・技術的な変更・設計判断・まとめという順序で総論→各論→結論が明確に構成されています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライトは ``ruby:activesupport/lib/active_support/inflector/transliterate.rb`` の形式で正しく記述され、PRリンクも #57675 を含む正しいリンク形式です。

対象読者への適合性 ✓ PASS

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

専門的な Ruby / Rails の知識を前提としており、初心者向けの過度な説明はありません。

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

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

各セクションの冒頭が総論パラグラフとなり、続く段落が1トピックで構成され、トピックセンテンスで始まり、適切な長さに分割されています。空行も確保されています。

Diff内容との照合 ✓ PASS

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

記事中の diff ブロックは提供された Diff と完全に一致しており、変更内容が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている用語(transliterate、dup、force_encoding、ASCII など)は PR 内容と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な説明は PR の問題点・修正内容・テスト追加に基づき、根拠があり論理的です。

事実の突合 ✓ PASS

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

全ての主張は PR の説明・Diff・テストコードで裏付けられており、推測や捏造は見られません。

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

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

PR 番号 #57675 が正しく記載されており、他の数値や固有名詞の誤りはありません。

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

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

記事タイトルは PR の趣旨「Don't mutate the caller's string in Inflector#transliterate」を正確に日本語化しています。

外部知識の正確性 ✓ PASS

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

Rails のバージョンサポートやリリース日程等、PR に含まれない外部知識は記載されていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記述と一致しています。