diff --git a/lib/zeitwerk/gem_inflector.rb b/lib/zeitwerk/gem_inflector.rb index 4cf359b..3d24fa4 100644 --- a/lib/zeitwerk/gem_inflector.rb +++ b/lib/zeitwerk/gem_inflector.rb @@ -9,8 +9,11 @@ def initialize(root_file) @version_file = File.join(root_dir, namespace, "version.rb") end - # @sig (String, String) -> String - def camelize(basename, abspath) + # See the rationale for the third optional argument in + # Zeitwerk::Inflector#camelize. + # + # @sig (String, String, String?) -> String + def camelize(basename, abspath, _namespace_name = nil) abspath == @version_file ? "VERSION" : super end end diff --git a/lib/zeitwerk/gem_loader.rb b/lib/zeitwerk/gem_loader.rb index 0067f1a..4b51d22 100644 --- a/lib/zeitwerk/gem_loader.rb +++ b/lib/zeitwerk/gem_loader.rb @@ -19,6 +19,8 @@ def self.__new(root_file, namespace:, warn_on_extra_files:) def initialize(root_file, namespace:, warn_on_extra_files:) super() + @namespace = namespace + @tag = File.basename(root_file, ".rb") @tag = real_mod_name(namespace) + "-" + @tag unless namespace.equal?(Object) @@ -47,7 +49,7 @@ def warn_on_extra_files next if abspath == expected_namespace_dir basename_without_ext = basename.delete_suffix(".rb") - cname = inflector.camelize(basename_without_ext, abspath).to_sym + cname = cname_for(basename_without_ext, abspath, real_mod_name(@namespace)) ftype = dir?(abspath) ? "directory" : "file" warn(<<~EOS) diff --git a/lib/zeitwerk/inflector.rb b/lib/zeitwerk/inflector.rb index 8cac2df..393dbf4 100644 --- a/lib/zeitwerk/inflector.rb +++ b/lib/zeitwerk/inflector.rb @@ -5,14 +5,20 @@ class Inflector # Very basic snake case -> camel case conversion. # # inflector = Zeitwerk::Inflector.new - # inflector.camelize("post", ...) # => "Post" - # inflector.camelize("users_controller", ...) # => "UsersController" - # inflector.camelize("api", ...) # => "Api" + # inflector.camelize("post", abspath, "Object") # => "Post" + # inflector.camelize("users_controller", abspath, "Admin") # => "UsersController" + # inflector.camelize("api", abspath, "Object") # => "Api" # # Takes into account hard-coded mappings configured with `inflect`. # - # @sig (String, String) -> String - def camelize(basename, _abspath) + # The third argument was added in 2.6.14. It is optional because existing + # subclasses using the previous signature with two arguments may be calling + # super(basename, abspath) or just super. We need these to work as they are. + # At the same time, super for new subclasses defining the new signature is + # going to work as well. + # + # @sig (String, String, String?) -> String + def camelize(basename, _abspath, _namespace_name = nil) overrides[basename] || basename.split('_').each(&:capitalize!).join end @@ -24,9 +30,9 @@ def camelize(basename, _abspath) # "mysql_adapter" => "MySQLAdapter" # ) # - # inflector.camelize("html_parser", abspath) # => "HTMLParser" - # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter" - # inflector.camelize("users_controller", abspath) # => "UsersController" + # inflector.camelize("html_parser", abspath, "MyGem") # => "HTMLParser" + # inflector.camelize("users_controller", abspath, "Admin") # => "UsersController" + # inflector.camelize("mysql_adapter", abspath, "Object") # => "MySQLAdapter" # # @sig (Hash[String, String]) -> void def inflect(inflections) diff --git a/lib/zeitwerk/loader.rb b/lib/zeitwerk/loader.rb index 330bf6a..d64cdbb 100644 --- a/lib/zeitwerk/loader.rb +++ b/lib/zeitwerk/loader.rb @@ -265,17 +265,18 @@ def cpath_expected_at(path) return unless root_namespace - if paths.empty? - real_mod_name(root_namespace) - else - cnames = paths.reverse_each.map { |b, a| cname_for(b, a) } + root_namespace_name = real_mod_name(root_namespace) + return root_namespace_name if paths.empty? - if root_namespace == Object - cnames.join("::") - else - "#{real_mod_name(root_namespace)}::#{cnames.join("::")}" - end + basename, abspath = paths.pop + cpath = cpath(root_namespace, cname_for(basename, abspath, root_namespace_name)) + cpath = cpath.dup if cpath.frozen? # Symbol#name returns a frozen string. + + paths.reverse_each do |b, a| + cpath << "::#{cname_for(b, a, cpath)}" end + + cpath end # Says if the given constant path would be unloaded on reload. This @@ -411,12 +412,12 @@ def all_dirs ls(dir) do |basename, abspath| if ruby?(basename) basename.delete_suffix!(".rb") - autoload_file(parent, cname_for(basename, abspath), abspath) + autoload_file(parent, cname_for(basename, abspath, real_mod_name(parent)), abspath) else if collapse?(abspath) define_autoloads_for_dir(abspath, parent) else - autoload_subdir(parent, cname_for(basename, abspath), abspath) + autoload_subdir(parent, cname_for(basename, abspath, real_mod_name(parent)), abspath) end end end diff --git a/lib/zeitwerk/loader/eager_load.rb b/lib/zeitwerk/loader/eager_load.rb index 6f864f4..a814d81 100644 --- a/lib/zeitwerk/loader/eager_load.rb +++ b/lib/zeitwerk/loader/eager_load.rb @@ -36,7 +36,7 @@ def eager_load_dir(path) raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath) - cnames = [] + paths = [] root_namespace = nil walk_up(abspath) do |dir| @@ -49,7 +49,7 @@ def eager_load_dir(path) return if hidden?(basename) unless collapse?(dir) - cnames << inflector.camelize(basename, dir).to_sym + paths << [basename, dir] end end @@ -58,9 +58,10 @@ def eager_load_dir(path) return if @eager_loaded namespace = root_namespace - cnames.reverse_each do |cname| + paths.reverse_each do |basename, abspath| # Can happen if there are no Ruby files. This is not an error condition, # the directory is actually managed. Could have Ruby files later. + cname = cname_for(basename, abspath, real_mod_name(namespace)) return unless cdef?(namespace, cname) namespace = cget(namespace, cname) end diff --git a/lib/zeitwerk/loader/helpers.rb b/lib/zeitwerk/loader/helpers.rb index 52c0cf5..c2167fa 100644 --- a/lib/zeitwerk/loader/helpers.rb +++ b/lib/zeitwerk/loader/helpers.rb @@ -146,8 +146,12 @@ module Zeitwerk::Loader::Helpers # @raise [Zeitwerk::NameError] # @sig (String, String) -> Symbol - private def cname_for(basename, abspath) - cname = inflector.camelize(basename, abspath) + private def cname_for(basename, abspath, namespace) + cname = if inflector.method(:camelize).arity == 3 + inflector.camelize(basename, abspath, namespace) + else + inflector.camelize(basename, abspath) + end unless cname.is_a?(String) raise TypeError, "#{inflector.class}#camelize must return a String, received #{cname.inspect}" diff --git a/lib/zeitwerk/null_inflector.rb b/lib/zeitwerk/null_inflector.rb index 195223b..1c7b22d 100644 --- a/lib/zeitwerk/null_inflector.rb +++ b/lib/zeitwerk/null_inflector.rb @@ -1,5 +1,9 @@ class Zeitwerk::NullInflector - def camelize(basename, _abspath) + # See the rationale for the third optional argument in + # Zeitwerk::Inflector#camelize. + # + # @sig (String, String, String?) -> String + def camelize(basename, _abspath, _namespace = nil) basename end end