🐡

[Bug #21125] method_missing が定義されている時に private メソッドが呼び出されてしまうバグ報告

2025/02/12に公開

[Bug #21125] Kernel is called first

  • 以下のようなコードを実行した時に Ruby 3.3 と Ruby 3.4 で挙動に差異があるというバグ報告
class ObjectifiedHash
  def initialize(hash)
    @hash = hash
  end

  private

  attr_reader :hash

  def method_missing(method_name, *args, &block)
    if hash.key?(method_name.to_sym)
      hash[method_name.to_sym]
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    hash.key?(method_name.to_sym) || super
  end
end

class Foo
  def system(...)
    # method_missing が呼び出されることを期待しているが
    # Ruby 3.4 からは Kernel#system が呼び出されるようになった
    raw.system(...)
  end

  def initialize(raw)
    @raw = raw
  end

  def get_system
    system
  end

  private

  attr_reader :raw
end

class Test
  def self.run
    obj = ObjectifiedHash.new({
      system: 'system'
    })

    foo = Foo.new(obj)

    p foo.get_system
    # Ruby 3.3 => "system"
    # Ruby 3.4 => error: 'Kernel#system': wrong number of arguments (given 0, expected 1+) (ArgumentError)
  end
end

Test.run
  • ちょっとコードが長いんですがポイントとしては Foo#system 内で raw.system(...) を呼び出した時に Ruby 3.3 では raw.method_missing が呼び出されていたが Ruby 3.4 からは raw.system が呼び出されるようになったという差異になります
  • これなんですが前提として『 private メソッドを呼び出す場合は #method_missing が呼び出される』ということが挙動として期待されます
    • Kernel#systemprivate メソッドなので Ruby 3.3 の #method_missing が呼びされることが期待される挙動になる
class X
  def method_missing(...)
    "method_missing"
  end

  def public_hoge
    "public_hoge"
  end

  private

  def private_hoge
    "private_hoge"
  end
end

def foo
  x = X.new

  # これは public メソッドが呼び出される
  pp x.public_hoge    # => "public_hoge"

  # これは private メソッドを呼び出そうとしているので method_missing が呼ばれる
  pp x.private_hoge   # => "method_missing"
end

foo
  • で、これなんですがどうやら転送引数(...) を利用している場合に Ruby 3.4 だと private メソッドのほうが呼ばれてしまう、っていうバグみたいですね
class X
  def method_missing(...)
    "method_missing"
  end

  def public_hoge
    "public_hoge"
  end

  private

  def private_hoge
    "private_hoge"
  end
end

def foo(...)
  x = X.new

  # これは OK
  # method_missing が呼ばれる
  pp x.private_hoge # => "method_missing"

  # method_missing ではなくて private メソッドが呼び出されてしまう
  pp x.private_hoge(...) # => "private"
end

foo
  • 最適化周りの問題なのかな?
GitHubで編集を提案

Discussion