📑

[Bug #20807] String のサブクラスで gsub を再定義すると Regexp.last_match が取得できないバグ報告

2024/10/14に公開

[Bug #20807] String#gsub fails when called from string subclass with a block passed

  • 以下のようにサブクラスで 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'

call_gsub(String.new(text))
# => #<MatchData "h">

call_gsub(MyString.new(text))
# => nil
  • これなんですが Regexp.last_match などの正規表現関連のグローバル値は複数のフレームに渡って動作しないのが起因みたいですね
  • 今回のケースだと #gsub を再定義しているので MyString#gsub 内でのみ参照できるらしい
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">}
  • call_gsub メソッドだけ見ると String#gsubRegexp.last_match が利用できる期待値なのでこういうケースはなかなか気づきづらそうですねえ
  • 相当なエッジケースとはいえ、ハマると一生気づかなさそう…
GitHubで編集を提案

Discussion