Bug #17519
closedset_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) almost 4 years 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) almost 4 years 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) almost 4 years 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) almost 4 years ago
I've added a pull request to fix this: https://github.com/ruby/ruby/pull/4200
Updated by jeremyevans (Jeremy Evans) over 3 years ago
- Status changed from Open to Closed
Applied in changeset git|58660e943488778563b9e41005a601e9660ce21f.
Skip refined method when exporting methods with changed visibility
Previously, attempting to change the visibility of a method in a
singleton class for a class/module that is prepended to and refined
would raise a NoMethodError.
Fixes [Bug #17519]
Updated by fledman (David Feldman) over 3 years ago
do you plan to backport the fix to any of the supported 2.x versions?
Updated by jeremyevans0 (Jeremy Evans) over 3 years ago
- Backport changed from 2.5: UNKNOWN, 2.6: UNKNOWN, 2.7: UNKNOWN, 3.0: UNKNOWN to 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: REQUIRED, 3.0: REQUIRED
fledman (David Feldman) wrote in #note-6:
do you plan to backport the fix to any of the supported 2.x versions?
The choice of which patches to backport is up to the branch maintainer. I've marked this for backporting, but it is their decision.
Updated by nagachika (Tomoyuki Chikanaga) over 3 years ago
- Backport changed from 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: REQUIRED, 3.0: REQUIRED to 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: DONE, 3.0: REQUIRED
ruby_2_7 6e962f02b266c3a6c47e50cf2e9ab7b1db25e515 merged revision(s) 58660e943488778563b9e41005a601e9660ce21f.
Updated by naruse (Yui NARUSE) over 3 years ago
- Backport changed from 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: DONE, 3.0: REQUIRED to 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: DONE, 3.0: DONE
ruby_3_0 d1cec0bca588266b9af1d55e592016c45ee68fbb merged revision(s) 58660e943488778563b9e41005a601e9660ce21f.
Updated by fledman (David Feldman) over 3 years ago
this fix seems to have introduced a new but similar bug: https://github.com/ruby/ruby/pull/4200#issuecomment-813671308
Updated by jeremyevans0 (Jeremy Evans) over 3 years ago
fledman (David Feldman) wrote in #note-10:
this fix seems to have introduced a new but similar bug: https://github.com/ruby/ruby/pull/4200#issuecomment-813671308
I can confirm the issue. Here's a pull request that fixes it: https://github.com/ruby/ruby/pull/4357
Updated by jeremyevans0 (Jeremy Evans) over 3 years ago
- Status changed from Closed to Open
Updated by jeremyevans (Jeremy Evans) over 3 years ago
- Status changed from Open to Closed
Applied in changeset git|4b36a597f48c857aa5eb9ed80fec0d02f6284646.
Fix setting method visibility for a refinement without an origin class
If a class has been refined but does not have an origin class,
there is a single method entry marked with VM_METHOD_TYPE_REFINED,
but it contains the original method entry. If the original method
entry is present, we shouldn't skip the method when searching even
when skipping refined methods.
Fixes [Bug #17519]
Updated by nagachika (Tomoyuki Chikanaga) over 3 years ago
- Backport changed from 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: DONE, 3.0: DONE to 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: REQUIRED, 3.0: REQUIRED
reset Backport field to backport git|4b36a597f48c857aa5eb9ed80fec0d02f6284646.
Updated by nagachika (Tomoyuki Chikanaga) over 3 years ago
- Backport changed from 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: REQUIRED, 3.0: REQUIRED to 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: REQUIRED, 3.0: DONE
ruby_3_0 7b6a2ad04a3272a31323493133498dfc60d77d76 merged revision(s) 4b36a597f48c857aa5eb9ed80fec0d02f6284646.