Skip to content

Instantly share code, notes, and snippets.

@urbanautomaton
Last active December 19, 2015 22:58
Show Gist options
  • Save urbanautomaton/6031355 to your computer and use it in GitHub Desktop.
Save urbanautomaton/6031355 to your computer and use it in GitHub Desktop.
Fuck Rails const autoloading
# lib/a.rb
module A
end
# lib/b.rb
class B
end
# $ rails console
# > A.const_missing("B")
# => B
# > A.const_missing("B")
# NameError: uninitialized constant A::B
@urbanautomaton
Copy link
Author

It turns out that this more-or-less makes sense, if you take it on faith that reimplementing the entirety of Ruby's loading system in the service of questionable convenience is a sensible thing to attempt in the first place.

On the first call to A.const_missing("B"), no constants B in any nesting are loaded. But that doesn't mean they don't exist, because of autoloading. Therefore A.const_missing("B") can't simply say "oh, there's no A::B" and give up, because a reference to B within the scope of A might have been to a not-yet-loaded top-level B.

So it starts climbing the list of A's parents. It next calls Object.const_missing("B"), which finds B and says "here, you were looking for this."

On the second call to A.const_missing("B"), there's still no A::B. But this time the top-level B is already loaded, and therefore can't have caused A.const_missing("B") to be invoked, because that reference would have been resolved normally, without recourse to const_missing.

Therefore the reference to B within A can only have meant A::B, which does not exist, and therefore a NameError is raised.

Nesting

As a side note, this highlights a weakness of Rails' re-implementation of constant loading. As Ruby does not pass nesting information along with const_missing calls, Rails' const_missing implementation is unable to distinguish between the following:

module A
  module B
    C
  end
end

module A::B
  C
end

It therefore makes the assumption that the former nesting is the case. This means that the reference to C in the second example will potentially (and incorrectly) resolve to a constant A::C.

So what?

The reason this was relevant to me is that it bollockses up String#constantize in Activesupport versions before 4.0, because it incorrectly interpreted a response from A.const_missing("B") to mean that a constant A::B had been found. This has now been fixed, so you don't get results like this:

> "A::B".constantize
=> B
> "A::B".constantize
=> NameError: uninitialized constant A::B

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment