2

I'm working on upgrading a large legacy Rails application from 5.2 to 6.0. In the process of fixing the autoload paths, I've run into an issue where Rails/Zeitwerk seem to be breaking their own rules about how the names of constants are defined in relation to their filenames. I can't share actual code from this application, but the situation is essentially this:

In config/application.rb:

config.autoload_paths << "#{config.root}/app/models/coupons"

In app/models/coupons/burgerfrenchfry_coupon.rb:

class BurgerfrenchfryCoupon << ApplicationRecord
end

When another class in the application references the BurgerfrenchfryCoupon class, a NameError is thrown with BurgerFrenchfryCoupon as a suggested classname (that class does not exist in the application). When I require the app/models/coupons/burgerfrenchfry_coupon path directly in the file referencing BurgerfrenchfryCoupon I get a Zeitwerk error: Zeitwerk::NameError: expected file /redacted/app/models/coupons/burgerfrenchfry_coupon.rb to define constant BurgerFrenchfryCoupon, but didn't

I've done a thorough search of the application to find anywhere where the expectation could have been customized and I've come up with nothing. Does anyone have any ideas about the follow:

  1. Why this is happening?
  2. Where or how an override on the constant name expectation might have been made?
  3. How I can configure Rails to recognize that this constant should be defined in this file without changing all of the references to it in the application to BurgerFrenchfryCoupon?
RickyTomatoes
  • 722
  • 6
  • 11
  • Does your class really end with `class`? – Schwern Jul 19 '22 at 21:04
  • Sorry, I can't replicate your issue with a fresh Rails 7 install. It's possible you have something quirky in your configuration or something is broken in Rails 6.0. You should try the latest 6.x instead which is currently 6.1.6.1. – Schwern Jul 19 '22 at 21:08
  • Do you have any [custom inflections](https://guides.rubyonrails.org/v6.0/autoloading_and_reloading_constants.html#customizing-inflections)? – Schwern Jul 19 '22 at 21:11
  • What does `"burgerfrenchfry_coupon".camelize` say? – Schwern Jul 19 '22 at 21:15
  • 1
    Also `Zeitwerk::Inflector.new.camelize("burgerfrenchfry_coupon", "/app/model/coupons")` – Schwern Jul 19 '22 at 21:17
  • Regarding the last comment, by default the Rails integration sets the loaders inflectors to just delegate to Active Support inflections (see https://github.com/rails/rails/blob/b5a758db1ba16233f53c76dea588c0e3853c7c42/railties/lib/rails/autoloaders/inflector.rb#L13). `Zeitwerk::Inflector.new` shouldn't matter in this common situation. You need to check `"burgerfrenchfry_coupon".camelize` as @Schwern said. – Xavier Noria Jul 20 '22 at 09:01
  • @XavierNoria Sure, that's the default, but this is a "large legacy Rails application" and who knows how it's configured? [Rails can be configured to use the Zeitwerk::Inflector](https://guides.rubyonrails.org/v6.0/autoloading_and_reloading_constants.html#customizing-inflections). It's worth a look. – Schwern Jul 20 '22 at 09:41
  • My point is, telling the OP to check `Zeitwerk::Inflector.new.camelize(...)` is pointless. First, it is unlikely that the inflector is not the default one, so at least you ask first. Second, even if ad-hoc, you don't know if it acts like a vanilla brand new inflector, so the test is pointless too without further information. – Xavier Noria Jul 20 '22 at 12:30
  • 1
    @Schwern if you don't want to assume AS what you really want to ask for is the output of `Rails.autoloaders.main.inflector.camelize(...)`. See what I mean? – Xavier Noria Jul 20 '22 at 12:34
  • Thanks all, this was indeed a problem with a custom active support inflection. In Rails 5.2 the class name was used to infer the file name so it essentially bypassed the inflection, but now that the class name is inferred from the file name the inflection came into play. – RickyTomatoes Jul 20 '22 at 14:49
  • @XavierNoria When investigating a problem, if you spend more time arguing about whether to try something than it takes to try it, just try it. It might be unlikely, but it's also just a line in console: check your assumptions, especially if it's cheap, just to be sure. As for `Rails.autoloaders.main.inflector.camelize(...)`, we already know what it will return, and that doesn't say *why* it's behaving that way. Though it would answer if it's the autoloader inflector or something even more wacky, so try it anyway. – Schwern Jul 20 '22 at 19:48

2 Answers2

2

The problem is that, for some reason, the autoloader's inflector is configured to camelize "burgerfrenchfry_coupon" as "BurgerFrenchfryCoupon". If using Active Support inflections (the default), there's some custom inflection rule somewhere that affects this.

You can fix this particular one without affecting the rest of the application by overriding this way:

# config/initializers/autoloading.rb

inflector = Rails.autoloaders.main.inflector
inflector.inflect("burgerfrenchfry_coupon" => "BurgerfrenchfryCoupon")

That sets a special mapping in the autoloader's inflector that ignores everything else.

Xavier Noria
  • 1,640
  • 1
  • 8
  • 10
1

The answer here ended up being a custom inflection that had been added to activesupport.

ActiveSupport::Inflector.inflections do |inflect|
  inflect.acronym 'BurgerFrenchfry'
end

As this inflection was necessary for other parts of the application, to fix the Zeitwerk error I added the file config/initializers/zeitwerk.rb with the following content:

Rails.autoloaders.each do |autoloader|
  autoloader.inflector.inflect(
    "burgerfrenchfry_coupon" => "BurgerfrenchfryCoupon"
  )
end

Which overrides the inflection for this one file

RickyTomatoes
  • 722
  • 6
  • 11