Ruby 3.4の凍結文字列警告をStringIOで解決
Ruby 3.4では、将来的に文字列リテラルがデフォルトで凍結されることを見据えた警告が強化されました。marcelでは、StringIOオブジェクトに対するbinmodeやset_encodingの呼び出しが警告を引き起こす問題がありました。#140では、StringIOを読み取り専用にすることでこの警告を解消しています。
背景
#123で一部の凍結文字列警告は修正されていましたが、StringIOオブジェクトに対するエンコーディング操作で発生する警告が残っていました。Railsアプリケーションで詳細な警告オプション(-W2)を有効にしてテストを実行すると、以下の警告が繰り返し表示されていました:
/gems/marcel-1.1.0/lib/marcel/magic.rb:120: warning: literal string will be frozen in the future
この警告は、StringIOが内部で保持する文字列を変更しようとする際に発生します。Ruby 3.4では、将来的な破壊的変更を事前に検出するための警告機能が強化されており、StringIOのbinmodeやset_encodingメソッド呼び出し時に内部文字列の変更が試みられることで警告が発生していました。
技術的な変更
変更前:
def self.magic_match(io, method)
return magic_match(StringIO.new(io.to_s), method) unless io.respond_to?(:read)
io.binmode if io.respond_to?(:binmode)
io.set_encoding(Encoding::BINARY) if io.respond_to?(:set_encoding)
# ...
end
変更後:
def self.magic_match(io, method)
return magic_match(StringIO.new(+io.to_s), method) unless io.respond_to?(:read)
# Make StringIO readonly before encoding changes to prevent Ruby 3.4 frozen string warnings.
# Should be fixed in Ruby 3.5+: https://redmine.ruby-lang.org/issues/21280
io.close_write if io.respond_to?(:closed_write?) && !io.closed_write?
io.binmode if io.respond_to?(:binmode)
io.set_encoding(Encoding::BINARY) if io.respond_to?(:set_encoding)
# ...
end
主な変更点は2つあります:
StringIO生成時の文字列の複製:
StringIO.new(+io.to_s)の+演算子により、文字列を明示的に複製して新しい可変文字列を生成します。書き込みクローズによる読み取り専用化:
close_writeメソッドでStringIOを読み取り専用にすることで、後続のbinmodeやset_encoding呼び出しで内部文字列の変更を試みないようにします。
StringIOとFileの違いを利用した条件分岐
この実装の巧妙な点は、respond_to?(:closed_write?)による型判定です:
- StringIOは
closed_write?メソッドを持つ - Fileオブジェクトは
closed_write?メソッドを持たない
この違いを利用することで、StringIOのみに対してワークアラウンドを適用し、Fileオブジェクトの動作には影響を与えないようにしています。既存コードで使われているrespond_to?パターンとも一貫性が保たれています。
テストの追加
警告の発生を検証するテストが追加されました:
test "no Ruby 3.4 frozen string warnings with StringIO" do
content = "Test content for mime detection"
io = StringIO.new(content)
# Capture warnings
warnings = []
original_stderr = $stderr
$stderr = StringIO.new
begin
Marcel::MimeType.for(io)
warnings = $stderr.string.lines.grep(/marcel.*magic\.rb.*frozen/)
ensure
$stderr = original_stderr
end
assert_empty warnings, "Expected no frozen string warnings, but got:\n#{warnings.join}"
end
このテストは、標準エラー出力をキャプチャして凍結文字列関連の警告が出力されないことを確認します。実用的な検証例として機能し、修正が正しく動作することを保証します。
設計判断
例外処理ではなく事前チェック
PR内の議論では、rescueによる例外処理ではなく、respond_to?による事前チェックを採用しています。これには以下の利点があります:
- パフォーマンスへの影響が最小限
- 例外の発生と捕捉によるオーバーヘッドがない
- 既存コードのパターンとの一貫性
一時的なワークアラウンドとしての位置付け
コメントにもあるように、この修正はRuby 3.5までの一時的なワークアラウンドです。Ruby 3.5では、StringIOのエンコーディング操作で誤った警告が出ないようにする修正が取り込まれる予定です(Redmine #21280)。
このような将来的な解消が見込まれる問題に対して、コメントで明示的に期限を示すことで、後のメンテナンス時に不要なコードを識別しやすくしています。