Reputation: 7265
This is the strangest bug I've seen in my years of programming. Please ignore the strange idioms I am using below as I am using trailblazer, a programming framework for Rails:
The code below
let (:app) { App::Create.(app: {name: "Half-Life", steam_id: "70"}).model }
it 'gets app details from Steam' do
VCR.use_cassette('app/get') do
p app.id
app_id = app.id
app = App::Get.(id: app_id).model
end
expect(app.app_type).to eq('game')
end
works as expected.
The code below
let (:app) { App::Create.(app: {name: "Half-Life", steam_id: "70"}).model }
it 'gets app details from Steam' do
VCR.use_cassette('app/get') do
p app.id
app = App::Get.(id: app.id).model
end
expect(app.app_type).to eq('game')
end
throws an undefined method `id' for nil:NilClass in the line
app = App::Get.(id: app.id).model
However the line
p app.id
which is just before the offending line displays the correct id.
What could possibly be going on?
Upvotes: 0
Views: 364
Reputation: 60143
This line:
let (:app) { App::Create.(app: {name: "Half-Life", steam_id: "70"}).model }
creates a method called app
that returns an App
when invoked. (It memoizes the result, so it will always return the same App
after the initial invocation.)
This line:
app = App::Get.(id: app.id).model
creates a new local variable called app
that is ambiguous with the method app
. According to the Ruby docs on assignment:
In Ruby local variable names and method names are nearly identical. If you have not assigned to one of these ambiguous names ruby will assume you wish to call a method. Once you have assigned to the name ruby will assume you wish to reference a local variable.
The local variable is created when the parser encounters the assignment, not when the assignment occurs:
So at the moment when you try to assign to app
, a new local variable is created. When the right-hand side of the assignment is evaluated, app.id
tries to call id
on the local variable app
(which so far is nil
).
Here's the simplest code I can think of that reproduces the issue:
def a
"Hello"
end
p a.upcase # "HELLO"
a = a.upcase # undefined method `upcase' for nil:NilClass (NoMethodError)
There are a couple ways to fix your code. I think the best solution by far is:
app2 = App::Get.(id: app.id).model
The name app
was already used by a method... don't hide it by creating a local variable with the same name.
You could also do this:
app = App::Get.(id: app().id).model # the parens disambiguate
or the solution you already have:
app_id = app.id # capture the ID before the local variable gets created
app = App::Get.(id: app_id).model
But I think both of those are worse than simply not creating the naming collision in the first place.
Upvotes: 1