Open
Conversation
Bumps [zeitwerk](https://github.com/fxn/zeitwerk) from 2.7.4 to 2.7.5. - [Changelog](https://github.com/fxn/zeitwerk/blob/main/CHANGELOG.md) - [Commits](fxn/zeitwerk@v2.7.4...v2.7.5) --- updated-dependencies: - dependency-name: zeitwerk dependency-version: 2.7.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Contributor
4 similar comments
Contributor
Contributor
Contributor
Contributor
Contributor
gem compare zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT date:
2.7.4: 2025-12-16 00:00:00 UTC
2.7.5: 1980-01-02 00:00:00 UTC
DIFFERENT rubygems_version:
2.7.4: 3.4.19
2.7.5: 4.0.3
DIFFERENT version:
2.7.4: 2.7.4
2.7.5: 2.7.5
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb +165/-0
* Changed:
README.md +19/-0
lib/zeitwerk/gem_loader.rb +2/-2
lib/zeitwerk/loader.rb +43/-48
lib/zeitwerk/loader/callbacks.rb +3/-3
lib/zeitwerk/loader/config.rb +9/-9
lib/zeitwerk/loader/eager_load.rb +26/-28
lib/zeitwerk/loader/helpers.rb +1/-100
lib/zeitwerk/real_mod_name.rb +1/-1
lib/zeitwerk/registry.rb +26/-0
lib/zeitwerk/registry/loaders.rb +2/-2
lib/zeitwerk/version.rb +1/-1 |
3 similar comments
Contributor
gem compare zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT date:
2.7.4: 2025-12-16 00:00:00 UTC
2.7.5: 1980-01-02 00:00:00 UTC
DIFFERENT rubygems_version:
2.7.4: 3.4.19
2.7.5: 4.0.3
DIFFERENT version:
2.7.4: 2.7.4
2.7.5: 2.7.5
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb +165/-0
* Changed:
README.md +19/-0
lib/zeitwerk/gem_loader.rb +2/-2
lib/zeitwerk/loader.rb +43/-48
lib/zeitwerk/loader/callbacks.rb +3/-3
lib/zeitwerk/loader/config.rb +9/-9
lib/zeitwerk/loader/eager_load.rb +26/-28
lib/zeitwerk/loader/helpers.rb +1/-100
lib/zeitwerk/real_mod_name.rb +1/-1
lib/zeitwerk/registry.rb +26/-0
lib/zeitwerk/registry/loaders.rb +2/-2
lib/zeitwerk/version.rb +1/-1 |
Contributor
gem compare zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT date:
2.7.4: 2025-12-16 00:00:00 UTC
2.7.5: 1980-01-02 00:00:00 UTC
DIFFERENT rubygems_version:
2.7.4: 3.4.19
2.7.5: 4.0.3
DIFFERENT version:
2.7.4: 2.7.4
2.7.5: 2.7.5
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb +165/-0
* Changed:
README.md +19/-0
lib/zeitwerk/gem_loader.rb +2/-2
lib/zeitwerk/loader.rb +43/-48
lib/zeitwerk/loader/callbacks.rb +3/-3
lib/zeitwerk/loader/config.rb +9/-9
lib/zeitwerk/loader/eager_load.rb +26/-28
lib/zeitwerk/loader/helpers.rb +1/-100
lib/zeitwerk/real_mod_name.rb +1/-1
lib/zeitwerk/registry.rb +26/-0
lib/zeitwerk/registry/loaders.rb +2/-2
lib/zeitwerk/version.rb +1/-1 |
Contributor
gem compare zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT date:
2.7.4: 2025-12-16 00:00:00 UTC
2.7.5: 1980-01-02 00:00:00 UTC
DIFFERENT rubygems_version:
2.7.4: 3.4.19
2.7.5: 4.0.3
DIFFERENT version:
2.7.4: 2.7.4
2.7.5: 2.7.5
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb +165/-0
* Changed:
README.md +19/-0
lib/zeitwerk/gem_loader.rb +2/-2
lib/zeitwerk/loader.rb +43/-48
lib/zeitwerk/loader/callbacks.rb +3/-3
lib/zeitwerk/loader/config.rb +9/-9
lib/zeitwerk/loader/eager_load.rb +26/-28
lib/zeitwerk/loader/helpers.rb +1/-100
lib/zeitwerk/real_mod_name.rb +1/-1
lib/zeitwerk/registry.rb +26/-0
lib/zeitwerk/registry/loaders.rb +2/-2
lib/zeitwerk/version.rb +1/-1 |
Contributor
gem compare --diff zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb
--- /tmp/20260220-408-xtugdb 2026-02-20 03:33:17.222227403 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/loader/file_system.rb 2026-02-20 03:33:17.219227414 +0000
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+# This private class encapsulates interactions with the file system.
+#
+# It is used to list directories and check file types, and it encodes the
+# conventions documented in the README.
+#
+# @private
+class Zeitwerk::Loader::FileSystem # :nodoc:
+ #: (Zeitwerk::Loader) -> void
+ def initialize(loader)
+ @loader = loader
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def ls(dir)
+ children = relevant_dir_entries(dir)
+
+ # The order in which a directory is listed depends on the file system.
+ #
+ # Since client code may run on different platforms, it seems convenient to
+ # sort directory entries. This provides more deterministic behavior, with
+ # consistent eager loading in particular.
+ children.sort_by!(&:first)
+
+ children.each do |basename, abspath, ftype|
+ if :directory == ftype && !has_at_least_one_ruby_file?(abspath)
+ @loader.__log { "directory #{abspath} is ignored because it has no Ruby files" }
+ next
+ end
+
+ yield basename, abspath, ftype
+ end
+ end
+
+ #: (String) { (String) -> void } -> void
+ def walk_up(abspath)
+ loop do
+ yield abspath
+ abspath, basename = File.split(abspath)
+ break if basename == "/"
+ end
+ end
+
+ # Encodes the documented conventions.
+ #
+ #: (String) -> Symbol?
+ def supported_ftype?(abspath)
+ if rb_extension?(abspath)
+ :file # By convention, we can avoid a syscall here.
+ elsif dir?(abspath)
+ :directory
+ end
+ end
+
+ #: (String) -> bool
+ def rb_extension?(path)
+ path.end_with?(".rb")
+ end
+
+ #: (String) -> bool
+ def dir?(path)
+ File.directory?(path)
+ end
+
+ #: (String) -> bool
+ def hidden?(basename)
+ basename.start_with?(".")
+ end
+
+ private
+
+ # Looks for a Ruby file using breadth-first search. This type of search is
+ # important to list as less directories as possible and return fast in the
+ # common case in which there are Ruby files in the passed directory.
+ #
+ #: (String) -> bool
+ def has_at_least_one_ruby_file?(dir)
+ to_visit = [dir]
+
+ while (dir = to_visit.shift)
+ relevant_dir_entries(dir) do |_, abspath, ftype|
+ return true if :file == ftype
+ to_visit << abspath
+ end
+ end
+
+ false
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ #: (String) -> [[String, String, Symbol]]
+ def relevant_dir_entries(dir)
+ return enum_for(__method__, dir).to_a unless block_given?
+
+ each_ruby_file_or_directory(dir) do |basename, abspath, ftype|
+ next if @loader.__ignored_path?(abspath)
+
+ if :link == ftype
+ begin
+ ftype = File.stat(abspath).ftype.to_sym
+ rescue Errno::ENOENT
+ warn "ignoring broken symlink #{abspath}"
+ next
+ end
+ end
+
+ if :file == ftype
+ yield basename, abspath, ftype if rb_extension?(basename)
+ elsif :directory == ftype
+ # Conceptually, root directories represent a separate project tree.
+ yield basename, abspath, ftype unless @loader.__root_dir?(abspath)
+ end
+ end
+ end
+
+ # Dir.scan is more efficient in common platforms, but it is going to take a
+ # while for it to be available.
+ #
+ # The following compatibility methods have the same semantics but are written
+ # to favor the performance of the Ruby fallback, which can save syscalls.
+ #
+ # In particular, by convention, any directory entry with a .rb extension is
+ # assumed to be a file or a symlink to a file.
+ #
+ # These methods also freeze abspaths because that saves allocations when
+ # passed later to File methods. See https://github.com/fxn/zeitwerk/pull/125.
+
+ if Dir.respond_to?(:scan) # Available in Ruby 4.1.
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.scan(dir) do |basename, ftype|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ elsif :directory == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory
+ elsif :link == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory if dir?(abspath)
+ end
+ end
+ end
+ else
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.each_child(dir) do |basename|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ else
+ abspath = File.join(dir, basename).freeze
+ if dir?(abspath)
+ yield basename, abspath, :directory
+ end
+ end
+ end
+ end
+ end
+end
* Changed:
README.md
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/README.md 2026-02-20 03:33:17.213227435 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/README.md 2026-02-20 03:33:17.217227421 +0000
@@ -60,0 +61 @@
+ - [Autoloaded Constants](#autoloaded-constants)
@@ -1265,0 +1267,12 @@
+<a id="markdown-autoloaded-constants" name="autoloaded-constants"></a>
+#### Autoloaded Constants
+
+Zeitwerk does not keep track of autoloaded constants to minimize its memory footprint, but you can collect them with `on_load` if you will:
+
+```ruby
+autoloaded_cpaths = []
+loader.on_load do |cpath, _value, _abspath|
+ autoloaded_cpaths << cpath
+end
+```
+
@@ -1408,0 +1422,6 @@
+```
+
+That also accepts a line number:
+
+```
+bin/test test/lib/zeitwerk/test_eager_load.rb:52
lib/zeitwerk/gem_loader.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:17.214227431 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:17.218227417 +0000
@@ -45 +45 @@
- ls(@root_dir) do |basename, abspath, ftype|
+ @fs.ls(@root_dir) do |basename, abspath, ftype|
@@ -50 +50 @@
- cname = inflector.camelize(basename_without_ext, abspath).to_sym
+ cname = cname_for(basename_without_ext, abspath)
lib/zeitwerk/loader.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/loader.rb 2026-02-20 03:33:17.214227431 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/loader.rb 2026-02-20 03:33:17.218227417 +0000
@@ -11,0 +12 @@
+ require_relative "loader/file_system"
@@ -21,3 +21,0 @@
- MUTEX = Mutex.new #: Mutex
- private_constant :MUTEX
-
@@ -117,0 +116 @@
+ @fs = FileSystem.new(self)
@@ -174 +173 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -192 +191 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -202 +201 @@
- # https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
+ # https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
@@ -258 +257 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -261 +260 @@
- result[abspath] = prefix + inflector.camelize(basename, abspath)
+ result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
@@ -266 +265 @@
- queue << [abspath, prefix + inflector.camelize(basename, abspath)]
+ queue << [abspath, "#{prefix}#{cname_for(basename, abspath)}"]
@@ -282 +281,3 @@
- return unless dir?(abspath) || ruby?(abspath)
+ ftype = @fs.supported_ftype?(abspath)
+ return unless ftype
+
@@ -287 +288 @@
- if ruby?(abspath)
+ if :file == ftype
@@ -289 +290 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -299 +300 @@
- walk_up(walk_up_from) do |dir|
+ @fs.walk_up(walk_up_from) do |dir|
@@ -304 +305 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -365,0 +367,10 @@
+ #: { () -> String } -> void
+ internal def log
+ return unless logger
+
+ message = yield
+ method_name = logger.respond_to?(:debug) ? :debug : :call
+ logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
+ end
+
+
@@ -467 +478 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -486 +497 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -507 +518 @@
- log("the namespace #{cref} already exists, descending into #{subdir}") if logger
+ log { "the namespace #{cref} already exists, descending into #{subdir}" }
@@ -516 +527 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -518 +529 @@
- log("file #{file} is ignored because #{autoload_path} has precedence") if logger
+ log { "file #{file} is ignored because #{autoload_path} has precedence" }
@@ -524 +535 @@
- log("file #{file} is ignored because #{cref} is already defined") if logger
+ log { "file #{file} is ignored because #{cref} is already defined" }
@@ -538 +549 @@
- log("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
+ log { "earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}" }
@@ -551,2 +562,2 @@
- if ruby?(abspath)
- log("autoload set for #{cref}, to be loaded from #{abspath}")
+ if @fs.rb_extension?(abspath)
+ log { "autoload set for #{cref}, to be loaded from #{abspath}" }
@@ -554 +565 @@
- log("autoload set for #{cref}, to be autovivified from #{abspath}")
+ log { "autoload set for #{cref}, to be autovivified from #{abspath}" }
@@ -598,22 +609,6 @@
- private def raise_if_conflicting_directory(dir)
- MUTEX.synchronize do
- Registry.loaders.each do |loader|
- next if loader == self
-
- loader.__roots.each_key do |root_dir|
- # Conflicting directories are rare, optimize for the common case.
- next if !dir.start_with?(root_dir) && !root_dir.start_with?(dir)
-
- dir_slash = dir + "/"
- root_dir_slash = root_dir + "/"
- next if !dir_slash.start_with?(root_dir_slash) && !root_dir_slash.start_with?(dir_slash)
-
- next if ignores?(root_dir)
- break if loader.__ignores?(dir)
-
- require "pp" # Needed to have pretty_inspect available.
- raise Error,
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
- end
- end
+ private def raise_if_conflicting_root_dir(root_dir)
+ if loader = Registry.conflicting_root_dir?(self, root_dir)
+ require "pp" # Needed to have pretty_inspect available.
+ raise Error,
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{root_dir}," \
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
@@ -633 +628 @@
- log("autoload for #{cref} removed") if logger
+ log { "autoload for #{cref} removed" }
@@ -645 +640 @@
- log("#{cref} unloaded") if logger
+ log { "#{cref} unloaded" }
lib/zeitwerk/loader/callbacks.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:17.214227431 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:17.218227417 +0000
@@ -15 +15 @@
- log("constant #{cref} loaded from file #{file}") if logger
+ log { "constant #{cref} loaded from file #{file}" }
@@ -20 +20 @@
- log(msg) if logger
+ log { msg }
@@ -55 +55 @@
- log("module #{cref} autovivified from directory #{dir}") if logger
+ log { "module #{cref} autovivified from directory #{dir}" }
lib/zeitwerk/loader/config.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:17.214227431 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:17.218227417 +0000
@@ -119,2 +119,2 @@
- if dir?(abspath)
- raise_if_conflicting_directory(abspath)
+ if @fs.dir?(abspath)
+ raise_if_conflicting_root_dir(abspath)
@@ -293 +293 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -295 +295 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
@@ -302 +302 @@
- private def ignored_path?(abspath)
+ internal def ignored_path?(abspath)
@@ -309 +309 @@
- !dir?(root_dir) || ignored_path?(root_dir)
+ !@fs.dir?(root_dir) || ignored_path?(root_dir)
@@ -314 +314 @@
- private def root_dir?(dir)
+ internal def root_dir?(dir)
@@ -323 +323 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -325 +325 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
lib/zeitwerk/loader/eager_load.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:17.215227428 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:17.218227417 +0000
@@ -14 +14 @@
- log("eager load start") if logger
+ log { "eager load start" }
@@ -27 +27 @@
- log("eager load end") if logger
+ log { "eager load end" }
@@ -37 +37 @@
- raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)
@@ -39 +39 @@
- cnames = []
+ paths = []
@@ -42 +42 @@
- walk_up(abspath) do |dir|
+ @fs.walk_up(abspath) do |dir|
@@ -49 +49 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -51,3 +51 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -61 +59,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -120 +119 @@
- raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
@@ -123,4 +122,2 @@
- basename = File.basename(abspath, ".rb")
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
-
- base_cname = inflector.camelize(basename, abspath).to_sym
+ file_basename = File.basename(abspath, ".rb")
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
@@ -129 +126 @@
- cnames = []
+ paths = []
@@ -131 +128 @@
- walk_up(File.dirname(abspath)) do |dir|
+ @fs.walk_up(File.dirname(abspath)) do |dir|
@@ -137 +134 @@
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)
@@ -139,3 +136 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -145,0 +141,2 @@
+ base_cname = cname_for(file_basename, abspath)
+
@@ -147 +144,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -164 +162 @@
- log("eager load directory #{dir} start") if logger
+ log { "eager load directory #{dir} start" }
@@ -168 +166 @@
- ls(current_dir) do |basename, abspath, ftype|
+ @fs.ls(current_dir) do |basename, abspath, ftype|
@@ -179 +177 @@
- cname = inflector.camelize(basename, abspath).to_sym
+ cname = cname_for(basename, abspath)
@@ -186 +184 @@
- log("eager load directory #{dir} end") if logger
+ log { "eager load directory #{dir} end" }
@@ -211 +209 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -216 +214 @@
- elsif segment == inflector.camelize(basename, abspath)
+ elsif segment == cname_for(basename, abspath).to_s
lib/zeitwerk/loader/helpers.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:17.215227428 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:17.219227414 +0000
@@ -4,99 +3,0 @@
- # --- Logging -----------------------------------------------------------------------------------
-
- #: (to_s() -> String) -> void
- private def log(message)
- method_name = logger.respond_to?(:debug) ? :debug : :call
- logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
- end
-
- # --- Files and directories ---------------------------------------------------------------------
-
- #: (String) { (String, String, Symbol) -> void } -> void
- private def ls(dir)
- children = Dir.children(dir)
-
- # The order in which a directory is listed depends on the file system.
- #
- # Since client code may run in different platforms, it seems convenient to
- # order directory entries. This provides consistent eager loading across
- # platforms, for example.
- children.sort!
-
- children.each do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- next if roots.key?(abspath)
-
- if !has_at_least_one_ruby_file?(abspath)
- log("directory #{abspath} is ignored because it has no Ruby files") if logger
- next
- end
-
- ftype = :directory
- else
- next unless ruby?(abspath)
- ftype = :file
- end
-
- # We freeze abspath because that saves allocations when passed later to
- # File methods. See #125.
- yield basename, abspath.freeze, ftype
- end
- end
-
- # Looks for a Ruby file using breadth-first search. This type of search is
- # important to list as less directories as possible and return fast in the
- # common case in which there are Ruby files.
- #
- #: (String) -> bool
- private def has_at_least_one_ruby_file?(dir)
- to_visit = [dir]
-
- while (dir = to_visit.shift)
- Dir.each_child(dir) do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- to_visit << abspath unless roots.key?(abspath)
- else
- return true if ruby?(abspath)
- end
- end
- end
-
- false
- end
-
- #: (String) -> bool
- private def ruby?(path)
- path.end_with?(".rb")
- end
-
- #: (String) -> bool
- private def dir?(path)
- File.directory?(path)
- end
-
- #: (String) -> bool
- private def hidden?(basename)
- basename.start_with?(".")
- end
-
- #: (String) { (String) -> void } -> void
- private def walk_up(abspath)
- loop do
- yield abspath
- abspath, basename = File.split(abspath)
- break if basename == "/"
- end
- end
-
- # --- Inflection --------------------------------------------------------------------------------
-
@@ -127 +28 @@
- path_type = ruby?(abspath) ? "file" : "directory"
+ path_type = @fs.rb_extension?(abspath) ? "file" : "directory"
lib/zeitwerk/real_mod_name.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:17.215227428 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:17.219227414 +0000
@@ -10 +10 @@
- # We need this indirection becasue the `name` method can be overridden, and
+ # We need this indirection because the `name` method can be overridden, and
lib/zeitwerk/registry.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/registry.rb 2026-02-20 03:33:17.215227428 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/registry.rb 2026-02-20 03:33:17.220227410 +0000
@@ -46,0 +47,25 @@
+ #: (Zeitwerk::Loader, String) -> Zeitwerk::Loader?
+ def conflicting_root_dir?(loader, new_root_dir)
+ @mutex.synchronize do
+ loaders.each do |existing_loader|
+ next if existing_loader == loader
+
+ existing_loader.__roots.each_key do |existing_root_dir|
+ # Conflicting directories are rare, optimize for the common case.
+ next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
+
+ new_root_dir_slash = new_root_dir + "/"
+ existing_root_dir_slash = existing_root_dir + "/"
+ next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
+
+ next if loader.__ignores?(existing_root_dir)
+ break if existing_loader.__ignores?(new_root_dir)
+
+ return existing_loader
+ end
+ end
+
+ nil
+ end
+ end
+
@@ -61,0 +87 @@
+ @mutex = Mutex.new
lib/zeitwerk/registry/loaders.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:17.215227428 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:17.221227407 +0000
@@ -9,2 +9,2 @@
- def each(&block)
- @loaders.each(&block)
+ def each(&)
+ @loaders.each(&)
lib/zeitwerk/version.rb
--- /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.4/lib/zeitwerk/version.rb 2026-02-20 03:33:17.215227428 +0000
+++ /tmp/d20260220-408-j9xm9w/zeitwerk-2.7.5/lib/zeitwerk/version.rb 2026-02-20 03:33:17.221227407 +0000
@@ -5 +5 @@
- VERSION = "2.7.4"
+ VERSION = "2.7.5" |
Contributor
gem compare --diff zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb
--- /tmp/20260220-425-wdniue 2026-02-20 03:33:18.881928902 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/loader/file_system.rb 2026-02-20 03:33:18.880928900 +0000
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+# This private class encapsulates interactions with the file system.
+#
+# It is used to list directories and check file types, and it encodes the
+# conventions documented in the README.
+#
+# @private
+class Zeitwerk::Loader::FileSystem # :nodoc:
+ #: (Zeitwerk::Loader) -> void
+ def initialize(loader)
+ @loader = loader
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def ls(dir)
+ children = relevant_dir_entries(dir)
+
+ # The order in which a directory is listed depends on the file system.
+ #
+ # Since client code may run on different platforms, it seems convenient to
+ # sort directory entries. This provides more deterministic behavior, with
+ # consistent eager loading in particular.
+ children.sort_by!(&:first)
+
+ children.each do |basename, abspath, ftype|
+ if :directory == ftype && !has_at_least_one_ruby_file?(abspath)
+ @loader.__log { "directory #{abspath} is ignored because it has no Ruby files" }
+ next
+ end
+
+ yield basename, abspath, ftype
+ end
+ end
+
+ #: (String) { (String) -> void } -> void
+ def walk_up(abspath)
+ loop do
+ yield abspath
+ abspath, basename = File.split(abspath)
+ break if basename == "/"
+ end
+ end
+
+ # Encodes the documented conventions.
+ #
+ #: (String) -> Symbol?
+ def supported_ftype?(abspath)
+ if rb_extension?(abspath)
+ :file # By convention, we can avoid a syscall here.
+ elsif dir?(abspath)
+ :directory
+ end
+ end
+
+ #: (String) -> bool
+ def rb_extension?(path)
+ path.end_with?(".rb")
+ end
+
+ #: (String) -> bool
+ def dir?(path)
+ File.directory?(path)
+ end
+
+ #: (String) -> bool
+ def hidden?(basename)
+ basename.start_with?(".")
+ end
+
+ private
+
+ # Looks for a Ruby file using breadth-first search. This type of search is
+ # important to list as less directories as possible and return fast in the
+ # common case in which there are Ruby files in the passed directory.
+ #
+ #: (String) -> bool
+ def has_at_least_one_ruby_file?(dir)
+ to_visit = [dir]
+
+ while (dir = to_visit.shift)
+ relevant_dir_entries(dir) do |_, abspath, ftype|
+ return true if :file == ftype
+ to_visit << abspath
+ end
+ end
+
+ false
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ #: (String) -> [[String, String, Symbol]]
+ def relevant_dir_entries(dir)
+ return enum_for(__method__, dir).to_a unless block_given?
+
+ each_ruby_file_or_directory(dir) do |basename, abspath, ftype|
+ next if @loader.__ignored_path?(abspath)
+
+ if :link == ftype
+ begin
+ ftype = File.stat(abspath).ftype.to_sym
+ rescue Errno::ENOENT
+ warn "ignoring broken symlink #{abspath}"
+ next
+ end
+ end
+
+ if :file == ftype
+ yield basename, abspath, ftype if rb_extension?(basename)
+ elsif :directory == ftype
+ # Conceptually, root directories represent a separate project tree.
+ yield basename, abspath, ftype unless @loader.__root_dir?(abspath)
+ end
+ end
+ end
+
+ # Dir.scan is more efficient in common platforms, but it is going to take a
+ # while for it to be available.
+ #
+ # The following compatibility methods have the same semantics but are written
+ # to favor the performance of the Ruby fallback, which can save syscalls.
+ #
+ # In particular, by convention, any directory entry with a .rb extension is
+ # assumed to be a file or a symlink to a file.
+ #
+ # These methods also freeze abspaths because that saves allocations when
+ # passed later to File methods. See https://github.com/fxn/zeitwerk/pull/125.
+
+ if Dir.respond_to?(:scan) # Available in Ruby 4.1.
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.scan(dir) do |basename, ftype|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ elsif :directory == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory
+ elsif :link == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory if dir?(abspath)
+ end
+ end
+ end
+ else
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.each_child(dir) do |basename|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ else
+ abspath = File.join(dir, basename).freeze
+ if dir?(abspath)
+ yield basename, abspath, :directory
+ end
+ end
+ end
+ end
+ end
+end
* Changed:
README.md
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/README.md 2026-02-20 03:33:18.875928890 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/README.md 2026-02-20 03:33:18.878928896 +0000
@@ -60,0 +61 @@
+ - [Autoloaded Constants](#autoloaded-constants)
@@ -1265,0 +1267,12 @@
+<a id="markdown-autoloaded-constants" name="autoloaded-constants"></a>
+#### Autoloaded Constants
+
+Zeitwerk does not keep track of autoloaded constants to minimize its memory footprint, but you can collect them with `on_load` if you will:
+
+```ruby
+autoloaded_cpaths = []
+loader.on_load do |cpath, _value, _abspath|
+ autoloaded_cpaths << cpath
+end
+```
+
@@ -1408,0 +1422,6 @@
+```
+
+That also accepts a line number:
+
+```
+bin/test test/lib/zeitwerk/test_eager_load.rb:52
lib/zeitwerk/gem_loader.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:18.876928892 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:18.879928898 +0000
@@ -45 +45 @@
- ls(@root_dir) do |basename, abspath, ftype|
+ @fs.ls(@root_dir) do |basename, abspath, ftype|
@@ -50 +50 @@
- cname = inflector.camelize(basename_without_ext, abspath).to_sym
+ cname = cname_for(basename_without_ext, abspath)
lib/zeitwerk/loader.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/loader.rb 2026-02-20 03:33:18.876928892 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/loader.rb 2026-02-20 03:33:18.880928900 +0000
@@ -11,0 +12 @@
+ require_relative "loader/file_system"
@@ -21,3 +21,0 @@
- MUTEX = Mutex.new #: Mutex
- private_constant :MUTEX
-
@@ -117,0 +116 @@
+ @fs = FileSystem.new(self)
@@ -174 +173 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -192 +191 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -202 +201 @@
- # https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
+ # https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
@@ -258 +257 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -261 +260 @@
- result[abspath] = prefix + inflector.camelize(basename, abspath)
+ result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
@@ -266 +265 @@
- queue << [abspath, prefix + inflector.camelize(basename, abspath)]
+ queue << [abspath, "#{prefix}#{cname_for(basename, abspath)}"]
@@ -282 +281,3 @@
- return unless dir?(abspath) || ruby?(abspath)
+ ftype = @fs.supported_ftype?(abspath)
+ return unless ftype
+
@@ -287 +288 @@
- if ruby?(abspath)
+ if :file == ftype
@@ -289 +290 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -299 +300 @@
- walk_up(walk_up_from) do |dir|
+ @fs.walk_up(walk_up_from) do |dir|
@@ -304 +305 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -365,0 +367,10 @@
+ #: { () -> String } -> void
+ internal def log
+ return unless logger
+
+ message = yield
+ method_name = logger.respond_to?(:debug) ? :debug : :call
+ logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
+ end
+
+
@@ -467 +478 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -486 +497 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -507 +518 @@
- log("the namespace #{cref} already exists, descending into #{subdir}") if logger
+ log { "the namespace #{cref} already exists, descending into #{subdir}" }
@@ -516 +527 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -518 +529 @@
- log("file #{file} is ignored because #{autoload_path} has precedence") if logger
+ log { "file #{file} is ignored because #{autoload_path} has precedence" }
@@ -524 +535 @@
- log("file #{file} is ignored because #{cref} is already defined") if logger
+ log { "file #{file} is ignored because #{cref} is already defined" }
@@ -538 +549 @@
- log("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
+ log { "earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}" }
@@ -551,2 +562,2 @@
- if ruby?(abspath)
- log("autoload set for #{cref}, to be loaded from #{abspath}")
+ if @fs.rb_extension?(abspath)
+ log { "autoload set for #{cref}, to be loaded from #{abspath}" }
@@ -554 +565 @@
- log("autoload set for #{cref}, to be autovivified from #{abspath}")
+ log { "autoload set for #{cref}, to be autovivified from #{abspath}" }
@@ -598,22 +609,6 @@
- private def raise_if_conflicting_directory(dir)
- MUTEX.synchronize do
- Registry.loaders.each do |loader|
- next if loader == self
-
- loader.__roots.each_key do |root_dir|
- # Conflicting directories are rare, optimize for the common case.
- next if !dir.start_with?(root_dir) && !root_dir.start_with?(dir)
-
- dir_slash = dir + "/"
- root_dir_slash = root_dir + "/"
- next if !dir_slash.start_with?(root_dir_slash) && !root_dir_slash.start_with?(dir_slash)
-
- next if ignores?(root_dir)
- break if loader.__ignores?(dir)
-
- require "pp" # Needed to have pretty_inspect available.
- raise Error,
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
- end
- end
+ private def raise_if_conflicting_root_dir(root_dir)
+ if loader = Registry.conflicting_root_dir?(self, root_dir)
+ require "pp" # Needed to have pretty_inspect available.
+ raise Error,
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{root_dir}," \
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
@@ -633 +628 @@
- log("autoload for #{cref} removed") if logger
+ log { "autoload for #{cref} removed" }
@@ -645 +640 @@
- log("#{cref} unloaded") if logger
+ log { "#{cref} unloaded" }
lib/zeitwerk/loader/callbacks.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:18.876928892 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:18.880928900 +0000
@@ -15 +15 @@
- log("constant #{cref} loaded from file #{file}") if logger
+ log { "constant #{cref} loaded from file #{file}" }
@@ -20 +20 @@
- log(msg) if logger
+ log { msg }
@@ -55 +55 @@
- log("module #{cref} autovivified from directory #{dir}") if logger
+ log { "module #{cref} autovivified from directory #{dir}" }
lib/zeitwerk/loader/config.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:18.876928892 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:18.880928900 +0000
@@ -119,2 +119,2 @@
- if dir?(abspath)
- raise_if_conflicting_directory(abspath)
+ if @fs.dir?(abspath)
+ raise_if_conflicting_root_dir(abspath)
@@ -293 +293 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -295 +295 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
@@ -302 +302 @@
- private def ignored_path?(abspath)
+ internal def ignored_path?(abspath)
@@ -309 +309 @@
- !dir?(root_dir) || ignored_path?(root_dir)
+ !@fs.dir?(root_dir) || ignored_path?(root_dir)
@@ -314 +314 @@
- private def root_dir?(dir)
+ internal def root_dir?(dir)
@@ -323 +323 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -325 +325 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
lib/zeitwerk/loader/eager_load.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:18.876928892 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:18.880928900 +0000
@@ -14 +14 @@
- log("eager load start") if logger
+ log { "eager load start" }
@@ -27 +27 @@
- log("eager load end") if logger
+ log { "eager load end" }
@@ -37 +37 @@
- raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)
@@ -39 +39 @@
- cnames = []
+ paths = []
@@ -42 +42 @@
- walk_up(abspath) do |dir|
+ @fs.walk_up(abspath) do |dir|
@@ -49 +49 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -51,3 +51 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -61 +59,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -120 +119 @@
- raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
@@ -123,4 +122,2 @@
- basename = File.basename(abspath, ".rb")
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
-
- base_cname = inflector.camelize(basename, abspath).to_sym
+ file_basename = File.basename(abspath, ".rb")
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
@@ -129 +126 @@
- cnames = []
+ paths = []
@@ -131 +128 @@
- walk_up(File.dirname(abspath)) do |dir|
+ @fs.walk_up(File.dirname(abspath)) do |dir|
@@ -137 +134 @@
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)
@@ -139,3 +136 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -145,0 +141,2 @@
+ base_cname = cname_for(file_basename, abspath)
+
@@ -147 +144,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -164 +162 @@
- log("eager load directory #{dir} start") if logger
+ log { "eager load directory #{dir} start" }
@@ -168 +166 @@
- ls(current_dir) do |basename, abspath, ftype|
+ @fs.ls(current_dir) do |basename, abspath, ftype|
@@ -179 +177 @@
- cname = inflector.camelize(basename, abspath).to_sym
+ cname = cname_for(basename, abspath)
@@ -186 +184 @@
- log("eager load directory #{dir} end") if logger
+ log { "eager load directory #{dir} end" }
@@ -211 +209 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -216 +214 @@
- elsif segment == inflector.camelize(basename, abspath)
+ elsif segment == cname_for(basename, abspath).to_s
lib/zeitwerk/loader/helpers.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:18.877928894 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:18.880928900 +0000
@@ -4,99 +3,0 @@
- # --- Logging -----------------------------------------------------------------------------------
-
- #: (to_s() -> String) -> void
- private def log(message)
- method_name = logger.respond_to?(:debug) ? :debug : :call
- logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
- end
-
- # --- Files and directories ---------------------------------------------------------------------
-
- #: (String) { (String, String, Symbol) -> void } -> void
- private def ls(dir)
- children = Dir.children(dir)
-
- # The order in which a directory is listed depends on the file system.
- #
- # Since client code may run in different platforms, it seems convenient to
- # order directory entries. This provides consistent eager loading across
- # platforms, for example.
- children.sort!
-
- children.each do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- next if roots.key?(abspath)
-
- if !has_at_least_one_ruby_file?(abspath)
- log("directory #{abspath} is ignored because it has no Ruby files") if logger
- next
- end
-
- ftype = :directory
- else
- next unless ruby?(abspath)
- ftype = :file
- end
-
- # We freeze abspath because that saves allocations when passed later to
- # File methods. See #125.
- yield basename, abspath.freeze, ftype
- end
- end
-
- # Looks for a Ruby file using breadth-first search. This type of search is
- # important to list as less directories as possible and return fast in the
- # common case in which there are Ruby files.
- #
- #: (String) -> bool
- private def has_at_least_one_ruby_file?(dir)
- to_visit = [dir]
-
- while (dir = to_visit.shift)
- Dir.each_child(dir) do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- to_visit << abspath unless roots.key?(abspath)
- else
- return true if ruby?(abspath)
- end
- end
- end
-
- false
- end
-
- #: (String) -> bool
- private def ruby?(path)
- path.end_with?(".rb")
- end
-
- #: (String) -> bool
- private def dir?(path)
- File.directory?(path)
- end
-
- #: (String) -> bool
- private def hidden?(basename)
- basename.start_with?(".")
- end
-
- #: (String) { (String) -> void } -> void
- private def walk_up(abspath)
- loop do
- yield abspath
- abspath, basename = File.split(abspath)
- break if basename == "/"
- end
- end
-
- # --- Inflection --------------------------------------------------------------------------------
-
@@ -127 +28 @@
- path_type = ruby?(abspath) ? "file" : "directory"
+ path_type = @fs.rb_extension?(abspath) ? "file" : "directory"
lib/zeitwerk/real_mod_name.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:18.877928894 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:18.880928900 +0000
@@ -10 +10 @@
- # We need this indirection becasue the `name` method can be overridden, and
+ # We need this indirection because the `name` method can be overridden, and
lib/zeitwerk/registry.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/registry.rb 2026-02-20 03:33:18.877928894 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/registry.rb 2026-02-20 03:33:18.880928900 +0000
@@ -46,0 +47,25 @@
+ #: (Zeitwerk::Loader, String) -> Zeitwerk::Loader?
+ def conflicting_root_dir?(loader, new_root_dir)
+ @mutex.synchronize do
+ loaders.each do |existing_loader|
+ next if existing_loader == loader
+
+ existing_loader.__roots.each_key do |existing_root_dir|
+ # Conflicting directories are rare, optimize for the common case.
+ next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
+
+ new_root_dir_slash = new_root_dir + "/"
+ existing_root_dir_slash = existing_root_dir + "/"
+ next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
+
+ next if loader.__ignores?(existing_root_dir)
+ break if existing_loader.__ignores?(new_root_dir)
+
+ return existing_loader
+ end
+ end
+
+ nil
+ end
+ end
+
@@ -61,0 +87 @@
+ @mutex = Mutex.new
lib/zeitwerk/registry/loaders.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:18.877928894 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:18.881928902 +0000
@@ -9,2 +9,2 @@
- def each(&block)
- @loaders.each(&block)
+ def each(&)
+ @loaders.each(&)
lib/zeitwerk/version.rb
--- /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.4/lib/zeitwerk/version.rb 2026-02-20 03:33:18.877928894 +0000
+++ /tmp/d20260220-425-sxh2jq/zeitwerk-2.7.5/lib/zeitwerk/version.rb 2026-02-20 03:33:18.881928902 +0000
@@ -5 +5 @@
- VERSION = "2.7.4"
+ VERSION = "2.7.5" |
Contributor
gem compare --diff zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb
--- /tmp/20260220-438-r2xjj4 2026-02-20 03:33:19.204922098 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/loader/file_system.rb 2026-02-20 03:33:19.203922101 +0000
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+# This private class encapsulates interactions with the file system.
+#
+# It is used to list directories and check file types, and it encodes the
+# conventions documented in the README.
+#
+# @private
+class Zeitwerk::Loader::FileSystem # :nodoc:
+ #: (Zeitwerk::Loader) -> void
+ def initialize(loader)
+ @loader = loader
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def ls(dir)
+ children = relevant_dir_entries(dir)
+
+ # The order in which a directory is listed depends on the file system.
+ #
+ # Since client code may run on different platforms, it seems convenient to
+ # sort directory entries. This provides more deterministic behavior, with
+ # consistent eager loading in particular.
+ children.sort_by!(&:first)
+
+ children.each do |basename, abspath, ftype|
+ if :directory == ftype && !has_at_least_one_ruby_file?(abspath)
+ @loader.__log { "directory #{abspath} is ignored because it has no Ruby files" }
+ next
+ end
+
+ yield basename, abspath, ftype
+ end
+ end
+
+ #: (String) { (String) -> void } -> void
+ def walk_up(abspath)
+ loop do
+ yield abspath
+ abspath, basename = File.split(abspath)
+ break if basename == "/"
+ end
+ end
+
+ # Encodes the documented conventions.
+ #
+ #: (String) -> Symbol?
+ def supported_ftype?(abspath)
+ if rb_extension?(abspath)
+ :file # By convention, we can avoid a syscall here.
+ elsif dir?(abspath)
+ :directory
+ end
+ end
+
+ #: (String) -> bool
+ def rb_extension?(path)
+ path.end_with?(".rb")
+ end
+
+ #: (String) -> bool
+ def dir?(path)
+ File.directory?(path)
+ end
+
+ #: (String) -> bool
+ def hidden?(basename)
+ basename.start_with?(".")
+ end
+
+ private
+
+ # Looks for a Ruby file using breadth-first search. This type of search is
+ # important to list as less directories as possible and return fast in the
+ # common case in which there are Ruby files in the passed directory.
+ #
+ #: (String) -> bool
+ def has_at_least_one_ruby_file?(dir)
+ to_visit = [dir]
+
+ while (dir = to_visit.shift)
+ relevant_dir_entries(dir) do |_, abspath, ftype|
+ return true if :file == ftype
+ to_visit << abspath
+ end
+ end
+
+ false
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ #: (String) -> [[String, String, Symbol]]
+ def relevant_dir_entries(dir)
+ return enum_for(__method__, dir).to_a unless block_given?
+
+ each_ruby_file_or_directory(dir) do |basename, abspath, ftype|
+ next if @loader.__ignored_path?(abspath)
+
+ if :link == ftype
+ begin
+ ftype = File.stat(abspath).ftype.to_sym
+ rescue Errno::ENOENT
+ warn "ignoring broken symlink #{abspath}"
+ next
+ end
+ end
+
+ if :file == ftype
+ yield basename, abspath, ftype if rb_extension?(basename)
+ elsif :directory == ftype
+ # Conceptually, root directories represent a separate project tree.
+ yield basename, abspath, ftype unless @loader.__root_dir?(abspath)
+ end
+ end
+ end
+
+ # Dir.scan is more efficient in common platforms, but it is going to take a
+ # while for it to be available.
+ #
+ # The following compatibility methods have the same semantics but are written
+ # to favor the performance of the Ruby fallback, which can save syscalls.
+ #
+ # In particular, by convention, any directory entry with a .rb extension is
+ # assumed to be a file or a symlink to a file.
+ #
+ # These methods also freeze abspaths because that saves allocations when
+ # passed later to File methods. See https://github.com/fxn/zeitwerk/pull/125.
+
+ if Dir.respond_to?(:scan) # Available in Ruby 4.1.
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.scan(dir) do |basename, ftype|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ elsif :directory == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory
+ elsif :link == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory if dir?(abspath)
+ end
+ end
+ end
+ else
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.each_child(dir) do |basename|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ else
+ abspath = File.join(dir, basename).freeze
+ if dir?(abspath)
+ yield basename, abspath, :directory
+ end
+ end
+ end
+ end
+ end
+end
* Changed:
README.md
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/README.md 2026-02-20 03:33:19.197922114 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/README.md 2026-02-20 03:33:19.201922105 +0000
@@ -60,0 +61 @@
+ - [Autoloaded Constants](#autoloaded-constants)
@@ -1265,0 +1267,12 @@
+<a id="markdown-autoloaded-constants" name="autoloaded-constants"></a>
+#### Autoloaded Constants
+
+Zeitwerk does not keep track of autoloaded constants to minimize its memory footprint, but you can collect them with `on_load` if you will:
+
+```ruby
+autoloaded_cpaths = []
+loader.on_load do |cpath, _value, _abspath|
+ autoloaded_cpaths << cpath
+end
+```
+
@@ -1408,0 +1422,6 @@
+```
+
+That also accepts a line number:
+
+```
+bin/test test/lib/zeitwerk/test_eager_load.rb:52
lib/zeitwerk/gem_loader.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:19.198922112 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:19.202922103 +0000
@@ -45 +45 @@
- ls(@root_dir) do |basename, abspath, ftype|
+ @fs.ls(@root_dir) do |basename, abspath, ftype|
@@ -50 +50 @@
- cname = inflector.camelize(basename_without_ext, abspath).to_sym
+ cname = cname_for(basename_without_ext, abspath)
lib/zeitwerk/loader.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/loader.rb 2026-02-20 03:33:19.199922109 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/loader.rb 2026-02-20 03:33:19.202922103 +0000
@@ -11,0 +12 @@
+ require_relative "loader/file_system"
@@ -21,3 +21,0 @@
- MUTEX = Mutex.new #: Mutex
- private_constant :MUTEX
-
@@ -117,0 +116 @@
+ @fs = FileSystem.new(self)
@@ -174 +173 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -192 +191 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -202 +201 @@
- # https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
+ # https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
@@ -258 +257 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -261 +260 @@
- result[abspath] = prefix + inflector.camelize(basename, abspath)
+ result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
@@ -266 +265 @@
- queue << [abspath, prefix + inflector.camelize(basename, abspath)]
+ queue << [abspath, "#{prefix}#{cname_for(basename, abspath)}"]
@@ -282 +281,3 @@
- return unless dir?(abspath) || ruby?(abspath)
+ ftype = @fs.supported_ftype?(abspath)
+ return unless ftype
+
@@ -287 +288 @@
- if ruby?(abspath)
+ if :file == ftype
@@ -289 +290 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -299 +300 @@
- walk_up(walk_up_from) do |dir|
+ @fs.walk_up(walk_up_from) do |dir|
@@ -304 +305 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -365,0 +367,10 @@
+ #: { () -> String } -> void
+ internal def log
+ return unless logger
+
+ message = yield
+ method_name = logger.respond_to?(:debug) ? :debug : :call
+ logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
+ end
+
+
@@ -467 +478 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -486 +497 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -507 +518 @@
- log("the namespace #{cref} already exists, descending into #{subdir}") if logger
+ log { "the namespace #{cref} already exists, descending into #{subdir}" }
@@ -516 +527 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -518 +529 @@
- log("file #{file} is ignored because #{autoload_path} has precedence") if logger
+ log { "file #{file} is ignored because #{autoload_path} has precedence" }
@@ -524 +535 @@
- log("file #{file} is ignored because #{cref} is already defined") if logger
+ log { "file #{file} is ignored because #{cref} is already defined" }
@@ -538 +549 @@
- log("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
+ log { "earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}" }
@@ -551,2 +562,2 @@
- if ruby?(abspath)
- log("autoload set for #{cref}, to be loaded from #{abspath}")
+ if @fs.rb_extension?(abspath)
+ log { "autoload set for #{cref}, to be loaded from #{abspath}" }
@@ -554 +565 @@
- log("autoload set for #{cref}, to be autovivified from #{abspath}")
+ log { "autoload set for #{cref}, to be autovivified from #{abspath}" }
@@ -598,22 +609,6 @@
- private def raise_if_conflicting_directory(dir)
- MUTEX.synchronize do
- Registry.loaders.each do |loader|
- next if loader == self
-
- loader.__roots.each_key do |root_dir|
- # Conflicting directories are rare, optimize for the common case.
- next if !dir.start_with?(root_dir) && !root_dir.start_with?(dir)
-
- dir_slash = dir + "/"
- root_dir_slash = root_dir + "/"
- next if !dir_slash.start_with?(root_dir_slash) && !root_dir_slash.start_with?(dir_slash)
-
- next if ignores?(root_dir)
- break if loader.__ignores?(dir)
-
- require "pp" # Needed to have pretty_inspect available.
- raise Error,
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
- end
- end
+ private def raise_if_conflicting_root_dir(root_dir)
+ if loader = Registry.conflicting_root_dir?(self, root_dir)
+ require "pp" # Needed to have pretty_inspect available.
+ raise Error,
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{root_dir}," \
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
@@ -633 +628 @@
- log("autoload for #{cref} removed") if logger
+ log { "autoload for #{cref} removed" }
@@ -645 +640 @@
- log("#{cref} unloaded") if logger
+ log { "#{cref} unloaded" }
lib/zeitwerk/loader/callbacks.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:19.199922109 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:19.202922103 +0000
@@ -15 +15 @@
- log("constant #{cref} loaded from file #{file}") if logger
+ log { "constant #{cref} loaded from file #{file}" }
@@ -20 +20 @@
- log(msg) if logger
+ log { msg }
@@ -55 +55 @@
- log("module #{cref} autovivified from directory #{dir}") if logger
+ log { "module #{cref} autovivified from directory #{dir}" }
lib/zeitwerk/loader/config.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:19.199922109 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:19.202922103 +0000
@@ -119,2 +119,2 @@
- if dir?(abspath)
- raise_if_conflicting_directory(abspath)
+ if @fs.dir?(abspath)
+ raise_if_conflicting_root_dir(abspath)
@@ -293 +293 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -295 +295 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
@@ -302 +302 @@
- private def ignored_path?(abspath)
+ internal def ignored_path?(abspath)
@@ -309 +309 @@
- !dir?(root_dir) || ignored_path?(root_dir)
+ !@fs.dir?(root_dir) || ignored_path?(root_dir)
@@ -314 +314 @@
- private def root_dir?(dir)
+ internal def root_dir?(dir)
@@ -323 +323 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -325 +325 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
lib/zeitwerk/loader/eager_load.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:19.199922109 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:19.203922101 +0000
@@ -14 +14 @@
- log("eager load start") if logger
+ log { "eager load start" }
@@ -27 +27 @@
- log("eager load end") if logger
+ log { "eager load end" }
@@ -37 +37 @@
- raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)
@@ -39 +39 @@
- cnames = []
+ paths = []
@@ -42 +42 @@
- walk_up(abspath) do |dir|
+ @fs.walk_up(abspath) do |dir|
@@ -49 +49 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -51,3 +51 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -61 +59,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -120 +119 @@
- raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
@@ -123,4 +122,2 @@
- basename = File.basename(abspath, ".rb")
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
-
- base_cname = inflector.camelize(basename, abspath).to_sym
+ file_basename = File.basename(abspath, ".rb")
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
@@ -129 +126 @@
- cnames = []
+ paths = []
@@ -131 +128 @@
- walk_up(File.dirname(abspath)) do |dir|
+ @fs.walk_up(File.dirname(abspath)) do |dir|
@@ -137 +134 @@
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)
@@ -139,3 +136 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -145,0 +141,2 @@
+ base_cname = cname_for(file_basename, abspath)
+
@@ -147 +144,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -164 +162 @@
- log("eager load directory #{dir} start") if logger
+ log { "eager load directory #{dir} start" }
@@ -168 +166 @@
- ls(current_dir) do |basename, abspath, ftype|
+ @fs.ls(current_dir) do |basename, abspath, ftype|
@@ -179 +177 @@
- cname = inflector.camelize(basename, abspath).to_sym
+ cname = cname_for(basename, abspath)
@@ -186 +184 @@
- log("eager load directory #{dir} end") if logger
+ log { "eager load directory #{dir} end" }
@@ -211 +209 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -216 +214 @@
- elsif segment == inflector.camelize(basename, abspath)
+ elsif segment == cname_for(basename, abspath).to_s
lib/zeitwerk/loader/helpers.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:19.199922109 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:19.203922101 +0000
@@ -4,99 +3,0 @@
- # --- Logging -----------------------------------------------------------------------------------
-
- #: (to_s() -> String) -> void
- private def log(message)
- method_name = logger.respond_to?(:debug) ? :debug : :call
- logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
- end
-
- # --- Files and directories ---------------------------------------------------------------------
-
- #: (String) { (String, String, Symbol) -> void } -> void
- private def ls(dir)
- children = Dir.children(dir)
-
- # The order in which a directory is listed depends on the file system.
- #
- # Since client code may run in different platforms, it seems convenient to
- # order directory entries. This provides consistent eager loading across
- # platforms, for example.
- children.sort!
-
- children.each do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- next if roots.key?(abspath)
-
- if !has_at_least_one_ruby_file?(abspath)
- log("directory #{abspath} is ignored because it has no Ruby files") if logger
- next
- end
-
- ftype = :directory
- else
- next unless ruby?(abspath)
- ftype = :file
- end
-
- # We freeze abspath because that saves allocations when passed later to
- # File methods. See #125.
- yield basename, abspath.freeze, ftype
- end
- end
-
- # Looks for a Ruby file using breadth-first search. This type of search is
- # important to list as less directories as possible and return fast in the
- # common case in which there are Ruby files.
- #
- #: (String) -> bool
- private def has_at_least_one_ruby_file?(dir)
- to_visit = [dir]
-
- while (dir = to_visit.shift)
- Dir.each_child(dir) do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- to_visit << abspath unless roots.key?(abspath)
- else
- return true if ruby?(abspath)
- end
- end
- end
-
- false
- end
-
- #: (String) -> bool
- private def ruby?(path)
- path.end_with?(".rb")
- end
-
- #: (String) -> bool
- private def dir?(path)
- File.directory?(path)
- end
-
- #: (String) -> bool
- private def hidden?(basename)
- basename.start_with?(".")
- end
-
- #: (String) { (String) -> void } -> void
- private def walk_up(abspath)
- loop do
- yield abspath
- abspath, basename = File.split(abspath)
- break if basename == "/"
- end
- end
-
- # --- Inflection --------------------------------------------------------------------------------
-
@@ -127 +28 @@
- path_type = ruby?(abspath) ? "file" : "directory"
+ path_type = @fs.rb_extension?(abspath) ? "file" : "directory"
lib/zeitwerk/real_mod_name.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:19.199922109 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:19.203922101 +0000
@@ -10 +10 @@
- # We need this indirection becasue the `name` method can be overridden, and
+ # We need this indirection because the `name` method can be overridden, and
lib/zeitwerk/registry.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/registry.rb 2026-02-20 03:33:19.199922109 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/registry.rb 2026-02-20 03:33:19.203922101 +0000
@@ -46,0 +47,25 @@
+ #: (Zeitwerk::Loader, String) -> Zeitwerk::Loader?
+ def conflicting_root_dir?(loader, new_root_dir)
+ @mutex.synchronize do
+ loaders.each do |existing_loader|
+ next if existing_loader == loader
+
+ existing_loader.__roots.each_key do |existing_root_dir|
+ # Conflicting directories are rare, optimize for the common case.
+ next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
+
+ new_root_dir_slash = new_root_dir + "/"
+ existing_root_dir_slash = existing_root_dir + "/"
+ next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
+
+ next if loader.__ignores?(existing_root_dir)
+ break if existing_loader.__ignores?(new_root_dir)
+
+ return existing_loader
+ end
+ end
+
+ nil
+ end
+ end
+
@@ -61,0 +87 @@
+ @mutex = Mutex.new
lib/zeitwerk/registry/loaders.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:19.200922107 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:19.203922101 +0000
@@ -9,2 +9,2 @@
- def each(&block)
- @loaders.each(&block)
+ def each(&)
+ @loaders.each(&)
lib/zeitwerk/version.rb
--- /tmp/d20260220-438-2syz3k/zeitwerk-2.7.4/lib/zeitwerk/version.rb 2026-02-20 03:33:19.200922107 +0000
+++ /tmp/d20260220-438-2syz3k/zeitwerk-2.7.5/lib/zeitwerk/version.rb 2026-02-20 03:33:19.203922101 +0000
@@ -5 +5 @@
- VERSION = "2.7.4"
+ VERSION = "2.7.5" |
Contributor
gem compare zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT date:
2.7.4: 2025-12-16 00:00:00 UTC
2.7.5: 1980-01-02 00:00:00 UTC
DIFFERENT rubygems_version:
2.7.4: 3.4.19
2.7.5: 4.0.3
DIFFERENT version:
2.7.4: 2.7.4
2.7.5: 2.7.5
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb +165/-0
* Changed:
README.md +19/-0
lib/zeitwerk/gem_loader.rb +2/-2
lib/zeitwerk/loader.rb +43/-48
lib/zeitwerk/loader/callbacks.rb +3/-3
lib/zeitwerk/loader/config.rb +9/-9
lib/zeitwerk/loader/eager_load.rb +26/-28
lib/zeitwerk/loader/helpers.rb +1/-100
lib/zeitwerk/real_mod_name.rb +1/-1
lib/zeitwerk/registry.rb +26/-0
lib/zeitwerk/registry/loaders.rb +2/-2
lib/zeitwerk/version.rb +1/-1 |
Contributor
gem compare --diff zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb
--- /tmp/20260220-412-b2n0kt 2026-02-20 03:33:37.396972916 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/loader/file_system.rb 2026-02-20 03:33:37.395972906 +0000
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+# This private class encapsulates interactions with the file system.
+#
+# It is used to list directories and check file types, and it encodes the
+# conventions documented in the README.
+#
+# @private
+class Zeitwerk::Loader::FileSystem # :nodoc:
+ #: (Zeitwerk::Loader) -> void
+ def initialize(loader)
+ @loader = loader
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def ls(dir)
+ children = relevant_dir_entries(dir)
+
+ # The order in which a directory is listed depends on the file system.
+ #
+ # Since client code may run on different platforms, it seems convenient to
+ # sort directory entries. This provides more deterministic behavior, with
+ # consistent eager loading in particular.
+ children.sort_by!(&:first)
+
+ children.each do |basename, abspath, ftype|
+ if :directory == ftype && !has_at_least_one_ruby_file?(abspath)
+ @loader.__log { "directory #{abspath} is ignored because it has no Ruby files" }
+ next
+ end
+
+ yield basename, abspath, ftype
+ end
+ end
+
+ #: (String) { (String) -> void } -> void
+ def walk_up(abspath)
+ loop do
+ yield abspath
+ abspath, basename = File.split(abspath)
+ break if basename == "/"
+ end
+ end
+
+ # Encodes the documented conventions.
+ #
+ #: (String) -> Symbol?
+ def supported_ftype?(abspath)
+ if rb_extension?(abspath)
+ :file # By convention, we can avoid a syscall here.
+ elsif dir?(abspath)
+ :directory
+ end
+ end
+
+ #: (String) -> bool
+ def rb_extension?(path)
+ path.end_with?(".rb")
+ end
+
+ #: (String) -> bool
+ def dir?(path)
+ File.directory?(path)
+ end
+
+ #: (String) -> bool
+ def hidden?(basename)
+ basename.start_with?(".")
+ end
+
+ private
+
+ # Looks for a Ruby file using breadth-first search. This type of search is
+ # important to list as less directories as possible and return fast in the
+ # common case in which there are Ruby files in the passed directory.
+ #
+ #: (String) -> bool
+ def has_at_least_one_ruby_file?(dir)
+ to_visit = [dir]
+
+ while (dir = to_visit.shift)
+ relevant_dir_entries(dir) do |_, abspath, ftype|
+ return true if :file == ftype
+ to_visit << abspath
+ end
+ end
+
+ false
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ #: (String) -> [[String, String, Symbol]]
+ def relevant_dir_entries(dir)
+ return enum_for(__method__, dir).to_a unless block_given?
+
+ each_ruby_file_or_directory(dir) do |basename, abspath, ftype|
+ next if @loader.__ignored_path?(abspath)
+
+ if :link == ftype
+ begin
+ ftype = File.stat(abspath).ftype.to_sym
+ rescue Errno::ENOENT
+ warn "ignoring broken symlink #{abspath}"
+ next
+ end
+ end
+
+ if :file == ftype
+ yield basename, abspath, ftype if rb_extension?(basename)
+ elsif :directory == ftype
+ # Conceptually, root directories represent a separate project tree.
+ yield basename, abspath, ftype unless @loader.__root_dir?(abspath)
+ end
+ end
+ end
+
+ # Dir.scan is more efficient in common platforms, but it is going to take a
+ # while for it to be available.
+ #
+ # The following compatibility methods have the same semantics but are written
+ # to favor the performance of the Ruby fallback, which can save syscalls.
+ #
+ # In particular, by convention, any directory entry with a .rb extension is
+ # assumed to be a file or a symlink to a file.
+ #
+ # These methods also freeze abspaths because that saves allocations when
+ # passed later to File methods. See https://github.com/fxn/zeitwerk/pull/125.
+
+ if Dir.respond_to?(:scan) # Available in Ruby 4.1.
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.scan(dir) do |basename, ftype|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ elsif :directory == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory
+ elsif :link == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory if dir?(abspath)
+ end
+ end
+ end
+ else
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.each_child(dir) do |basename|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ else
+ abspath = File.join(dir, basename).freeze
+ if dir?(abspath)
+ yield basename, abspath, :directory
+ end
+ end
+ end
+ end
+ end
+end
* Changed:
README.md
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/README.md 2026-02-20 03:33:37.389972846 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/README.md 2026-02-20 03:33:37.393972886 +0000
@@ -60,0 +61 @@
+ - [Autoloaded Constants](#autoloaded-constants)
@@ -1265,0 +1267,12 @@
+<a id="markdown-autoloaded-constants" name="autoloaded-constants"></a>
+#### Autoloaded Constants
+
+Zeitwerk does not keep track of autoloaded constants to minimize its memory footprint, but you can collect them with `on_load` if you will:
+
+```ruby
+autoloaded_cpaths = []
+loader.on_load do |cpath, _value, _abspath|
+ autoloaded_cpaths << cpath
+end
+```
+
@@ -1408,0 +1422,6 @@
+```
+
+That also accepts a line number:
+
+```
+bin/test test/lib/zeitwerk/test_eager_load.rb:52
lib/zeitwerk/gem_loader.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:37.390972856 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:37.394972896 +0000
@@ -45 +45 @@
- ls(@root_dir) do |basename, abspath, ftype|
+ @fs.ls(@root_dir) do |basename, abspath, ftype|
@@ -50 +50 @@
- cname = inflector.camelize(basename_without_ext, abspath).to_sym
+ cname = cname_for(basename_without_ext, abspath)
lib/zeitwerk/loader.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/loader.rb 2026-02-20 03:33:37.390972856 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/loader.rb 2026-02-20 03:33:37.394972896 +0000
@@ -11,0 +12 @@
+ require_relative "loader/file_system"
@@ -21,3 +21,0 @@
- MUTEX = Mutex.new #: Mutex
- private_constant :MUTEX
-
@@ -117,0 +116 @@
+ @fs = FileSystem.new(self)
@@ -174 +173 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -192 +191 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -202 +201 @@
- # https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
+ # https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
@@ -258 +257 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -261 +260 @@
- result[abspath] = prefix + inflector.camelize(basename, abspath)
+ result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
@@ -266 +265 @@
- queue << [abspath, prefix + inflector.camelize(basename, abspath)]
+ queue << [abspath, "#{prefix}#{cname_for(basename, abspath)}"]
@@ -282 +281,3 @@
- return unless dir?(abspath) || ruby?(abspath)
+ ftype = @fs.supported_ftype?(abspath)
+ return unless ftype
+
@@ -287 +288 @@
- if ruby?(abspath)
+ if :file == ftype
@@ -289 +290 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -299 +300 @@
- walk_up(walk_up_from) do |dir|
+ @fs.walk_up(walk_up_from) do |dir|
@@ -304 +305 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -365,0 +367,10 @@
+ #: { () -> String } -> void
+ internal def log
+ return unless logger
+
+ message = yield
+ method_name = logger.respond_to?(:debug) ? :debug : :call
+ logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
+ end
+
+
@@ -467 +478 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -486 +497 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -507 +518 @@
- log("the namespace #{cref} already exists, descending into #{subdir}") if logger
+ log { "the namespace #{cref} already exists, descending into #{subdir}" }
@@ -516 +527 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -518 +529 @@
- log("file #{file} is ignored because #{autoload_path} has precedence") if logger
+ log { "file #{file} is ignored because #{autoload_path} has precedence" }
@@ -524 +535 @@
- log("file #{file} is ignored because #{cref} is already defined") if logger
+ log { "file #{file} is ignored because #{cref} is already defined" }
@@ -538 +549 @@
- log("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
+ log { "earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}" }
@@ -551,2 +562,2 @@
- if ruby?(abspath)
- log("autoload set for #{cref}, to be loaded from #{abspath}")
+ if @fs.rb_extension?(abspath)
+ log { "autoload set for #{cref}, to be loaded from #{abspath}" }
@@ -554 +565 @@
- log("autoload set for #{cref}, to be autovivified from #{abspath}")
+ log { "autoload set for #{cref}, to be autovivified from #{abspath}" }
@@ -598,22 +609,6 @@
- private def raise_if_conflicting_directory(dir)
- MUTEX.synchronize do
- Registry.loaders.each do |loader|
- next if loader == self
-
- loader.__roots.each_key do |root_dir|
- # Conflicting directories are rare, optimize for the common case.
- next if !dir.start_with?(root_dir) && !root_dir.start_with?(dir)
-
- dir_slash = dir + "/"
- root_dir_slash = root_dir + "/"
- next if !dir_slash.start_with?(root_dir_slash) && !root_dir_slash.start_with?(dir_slash)
-
- next if ignores?(root_dir)
- break if loader.__ignores?(dir)
-
- require "pp" # Needed to have pretty_inspect available.
- raise Error,
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
- end
- end
+ private def raise_if_conflicting_root_dir(root_dir)
+ if loader = Registry.conflicting_root_dir?(self, root_dir)
+ require "pp" # Needed to have pretty_inspect available.
+ raise Error,
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{root_dir}," \
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
@@ -633 +628 @@
- log("autoload for #{cref} removed") if logger
+ log { "autoload for #{cref} removed" }
@@ -645 +640 @@
- log("#{cref} unloaded") if logger
+ log { "#{cref} unloaded" }
lib/zeitwerk/loader/callbacks.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:37.390972856 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:37.394972896 +0000
@@ -15 +15 @@
- log("constant #{cref} loaded from file #{file}") if logger
+ log { "constant #{cref} loaded from file #{file}" }
@@ -20 +20 @@
- log(msg) if logger
+ log { msg }
@@ -55 +55 @@
- log("module #{cref} autovivified from directory #{dir}") if logger
+ log { "module #{cref} autovivified from directory #{dir}" }
lib/zeitwerk/loader/config.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:37.390972856 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:37.394972896 +0000
@@ -119,2 +119,2 @@
- if dir?(abspath)
- raise_if_conflicting_directory(abspath)
+ if @fs.dir?(abspath)
+ raise_if_conflicting_root_dir(abspath)
@@ -293 +293 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -295 +295 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
@@ -302 +302 @@
- private def ignored_path?(abspath)
+ internal def ignored_path?(abspath)
@@ -309 +309 @@
- !dir?(root_dir) || ignored_path?(root_dir)
+ !@fs.dir?(root_dir) || ignored_path?(root_dir)
@@ -314 +314 @@
- private def root_dir?(dir)
+ internal def root_dir?(dir)
@@ -323 +323 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -325 +325 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
lib/zeitwerk/loader/eager_load.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:37.390972856 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:37.394972896 +0000
@@ -14 +14 @@
- log("eager load start") if logger
+ log { "eager load start" }
@@ -27 +27 @@
- log("eager load end") if logger
+ log { "eager load end" }
@@ -37 +37 @@
- raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)
@@ -39 +39 @@
- cnames = []
+ paths = []
@@ -42 +42 @@
- walk_up(abspath) do |dir|
+ @fs.walk_up(abspath) do |dir|
@@ -49 +49 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -51,3 +51 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -61 +59,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -120 +119 @@
- raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
@@ -123,4 +122,2 @@
- basename = File.basename(abspath, ".rb")
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
-
- base_cname = inflector.camelize(basename, abspath).to_sym
+ file_basename = File.basename(abspath, ".rb")
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
@@ -129 +126 @@
- cnames = []
+ paths = []
@@ -131 +128 @@
- walk_up(File.dirname(abspath)) do |dir|
+ @fs.walk_up(File.dirname(abspath)) do |dir|
@@ -137 +134 @@
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)
@@ -139,3 +136 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -145,0 +141,2 @@
+ base_cname = cname_for(file_basename, abspath)
+
@@ -147 +144,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -164 +162 @@
- log("eager load directory #{dir} start") if logger
+ log { "eager load directory #{dir} start" }
@@ -168 +166 @@
- ls(current_dir) do |basename, abspath, ftype|
+ @fs.ls(current_dir) do |basename, abspath, ftype|
@@ -179 +177 @@
- cname = inflector.camelize(basename, abspath).to_sym
+ cname = cname_for(basename, abspath)
@@ -186 +184 @@
- log("eager load directory #{dir} end") if logger
+ log { "eager load directory #{dir} end" }
@@ -211 +209 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -216 +214 @@
- elsif segment == inflector.camelize(basename, abspath)
+ elsif segment == cname_for(basename, abspath).to_s
lib/zeitwerk/loader/helpers.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:37.390972856 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:37.395972906 +0000
@@ -4,99 +3,0 @@
- # --- Logging -----------------------------------------------------------------------------------
-
- #: (to_s() -> String) -> void
- private def log(message)
- method_name = logger.respond_to?(:debug) ? :debug : :call
- logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
- end
-
- # --- Files and directories ---------------------------------------------------------------------
-
- #: (String) { (String, String, Symbol) -> void } -> void
- private def ls(dir)
- children = Dir.children(dir)
-
- # The order in which a directory is listed depends on the file system.
- #
- # Since client code may run in different platforms, it seems convenient to
- # order directory entries. This provides consistent eager loading across
- # platforms, for example.
- children.sort!
-
- children.each do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- next if roots.key?(abspath)
-
- if !has_at_least_one_ruby_file?(abspath)
- log("directory #{abspath} is ignored because it has no Ruby files") if logger
- next
- end
-
- ftype = :directory
- else
- next unless ruby?(abspath)
- ftype = :file
- end
-
- # We freeze abspath because that saves allocations when passed later to
- # File methods. See #125.
- yield basename, abspath.freeze, ftype
- end
- end
-
- # Looks for a Ruby file using breadth-first search. This type of search is
- # important to list as less directories as possible and return fast in the
- # common case in which there are Ruby files.
- #
- #: (String) -> bool
- private def has_at_least_one_ruby_file?(dir)
- to_visit = [dir]
-
- while (dir = to_visit.shift)
- Dir.each_child(dir) do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- to_visit << abspath unless roots.key?(abspath)
- else
- return true if ruby?(abspath)
- end
- end
- end
-
- false
- end
-
- #: (String) -> bool
- private def ruby?(path)
- path.end_with?(".rb")
- end
-
- #: (String) -> bool
- private def dir?(path)
- File.directory?(path)
- end
-
- #: (String) -> bool
- private def hidden?(basename)
- basename.start_with?(".")
- end
-
- #: (String) { (String) -> void } -> void
- private def walk_up(abspath)
- loop do
- yield abspath
- abspath, basename = File.split(abspath)
- break if basename == "/"
- end
- end
-
- # --- Inflection --------------------------------------------------------------------------------
-
@@ -127 +28 @@
- path_type = ruby?(abspath) ? "file" : "directory"
+ path_type = @fs.rb_extension?(abspath) ? "file" : "directory"
lib/zeitwerk/real_mod_name.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:37.391972866 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:37.395972906 +0000
@@ -10 +10 @@
- # We need this indirection becasue the `name` method can be overridden, and
+ # We need this indirection because the `name` method can be overridden, and
lib/zeitwerk/registry.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/registry.rb 2026-02-20 03:33:37.391972866 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/registry.rb 2026-02-20 03:33:37.395972906 +0000
@@ -46,0 +47,25 @@
+ #: (Zeitwerk::Loader, String) -> Zeitwerk::Loader?
+ def conflicting_root_dir?(loader, new_root_dir)
+ @mutex.synchronize do
+ loaders.each do |existing_loader|
+ next if existing_loader == loader
+
+ existing_loader.__roots.each_key do |existing_root_dir|
+ # Conflicting directories are rare, optimize for the common case.
+ next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
+
+ new_root_dir_slash = new_root_dir + "/"
+ existing_root_dir_slash = existing_root_dir + "/"
+ next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
+
+ next if loader.__ignores?(existing_root_dir)
+ break if existing_loader.__ignores?(new_root_dir)
+
+ return existing_loader
+ end
+ end
+
+ nil
+ end
+ end
+
@@ -61,0 +87 @@
+ @mutex = Mutex.new
lib/zeitwerk/registry/loaders.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:37.391972866 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:37.395972906 +0000
@@ -9,2 +9,2 @@
- def each(&block)
- @loaders.each(&block)
+ def each(&)
+ @loaders.each(&)
lib/zeitwerk/version.rb
--- /tmp/d20260220-412-tjs18b/zeitwerk-2.7.4/lib/zeitwerk/version.rb 2026-02-20 03:33:37.391972866 +0000
+++ /tmp/d20260220-412-tjs18b/zeitwerk-2.7.5/lib/zeitwerk/version.rb 2026-02-20 03:33:37.395972906 +0000
@@ -5 +5 @@
- VERSION = "2.7.4"
+ VERSION = "2.7.5" |
Contributor
gem compare --diff zeitwerk 2.7.4 2.7.5Compared versions: ["2.7.4", "2.7.5"]
DIFFERENT files:
2.7.4->2.7.5:
* Added:
lib/zeitwerk/loader/file_system.rb
--- /tmp/20260220-410-n0kuna 2026-02-20 03:33:38.328375284 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/loader/file_system.rb 2026-02-20 03:33:38.327375285 +0000
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+# This private class encapsulates interactions with the file system.
+#
+# It is used to list directories and check file types, and it encodes the
+# conventions documented in the README.
+#
+# @private
+class Zeitwerk::Loader::FileSystem # :nodoc:
+ #: (Zeitwerk::Loader) -> void
+ def initialize(loader)
+ @loader = loader
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def ls(dir)
+ children = relevant_dir_entries(dir)
+
+ # The order in which a directory is listed depends on the file system.
+ #
+ # Since client code may run on different platforms, it seems convenient to
+ # sort directory entries. This provides more deterministic behavior, with
+ # consistent eager loading in particular.
+ children.sort_by!(&:first)
+
+ children.each do |basename, abspath, ftype|
+ if :directory == ftype && !has_at_least_one_ruby_file?(abspath)
+ @loader.__log { "directory #{abspath} is ignored because it has no Ruby files" }
+ next
+ end
+
+ yield basename, abspath, ftype
+ end
+ end
+
+ #: (String) { (String) -> void } -> void
+ def walk_up(abspath)
+ loop do
+ yield abspath
+ abspath, basename = File.split(abspath)
+ break if basename == "/"
+ end
+ end
+
+ # Encodes the documented conventions.
+ #
+ #: (String) -> Symbol?
+ def supported_ftype?(abspath)
+ if rb_extension?(abspath)
+ :file # By convention, we can avoid a syscall here.
+ elsif dir?(abspath)
+ :directory
+ end
+ end
+
+ #: (String) -> bool
+ def rb_extension?(path)
+ path.end_with?(".rb")
+ end
+
+ #: (String) -> bool
+ def dir?(path)
+ File.directory?(path)
+ end
+
+ #: (String) -> bool
+ def hidden?(basename)
+ basename.start_with?(".")
+ end
+
+ private
+
+ # Looks for a Ruby file using breadth-first search. This type of search is
+ # important to list as less directories as possible and return fast in the
+ # common case in which there are Ruby files in the passed directory.
+ #
+ #: (String) -> bool
+ def has_at_least_one_ruby_file?(dir)
+ to_visit = [dir]
+
+ while (dir = to_visit.shift)
+ relevant_dir_entries(dir) do |_, abspath, ftype|
+ return true if :file == ftype
+ to_visit << abspath
+ end
+ end
+
+ false
+ end
+
+ #: (String) { (String, String, Symbol) -> void } -> void
+ #: (String) -> [[String, String, Symbol]]
+ def relevant_dir_entries(dir)
+ return enum_for(__method__, dir).to_a unless block_given?
+
+ each_ruby_file_or_directory(dir) do |basename, abspath, ftype|
+ next if @loader.__ignored_path?(abspath)
+
+ if :link == ftype
+ begin
+ ftype = File.stat(abspath).ftype.to_sym
+ rescue Errno::ENOENT
+ warn "ignoring broken symlink #{abspath}"
+ next
+ end
+ end
+
+ if :file == ftype
+ yield basename, abspath, ftype if rb_extension?(basename)
+ elsif :directory == ftype
+ # Conceptually, root directories represent a separate project tree.
+ yield basename, abspath, ftype unless @loader.__root_dir?(abspath)
+ end
+ end
+ end
+
+ # Dir.scan is more efficient in common platforms, but it is going to take a
+ # while for it to be available.
+ #
+ # The following compatibility methods have the same semantics but are written
+ # to favor the performance of the Ruby fallback, which can save syscalls.
+ #
+ # In particular, by convention, any directory entry with a .rb extension is
+ # assumed to be a file or a symlink to a file.
+ #
+ # These methods also freeze abspaths because that saves allocations when
+ # passed later to File methods. See https://github.com/fxn/zeitwerk/pull/125.
+
+ if Dir.respond_to?(:scan) # Available in Ruby 4.1.
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.scan(dir) do |basename, ftype|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ elsif :directory == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory
+ elsif :link == ftype
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :directory if dir?(abspath)
+ end
+ end
+ end
+ else
+ #: (String) { (String, String, Symbol) -> void } -> void
+ def each_ruby_file_or_directory(dir)
+ Dir.each_child(dir) do |basename|
+ next if hidden?(basename)
+
+ if rb_extension?(basename)
+ abspath = File.join(dir, basename).freeze
+ yield basename, abspath, :file # By convention.
+ else
+ abspath = File.join(dir, basename).freeze
+ if dir?(abspath)
+ yield basename, abspath, :directory
+ end
+ end
+ end
+ end
+ end
+end
* Changed:
README.md
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/README.md 2026-02-20 03:33:38.321375287 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/README.md 2026-02-20 03:33:38.324375286 +0000
@@ -60,0 +61 @@
+ - [Autoloaded Constants](#autoloaded-constants)
@@ -1265,0 +1267,12 @@
+<a id="markdown-autoloaded-constants" name="autoloaded-constants"></a>
+#### Autoloaded Constants
+
+Zeitwerk does not keep track of autoloaded constants to minimize its memory footprint, but you can collect them with `on_load` if you will:
+
+```ruby
+autoloaded_cpaths = []
+loader.on_load do |cpath, _value, _abspath|
+ autoloaded_cpaths << cpath
+end
+```
+
@@ -1408,0 +1422,6 @@
+```
+
+That also accepts a line number:
+
+```
+bin/test test/lib/zeitwerk/test_eager_load.rb:52
lib/zeitwerk/gem_loader.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:38.322375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/gem_loader.rb 2026-02-20 03:33:38.326375285 +0000
@@ -45 +45 @@
- ls(@root_dir) do |basename, abspath, ftype|
+ @fs.ls(@root_dir) do |basename, abspath, ftype|
@@ -50 +50 @@
- cname = inflector.camelize(basename_without_ext, abspath).to_sym
+ cname = cname_for(basename_without_ext, abspath)
lib/zeitwerk/loader.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/loader.rb 2026-02-20 03:33:38.323375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/loader.rb 2026-02-20 03:33:38.326375285 +0000
@@ -11,0 +12 @@
+ require_relative "loader/file_system"
@@ -21,3 +21,0 @@
- MUTEX = Mutex.new #: Mutex
- private_constant :MUTEX
-
@@ -117,0 +116 @@
+ @fs = FileSystem.new(self)
@@ -174 +173 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -192 +191 @@
- unloaded_files.add(abspath) if ruby?(abspath)
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
@@ -202 +201 @@
- # https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
+ # https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
@@ -258 +257 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -261 +260 @@
- result[abspath] = prefix + inflector.camelize(basename, abspath)
+ result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
@@ -266 +265 @@
- queue << [abspath, prefix + inflector.camelize(basename, abspath)]
+ queue << [abspath, "#{prefix}#{cname_for(basename, abspath)}"]
@@ -282 +281,3 @@
- return unless dir?(abspath) || ruby?(abspath)
+ ftype = @fs.supported_ftype?(abspath)
+ return unless ftype
+
@@ -287 +288 @@
- if ruby?(abspath)
+ if :file == ftype
@@ -289 +290 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -299 +300 @@
- walk_up(walk_up_from) do |dir|
+ @fs.walk_up(walk_up_from) do |dir|
@@ -304 +305 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -365,0 +367,10 @@
+ #: { () -> String } -> void
+ internal def log
+ return unless logger
+
+ message = yield
+ method_name = logger.respond_to?(:debug) ? :debug : :call
+ logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
+ end
+
+
@@ -467 +478 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -486 +497 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -507 +518 @@
- log("the namespace #{cref} already exists, descending into #{subdir}") if logger
+ log { "the namespace #{cref} already exists, descending into #{subdir}" }
@@ -516 +527 @@
- if ruby?(autoload_path)
+ if @fs.rb_extension?(autoload_path)
@@ -518 +529 @@
- log("file #{file} is ignored because #{autoload_path} has precedence") if logger
+ log { "file #{file} is ignored because #{autoload_path} has precedence" }
@@ -524 +535 @@
- log("file #{file} is ignored because #{cref} is already defined") if logger
+ log { "file #{file} is ignored because #{cref} is already defined" }
@@ -538 +549 @@
- log("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
+ log { "earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}" }
@@ -551,2 +562,2 @@
- if ruby?(abspath)
- log("autoload set for #{cref}, to be loaded from #{abspath}")
+ if @fs.rb_extension?(abspath)
+ log { "autoload set for #{cref}, to be loaded from #{abspath}" }
@@ -554 +565 @@
- log("autoload set for #{cref}, to be autovivified from #{abspath}")
+ log { "autoload set for #{cref}, to be autovivified from #{abspath}" }
@@ -598,22 +609,6 @@
- private def raise_if_conflicting_directory(dir)
- MUTEX.synchronize do
- Registry.loaders.each do |loader|
- next if loader == self
-
- loader.__roots.each_key do |root_dir|
- # Conflicting directories are rare, optimize for the common case.
- next if !dir.start_with?(root_dir) && !root_dir.start_with?(dir)
-
- dir_slash = dir + "/"
- root_dir_slash = root_dir + "/"
- next if !dir_slash.start_with?(root_dir_slash) && !root_dir_slash.start_with?(dir_slash)
-
- next if ignores?(root_dir)
- break if loader.__ignores?(dir)
-
- require "pp" # Needed to have pretty_inspect available.
- raise Error,
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
- end
- end
+ private def raise_if_conflicting_root_dir(root_dir)
+ if loader = Registry.conflicting_root_dir?(self, root_dir)
+ require "pp" # Needed to have pretty_inspect available.
+ raise Error,
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{root_dir}," \
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
@@ -633 +628 @@
- log("autoload for #{cref} removed") if logger
+ log { "autoload for #{cref} removed" }
@@ -645 +640 @@
- log("#{cref} unloaded") if logger
+ log { "#{cref} unloaded" }
lib/zeitwerk/loader/callbacks.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:38.323375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/loader/callbacks.rb 2026-02-20 03:33:38.326375285 +0000
@@ -15 +15 @@
- log("constant #{cref} loaded from file #{file}") if logger
+ log { "constant #{cref} loaded from file #{file}" }
@@ -20 +20 @@
- log(msg) if logger
+ log { msg }
@@ -55 +55 @@
- log("module #{cref} autovivified from directory #{dir}") if logger
+ log { "module #{cref} autovivified from directory #{dir}" }
lib/zeitwerk/loader/config.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:38.323375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/loader/config.rb 2026-02-20 03:33:38.326375285 +0000
@@ -119,2 +119,2 @@
- if dir?(abspath)
- raise_if_conflicting_directory(abspath)
+ if @fs.dir?(abspath)
+ raise_if_conflicting_root_dir(abspath)
@@ -293 +293 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -295 +295 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
@@ -302 +302 @@
- private def ignored_path?(abspath)
+ internal def ignored_path?(abspath)
@@ -309 +309 @@
- !dir?(root_dir) || ignored_path?(root_dir)
+ !@fs.dir?(root_dir) || ignored_path?(root_dir)
@@ -314 +314 @@
- private def root_dir?(dir)
+ internal def root_dir?(dir)
@@ -323 +323 @@
- walk_up(abspath) do |path|
+ @fs.walk_up(abspath) do |path|
@@ -325 +325 @@
- return false if roots.key?(path)
+ return false if root_dir?(path)
lib/zeitwerk/loader/eager_load.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:38.323375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/loader/eager_load.rb 2026-02-20 03:33:38.327375285 +0000
@@ -14 +14 @@
- log("eager load start") if logger
+ log { "eager load start" }
@@ -27 +27 @@
- log("eager load end") if logger
+ log { "eager load end" }
@@ -37 +37 @@
- raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)
@@ -39 +39 @@
- cnames = []
+ paths = []
@@ -42 +42 @@
- walk_up(abspath) do |dir|
+ @fs.walk_up(abspath) do |dir|
@@ -49 +49 @@
- return if hidden?(basename)
+ return if @fs.hidden?(basename)
@@ -51,3 +51 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -61 +59,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -120 +119 @@
- raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
+ raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
@@ -123,4 +122,2 @@
- basename = File.basename(abspath, ".rb")
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
-
- base_cname = inflector.camelize(basename, abspath).to_sym
+ file_basename = File.basename(abspath, ".rb")
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
@@ -129 +126 @@
- cnames = []
+ paths = []
@@ -131 +128 @@
- walk_up(File.dirname(abspath)) do |dir|
+ @fs.walk_up(File.dirname(abspath)) do |dir|
@@ -137 +134 @@
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)
@@ -139,3 +136 @@
- unless collapse?(dir)
- cnames << inflector.camelize(basename, dir).to_sym
- end
+ paths << [basename, dir] unless collapse?(dir)
@@ -145,0 +141,2 @@
+ base_cname = cname_for(file_basename, abspath)
+
@@ -147 +144,2 @@
- cnames.reverse_each do |cname|
+ paths.reverse_each do |basename, dir|
+ cname = cname_for(basename, dir)
@@ -164 +162 @@
- log("eager load directory #{dir} start") if logger
+ log { "eager load directory #{dir} start" }
@@ -168 +166 @@
- ls(current_dir) do |basename, abspath, ftype|
+ @fs.ls(current_dir) do |basename, abspath, ftype|
@@ -179 +177 @@
- cname = inflector.camelize(basename, abspath).to_sym
+ cname = cname_for(basename, abspath)
@@ -186 +184 @@
- log("eager load directory #{dir} end") if logger
+ log { "eager load directory #{dir} end" }
@@ -211 +209 @@
- ls(dir) do |basename, abspath, ftype|
+ @fs.ls(dir) do |basename, abspath, ftype|
@@ -216 +214 @@
- elsif segment == inflector.camelize(basename, abspath)
+ elsif segment == cname_for(basename, abspath).to_s
lib/zeitwerk/loader/helpers.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:38.323375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/loader/helpers.rb 2026-02-20 03:33:38.327375285 +0000
@@ -4,99 +3,0 @@
- # --- Logging -----------------------------------------------------------------------------------
-
- #: (to_s() -> String) -> void
- private def log(message)
- method_name = logger.respond_to?(:debug) ? :debug : :call
- logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
- end
-
- # --- Files and directories ---------------------------------------------------------------------
-
- #: (String) { (String, String, Symbol) -> void } -> void
- private def ls(dir)
- children = Dir.children(dir)
-
- # The order in which a directory is listed depends on the file system.
- #
- # Since client code may run in different platforms, it seems convenient to
- # order directory entries. This provides consistent eager loading across
- # platforms, for example.
- children.sort!
-
- children.each do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- next if roots.key?(abspath)
-
- if !has_at_least_one_ruby_file?(abspath)
- log("directory #{abspath} is ignored because it has no Ruby files") if logger
- next
- end
-
- ftype = :directory
- else
- next unless ruby?(abspath)
- ftype = :file
- end
-
- # We freeze abspath because that saves allocations when passed later to
- # File methods. See #125.
- yield basename, abspath.freeze, ftype
- end
- end
-
- # Looks for a Ruby file using breadth-first search. This type of search is
- # important to list as less directories as possible and return fast in the
- # common case in which there are Ruby files.
- #
- #: (String) -> bool
- private def has_at_least_one_ruby_file?(dir)
- to_visit = [dir]
-
- while (dir = to_visit.shift)
- Dir.each_child(dir) do |basename|
- next if hidden?(basename)
-
- abspath = File.join(dir, basename)
- next if ignored_path?(abspath)
-
- if dir?(abspath)
- to_visit << abspath unless roots.key?(abspath)
- else
- return true if ruby?(abspath)
- end
- end
- end
-
- false
- end
-
- #: (String) -> bool
- private def ruby?(path)
- path.end_with?(".rb")
- end
-
- #: (String) -> bool
- private def dir?(path)
- File.directory?(path)
- end
-
- #: (String) -> bool
- private def hidden?(basename)
- basename.start_with?(".")
- end
-
- #: (String) { (String) -> void } -> void
- private def walk_up(abspath)
- loop do
- yield abspath
- abspath, basename = File.split(abspath)
- break if basename == "/"
- end
- end
-
- # --- Inflection --------------------------------------------------------------------------------
-
@@ -127 +28 @@
- path_type = ruby?(abspath) ? "file" : "directory"
+ path_type = @fs.rb_extension?(abspath) ? "file" : "directory"
lib/zeitwerk/real_mod_name.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:38.323375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/real_mod_name.rb 2026-02-20 03:33:38.327375285 +0000
@@ -10 +10 @@
- # We need this indirection becasue the `name` method can be overridden, and
+ # We need this indirection because the `name` method can be overridden, and
lib/zeitwerk/registry.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/registry.rb 2026-02-20 03:33:38.323375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/registry.rb 2026-02-20 03:33:38.327375285 +0000
@@ -46,0 +47,25 @@
+ #: (Zeitwerk::Loader, String) -> Zeitwerk::Loader?
+ def conflicting_root_dir?(loader, new_root_dir)
+ @mutex.synchronize do
+ loaders.each do |existing_loader|
+ next if existing_loader == loader
+
+ existing_loader.__roots.each_key do |existing_root_dir|
+ # Conflicting directories are rare, optimize for the common case.
+ next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
+
+ new_root_dir_slash = new_root_dir + "/"
+ existing_root_dir_slash = existing_root_dir + "/"
+ next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
+
+ next if loader.__ignores?(existing_root_dir)
+ break if existing_loader.__ignores?(new_root_dir)
+
+ return existing_loader
+ end
+ end
+
+ nil
+ end
+ end
+
@@ -61,0 +87 @@
+ @mutex = Mutex.new
lib/zeitwerk/registry/loaders.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:38.324375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/registry/loaders.rb 2026-02-20 03:33:38.327375285 +0000
@@ -9,2 +9,2 @@
- def each(&block)
- @loaders.each(&block)
+ def each(&)
+ @loaders.each(&)
lib/zeitwerk/version.rb
--- /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.4/lib/zeitwerk/version.rb 2026-02-20 03:33:38.324375286 +0000
+++ /tmp/d20260220-410-3n1fwj/zeitwerk-2.7.5/lib/zeitwerk/version.rb 2026-02-20 03:33:38.327375285 +0000
@@ -5 +5 @@
- VERSION = "2.7.4"
+ VERSION = "2.7.5" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps zeitwerk from 2.7.4 to 2.7.5.
Changelog
Sourced from zeitwerk's changelog.
Commits
adfeec4Ready for 2.7.5a22d742Use the now yielded cwd in a few tests5df497fAdds unit tests for Zeitwerk::Loader::FileSystem0a7021aLet with_(files|setup) yield the cwd976b8f1Update code comment8398da8Let the log method take a block812d0eeUse Dir.scan if availablef845a27Delete PoC file112cfdfdirectory -> dir for consistency064b76cAdd a section about predicates to PROJECT_RULES.mdDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)