简体   繁体   中英

Bug in rails autoload_paths?

I am encountering a strange bug in my code. I have a rails application with the following two files in the lib:

lib/module_one/module_two/class_one.rb

module ModuleOne
  module Moduletwo
    class ClassOne
      class << self
        def test
          puts 'Class one'
          ClassTwo.test
        end
      end
    end
  end
end

and

lib/module_one/module_two/class_two.rb

module ModuleOne
  module ModuleTwo
    class ClassTwo
      def self.test
        puts 'Class two'
      end
    end
  end
end

Now my problem is, that when I go into the console and write:

ModuleOne::ModuleTwo::ClassOne.test

it throws the following: NameError: uninitialized constant ClassTwo

The strange thing is, that the problem seems to be connected to the use of class << self instead of self.method . If I change the class_one.rb file like this it works!:

module ModuleOne
  module ModuleTwo
    class ClassOne
      def self.test
        puts 'Class one'
        ClassTwo.test
      end
    end
  end
end

Im loading the files in application.rb like this:

config.autoload_paths += %W(#{config.root}/lib)

Is this a bug in rails, or is it just me getting something all wrong?

Im using rails 3.1.3 btw

Constant lookup

So first off, the basic process for constant resolution is that ruby first searches the lexical scope of the receiver (the class or module enclosing the reference) for ClassTwo and when it can't find it, it goes up a level ( Module.nesting returns this search path) and so on. In your case that means looking for ModuleOne::ModuleTwo::ClassOne:ClassTwo , then ModuleOne::ModuleTwo::ClassTwo , then ModuleOne::ClassTwo and so on.

If that fails the ruby looks in the inheritance hierarchy of the enclosing class/module (eg has the superclass of ClassOne defined something. The ancestors method on module returns this search path. Lastly, the top level constants are searched.

Rails autoloading

Back to rails's magic loading. Here a const_missing hook is added by rails that is called when ruby can't find the class, which basically tries to replicate this search logic, seeing at each step whether a file could have been loaded which would contain the missing constant.

Ideally ruby would pass the search path (ie the nesting) to search through, but unfortunately it doesn't: when you reference ClassTwo , const_missing gets called with just 'ClassTwo'.

Rails guesses the nesting by prepending that with the name of the class on which const_missing is being called (ie the class enclosing the access to the constant). For example, in your second example it ends up with ModuleOne::ModuleTwo::ClassOne::ClassTwo . You can see this easily enough by defining const_missing to log what it's called with

class Object
  def self.const_missing missing_name
    puts "qualified name is #{self.name}::#{missing_name}"
    super
  end
end

Rails then strips off 'ClassOne' and tries ModuleOne::ModuleTwo::ClassTwo and so on up the chain.

So why does class << self make a difference? If you repeat your first case with the const_missing logging you'd see that the logged qualified name is now just ::ClassTwo . const_missing is now being called on ClassOne's metaclass, and because class << self hasn't been assigned to a constant, it has no name and thus rails' attempt to fudge the nesting doesn't work.

This opens the door to a horrible workaround:

module ModuleOne
  module ModuleTwo
    class ClassOne
      class << self
        def test
          puts 'Class one'
          ClassTwo.test
        end
      end
      FOO = class << self; self; end
    end
  end
end

Because the class that const_missing gets called on now has a name (ModuleOne::ModuleTwo::ClassOne::FOO) rails' workaround now works.

Dave's workaround works I think because const_missing gets called on ModuleOne::ModuleTwo::ClassOne rather than the anonymous eigenclass/metaclass.

The real fix would be for ruby to pass const_missing a nesting. There is a bug logged against ruby to this effect although it has been open for a long time. So yes, this could be considered a bug in the magic loading stuff (there are other edge cases) but the underlying reason is a weakness in the ruby api that forces the use of brittle workarounds.

(Only a partial answer, but need formatting.)

It's because of how class << self works.

For example, if you change it to:

class << self
  def test
    self::ClassTwo.test
  end
end

it works fine.


Edit; too long for reasonable comment.

I'm poking around a bit... On an intuitive level it makes sense to me, I'm just not sure why yet. Don't know if I knew a real reason once, or if I'm just making it up.

I'm not sure why self seems to refer to the module, though; the "Programming Ruby 1.9" book doesn't go in to enough depth on the class << semantics. I'll tweet something and refer to this question and someone smarter will create a real answer.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM