Rails Auto Loading Magic Gone Wrong!
Tags: rails, ruby, bugs
Published: 2019-07-16 21:27:13

TL;DR, use explicit module namespacing or avoid complex namespacing as often as possible when inheriting classes. 

Class and Module constants depend on the order of [autoload-paths lookup](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoload-paths) (a Ruby on Rails feature), more on that below. Because of this, class discoverability can vary depending on the name and location of an inheriting class. When multiple classes share the same name, as is the case in our Rails app, this subtlety can impact implicit class inheritance. 

Ruby on Rails has [two main algorithms](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#resolution-algorithm-for-relative-constants) for constant lookup in its `autoload_paths` (by default all subdirectories of `app/` in the application and engines present at boot time, and `app/*/concern`).

The values of `autoload_paths` can be inspected with the following command:

```bash
$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'
.../app/assets
.../app/controllers
.../app/helpers
.../app/mailers
.../app/models
.../app/resources # This is loaded before models/ 
.../app/controllers/concerns
.../app/models/concerns
.../test/mailers/previews
```

In my attempt to clean up some of our code, I unwittingly created a difficult bug to spot. Here is some example code to see the issue in action:

```ruby
# app/models/campaign.rb 
# This class is completely irrelevant to our code but has the same class name as the other campaign class.
class Campaign; end 

# app/resources/homepage/apple.rb
module Resources
  module Homepage
    class Apple < Campaign
      campaign_method!  # This fails because apple.rb is loaded before resources/homepage/campaign.rb 
    end
  end
end

# app/resources/homepage/campaign.rb
module Resources
  module Homepage
    class Campaign
      def self.campaign_method!
        "I should be callable"
      end
    end
  end
end

# app/resources/homepage/yolo.rb
module Resources
  module Homepage
    class Yolo < Resources::Homepage::Campaign
       campaign_method!  # This works because the parent class is unambiguous
    end
  end
end

# app/resources/homepage/zolo.rb
module Resources
  module Homepage
    class Zolo < Campaign
       campaign_method!  # This works by luck because Zolo is loaded alphabetically after resources/homepage/campaign.rb
    end
  end
end
```

I was able to debug this issue using `rails console`
```shell
pry(main)> Resources::Homepage::Yolo # returns a constant
=> Resources::Homepage::Yolo
pry(main)> Resources::Homepage::Apple # returns an error message
NameError: undefined local variable or method `campaign_method!' for Resources::Homepage::Apple:Class`
```

After changing `class Apple < Campaign` to `class Apple < Resources::Homepage::Campaign` and reloading the console, the code loads correctly. 

Explicitly loading the required file also works, which is what Rails AutoLoader (usually) does correctly for you. The Rails Guides discourages using `require` or `require_relative` for autoloaded files. Instead, use `require_dependency`   
```ruby
# resources/homepage/apple.rb
require_dependency 'resources/homepage/campaign' 
module Resources
  module Homepage
    class Apple < Campaign
      campaign_method!  # this works and loads campaign's class method
    end
  end
end
```

Hopefully, Ruby on Rails autoloading is clearer now. You can always read more and improve Ruby on Rails [documentation here](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html). If you have any questions or found errors, please comment here or contact me on [twitter](https://twitter.com/shushugah)!
In Rails 5, autoloading is [disabled by default](https://blog.bigbinary.com/2016/08/29/rails-5-disables-autoloading-after-booting-the-app-in-production.html) in production.