A Ruby shadowing bug in the wild

20 Apr 2017

Here's some code; can you tell what's wrong with it? I didn't spot the issue for quite a while. To boil it down a bit:

class Foo
  attr_accessor :buz
  def initialize
    @buz = 42
  end
  def bar
    unless buz
      buz = 21
    end
    buz
  end
end

See the issue now? Another variant with no use of attr_accessor:

class Foo
  def buz
    42
  end
  def bar
    unless buz
      buz = 21
    end
    buz
  end
end

With this code, when I run p Foo.new.bar, I'd expect to get 42 as a result - that's what the buz instance method is returning, it's not nil, and so the unless condition is false and so the buz = 21 assignment never gets executed. But wrong! Instead, running that program produces output of nil.

This is a somewhat surprising, but functioning as designed, feature of Ruby shadowing behavior - long story short, local variables shadow instance methods, and crucially, local variable definitions are made when the code is parsed, not when it is executed. The core Ruby docs describe this behavior here, Travis Dahlke has a nice post about it, and it's also briefly mentioned in the Ruby Hacking Guide, although there it refers to "compile time" vs parse time.

Misc notes: A simple fix is to explicitly invoke the instance method. So in the attr_accessor example, just adding empty parentheses to the last buz invocation causes Foo.new.bar to return 42. And as Ronie Henrich pointed out, in the second example the assignment would need to be either self.buz = 21 or @buz = 21. I thought Rubocop's shadowing lint cop might find this, but that's different; it's a "local shadows local" check. Also, there's a PR to fix this issue in the docusign_rest project, hats off to Mark Wilson.