Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autoloader confuses constants #14844

Closed
apotonick opened this issue Apr 23, 2014 · 13 comments
Closed

Autoloader confuses constants #14844

apotonick opened this issue Apr 23, 2014 · 13 comments

Comments

@apotonick
Copy link
Contributor

The autoloader has a problem with pre-loaded constants that are later found in namespaces (completely unrelated).

  1. This file is simulating a gem.

     # lib/form.rb
    module Form
    end
  2. Let's assume the file lib/form.rb is required in an initializer (NOT autoloaded). This usually happens with gem files, too. The constant ::Form is now defined.

  3. Next, I create a model.

     # app/models/comment.rb
    class Comment < ActiveRecord::Base # or whatever...
    end
  4. As a last step, I add a Form class into the Comment namespace.

     # app/models/comment/form.rb
    class Comment::Form
      def self.whatever
      end
    end

Provoking the bug is as simple as calling

Comment::Form.whatever

in a controller, console, etc.

It throws

(undefined method `whatever' for Form:Module):

Apparently, the autoloader references the top-level ::Form module where should be the namespaced Comment::Form constant.

It looks as if comment/form.rb isn't even loaded.

This bug exists in Rails 3.1, 3.2, 4.0 and 4.1.

@apotonick
Copy link
Contributor Author

The problem is that #const_missing is not invoked for Comment::Form as both Comment is already autoloaded and ::Form is already required.

This is why I get the warning

warning: toplevel constant Form referenced by Comment::Form

I can't think of any quick work-around for that.

@fxn
Copy link
Member

fxn commented Apr 23, 2014

Exactly.

Comment gets autoloaded, Form is defined at boot time, therefore Ruby is able to resolve Comment::Form the same way it is able to resolve Comment::String, they are constants that belong to an ancestor of Comment (Object). Here AS has no chance to intervene.

A thing you can do there is for example

require_dependency 'comment/form'

at the bottom of comment.rb. That way comment/form.rb is going to be evaluated, AS treats it as it had been autoloaded, and Ruby will find Form within Comment right at the start of the lookup, resolving to what you want.

Alternatively you can loop and do that for all files in the comment directory or whatever fits better the particular case of that application. But that would be the basic idea of a workaround.

@fxn
Copy link
Member

fxn commented Apr 23, 2014

Closing by now, since it is a consequence of how Ruby works. Please feel free to reopen or comment if there's anything else to add.

@fxn fxn closed this as completed Apr 23, 2014
@apotonick
Copy link
Contributor Author

Thanks @fxn - I figured it's the way of the autoloader's "passive" technique of loading constants (i.e. waiting for a constant to be missing.

As a workaround, I do

class Comment < ..
  autoload :Form, 'comment/form'
end

I am actually thinking about an alternative approach to an autoloader, a more explicit way where we use Ruby's autoload and automatically compile a list of constants by traversing the autoload directories. I'll let you know how far I get there. Thanks again, @fxn for that prompt response, it is highly appreciated ❤️

@fxn
Copy link
Member

fxn commented Apr 23, 2014

Good!

I explored the autoload approach, but I believe it is not possible. There are a number of reasons, it only accepts constant names, not constant paths, and that interferes with namespaces, it uses require there is no way to tell it to use load. In addition, that require is an internal one different to Kernel#require. Also, there is no API to remove an autoload, which would be needed if you remove a file... You see, I believe it is just not possible.

@apotonick
Copy link
Contributor Author

Ok- does that mean using autoload does not reload classes in development mode?

Yeah, it would be awesome if autoload accepted paths, like

autoload "Comment::Form", 'comment/form'

@fxn
Copy link
Member

fxn commented Apr 23, 2014

Exactly, this feature is not based on autoload, it is based only on const_missing.

That line wouldn't work, autoload only accepts a constant name, "Comment::Form" is a constant path (raises).

@apotonick
Copy link
Contributor Author

Yeah, I was just thinking (dreaming?) aloud 😁

Is there a way to hook into autoload, in terms of "let me know when you autoloaded constant XYZ"?

@fxn
Copy link
Member

fxn commented Apr 23, 2014

Nope, it is designed to work transparently and triggers no event.

@apotonick
Copy link
Contributor Author

Thanks for pointing me to #require_dependency, this kinda does the trick for me right now. Does it tell the autoloader to load and register the new constant(s) and make then re-loadable?

@fxn
Copy link
Member

fxn commented Apr 24, 2014

Exactly. We need a guide about that topic :(.

@apotonick
Copy link
Contributor Author

A guide as in guides.rubyonrails.org/ or would a blog post be sufficient? Where are the source for guides?

@fxn
Copy link
Member

fxn commented Apr 24, 2014

A guide in guides.rubyonrails.org: constant autoloading is basically undocumented but it is a key feature users need to understand.

The source code for guides lives in the guides directory under the project root. If I find the time that's something I'd like to work on after RailsConf.

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

No branches or pull requests

3 participants