Project

General

Profile

Bug #17519

set_visibility fails when a prepended module and a refinement both exist

Added by fledman (David Feldman) about 2 months ago. Updated 16 days ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
ruby -v:
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin19]
[ruby-core:101981]

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)

#4

Updated by jeremyevans0 (Jeremy Evans) 16 days ago

I've added a pull request to fix this: https://github.com/ruby/ruby/pull/4200

Also available in: Atom PDF