Trevor Squires
2005-Nov-10 21:38 UTC
[Rails-core] a small potential fix for a subset of the namespace pollution problem
Hi, I''ve been sitting on this for a while because I''m not 100% certain about it. One of the current namespace pollution issues occurs in vanilla rails (no plugins or engines) and it''s related to how ''foo/wibble'' gets resolved as a controller name. Currently if you have a controller called ::Wibble you cannot have another controller (perhaps in the components directory) called ::Foo::Wibble. This is because of the way Inflector.constantize works: def constantize(camel_cased_word) camel_cased_word.split("::").inject(Object) do |final_type, part| final_type = final_type.const_get(part) end end So if you do: Module Foo end Module Bar end Module Baz end "Foo::Bar::Baz".constantize will return ::Baz This is clearly wrong. I think the following fix is a good solution (works for me) but I don''t know enough about the guts of rails to know if there are dragons lurking there... def constantize(camel_cased_word) camel_cased_word.split("::").inject(Object) do |final_type, part| if final_type.const_defined?(part) final_type = final_type.const_get(part) else final_type = final_type.const_missing(part) end end end Comments would be appreciated. Regards, Trevor -- Trevor Squires http://somethinglearned.com
Nicholas Seckar
2005-Nov-10 22:58 UTC
[Rails-core] a small potential fix for a subset of the namespace pollution problem
Hi Trevor,> One of the current namespace pollution issues occurs in vanilla > rails (no plugins or engines) and it''s related to how ''foo/wibble'' > gets resolved as a controller name. > > Currently if you have a controller called ::Wibble you cannot have > another controller (perhaps in the components directory) > called ::Foo::Wibble.This seems to work for me. When does it give you issues?> > This is because of the way Inflector.constantize works: > > def constantize(camel_cased_word) > camel_cased_word.split("::").inject(Object) do |final_type, part| > final_type = final_type.const_get(part) > end > endI should point out that this is because the semantics of const_get are not the same as :: irb(main):001:0> module A irb(main):002:1> end irb(main):003:0> B = 10 irb(main):004:0> A.const_get :B => 10 irb(main):005:0> A::B NameError: uninitialized constant A::B from (irb):5 This difference frustrates us not because it exists, but because there seems to be no way to detect which case we are in from inside const_missing. The two cases to consider are module A; B; end And module A; end; A::B In the first, we should look in :: for B; in the second, we should not. Typically this is not an issue, since in the first case Ruby will find ::B and const_missing will never be called. However, Rails'' const_missing hook gets in the way here. If ::B does not yet exist, we require ''b.rb''; This solves the first case quite well, but in the second it results in the inconsistent behavior with regard to the rest of Ruby. (We could likewise load a/b.rb first and look for A::B. We would still have to fall back to looking for ::B though.) If we could detect the difference between the two cases we would be able to resolve this and behave in a logical fashion. However, I have not found a clear way to detect the two cases. The only procedure I have thought of is to check and see if ::B is defined in the const_missing hook. If it is, then we know A::B is being performed, or else ::B would have been provided. This is of little use however, since in many cases ::B will not exist. Any ideas?
Trevor Squires
2005-Nov-11 08:04 UTC
[Rails-core] a small potential fix for a subset of the namespace pollution problem
Hi, Thanks for looking at this Nicholas. My comments below: On 10-Nov-05, at 5:45 PM, Nicholas Seckar wrote:> Hi Trevor, > >> One of the current namespace pollution issues occurs in vanilla rails >> (no plugins or engines) and it''s related to how ''foo/wibble'' gets >> resolved as a controller name. >> >> Currently if you have a controller called ::Wibble you cannot have >> another controller (perhaps in the components directory) called >> ::Foo::Wibble. > > This seems to work for me. When does it give you issues?In app/controllers/wibble_controller.rb you have: class WibbleController < ApplicationController end In components/foo/wibble_controller.rb you can either have: module Foo class WibbleController < ApplicationController end end or class Foo::WibbleController < ApplicationController end In the console type: Controllers::ApplicationController "WibbleController".constantize "Foo::WibbleController".constantize on the second constantize it returns ::WibbleController There''s more below:> >> >> This is because of the way Inflector.constantize works: >> >> def constantize(camel_cased_word) >> camel_cased_word.split("::").inject(Object) do |final_type, part| >> final_type = final_type.const_get(part) >> end >> end > > I should point out that this is because the semantics of const_get are > not the same as :: > > irb(main):001:0> module A > irb(main):002:1> end > irb(main):003:0> B = 10 > irb(main):004:0> A.const_get :B > => 10 > irb(main):005:0> A::B > NameError: uninitialized constant A::B > from (irb):5 > > This difference frustrates us not because it exists, but because there > seems to be no way to detect which case we are in from inside > const_missing. > > The two cases to consider are > module A; B; end > And > module A; end; A::B > > In the first, we should look in :: for B; in the second, we should > not. Typically this is not an issue, since in the first case Ruby will > find ::B and const_missing will never be called. > > However, Rails'' const_missing hook gets in the way here. If ::B does > not yet exist, we require ''b.rb''; This solves the first case quite > well, but in the second it results in the inconsistent behavior with > regard to the rest of Ruby. (We could likewise load a/b.rb first and > look for A::B. We would still have to fall back to looking for ::B > though.) > > If we could detect the difference between the two cases we would be > able to resolve this and behave in a logical fashion. However, I have > not found a clear way to detect the two cases. > > The only procedure I have thought of is to check and see if ::B is > defined in the const_missing hook. If it is, then we know A::B is > being performed, or else ::B would have been provided. This is of > little use however, since in many cases ::B will not exist. > > Any ideas? >I''m so glad you asked. Indeed I *do* have ideas :-P ... note that I''m not calling them answers, just ideas. You could take the approach in const_missing that if self.instance_of? Module, the chances are any const you are looking for is in your namespace. I.e. if Foo::Bar is missing the const Baz, it''s relatively safe to assume that Baz *must* be defined in foo/bar/baz.rb - with *no* fallback to looking for ::Baz (baz.rb). That might seem limiting at first but it would only affect people referencing top-level, dynamically loaded classes outside of the defs in their module. I''m assuming might be a minute subset of rails users who would be forced to properly scope Baz with ''::''. It might seem a small price to pay for the benefit of knowing ''when to stop'' with const_missing. Note, there''s one exception that I''m aware of - when self is the CompiledTemplates module. In that case const_missing would look in the :: namespace for it''s missing consts. Carrying on with that approach you might consider that if Foo::Bar is *not* an instance_of? Module (and Foo was), you''d look for Baz first in foo/baz.rb followed by baz.rb But (man, there''s always a ''but'' isn''t there?), all this messing about completely falls to bits in the case where ::Baz is already a const but you want to auto-load the Baz in foo/bar/baz.rb because your const_missing won''t get called.. This is what my ''fix'' to Inflector.constantize is trying to avoid because it *does* know whether the constant being sought is in A::Module::Namespace. Everywhere else... the safest, sanest, bulletproof-est, one-true-solution is to explicitly ''require'' everything. :-P What that in mind I''ve been tinkering with an idea in my spare time. It only affects rails users that need module namespaces and in a way that''s relatively pain free. I like how it works but it *is* getting into the territory of rails becoming opinionated. First off, rails needs to *only* automatically load classes/modules into the root (::) namespace *and* only load files in the root of the various load paths. I.e. models/*, controllers/*, components/* but no deeper. Then, a convention is imposed where if you want automatic loading deeper than the root, say into a ''foo'' directory, there must be a foo.rb file beside it. In foo.rb you''d have: module Foo auto_depend :Bar auto_depend :Zippy end In a nutshell, auto_depend (not the greatest name) is a method added to class Module which uses the argument and the name of the current module to construct a call to Kernel.autoload such as "autoload :Bar, ''foo/bar''". The main benefit this has over const_missing is that the consts aren''t missing. Of course, the main drawback is that it''s a change in convention and it requires a small effort on behalf of the user (although not as much as straight ''require_dependency'' calls). Sorry for going on so long here. I''ve been finding the issue really interesting so it''s kind of hard not to blather... and you did ask if I had any ideas :-) Regards, Trevor -- Trevor Squires http://somethinglearned.com