Since Ruby 2.5 top-level constant lookup ("the bad") has been removed.
Since Rails 6 autoloading ("the ugly") is done using Zeitwerk, which fixes most edge cases.
In Ruby, classes and modules are called constants. This card explains how Ruby resolves the meaning of a constant.
The good
E. g. in the following example, Array
could mean either Foo::Array
or simply Array
:
class Foo
def list
Array.new
end
end
What Ruby does here is to see if the name Array
makes sense inside of Foo::
, and if that fails, resolves it to ::Array
(without a namespace).
The bad
This is relevant for old Ruby versions. Ruby 2.5+ removes top-level constant lookup which means that Array::String
will not resolve to String
and raise an error instead.
You might be surprised that these are all valid ways to reference Ruby's String
class:
String
Array::String
Array::Hash::String
When you see Array::String
, do not think "a class String
inside the Array
namespace". Rather think: "What does String
resolve to from the viewpoint of the Array
class?".
When you do this, Ruby will print a warning:
warning: toplevel constant String referenced by Array::String
However, that warning is usually lost in a sea of log messages. And here is where it gets ugly (see below).
The ugly
This part is a bit lengthy, but it allows you to debug strange bugs with Rails autoloading the wrong constants. Due to the constant lookup rules above, Rails sometimes guesses wrong and loads the wrong file. This happens when you heavily namespace your models. The mechanism how files are loaded differs in development and production, but the problem exist for both environment.
Development
During development, Rails unloads all classes after every request. This way code changes take effect immediately, without requiring you to restart the server. If you have been working with Rails for a while, you might even have forgotten that this is a feature of Rails. In the regular world of Ruby, classes don't refresh themselves automatically after a change.
Rails also autoloads classes on demand during development. This means every request begins with none of your models, controllers, loaded. Whenever Ruby encounters a constant name it doesn't know yet, it guesses the correct .rb
file and require
s the file. Again, this is a feature activated by Rails. If you write a regular Ruby script without Rails, you need to require
all the files you want to use. Classes don't magically load themselves from disk.
Production
All files are loaded in a deterministic order once the server boots.
Take this example with some constants referencing each other:
# app/models/contract/document/uploader.rb
class Contract::Document::Uploader < CarrierWave::Uploader::Base
...
end
# app/models/document/uploader.rb
class Document::Uploader < CarrierWave::Uploader::Base
...
end
# app/models/document.rb
class Document < ActiveRecord::Base
mount_uploader Document::Uploader
end
# app/models/contract.rb
class Contract < ActiveRecord::Base
mount_uploader Contract::Document::Uploader
end
In the example above it can happen that the Contract
class actually mounts the uploader Document::Uploader
instead of the requested Contract::Document::Uploader
.
-
In development this can happen randomly, as you never know in which order the files are autoloaded. If the autoload mechanism has already loaded
app/models/document/uploader.rb
, a constant lookup forContract::Document::Uploader
will never fail as it is actually a valid way to referenceDocument::Uploader
. The fileapp/models/contract/document/uploader.rb
is never autoloaded. -
In production you either have the issue or not. The order
app/models/document/uploader.rb > app/models/contract.rb > ...
will have unexpected references, whereas the orderapp/models/contract/document/uploader.rb > app/models/document/uploader.rb > app/models/contract.rb > ...
will reference the classes as expected.
Conclusion
- You can't fix it with
require
- Never make a class name for which an existing class name is a suffix. E.g. don't create a class
Api::User
when you also have a::User
. - A workaround when you have control over every involved class name:
- prefix the root level class with 'Generic' e.g.
GenericDocument
- then the names
Contract::Document
andDeal::Document
are available
- prefix the root level class with 'Generic' e.g.
- You can write
require_dependency
if the fix above is not possible - If you want to know more about constant lookup in ruby read "Everything you ever wanted to know about constant lookup in Ruby"