zeitwerk 2.7.1 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/zeitwerk/core_ext/module.rb +5 -4
- data/lib/zeitwerk/cref/map.rb +124 -0
- data/lib/zeitwerk/cref.rb +6 -3
- data/lib/zeitwerk/internal.rb +1 -0
- data/lib/zeitwerk/loader/callbacks.rb +14 -16
- data/lib/zeitwerk/loader/config.rb +8 -8
- data/lib/zeitwerk/loader.rb +89 -56
- data/lib/zeitwerk/null_inflector.rb +1 -0
- data/lib/zeitwerk/registry/explicit_namespaces.rb +64 -0
- data/lib/zeitwerk/registry/inceptions.rb +31 -0
- data/lib/zeitwerk/registry.rb +3 -59
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +0 -1
- metadata +6 -7
- data/lib/zeitwerk/explicit_namespace.rb +0 -113
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 1a07f90eb2f155582d05f58527ffcbc2f4d76c9a1983260ca8d527becaeb7972
         | 
| 4 | 
            +
              data.tar.gz: 65e8dc78ca8e6de674f0fc7d88aad5c9bad0d7687bc9ed26f93d6fa0e6d18e90
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: d7b9d13e3d3d5bf0497ec259bf0817256586e245f7d47c951b8392784f715bc71c20d0ec3c9e465077da6d8e729ca6888fbaaa24820fe4459771e29340ee6d05
         | 
| 7 | 
            +
              data.tar.gz: 8b1322d36bc9115a56b6abab6be9549c868e0edd2025fe82dd2c5d0abb082fac8532c82ed03f895d34f2875f27b160f4861185112c4aef2a3129e46569115c0f
         | 
| @@ -1,16 +1,17 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            module Zeitwerk::ConstAdded
         | 
| 3 | 
            +
            module Zeitwerk::ConstAdded # :nodoc:
         | 
| 4 | 
            +
              # @sig (Symbol) -> void
         | 
| 4 5 | 
             
              def const_added(cname)
         | 
| 5 | 
            -
                if loader = Zeitwerk:: | 
| 6 | 
            +
                if loader = Zeitwerk::Registry::ExplicitNamespaces.__loader_for(self, cname)
         | 
| 6 7 | 
             
                  namespace = const_get(cname, false)
         | 
| 7 8 |  | 
| 8 9 | 
             
                  unless namespace.is_a?(Module)
         | 
| 9 10 | 
             
                    cref = Zeitwerk::Cref.new(self, cname)
         | 
| 10 | 
            -
                    raise Zeitwerk::Error, "#{cref | 
| 11 | 
            +
                    raise Zeitwerk::Error, "#{cref} is expected to be a namespace, should be a class or module (got #{namespace.class})"
         | 
| 11 12 | 
             
                  end
         | 
| 12 13 |  | 
| 13 | 
            -
                  loader. | 
| 14 | 
            +
                  loader.__on_namespace_loaded(Zeitwerk::Cref.new(self, cname), namespace)
         | 
| 14 15 | 
             
                end
         | 
| 15 16 | 
             
                super
         | 
| 16 17 | 
             
              end
         | 
| @@ -0,0 +1,124 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # This class emulates a hash table whose keys are of type Zeitwerk::Cref.
         | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            # It is a synchronized 2-level hash. The keys of the top one, stored in `@map`,
         | 
| 6 | 
            +
            # are class and module objects, but their hash code is forced to be their object
         | 
| 7 | 
            +
            # IDs (see why below). Then, each one of them stores a hash table keyed on
         | 
| 8 | 
            +
            # constant names as symbols. We finally store the values in those.
         | 
| 9 | 
            +
            #
         | 
| 10 | 
            +
            # For example, if we store values 0, 1, and 2 for the crefs that would
         | 
| 11 | 
            +
            # correspond to `M::X`, `M::Y`, and `N::Z`, the map will look like this:
         | 
| 12 | 
            +
            #
         | 
| 13 | 
            +
            #   { M => { X: 0, :Y => 1 }, N => { Z: 2 } }
         | 
| 14 | 
            +
            #
         | 
| 15 | 
            +
            # This structure is internal, so only the needed interface is implemented.
         | 
| 16 | 
            +
            #
         | 
| 17 | 
            +
            # Why not use tables that map pairs [Module, Symbol] to their values? Because
         | 
| 18 | 
            +
            # class and module objects are not guaranteed to be hashable, the `hash` method
         | 
| 19 | 
            +
            # may have been overridden:
         | 
| 20 | 
            +
            #
         | 
| 21 | 
            +
            #   https://github.com/fxn/zeitwerk/issues/188
         | 
| 22 | 
            +
            #
         | 
| 23 | 
            +
            # We can also use a 1-level hash whose keys are the corresponding class and
         | 
| 24 | 
            +
            # module names. In the example above it would be:
         | 
| 25 | 
            +
            #
         | 
| 26 | 
            +
            #   { "M::X" => 0, "M::Y" => 1, "N::Z" => 2 }
         | 
| 27 | 
            +
            #
         | 
| 28 | 
            +
            # The gem used this approach for several years.
         | 
| 29 | 
            +
            #
         | 
| 30 | 
            +
            # Another option would be to make crefs hashable. I tried with hash code
         | 
| 31 | 
            +
            #
         | 
| 32 | 
            +
            #   real_mod_hash(mod) ^ cname.hash
         | 
| 33 | 
            +
            #
         | 
| 34 | 
            +
            # and the matching eql?, but that was about 1.8x slower.
         | 
| 35 | 
            +
            #
         | 
| 36 | 
            +
            # Finally, I came with this solution which is 1.6x faster than the previous one
         | 
| 37 | 
            +
            # based on class and module names, even being synchronized. Also, client code
         | 
| 38 | 
            +
            # feels natural, since crefs are central objects in Zeitwerk's implementation.
         | 
| 39 | 
            +
            class Zeitwerk::Cref::Map # :nodoc: all
         | 
| 40 | 
            +
              def initialize
         | 
| 41 | 
            +
                @map = {}
         | 
| 42 | 
            +
                @map.compare_by_identity
         | 
| 43 | 
            +
                @mutex = Mutex.new
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              # @sig (Zeitwerk::Cref, V) -> V
         | 
| 47 | 
            +
              def []=(cref, value)
         | 
| 48 | 
            +
                @mutex.synchronize do
         | 
| 49 | 
            +
                  cnames = (@map[cref.mod] ||= {})
         | 
| 50 | 
            +
                  cnames[cref.cname] = value
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              # @sig (Zeitwerk::Cref) -> top?
         | 
| 55 | 
            +
              def [](cref)
         | 
| 56 | 
            +
                @mutex.synchronize do
         | 
| 57 | 
            +
                  @map[cref.mod]&.[](cref.cname)
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
              # @sig (Zeitwerk::Cref, { () -> V }) -> V
         | 
| 62 | 
            +
              def get_or_set(cref, &block)
         | 
| 63 | 
            +
                @mutex.synchronize do
         | 
| 64 | 
            +
                  cnames = (@map[cref.mod] ||= {})
         | 
| 65 | 
            +
                  cnames.fetch(cref.cname) { cnames[cref.cname] = block.call }
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
              # @sig (Zeitwerk::Cref) -> top?
         | 
| 70 | 
            +
              def delete(cref)
         | 
| 71 | 
            +
                delete_mod_cname(cref.mod, cref.cname)
         | 
| 72 | 
            +
              end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              # Ad-hoc for loader_for, called from const_added. That is a hot path, I prefer
         | 
| 75 | 
            +
              # to not create a cref in every call, since that is global.
         | 
| 76 | 
            +
              #
         | 
| 77 | 
            +
              # @sig (Module, Symbol) -> top?
         | 
| 78 | 
            +
              def delete_mod_cname(mod, cname)
         | 
| 79 | 
            +
                @mutex.synchronize do
         | 
| 80 | 
            +
                  if cnames = @map[mod]
         | 
| 81 | 
            +
                    value = cnames.delete(cname)
         | 
| 82 | 
            +
                    @map.delete(mod) if cnames.empty?
         | 
| 83 | 
            +
                    value
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
              # @sig (top) -> void
         | 
| 89 | 
            +
              def delete_by_value(value)
         | 
| 90 | 
            +
                @mutex.synchronize do
         | 
| 91 | 
            +
                  @map.delete_if do |mod, cnames|
         | 
| 92 | 
            +
                    cnames.delete_if { _2 == value }
         | 
| 93 | 
            +
                    cnames.empty?
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
              end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
              # Order of yielded crefs is undefined.
         | 
| 99 | 
            +
              #
         | 
| 100 | 
            +
              # @sig () { (Zeitwerk::Cref) -> void } -> void
         | 
| 101 | 
            +
              def each_key
         | 
| 102 | 
            +
                @mutex.synchronize do
         | 
| 103 | 
            +
                  @map.each do |mod, cnames|
         | 
| 104 | 
            +
                    cnames.each_key do |cname|
         | 
| 105 | 
            +
                      yield Zeitwerk::Cref.new(mod, cname)
         | 
| 106 | 
            +
                    end
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
              end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
              # @sig () -> void
         | 
| 112 | 
            +
              def clear
         | 
| 113 | 
            +
                @mutex.synchronize do
         | 
| 114 | 
            +
                  @map.clear
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
              end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
              # @sig () -> bool
         | 
| 119 | 
            +
              def empty? # for tests
         | 
| 120 | 
            +
                @mutex.synchronize do
         | 
| 121 | 
            +
                  @map.empty?
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
              end
         | 
| 124 | 
            +
            end
         | 
    
        data/lib/zeitwerk/cref.rb
    CHANGED
    
    | @@ -3,7 +3,7 @@ | |
| 3 3 | 
             
            # This private class encapsulates pairs (mod, cname).
         | 
| 4 4 | 
             
            #
         | 
| 5 5 | 
             
            # Objects represent the constant cname in the class or module object mod, and
         | 
| 6 | 
            -
            # have API to manage them | 
| 6 | 
            +
            # have API to manage them. Examples:
         | 
| 7 7 | 
             
            #
         | 
| 8 8 | 
             
            #   cref.path
         | 
| 9 9 | 
             
            #   cref.set(value)
         | 
| @@ -11,6 +11,8 @@ | |
| 11 11 | 
             
            #
         | 
| 12 12 | 
             
            # The constant may or may not exist in mod.
         | 
| 13 13 | 
             
            class Zeitwerk::Cref
         | 
| 14 | 
            +
              require_relative "cref/map"
         | 
| 15 | 
            +
             | 
| 14 16 | 
             
              include Zeitwerk::RealModName
         | 
| 15 17 |  | 
| 16 18 | 
             
              # @sig Module
         | 
| @@ -33,6 +35,7 @@ class Zeitwerk::Cref | |
| 33 35 | 
             
              def path
         | 
| 34 36 | 
             
                @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}".freeze
         | 
| 35 37 | 
             
              end
         | 
| 38 | 
            +
              alias to_s path
         | 
| 36 39 |  | 
| 37 40 | 
             
              # @sig () -> String?
         | 
| 38 41 | 
             
              def autoload?
         | 
| @@ -49,13 +52,13 @@ class Zeitwerk::Cref | |
| 49 52 | 
             
                @mod.const_defined?(@cname, false)
         | 
| 50 53 | 
             
              end
         | 
| 51 54 |  | 
| 52 | 
            -
              # @sig ( | 
| 55 | 
            +
              # @sig (top) -> top
         | 
| 53 56 | 
             
              def set(value)
         | 
| 54 57 | 
             
                @mod.const_set(@cname, value)
         | 
| 55 58 | 
             
              end
         | 
| 56 59 |  | 
| 57 60 | 
             
              # @raise [NameError]
         | 
| 58 | 
            -
              # @sig () ->  | 
| 61 | 
            +
              # @sig () -> top
         | 
| 59 62 | 
             
              def get
         | 
| 60 63 | 
             
                @mod.const_get(@cname, false)
         | 
| 61 64 | 
             
              end
         | 
    
        data/lib/zeitwerk/internal.rb
    CHANGED
    
    
| @@ -1,7 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            module Zeitwerk::Loader::Callbacks
         | 
| 4 | 
            -
              include Zeitwerk::RealModName
         | 
| 3 | 
            +
            module Zeitwerk::Loader::Callbacks # :nodoc: all
         | 
| 5 4 | 
             
              extend Zeitwerk::Internal
         | 
| 6 5 |  | 
| 7 6 | 
             
              # Invoked from our decorated Kernel#require when a managed file is autoloaded.
         | 
| @@ -14,11 +13,11 @@ module Zeitwerk::Loader::Callbacks | |
| 14 13 | 
             
                Zeitwerk::Registry.unregister_autoload(file)
         | 
| 15 14 |  | 
| 16 15 | 
             
                if cref.defined?
         | 
| 17 | 
            -
                  log("constant #{cref | 
| 18 | 
            -
                  to_unload[ | 
| 16 | 
            +
                  log("constant #{cref} loaded from file #{file}") if logger
         | 
| 17 | 
            +
                  to_unload[file] = cref if reloading_enabled?
         | 
| 19 18 | 
             
                  run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
         | 
| 20 19 | 
             
                else
         | 
| 21 | 
            -
                  msg = "expected file #{file} to define constant #{cref | 
| 20 | 
            +
                  msg = "expected file #{file} to define constant #{cref}, but didn't"
         | 
| 22 21 | 
             
                  log(msg) if logger
         | 
| 23 22 |  | 
| 24 23 | 
             
                  # Ruby still keeps the autoload defined, but we remove it because the
         | 
| @@ -28,7 +27,7 @@ module Zeitwerk::Loader::Callbacks | |
| 28 27 | 
             
                  # Since the expected constant was not defined, there is nothing to unload.
         | 
| 29 28 | 
             
                  # However, if the exception is rescued and reloading is enabled, we still
         | 
| 30 29 | 
             
                  # need to deleted the file from $LOADED_FEATURES.
         | 
| 31 | 
            -
                  to_unload[ | 
| 30 | 
            +
                  to_unload[file] = cref if reloading_enabled?
         | 
| 32 31 |  | 
| 33 32 | 
             
                  raise Zeitwerk::NameError.new(msg, cref.cname)
         | 
| 34 33 | 
             
                end
         | 
| @@ -57,7 +56,7 @@ module Zeitwerk::Loader::Callbacks | |
| 57 56 | 
             
                    cpath = implicit_namespace.name
         | 
| 58 57 | 
             
                    log("module #{cpath} autovivified from directory #{dir}") if logger
         | 
| 59 58 |  | 
| 60 | 
            -
                    to_unload[ | 
| 59 | 
            +
                    to_unload[dir] = cref if reloading_enabled?
         | 
| 61 60 |  | 
| 62 61 | 
             
                    # We don't unregister `dir` in the registry because concurrent threads
         | 
| 63 62 | 
             
                    # wouldn't find a loader associated to it in Kernel#require and would
         | 
| @@ -65,21 +64,20 @@ module Zeitwerk::Loader::Callbacks | |
| 65 64 | 
             
                    # these to be able to unregister later if eager loading.
         | 
| 66 65 | 
             
                    autoloaded_dirs << dir
         | 
| 67 66 |  | 
| 68 | 
            -
                    on_namespace_loaded(implicit_namespace)
         | 
| 67 | 
            +
                    on_namespace_loaded(cref, implicit_namespace)
         | 
| 69 68 |  | 
| 70 69 | 
             
                    run_on_load_callbacks(cpath, implicit_namespace, dir) unless on_load_callbacks.empty?
         | 
| 71 70 | 
             
                  end
         | 
| 72 71 | 
             
                end
         | 
| 73 72 | 
             
              end
         | 
| 74 73 |  | 
| 75 | 
            -
              # Invoked when a  | 
| 76 | 
            -
              #  | 
| 77 | 
            -
              #  | 
| 74 | 
            +
              # Invoked when a namespace is created, either from const_added or from module
         | 
| 75 | 
            +
              # autovivification. If the namespace has matching subdirectories, we descend
         | 
| 76 | 
            +
              # into them now.
         | 
| 78 77 | 
             
              #
         | 
| 79 | 
            -
              # @ | 
| 80 | 
            -
               | 
| 81 | 
            -
             | 
| 82 | 
            -
                if dirs = namespace_dirs.delete(real_mod_name(namespace))
         | 
| 78 | 
            +
              # @sig (Zeitwerk::Cref, Module) -> void
         | 
| 79 | 
            +
              internal def on_namespace_loaded(cref, namespace)
         | 
| 80 | 
            +
                if dirs = namespace_dirs.delete(cref)
         | 
| 83 81 | 
             
                  dirs.each do |dir|
         | 
| 84 82 | 
             
                    define_autoloads_for_dir(dir, namespace)
         | 
| 85 83 | 
             
                  end
         | 
| @@ -88,7 +86,7 @@ module Zeitwerk::Loader::Callbacks | |
| 88 86 |  | 
| 89 87 | 
             
              private
         | 
| 90 88 |  | 
| 91 | 
            -
              # @sig (String,  | 
| 89 | 
            +
              # @sig (String, top, String) -> void
         | 
| 92 90 | 
             
              def run_on_load_callbacks(cpath, value, abspath)
         | 
| 93 91 | 
             
                # Order matters. If present, run the most specific one.
         | 
| 94 92 | 
             
                callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
         | 
| @@ -71,15 +71,15 @@ module Zeitwerk::Loader::Config | |
| 71 71 |  | 
| 72 72 | 
             
              # User-oriented callbacks to be fired when a constant is loaded.
         | 
| 73 73 | 
             
              #
         | 
| 74 | 
            -
              # @sig Hash[String, Array[{ ( | 
| 75 | 
            -
              #      Hash[Symbol, Array[{ (String,  | 
| 74 | 
            +
              # @sig Hash[String, Array[{ (top, String) -> void }]]
         | 
| 75 | 
            +
              #      Hash[Symbol, Array[{ (String, top, String) -> void }]]
         | 
| 76 76 | 
             
              attr_reader :on_load_callbacks
         | 
| 77 77 | 
             
              private :on_load_callbacks
         | 
| 78 78 |  | 
| 79 79 | 
             
              # User-oriented callbacks to be fired before constants are removed.
         | 
| 80 80 | 
             
              #
         | 
| 81 | 
            -
              # @sig Hash[String, Array[{ ( | 
| 82 | 
            -
              #      Hash[Symbol, Array[{ (String,  | 
| 81 | 
            +
              # @sig Hash[String, Array[{ (top, String) -> void }]]
         | 
| 82 | 
            +
              #      Hash[Symbol, Array[{ (String, top, String) -> void }]]
         | 
| 83 83 | 
             
              attr_reader :on_unload_callbacks
         | 
| 84 84 | 
             
              private :on_unload_callbacks
         | 
| 85 85 |  | 
| @@ -247,8 +247,8 @@ module Zeitwerk::Loader::Config | |
| 247 247 | 
             
              #   end
         | 
| 248 248 | 
             
              #
         | 
| 249 249 | 
             
              # @raise [TypeError]
         | 
| 250 | 
            -
              # @sig (String) { ( | 
| 251 | 
            -
              #      (:ANY) { (String,  | 
| 250 | 
            +
              # @sig (String) { (top, String) -> void } -> void
         | 
| 251 | 
            +
              #      (:ANY) { (String, top, String) -> void } -> void
         | 
| 252 252 | 
             
              def on_load(cpath = :ANY, &block)
         | 
| 253 253 | 
             
                raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
         | 
| 254 254 |  | 
| @@ -272,8 +272,8 @@ module Zeitwerk::Loader::Config | |
| 272 272 | 
             
              #   end
         | 
| 273 273 | 
             
              #
         | 
| 274 274 | 
             
              # @raise [TypeError]
         | 
| 275 | 
            -
              # @sig (String) { ( | 
| 276 | 
            -
              #      (:ANY) { (String,  | 
| 275 | 
            +
              # @sig (String) { (top) -> void } -> void
         | 
| 276 | 
            +
              #      (:ANY) { (String, top) -> void } -> void
         | 
| 277 277 | 
             
              def on_unload(cpath = :ANY, &block)
         | 
| 278 278 | 
             
                raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
         | 
| 279 279 |  | 
    
        data/lib/zeitwerk/loader.rb
    CHANGED
    
    | @@ -32,6 +32,30 @@ module Zeitwerk | |
| 32 32 | 
             
                attr_reader :autoloads
         | 
| 33 33 | 
             
                internal :autoloads
         | 
| 34 34 |  | 
| 35 | 
            +
                # When the path passed to Module#autoload is in the stack of features being
         | 
| 36 | 
            +
                # loaded at the moment, Ruby passes. For example, Module#autoload? returns
         | 
| 37 | 
            +
                # `nil` even if the autoload has not been attempted. See
         | 
| 38 | 
            +
                #
         | 
| 39 | 
            +
                #     https://bugs.ruby-lang.org/issues/21035
         | 
| 40 | 
            +
                #
         | 
| 41 | 
            +
                # We call these "inceptions".
         | 
| 42 | 
            +
                #
         | 
| 43 | 
            +
                # A common case is the entry point of gems managed by Zeitwerk. Their main
         | 
| 44 | 
            +
                # file is normally required and, while doing so, the loader sets an autoload
         | 
| 45 | 
            +
                # on the gem namespace. That autoload hits this edge case.
         | 
| 46 | 
            +
                #
         | 
| 47 | 
            +
                # There is some logic that neeeds to know if an autoload for a given
         | 
| 48 | 
            +
                # constant already exists. We check Module#autoload? first, and fallback to
         | 
| 49 | 
            +
                # the inceptions just in case.
         | 
| 50 | 
            +
                #
         | 
| 51 | 
            +
                # This map keeps track of pairs (cref, autoload_path) found by the loader.
         | 
| 52 | 
            +
                # The module Zeitwerk::Registry::Inceptions, on the other hand, acts as a
         | 
| 53 | 
            +
                # global registry for them.
         | 
| 54 | 
            +
                #
         | 
| 55 | 
            +
                # @sig Zeitwerk::Cref::Map[String]
         | 
| 56 | 
            +
                attr_reader :inceptions
         | 
| 57 | 
            +
                internal :inceptions
         | 
| 58 | 
            +
             | 
| 35 59 | 
             
                # We keep track of autoloaded directories to remove them from the registry
         | 
| 36 60 | 
             
                # at the end of eager loading.
         | 
| 37 61 | 
             
                #
         | 
| @@ -42,48 +66,31 @@ module Zeitwerk | |
| 42 66 | 
             
                attr_reader :autoloaded_dirs
         | 
| 43 67 | 
             
                internal :autoloaded_dirs
         | 
| 44 68 |  | 
| 45 | 
            -
                #  | 
| 69 | 
            +
                # If reloading is enabled, this collection maps autoload paths to their
         | 
| 70 | 
            +
                # autoloaded crefs.
         | 
| 46 71 | 
             
                #
         | 
| 47 | 
            -
                # | 
| 48 | 
            -
                # | 
| 49 | 
            -
                #     #<Zeitwerk::Cref:... @mod=Admin, @cname=:Role, ...>
         | 
| 50 | 
            -
                #   ]
         | 
| 72 | 
            +
                # On unload, the autoload paths are passed to callbacks, files deleted from
         | 
| 73 | 
            +
                # $LOADED_FEATURES, and the crefs are deleted.
         | 
| 51 74 | 
             
                #
         | 
| 52 | 
            -
                #  | 
| 53 | 
            -
                # stored in order to be able to delete it from $LOADED_FEATURES, and the
         | 
| 54 | 
            -
                # cref is used to remove the constant from the parent class or module.
         | 
| 55 | 
            -
                #
         | 
| 56 | 
            -
                # If reloading is enabled, this hash is filled as constants are autoloaded
         | 
| 57 | 
            -
                # or eager loaded. Otherwise, the collection remains empty.
         | 
| 58 | 
            -
                #
         | 
| 59 | 
            -
                # @sig Hash[String, [String, Zeitwerk::Cref]]
         | 
| 75 | 
            +
                # @sig Hash[String, Zeitwerk::Cref]
         | 
| 60 76 | 
             
                attr_reader :to_unload
         | 
| 61 77 | 
             
                internal :to_unload
         | 
| 62 78 |  | 
| 63 | 
            -
                # Maps namespace  | 
| 79 | 
            +
                # Maps namespace crefs to the directories that conform the namespace.
         | 
| 64 80 | 
             
                #
         | 
| 65 | 
            -
                #  | 
| 81 | 
            +
                # When these crefs get defined we know their children are spread over those
         | 
| 82 | 
            +
                # directories. We'll visit them to set up the corresponding autoloads.
         | 
| 66 83 | 
             
                #
         | 
| 67 | 
            -
                # | 
| 68 | 
            -
                #     "/Users/fxn/blog/app/controllers/admin",
         | 
| 69 | 
            -
                #     "/Users/fxn/blog/app/models/admin",
         | 
| 70 | 
            -
                #     ...
         | 
| 71 | 
            -
                #   ]
         | 
| 72 | 
            -
                #
         | 
| 73 | 
            -
                # when `Admin` gets defined we know that it plays the role of a namespace
         | 
| 74 | 
            -
                # and that its children are spread over those directories. We'll visit them
         | 
| 75 | 
            -
                # to set up the corresponding autoloads.
         | 
| 76 | 
            -
                #
         | 
| 77 | 
            -
                # @sig Hash[String, Array[String]]
         | 
| 84 | 
            +
                # @sig Zeitwerk::Cref::Map[String]
         | 
| 78 85 | 
             
                attr_reader :namespace_dirs
         | 
| 79 86 | 
             
                internal :namespace_dirs
         | 
| 80 87 |  | 
| 81 88 | 
             
                # A shadowed file is a file managed by this loader that is ignored when
         | 
| 82 89 | 
             
                # setting autoloads because its matching constant is already taken.
         | 
| 83 90 | 
             
                #
         | 
| 84 | 
            -
                # This private set is populated as we descend. For example, if the | 
| 85 | 
            -
                # has only scanned the top-level, `shadowed_files` does not have  | 
| 86 | 
            -
                # files that may exist deep in the project tree | 
| 91 | 
            +
                # This private set is populated lazily, as we descend. For example, if the
         | 
| 92 | 
            +
                # loader has only scanned the top-level, `shadowed_files` does not have the
         | 
| 93 | 
            +
                # shadowed files that may exist deep in the project tree.
         | 
| 87 94 | 
             
                #
         | 
| 88 95 | 
             
                # @sig Set[String]
         | 
| 89 96 | 
             
                attr_reader :shadowed_files
         | 
| @@ -101,9 +108,10 @@ module Zeitwerk | |
| 101 108 | 
             
                  super
         | 
| 102 109 |  | 
| 103 110 | 
             
                  @autoloads       = {}
         | 
| 111 | 
            +
                  @inceptions      = Zeitwerk::Cref::Map.new
         | 
| 104 112 | 
             
                  @autoloaded_dirs = []
         | 
| 105 113 | 
             
                  @to_unload       = {}
         | 
| 106 | 
            -
                  @namespace_dirs  =  | 
| 114 | 
            +
                  @namespace_dirs  = Zeitwerk::Cref::Map.new
         | 
| 107 115 | 
             
                  @shadowed_files  = Set.new
         | 
| 108 116 | 
             
                  @setup           = false
         | 
| 109 117 | 
             
                  @eager_loaded    = false
         | 
| @@ -167,7 +175,7 @@ module Zeitwerk | |
| 167 175 | 
             
                      end
         | 
| 168 176 | 
             
                    end
         | 
| 169 177 |  | 
| 170 | 
            -
                    to_unload.each do | | 
| 178 | 
            +
                    to_unload.each do |abspath, cref|
         | 
| 171 179 | 
             
                      unless on_unload_callbacks.empty?
         | 
| 172 180 | 
             
                        begin
         | 
| 173 181 | 
             
                          value = cref.get
         | 
| @@ -176,7 +184,7 @@ module Zeitwerk | |
| 176 184 | 
             
                          # autoload failed to define the expected constant but the user
         | 
| 177 185 | 
             
                          # rescued the exception.
         | 
| 178 186 | 
             
                        else
         | 
| 179 | 
            -
                          run_on_unload_callbacks( | 
| 187 | 
            +
                          run_on_unload_callbacks(cref, value, abspath)
         | 
| 180 188 | 
             
                        end
         | 
| 181 189 | 
             
                      end
         | 
| 182 190 |  | 
| @@ -205,8 +213,10 @@ module Zeitwerk | |
| 205 213 | 
             
                    namespace_dirs.clear
         | 
| 206 214 | 
             
                    shadowed_files.clear
         | 
| 207 215 |  | 
| 216 | 
            +
                    unregister_inceptions
         | 
| 217 | 
            +
                    unregister_explicit_namespaces
         | 
| 218 | 
            +
             | 
| 208 219 | 
             
                    Registry.on_unload(self)
         | 
| 209 | 
            -
                    ExplicitNamespace.__unregister_loader(self)
         | 
| 210 220 |  | 
| 211 221 | 
             
                    @setup        = false
         | 
| 212 222 | 
             
                    @eager_loaded = false
         | 
| @@ -315,17 +325,23 @@ module Zeitwerk | |
| 315 325 | 
             
                # Says if the given constant path would be unloaded on reload. This
         | 
| 316 326 | 
             
                # predicate returns `false` if reloading is disabled.
         | 
| 317 327 | 
             
                #
         | 
| 328 | 
            +
                # This is an undocumented method that I wrote to help transition from the
         | 
| 329 | 
            +
                # classic autoloader in Rails. Its usage was removed from Rails in 7.0.
         | 
| 330 | 
            +
                #
         | 
| 318 331 | 
             
                # @sig (String) -> bool
         | 
| 319 332 | 
             
                def unloadable_cpath?(cpath)
         | 
| 320 | 
            -
                   | 
| 333 | 
            +
                  unloadable_cpaths.include?(cpath)
         | 
| 321 334 | 
             
                end
         | 
| 322 335 |  | 
| 323 336 | 
             
                # Returns an array with the constant paths that would be unloaded on reload.
         | 
| 324 337 | 
             
                # This predicate returns an empty array if reloading is disabled.
         | 
| 325 338 | 
             
                #
         | 
| 339 | 
            +
                # This is an undocumented method that I wrote to help transition from the
         | 
| 340 | 
            +
                # classic autoloader in Rails. Its usage was removed from Rails in 7.0.
         | 
| 341 | 
            +
                #
         | 
| 326 342 | 
             
                # @sig () -> Array[String]
         | 
| 327 343 | 
             
                def unloadable_cpaths
         | 
| 328 | 
            -
                  to_unload. | 
| 344 | 
            +
                  to_unload.values.map(&:path)
         | 
| 329 345 | 
             
                end
         | 
| 330 346 |  | 
| 331 347 | 
             
                # This is a dangerous method.
         | 
| @@ -333,8 +349,9 @@ module Zeitwerk | |
| 333 349 | 
             
                # @experimental
         | 
| 334 350 | 
             
                # @sig () -> void
         | 
| 335 351 | 
             
                def unregister
         | 
| 352 | 
            +
                  unregister_inceptions
         | 
| 353 | 
            +
                  unregister_explicit_namespaces
         | 
| 336 354 | 
             
                  Registry.unregister_loader(self)
         | 
| 337 | 
            -
                  ExplicitNamespace.__unregister_loader(self)
         | 
| 338 355 | 
             
                end
         | 
| 339 356 |  | 
| 340 357 | 
             
                # The return value of this predicate is only meaningful if the loader has
         | 
| @@ -474,22 +491,22 @@ module Zeitwerk | |
| 474 491 | 
             
                    # If the existing autoload points to a file, it has to be preserved, if
         | 
| 475 492 | 
             
                    # not, it is fine as it is. In either case, we do not need to override.
         | 
| 476 493 | 
             
                    # Just remember the subdirectory conforms this namespace.
         | 
| 477 | 
            -
                    namespace_dirs[ | 
| 494 | 
            +
                    namespace_dirs.get_or_set(cref) { [] } << subdir
         | 
| 478 495 | 
             
                  elsif !cref.defined?
         | 
| 479 496 | 
             
                    # First time we find this namespace, set an autoload for it.
         | 
| 480 | 
            -
                    namespace_dirs[ | 
| 497 | 
            +
                    namespace_dirs.get_or_set(cref) { [] } << subdir
         | 
| 481 498 | 
             
                    define_autoload(cref, subdir)
         | 
| 482 499 | 
             
                  else
         | 
| 483 500 | 
             
                    # For whatever reason the constant that corresponds to this namespace has
         | 
| 484 501 | 
             
                    # already been defined, we have to recurse.
         | 
| 485 | 
            -
                    log("the namespace #{cref | 
| 502 | 
            +
                    log("the namespace #{cref} already exists, descending into #{subdir}") if logger
         | 
| 486 503 | 
             
                    define_autoloads_for_dir(subdir, cref.get)
         | 
| 487 504 | 
             
                  end
         | 
| 488 505 | 
             
                end
         | 
| 489 506 |  | 
| 490 507 | 
             
                # @sig (Module, Symbol, String) -> void
         | 
| 491 508 | 
             
                private def autoload_file(cref, file)
         | 
| 492 | 
            -
                  if autoload_path = cref.autoload? || Registry. | 
| 509 | 
            +
                  if autoload_path = cref.autoload? || Registry::Inceptions.registered?(cref)
         | 
| 493 510 | 
             
                    # First autoload for a Ruby file wins, just ignore subsequent ones.
         | 
| 494 511 | 
             
                    if ruby?(autoload_path)
         | 
| 495 512 | 
             
                      shadowed_files << file
         | 
| @@ -499,7 +516,7 @@ module Zeitwerk | |
| 499 516 | 
             
                    end
         | 
| 500 517 | 
             
                  elsif cref.defined?
         | 
| 501 518 | 
             
                    shadowed_files << file
         | 
| 502 | 
            -
                    log("file #{file} is ignored because #{cref | 
| 519 | 
            +
                    log("file #{file} is ignored because #{cref} is already defined") if logger
         | 
| 503 520 | 
             
                  else
         | 
| 504 521 | 
             
                    define_autoload(cref, file)
         | 
| 505 522 | 
             
                  end
         | 
| @@ -513,7 +530,7 @@ module Zeitwerk | |
| 513 530 | 
             
                  autoloads.delete(dir)
         | 
| 514 531 | 
             
                  Registry.unregister_autoload(dir)
         | 
| 515 532 |  | 
| 516 | 
            -
                  log("earlier autoload for #{cref | 
| 533 | 
            +
                  log("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
         | 
| 517 534 |  | 
| 518 535 | 
             
                  # Order matters: When Module#const_added is triggered by the autoload, we
         | 
| 519 536 | 
             
                  # don't want the namespace to be registered yet.
         | 
| @@ -527,19 +544,16 @@ module Zeitwerk | |
| 527 544 |  | 
| 528 545 | 
             
                  if logger
         | 
| 529 546 | 
             
                    if ruby?(abspath)
         | 
| 530 | 
            -
                      log("autoload set for #{cref | 
| 547 | 
            +
                      log("autoload set for #{cref}, to be loaded from #{abspath}")
         | 
| 531 548 | 
             
                    else
         | 
| 532 | 
            -
                      log("autoload set for #{cref | 
| 549 | 
            +
                      log("autoload set for #{cref}, to be autovivified from #{abspath}")
         | 
| 533 550 | 
             
                    end
         | 
| 534 551 | 
             
                  end
         | 
| 535 552 |  | 
| 536 553 | 
             
                  autoloads[abspath] = cref
         | 
| 537 554 | 
             
                  Registry.register_autoload(self, abspath)
         | 
| 538 555 |  | 
| 539 | 
            -
                   | 
| 540 | 
            -
                  unless cref.autoload?
         | 
| 541 | 
            -
                    Registry.register_inception(cref.path, abspath, self)
         | 
| 542 | 
            -
                  end
         | 
| 556 | 
            +
                  register_inception(cref, abspath) unless cref.autoload?
         | 
| 543 557 | 
             
                end
         | 
| 544 558 |  | 
| 545 559 | 
             
                # @sig (Module, Symbol) -> String?
         | 
| @@ -547,13 +561,32 @@ module Zeitwerk | |
| 547 561 | 
             
                  if autoload_path = cref.autoload?
         | 
| 548 562 | 
             
                    autoload_path if autoloads.key?(autoload_path)
         | 
| 549 563 | 
             
                  else
         | 
| 550 | 
            -
                     | 
| 564 | 
            +
                    inceptions[cref]
         | 
| 551 565 | 
             
                  end
         | 
| 552 566 | 
             
                end
         | 
| 553 567 |  | 
| 554 568 | 
             
                # @sig (Zeitwerk::Cref) -> void
         | 
| 555 569 | 
             
                private def register_explicit_namespace(cref)
         | 
| 556 | 
            -
                   | 
| 570 | 
            +
                  Registry::ExplicitNamespaces.__register(cref, self)
         | 
| 571 | 
            +
                end
         | 
| 572 | 
            +
             | 
| 573 | 
            +
                # @sig () -> void
         | 
| 574 | 
            +
                private def unregister_explicit_namespaces
         | 
| 575 | 
            +
                  Registry::ExplicitNamespaces.__unregister_loader(self)
         | 
| 576 | 
            +
                end
         | 
| 577 | 
            +
             | 
| 578 | 
            +
                # @sig (Zeitwerk::Cref, String) -> void
         | 
| 579 | 
            +
                private def register_inception(cref, abspath)
         | 
| 580 | 
            +
                  inceptions[cref] = abspath
         | 
| 581 | 
            +
                  Registry::Inceptions.register(cref, abspath)
         | 
| 582 | 
            +
                end
         | 
| 583 | 
            +
             | 
| 584 | 
            +
                # @sig () -> void
         | 
| 585 | 
            +
                private def unregister_inceptions
         | 
| 586 | 
            +
                  inceptions.each_key do |cref|
         | 
| 587 | 
            +
                    Registry::Inceptions.unregister(cref)
         | 
| 588 | 
            +
                  end
         | 
| 589 | 
            +
                  inceptions.clear
         | 
| 557 590 | 
             
                end
         | 
| 558 591 |  | 
| 559 592 | 
             
                # @sig (String) -> void
         | 
| @@ -580,17 +613,17 @@ module Zeitwerk | |
| 580 613 | 
             
                  end
         | 
| 581 614 | 
             
                end
         | 
| 582 615 |  | 
| 583 | 
            -
                # @sig (String,  | 
| 584 | 
            -
                private def run_on_unload_callbacks( | 
| 616 | 
            +
                # @sig (String, top, String) -> void
         | 
| 617 | 
            +
                private def run_on_unload_callbacks(cref, value, abspath)
         | 
| 585 618 | 
             
                  # Order matters. If present, run the most specific one.
         | 
| 586 | 
            -
                  on_unload_callbacks[ | 
| 587 | 
            -
                  on_unload_callbacks[:ANY]&.each { |c| c.call( | 
| 619 | 
            +
                  on_unload_callbacks[cref.path]&.each { |c| c.call(value, abspath) }
         | 
| 620 | 
            +
                  on_unload_callbacks[:ANY]&.each { |c| c.call(cref.path, value, abspath) }
         | 
| 588 621 | 
             
                end
         | 
| 589 622 |  | 
| 590 623 | 
             
                # @sig (Module, Symbol) -> void
         | 
| 591 624 | 
             
                private def unload_autoload(cref)
         | 
| 592 625 | 
             
                  cref.remove
         | 
| 593 | 
            -
                  log("autoload for #{cref | 
| 626 | 
            +
                  log("autoload for #{cref} removed") if logger
         | 
| 594 627 | 
             
                end
         | 
| 595 628 |  | 
| 596 629 | 
             
                # @sig (Module, Symbol) -> void
         | 
| @@ -602,7 +635,7 @@ module Zeitwerk | |
| 602 635 | 
             
                  # There are a few edge scenarios in which this may happen. If the constant
         | 
| 603 636 | 
             
                  # is gone, that is OK, anyway.
         | 
| 604 637 | 
             
                else
         | 
| 605 | 
            -
                  log("#{cref | 
| 638 | 
            +
                  log("#{cref} unloaded") if logger
         | 
| 606 639 | 
             
                end
         | 
| 607 640 | 
             
              end
         | 
| 608 641 | 
             
            end
         | 
| @@ -0,0 +1,64 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Zeitwerk::Registry
         | 
| 4 | 
            +
              # This module is a registry for explicit namespaces.
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # When a loader determines that a certain file should define an explicit
         | 
| 7 | 
            +
              # namespace, it registers it here, associating its cref with itself.
         | 
| 8 | 
            +
              #
         | 
| 9 | 
            +
              # If the namespace is autoloaded, our const_added callback retrieves its
         | 
| 10 | 
            +
              # loader by calling loader_for. That way, the loader is able to scan the
         | 
| 11 | 
            +
              # subdirectories that conform the namespace and set autoloads for their
         | 
| 12 | 
            +
              # expected constants just in time.
         | 
| 13 | 
            +
              #
         | 
| 14 | 
            +
              # Once autoloaded, the namespace is unregistered.
         | 
| 15 | 
            +
              #
         | 
| 16 | 
            +
              # The implementation assumes an explicit namespace is managed by one loader.
         | 
| 17 | 
            +
              # Loaders that reopen namespaces owned by other projects are responsible for
         | 
| 18 | 
            +
              # loading their constant before setup. This is documented.
         | 
| 19 | 
            +
              module ExplicitNamespaces # :nodoc: all
         | 
| 20 | 
            +
                # Maps crefs of explicit namespaces with their corresponding loader.
         | 
| 21 | 
            +
                #
         | 
| 22 | 
            +
                # Entries are added as the namespaces are found, and removed as they are
         | 
| 23 | 
            +
                # autoloaded.
         | 
| 24 | 
            +
                #
         | 
| 25 | 
            +
                # @sig Zeitwerk::Cref::Map[Zeitwerk::Loader]
         | 
| 26 | 
            +
                @loaders = Zeitwerk::Cref::Map.new
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                class << self
         | 
| 29 | 
            +
                  extend Zeitwerk::Internal
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  # Registers `cref` as being the constant path of an explicit namespace
         | 
| 32 | 
            +
                  # managed by `loader`.
         | 
| 33 | 
            +
                  #
         | 
| 34 | 
            +
                  # @sig (Zeitwerk::Cref, Zeitwerk::Loader) -> void
         | 
| 35 | 
            +
                  internal def register(cref, loader)
         | 
| 36 | 
            +
                    @loaders[cref] = loader
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  # @sig (Module, Symbol) -> Zeitwerk::Loader?
         | 
| 40 | 
            +
                  internal def loader_for(mod, cname)
         | 
| 41 | 
            +
                    @loaders.delete_mod_cname(mod, cname)
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  # @sig (Zeitwerk::Loader) -> void
         | 
| 45 | 
            +
                  internal def unregister_loader(loader)
         | 
| 46 | 
            +
                    @loaders.delete_by_value(loader)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  # This is an internal method only used by the test suite.
         | 
| 50 | 
            +
                  #
         | 
| 51 | 
            +
                  # @sig (Symbol | String) -> Zeitwerk::Loader?
         | 
| 52 | 
            +
                  internal def registered?(cref)
         | 
| 53 | 
            +
                    @loaders[cref]
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  # This is an internal method only used by the test suite.
         | 
| 57 | 
            +
                  #
         | 
| 58 | 
            +
                  # @sig () -> void
         | 
| 59 | 
            +
                  internal def clear
         | 
| 60 | 
            +
                    @loaders.clear
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
            end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            module Zeitwerk::Registry
         | 
| 2 | 
            +
              # Loaders know their own inceptions, but there is a use case in which we need
         | 
| 3 | 
            +
              # to know if a given cpath is an inception globally. This is what this
         | 
| 4 | 
            +
              # registry is for.
         | 
| 5 | 
            +
              module Inceptions # :nodoc: all
         | 
| 6 | 
            +
                # @sig Zeitwerk::Cref::Map[String]
         | 
| 7 | 
            +
                @inceptions = Zeitwerk::Cref::Map.new
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                class << self
         | 
| 10 | 
            +
                  # @sig (Zeitwerk::Cref, String) -> void
         | 
| 11 | 
            +
                  def register(cref, autoload_path)
         | 
| 12 | 
            +
                    @inceptions[cref] = autoload_path
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  # @sig (String) -> String?
         | 
| 16 | 
            +
                  def registered?(cref)
         | 
| 17 | 
            +
                    @inceptions[cref]
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  # @sig (String) -> void
         | 
| 21 | 
            +
                  def unregister(cref)
         | 
| 22 | 
            +
                    @inceptions.delete(cref)
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  # @sig () -> void
         | 
| 26 | 
            +
                  def clear # for tests
         | 
| 27 | 
            +
                    @inceptions.clear
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
    
        data/lib/zeitwerk/registry.rb
    CHANGED
    
    | @@ -2,6 +2,9 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Zeitwerk
         | 
| 4 4 | 
             
              module Registry # :nodoc: all
         | 
| 5 | 
            +
                require_relative "registry/explicit_namespaces"
         | 
| 6 | 
            +
                require_relative "registry/inceptions"
         | 
| 7 | 
            +
             | 
| 5 8 | 
             
                class << self
         | 
| 6 9 | 
             
                  # Keeps track of all loaders. Useful to broadcast messages and to prevent
         | 
| 7 10 | 
             
                  # them from being garbage collected.
         | 
| @@ -25,45 +28,6 @@ module Zeitwerk | |
| 25 28 | 
             
                  # @sig Hash[String, Zeitwerk::Loader]
         | 
| 26 29 | 
             
                  attr_reader :autoloads
         | 
| 27 30 |  | 
| 28 | 
            -
                  # This hash table addresses an edge case in which an autoload is ignored.
         | 
| 29 | 
            -
                  #
         | 
| 30 | 
            -
                  # For example, let's suppose we want to autoload in a gem like this:
         | 
| 31 | 
            -
                  #
         | 
| 32 | 
            -
                  #   # lib/my_gem.rb
         | 
| 33 | 
            -
                  #   loader = Zeitwerk::Loader.new
         | 
| 34 | 
            -
                  #   loader.push_dir(__dir__)
         | 
| 35 | 
            -
                  #   loader.setup
         | 
| 36 | 
            -
                  #
         | 
| 37 | 
            -
                  #   module MyGem
         | 
| 38 | 
            -
                  #   end
         | 
| 39 | 
            -
                  #
         | 
| 40 | 
            -
                  # if you require "my_gem", as Bundler would do, this happens while setting
         | 
| 41 | 
            -
                  # up autoloads:
         | 
| 42 | 
            -
                  #
         | 
| 43 | 
            -
                  #   1. Object.autoload?(:MyGem) returns `nil` because the autoload for
         | 
| 44 | 
            -
                  #      the constant is issued by Zeitwerk while the same file is being
         | 
| 45 | 
            -
                  #      required.
         | 
| 46 | 
            -
                  #   2. The constant `MyGem` is undefined while setup runs.
         | 
| 47 | 
            -
                  #
         | 
| 48 | 
            -
                  # Therefore, a directory `lib/my_gem` would autovivify a module according to
         | 
| 49 | 
            -
                  # the existing information. But that would be wrong.
         | 
| 50 | 
            -
                  #
         | 
| 51 | 
            -
                  # To overcome this fundamental limitation, we keep track of the constant
         | 
| 52 | 
            -
                  # paths that are in this situation ---in the example above, "MyGem"--- and
         | 
| 53 | 
            -
                  # take this collection into account for the autovivification logic.
         | 
| 54 | 
            -
                  #
         | 
| 55 | 
            -
                  # Note that you cannot generally address this by moving the setup code
         | 
| 56 | 
            -
                  # below the constant definition, because we want libraries to be able to
         | 
| 57 | 
            -
                  # use managed constants in the module body:
         | 
| 58 | 
            -
                  #
         | 
| 59 | 
            -
                  #   module MyGem
         | 
| 60 | 
            -
                  #     include MyConcern
         | 
| 61 | 
            -
                  #   end
         | 
| 62 | 
            -
                  #
         | 
| 63 | 
            -
                  # @private
         | 
| 64 | 
            -
                  # @sig Hash[String, [String, Zeitwerk::Loader]]
         | 
| 65 | 
            -
                  attr_reader :inceptions
         | 
| 66 | 
            -
             | 
| 67 31 | 
             
                  # Registers a loader.
         | 
| 68 32 | 
             
                  #
         | 
| 69 33 | 
             
                  # @private
         | 
| @@ -78,7 +42,6 @@ module Zeitwerk | |
| 78 42 | 
             
                    loaders.delete(loader)
         | 
| 79 43 | 
             
                    gem_loaders_by_root_file.delete_if { |_, l| l == loader }
         | 
| 80 44 | 
             
                    autoloads.delete_if { |_, l| l == loader }
         | 
| 81 | 
            -
                    inceptions.delete_if { |_, (_, l)| l == loader }
         | 
| 82 45 | 
             
                  end
         | 
| 83 46 |  | 
| 84 47 | 
             
                  # This method returns always a loader, the same instance for the same root
         | 
| @@ -102,23 +65,6 @@ module Zeitwerk | |
| 102 65 | 
             
                    autoloads.delete(abspath)
         | 
| 103 66 | 
             
                  end
         | 
| 104 67 |  | 
| 105 | 
            -
                  # @private
         | 
| 106 | 
            -
                  # @sig (String, String, Zeitwerk::Loader) -> void
         | 
| 107 | 
            -
                  def register_inception(cpath, abspath, loader)
         | 
| 108 | 
            -
                    inceptions[cpath] = [abspath, loader]
         | 
| 109 | 
            -
                  end
         | 
| 110 | 
            -
             | 
| 111 | 
            -
                  # @private
         | 
| 112 | 
            -
                  # @sig (String) -> String?
         | 
| 113 | 
            -
                  def inception?(cpath, registered_by_loader=nil)
         | 
| 114 | 
            -
                    if pair = inceptions[cpath]
         | 
| 115 | 
            -
                      abspath, loader = pair
         | 
| 116 | 
            -
                      if registered_by_loader.nil? || registered_by_loader.equal?(loader)
         | 
| 117 | 
            -
                        abspath
         | 
| 118 | 
            -
                      end
         | 
| 119 | 
            -
                    end
         | 
| 120 | 
            -
                  end
         | 
| 121 | 
            -
             | 
| 122 68 | 
             
                  # @private
         | 
| 123 69 | 
             
                  # @sig (String) -> Zeitwerk::Loader?
         | 
| 124 70 | 
             
                  def loader_for(path)
         | 
| @@ -129,13 +75,11 @@ module Zeitwerk | |
| 129 75 | 
             
                  # @sig (Zeitwerk::Loader) -> void
         | 
| 130 76 | 
             
                  def on_unload(loader)
         | 
| 131 77 | 
             
                    autoloads.delete_if { |_path, object| object == loader }
         | 
| 132 | 
            -
                    inceptions.delete_if { |_cpath, (_path, object)| object == loader }
         | 
| 133 78 | 
             
                  end
         | 
| 134 79 | 
             
                end
         | 
| 135 80 |  | 
| 136 81 | 
             
                @loaders                  = []
         | 
| 137 82 | 
             
                @gem_loaders_by_root_file = {}
         | 
| 138 83 | 
             
                @autoloads                = {}
         | 
| 139 | 
            -
                @inceptions               = {}
         | 
| 140 84 | 
             
              end
         | 
| 141 85 | 
             
            end
         | 
    
        data/lib/zeitwerk/version.rb
    CHANGED
    
    
    
        data/lib/zeitwerk.rb
    CHANGED
    
    | @@ -7,7 +7,6 @@ module Zeitwerk | |
| 7 7 | 
             
              require_relative "zeitwerk/loader"
         | 
| 8 8 | 
             
              require_relative "zeitwerk/gem_loader"
         | 
| 9 9 | 
             
              require_relative "zeitwerk/registry"
         | 
| 10 | 
            -
              require_relative "zeitwerk/explicit_namespace"
         | 
| 11 10 | 
             
              require_relative "zeitwerk/inflector"
         | 
| 12 11 | 
             
              require_relative "zeitwerk/gem_inflector"
         | 
| 13 12 | 
             
              require_relative "zeitwerk/null_inflector"
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,13 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: zeitwerk
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 2.7. | 
| 4 | 
            +
              version: 2.7.2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Xavier Noria
         | 
| 8 | 
            -
            autorequire:
         | 
| 9 8 | 
             
            bindir: bin
         | 
| 10 9 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 10 | 
            +
            date: 2025-02-18 00:00:00.000000000 Z
         | 
| 12 11 | 
             
            dependencies: []
         | 
| 13 12 | 
             
            description: |2
         | 
| 14 13 | 
             
                  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
         | 
| @@ -26,8 +25,8 @@ files: | |
| 26 25 | 
             
            - lib/zeitwerk/core_ext/kernel.rb
         | 
| 27 26 | 
             
            - lib/zeitwerk/core_ext/module.rb
         | 
| 28 27 | 
             
            - lib/zeitwerk/cref.rb
         | 
| 28 | 
            +
            - lib/zeitwerk/cref/map.rb
         | 
| 29 29 | 
             
            - lib/zeitwerk/error.rb
         | 
| 30 | 
            -
            - lib/zeitwerk/explicit_namespace.rb
         | 
| 31 30 | 
             
            - lib/zeitwerk/gem_inflector.rb
         | 
| 32 31 | 
             
            - lib/zeitwerk/gem_loader.rb
         | 
| 33 32 | 
             
            - lib/zeitwerk/inflector.rb
         | 
| @@ -40,6 +39,8 @@ files: | |
| 40 39 | 
             
            - lib/zeitwerk/null_inflector.rb
         | 
| 41 40 | 
             
            - lib/zeitwerk/real_mod_name.rb
         | 
| 42 41 | 
             
            - lib/zeitwerk/registry.rb
         | 
| 42 | 
            +
            - lib/zeitwerk/registry/explicit_namespaces.rb
         | 
| 43 | 
            +
            - lib/zeitwerk/registry/inceptions.rb
         | 
| 43 44 | 
             
            - lib/zeitwerk/version.rb
         | 
| 44 45 | 
             
            homepage: https://github.com/fxn/zeitwerk
         | 
| 45 46 | 
             
            licenses:
         | 
| @@ -49,7 +50,6 @@ metadata: | |
| 49 50 | 
             
              changelog_uri: https://github.com/fxn/zeitwerk/blob/master/CHANGELOG.md
         | 
| 50 51 | 
             
              source_code_uri: https://github.com/fxn/zeitwerk
         | 
| 51 52 | 
             
              bug_tracker_uri: https://github.com/fxn/zeitwerk/issues
         | 
| 52 | 
            -
            post_install_message:
         | 
| 53 53 | 
             
            rdoc_options: []
         | 
| 54 54 | 
             
            require_paths:
         | 
| 55 55 | 
             
            - lib
         | 
| @@ -64,8 +64,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 64 64 | 
             
                - !ruby/object:Gem::Version
         | 
| 65 65 | 
             
                  version: '0'
         | 
| 66 66 | 
             
            requirements: []
         | 
| 67 | 
            -
            rubygems_version: 3. | 
| 68 | 
            -
            signing_key:
         | 
| 67 | 
            +
            rubygems_version: 3.6.4
         | 
| 69 68 | 
             
            specification_version: 4
         | 
| 70 69 | 
             
            summary: Efficient and thread-safe constant autoloader
         | 
| 71 70 | 
             
            test_files: []
         | 
| @@ -1,113 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module Zeitwerk
         | 
| 4 | 
            -
              # This module is essentially a registry for explicit namespaces.
         | 
| 5 | 
            -
              #
         | 
| 6 | 
            -
              # When a loader determines that a certain file should define an explicit
         | 
| 7 | 
            -
              # namespace, it registers it here, associating its cref with itself.
         | 
| 8 | 
            -
              #
         | 
| 9 | 
            -
              # If the namespace is autoloaded, our const_added callback retrieves its
         | 
| 10 | 
            -
              # loader by calling loader_for. That way, the loader is able to scan the
         | 
| 11 | 
            -
              # subdirectories that conform the namespace and set autoloads for their
         | 
| 12 | 
            -
              # expected constants just in time.
         | 
| 13 | 
            -
              #
         | 
| 14 | 
            -
              # Once autoloaded, the namespace is unregistered.
         | 
| 15 | 
            -
              #
         | 
| 16 | 
            -
              # The implementation assumes an explicit namespace is managed by one loader.
         | 
| 17 | 
            -
              # Loaders that reopen namespaces owned by other projects are responsible for
         | 
| 18 | 
            -
              # loading their constant before setup. This is documented.
         | 
| 19 | 
            -
              module ExplicitNamespace # :nodoc: all
         | 
| 20 | 
            -
                # Maps cnames or cpaths of explicit namespaces with their corresponding
         | 
| 21 | 
            -
                # loader. They are symbols for top-level ones, and strings for nested ones:
         | 
| 22 | 
            -
                #
         | 
| 23 | 
            -
                #   {
         | 
| 24 | 
            -
                #     :Admin => #<Zeitwerk::Loader:...>,
         | 
| 25 | 
            -
                #     "Hotel::Pricing" => #<Zeitwerk::Loader:...>
         | 
| 26 | 
            -
                #   }
         | 
| 27 | 
            -
                #
         | 
| 28 | 
            -
                # There are two types of keys to make loader_for as fast as possible, since
         | 
| 29 | 
            -
                # it is invoked by our const_added for all autoloads and constant actually
         | 
| 30 | 
            -
                # added. Globally. With this trick, for top-level constants we do not need
         | 
| 31 | 
            -
                # to call Symbol#name and perform a string lookup. Instead, we can directly
         | 
| 32 | 
            -
                # perform a fast symbol lookup.
         | 
| 33 | 
            -
                #
         | 
| 34 | 
            -
                # Entries are added as the namespaces are found, and removed as they are
         | 
| 35 | 
            -
                # autoloaded.
         | 
| 36 | 
            -
                #
         | 
| 37 | 
            -
                # @sig Hash[(Symbol | String) => Zeitwerk::Loader]
         | 
| 38 | 
            -
                @loaders = {}
         | 
| 39 | 
            -
             | 
| 40 | 
            -
                class << self
         | 
| 41 | 
            -
                  include RealModName
         | 
| 42 | 
            -
                  extend Internal
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                  # Registers `cref` as being the constant path of an explicit namespace
         | 
| 45 | 
            -
                  # managed by `loader`.
         | 
| 46 | 
            -
                  #
         | 
| 47 | 
            -
                  # @sig (String, Zeitwerk::Loader) -> void
         | 
| 48 | 
            -
                  internal def register(cref, loader)
         | 
| 49 | 
            -
                    if Object.equal?(cref.mod)
         | 
| 50 | 
            -
                      @loaders[cref.cname] = loader
         | 
| 51 | 
            -
                    else
         | 
| 52 | 
            -
                      @loaders[cref.path] = loader
         | 
| 53 | 
            -
                    end
         | 
| 54 | 
            -
                  end
         | 
| 55 | 
            -
             | 
| 56 | 
            -
                  # @sig (Module, Symbol) -> Zeitwerk::Loader?
         | 
| 57 | 
            -
                  internal def loader_for(mod, cname)
         | 
| 58 | 
            -
                    if Object.equal?(mod)
         | 
| 59 | 
            -
                      @loaders.delete(cname)
         | 
| 60 | 
            -
                    else
         | 
| 61 | 
            -
                      @loaders.delete("#{real_mod_name(mod)}::#{cname}")
         | 
| 62 | 
            -
                    end
         | 
| 63 | 
            -
                  end
         | 
| 64 | 
            -
             | 
| 65 | 
            -
                  # @sig (Zeitwerk::Loader) -> void
         | 
| 66 | 
            -
                  internal def unregister_loader(loader)
         | 
| 67 | 
            -
                    @loaders.delete_if { _2.equal?(loader) }
         | 
| 68 | 
            -
                  end
         | 
| 69 | 
            -
             | 
| 70 | 
            -
                  # This is an internal method only used by the test suite.
         | 
| 71 | 
            -
                  #
         | 
| 72 | 
            -
                  # @sig (String) -> Zeitwerk::Loader?
         | 
| 73 | 
            -
                  internal def registered?(cname_or_cpath)
         | 
| 74 | 
            -
                    @loaders[cname_or_cpath]
         | 
| 75 | 
            -
                  end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                  # This is an internal method only used by the test suite.
         | 
| 78 | 
            -
                  #
         | 
| 79 | 
            -
                  # @sig () -> void
         | 
| 80 | 
            -
                  internal def clear
         | 
| 81 | 
            -
                    @loaders.clear
         | 
| 82 | 
            -
                  end
         | 
| 83 | 
            -
             | 
| 84 | 
            -
                  module Synchronized
         | 
| 85 | 
            -
                    extend Internal
         | 
| 86 | 
            -
             | 
| 87 | 
            -
                    MUTEX = Mutex.new
         | 
| 88 | 
            -
             | 
| 89 | 
            -
                    internal def register(...)
         | 
| 90 | 
            -
                      MUTEX.synchronize { super }
         | 
| 91 | 
            -
                    end
         | 
| 92 | 
            -
             | 
| 93 | 
            -
                    internal def loader_for(...)
         | 
| 94 | 
            -
                      MUTEX.synchronize { super }
         | 
| 95 | 
            -
                    end
         | 
| 96 | 
            -
             | 
| 97 | 
            -
                    internal def unregister_loader(...)
         | 
| 98 | 
            -
                      MUTEX.synchronize { super }
         | 
| 99 | 
            -
                    end
         | 
| 100 | 
            -
             | 
| 101 | 
            -
                    internal def registered?(...)
         | 
| 102 | 
            -
                      MUTEX.synchronize { super }
         | 
| 103 | 
            -
                    end
         | 
| 104 | 
            -
             | 
| 105 | 
            -
                    internal def clear
         | 
| 106 | 
            -
                      MUTEX.synchronize { super }
         | 
| 107 | 
            -
                    end
         | 
| 108 | 
            -
                  end
         | 
| 109 | 
            -
             | 
| 110 | 
            -
                  prepend Synchronized unless RUBY_ENGINE == "ruby"
         | 
| 111 | 
            -
                end
         | 
| 112 | 
            -
              end
         | 
| 113 | 
            -
            end
         |