fxn/zeitwerk

Class that is prepended or replaced after zeitwerk setup triggers unnecessary NameError

martinemde opened this issue · 2 comments

I just debugged an error. Here's the simplest patch to a basic rails app that will cause the bug.

https://gist.github.com/martinemde/edce881f35f74562ee34be191e5c3ad6

The easiest workaround is to push the patch to Net::HTTP into config/initializers/net_http_purge.rb. This causes it to load explicitly, bypassing zeitwerk.

Explained briefly, the problem is that anything that changes a class ancestry after zeitwork is setup will cause a NameError on autoload. This includes redefining the constant and prepending a module onto the class (as far as I can tell).

In the example test_helper.rb, require "webmock" replaces Net::HTTP with it's own replacement Net::HTTP = Class.new(Net::HTTP).
At this point, zeitwerk has already recorded an autoload for "/abspath/lib/net/http/purge.rb" with a parent of Net::HTTP before webmock was loaded.
At this point, the autoload of Net::HTTP::Purge cause expected file /lib/net/http/purge.rb to define constant Net::HTTP::Purge, but didn't (Zeitwerk::NameError)

Net::HTTP.const_defined?(:Purge, false) returns true, but parent.const_defined?(:Purge, false) returns false. I can see that this happens because the parent is evaluated and saved when zeitwerk is setup.

I attempted a solution, incomplete but tests passing that does not store the parent in autoloads but stores an array of [parent, cname] pairs, recursively. Then using cget when the autoload callback is called for the entire array of parent modules, we can load the parent at autoload time to see if, in fact, the autoload worked. My version does not solve the problem. I think this is because I'm actually evaluating the parent along the way such that it just ends up stored again as the class instead of persisting it as an array. This is my first look at zeitwerk so I'm not familiar enough to figure it out right now.

fxn commented

This application is reopening a 3rd-party namespace, wants to autoload and reload from it, but it changes the module object stored in the 3rd-party constant on the fly. Too much!

This is not a bug, it is an unsupported situation. Autoloading and reloading from 3rd-party namespaces is supported, but you just can't change the namespace on the fly. Ruby is super flexible, Zeitwerk is somewhat flexible where possible, but puts constraints around what projects managed by Zeitwerk can do, to make autoloading solvable.

If you have that need, the file cannot be managed by the loader.

Makes sense. Thanks for reading this and replying! I agree. It's probably a pathological case because of how many things like to alter Net::HTTP.