🌟

【bugs.ruby Advent Calender】String のサブクラスで gsub を再定義したときのバグ報告【18日目】

2024/12/18に公開

bugs.ruby Advent Calender 18日目の記事です。

これはなに

今年1年間通してみてきた bugs.ruby のチケットの中から気になったものを1つずつ取り上げていく Advent Calender です。
取り上げるチケットは基本的にこのブログで取り上げたものになります。
記事のまとめは ここを参照 してください。

String のサブクラスで gsub を再定義したときのバグ報告

このチケットは以下のようにサブクラスで String#gsub を再定義したときに #gsub のブロック内で Regexp.last_match が取得できないというバグ報告になります。

def call_gsub(str)
  str.gsub(/^./) {
    # ブロック内でマッチした値を取得する
    pp Regexp.last_match
  }
end

class MyString < String
  def gsub(*args, &block)
    super(*args, &block)
  end
end

text = 'hoge'

# String オブジェクトの場合は Regexp.last_match が取得できる
call_gsub(String.new(text))
# => #<MatchData "h">

# 一方で String#gsub を再定義したオブジェクトだと Regexp.last_match は取得できない
call_gsub(MyString.new(text))
# => nil

これなんですが Regexp.last_match などの正規表現関連のグローバル値は複数のフレームに渡って動作しないのが起因です。

と、いってもわたしもあんまりよくわかっていないんですが、今回のケースでは #gsub を再定義したメソッド内であれば Regexp.last_match が取得できます。

def call_gsub(str)
  str.gsub(/^./)
  pp call_gsub: Regexp.last_match
end

class MyString1 < String
end

class MyString2 < String
  def gsub(*args, &block)
    # このメソッド内で gsub でマッチしたときの正規表現の情報が参照できる
    super(*args, &block)
  ensure
    pp ensure: $~
  end
end

text = 'hoge'

# #gsub を再定義していない場合はブロック内でも取得できる
call_gsub(MyString1.new(text))
# {call_gsub: #<MatchData "h">}

# #gsub を再定義している場合はそのメソッド内で値を取得できる
call_gsub(MyString2.new(text))
# {call_gsub: nil}
# {ensure: #<MatchData "h">}

#gsub を Ruby レイヤーで再定義することで参照することができるレイヤーがわかってしまう、みたいな感じなんですかね?
今年みた bugs.ruby のチケットでもかなり難しい内容でした。

String#gsub みたいなのを再定義することはあんまりないと思うんですが、このあたりは Ruby の内部事情など知らないとなかなか気づきづらそうですねー。

関連

GitHubで編集を提案

Discussion