Bug #17519
set_visibility fails when a prepended module and a refinement both exist
Description
the set_visibility functions (aka public/private/protected) fail with NameError when:
- called on a specific object's singleton class
- for a specific method
- and both of the following are true
- any module has been prepended to the object's class
- a refinement exists for the specific method
note that the refinement does not need to ever be used
I have reproduced this on 3.0.0, 2.7.2, and 2.6.6 (those were the only 3 version I tested)
def test_visibility(function)
h1 = {x:1, y:1}
h2 = {x:2, y:2}
h1.singleton_class.send(:private, function)
h2.singleton_class.send(:public, function)
begin
puts h1.public_send(function, :x)
rescue NoMethodError => err
puts "hit NoMethodError as expected: #{err.inspect}"
end
puts h2.public_send(function, :x)
end
succeeds without any prepended modules or refinements:
irb> test_visibility :except
hit NoMethodError as expected: #<NoMethodError: private method `except' called for {:x=>1, :y=>1}:Hash>
{:y=>2}
=> nil
succeeds with only a prepended module:
irb> module Nothing; end; Hash.prepend(Nothing); Hash.ancestors
=> [Nothing, Hash, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject]
irb> test_visibility :except
hit NoMethodError as expected: #<NoMethodError: private method `except' called for {:x=>1, :y=>1}:Hash>
{:y=>2}
=> nil
succeeds with only a refinement:
irb> module NeverUsed
refine Hash do
def except(*keys)
{never: 'used'}
end
end
end
=> #<refinement:Hash@NeverUsed>
irb> test_visibility :except
hit NoMethodError as expected: #<NoMethodError: private method `except' called for {:x=>1, :y=>1}:Hash>
{:y=>2}
=> nil
fails with both a refinement and a prepended module:
irb> module Nothing; end; Hash.prepend(Nothing); Hash.ancestors
=> [Nothing, Hash, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject]
irb> module NeverUsed
refine Hash do
def except(*keys)
{never: 'used'}
end
end
end
=> #<refinement:Hash@NeverUsed>
irb> test_visibility :except
Traceback (most recent call last):
6: from .rubies/ruby-3.0.0/bin/irb:23:in `<main>'
5: from .rubies/ruby-3.0.0/bin/irb:23:in `load'
4: from .rubies/ruby-3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
3: from (irb):25:in `<main>'
2: from (irb):5:in `test_visibility'
1: from (irb):5:in `private'
NameError (undefined method `except' for class `#<Class:#<Hash:0x00007ffd6b176f18>>')
Did you mean? exec
# non-refined method still works
irb> test_visibility :fetch
hit NoMethodError as expected: #<NoMethodError: private method `fetch' called for {:x=>1, :y=>1}:Hash>
2
=> nil
Updated by fledman (David Feldman) about 2 months ago
since :except is only available natively on Ruby3, here is a Ruby2 demonstration:
irb> RUBY_VERSION
=> "2.7.2"
irb> module Nothing; end; Hash.prepend(Nothing); Hash.ancestors
=> [Nothing, Hash, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject]
irb> module NeverUsed
refine Hash do
def fetch(key)
42
end
end
end
=> #<refinement:Hash@NeverUsed>
irb> test_visibility :fetch
Traceback (most recent call last):
6: from /Users/davidfeldman/.rubies/ruby-2.7.2/bin/irb:23:in `<main>'
5: from /Users/davidfeldman/.rubies/ruby-2.7.2/bin/irb:23:in `load'
4: from /Users/davidfeldman/.rubies/ruby-2.7.2/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>'
3: from (irb):25
2: from (irb):14:in `test_visibility'
1: from (irb):14:in `private'
NameError (undefined method `fetch` for class `#<Class:#<Hash:0x00007fcdf3ac7ec0>>')
# non-refined method still works
irb> test_visibility :[]
hit NoMethodError as expected: #<NoMethodError: private method `[]` called for {:x=>1, :y=>1}:Hash Did you mean? []=>
2
=> nil
Updated by fledman (David Feldman) about 2 months ago
and a demonstration with a custom class:
RUBY_VERSION
class Thing
def direction
'LEFT'
end
end
def test_thing_visibility
t1 = Thing.new
t2 = Thing.new
t1.singleton_class.send(:private, :direction)
t2.singleton_class.send(:public, :direction)
begin
puts t1.direction
rescue NoMethodError => err
puts "hit NoMethodError as expected: #{err.inspect}"
end
puts t2.direction
end
test_thing_visibility
module NeverUsed
refine Thing do
def direction
'UP'
end
end
end
test_thing_visibility
module Nothing; end; Thing.prepend(Nothing); Thing.ancestors
test_thing_visibility
outputs:
=> "2.6.6"
=> :direction
=> :test_thing_visibility
hit NoMethodError as expected: #<NoMethodError: private method `direction' called for #<Thing:0x00007f8c6f871ce0>>
LEFT
=> nil
=> #<refinement:Thing@NeverUsed>
hit NoMethodError as expected: #<NoMethodError: private method `direction' called for #<Thing:0x00007f8c6f8b4a68>>
LEFT
=> nil
=> [Nothing, Thing, Object, PP::ObjectMixin, Kernel, BasicObject]
Traceback (most recent call last):
6: from /Users/davidfeldman/.rubies/ruby-2.6.6/bin/irb:23:in `<main>'
5: from /Users/davidfeldman/.rubies/ruby-2.6.6/bin/irb:23:in `load'
4: from /Users/davidfeldman/.rubies/ruby-2.6.6/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
3: from (irb):37
2: from (irb):11:in `test_thing_visibility'
1: from (irb):11:in `private'
NameError (undefined method `direction' for class `#<Class:#<Thing:0x00007f8c6f8dcd10>>')
Updated by fledman (David Feldman) about 2 months ago
although the title makes it sound obscure, this bug is actually fairly easy to trigger when using rspec and rails:
- activesupport >= 5 prepends a module onto Hash
- i18n >= 1.3 refines several methods on Hash
- power_assert (used by test-unit and minitest) refines most of the basic operators of the core classes
e.g. try to expect(some_hash).to receive(:except)
Updated by jeremyevans0 (Jeremy Evans) 16 days ago
I've added a pull request to fix this: https://github.com/ruby/ruby/pull/4200