zeitwerk 2.5.4 → 2.6.1
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/README.md +78 -9
- data/lib/zeitwerk/error.rb +3 -0
- data/lib/zeitwerk/gem_loader.rb +65 -0
- data/lib/zeitwerk/loader/config.rb +19 -11
- data/lib/zeitwerk/loader/helpers.rb +34 -2
- data/lib/zeitwerk/loader.rb +28 -29
- data/lib/zeitwerk/registry.rb +9 -16
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +1 -0
- metadata +3 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: cf4acabb8b402560402158aced0f30ae6bae5120dc65d719f4fb15c1385a2454
         | 
| 4 | 
            +
              data.tar.gz: 0a297687455cf8c3924a64c5e36223c35b0f69712c807ee9bc70748f8c42ff37
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 1d6666a098ae430f452edcd8b5e458541f20b20c4ee5903c4d18fe19cdce5afe81d6311a1bff5f9412b18ae57274e9b13132f8d59d9189b0ccd6cf7711337d6e
         | 
| 7 | 
            +
              data.tar.gz: a131561faa463e489f7e9c4898bc8e212b2f6814ba3ce634b7ef62e25467f38b88e4c63106e8d53ee933681e1f0dc196244d16f13c299d2ca0771ee4031655a1
         | 
    
        data/README.md
    CHANGED
    
    | @@ -47,6 +47,8 @@ | |
| 47 47 | 
             
              - [Edge cases](#edge-cases)
         | 
| 48 48 | 
             
              - [Beware of circular dependencies](#beware-of-circular-dependencies)
         | 
| 49 49 | 
             
              - [Reopening third-party namespaces](#reopening-third-party-namespaces)
         | 
| 50 | 
            +
              - [Introspection](#introspection)
         | 
| 51 | 
            +
              - [Encodings](#encodings)
         | 
| 50 52 | 
             
              - [Rules of thumb](#rules-of-thumb)
         | 
| 51 53 | 
             
              - [Debuggers](#debuggers)
         | 
| 52 54 | 
             
                - [debug.rb](#debugrb)
         | 
| @@ -58,6 +60,7 @@ | |
| 58 60 | 
             
            - [Motivation](#motivation)
         | 
| 59 61 | 
             
              - [Kernel#require is brittle](#kernelrequire-is-brittle)
         | 
| 60 62 | 
             
              - [Rails autoloading was brittle](#rails-autoloading-was-brittle)
         | 
| 63 | 
            +
            - [Awards](#awards)
         | 
| 61 64 | 
             
            - [Thanks](#thanks)
         | 
| 62 65 | 
             
            - [License](#license)
         | 
| 63 66 |  | 
| @@ -237,13 +240,17 @@ should define `Geolocatable`, not `Concerns::Geolocatable`. | |
| 237 240 | 
             
            <a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
         | 
| 238 241 | 
             
            ### Implicit namespaces
         | 
| 239 242 |  | 
| 240 | 
            -
            Directories without a matching Ruby file get modules  | 
| 243 | 
            +
            If a namespace is just a simple module with no code, you do not need to define it in a file: Directories without a matching Ruby file get modules created automatically on your behalf.
         | 
| 244 | 
            +
             | 
| 245 | 
            +
            For example, if a project has an `admin` directory:
         | 
| 241 246 |  | 
| 242 247 | 
             
            ```
         | 
| 243 248 | 
             
            app/controllers/admin/users_controller.rb -> Admin::UsersController
         | 
| 244 249 | 
             
            ```
         | 
| 245 250 |  | 
| 246 | 
            -
             | 
| 251 | 
            +
            and does not have a file called `admin.rb`, Zeitwerk automatically creates an `Admin` module on your behalf the first time `Admin` is used.
         | 
| 252 | 
            +
             | 
| 253 | 
            +
            For this to happen, the directory has to contain non-ignored Ruby files with extension `.rb`, directly or recursively, otherwise it is ignored. This condition is evaluated again on reloads.
         | 
| 247 254 |  | 
| 248 255 | 
             
            <a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
         | 
| 249 256 | 
             
            ### Explicit namespaces
         | 
| @@ -349,8 +356,6 @@ lib/my_gem/foo.rb     # MyGem::Foo | |
| 349 356 |  | 
| 350 357 | 
             
            Neither a gemspec nor a version file are technically required, this helper works as long as the code is organized using that standard structure.
         | 
| 351 358 |  | 
| 352 | 
            -
            If the entry point of your gem lives in a subdirectory of `lib` because it is reopening a namespace defined somewhere else, please use the generic API to setup the loader, and make sure you check the section [_Reopening third-party namespaces_](https://github.com/fxn/zeitwerk#reopening-third-party-namespaces) down below.
         | 
| 353 | 
            -
             | 
| 354 359 | 
             
            Conceptually, `for_gem` translates to:
         | 
| 355 360 |  | 
| 356 361 | 
             
            ```ruby
         | 
| @@ -363,8 +368,6 @@ loader.inflector = Zeitwerk::GemInflector.new(__FILE__) | |
| 363 368 | 
             
            loader.push_dir(__dir__)
         | 
| 364 369 | 
             
            ```
         | 
| 365 370 |  | 
| 366 | 
            -
            except that this method returns the same object in subsequent calls from the same file, in the unlikely case the gem wants to be able to reload.
         | 
| 367 | 
            -
             | 
| 368 371 | 
             
            If the main module references project constants at the top-level, Zeitwerk has to be ready to load them. Their definitions, in turn, may reference other project constants. And this is recursive. Therefore, it is important that the `setup` call happens above the main module definition:
         | 
| 369 372 |  | 
| 370 373 | 
             
            ```ruby
         | 
| @@ -381,6 +384,26 @@ module MyGem | |
| 381 384 | 
             
            end
         | 
| 382 385 | 
             
            ```
         | 
| 383 386 |  | 
| 387 | 
            +
            Loaders returned by `Zeitwerk::Loader.for_gem` issue warnings if `lib` has extra Ruby files or directories.
         | 
| 388 | 
            +
             | 
| 389 | 
            +
            For example, if the gem has Rails generators under `lib/generators`, by convention that directory defines a `Generators` Ruby module. If `generators` is just a container for non-autoloadable code and templates, not acting as a project namespace, you need to setup things accordingly.
         | 
| 390 | 
            +
             | 
| 391 | 
            +
            If the warning is legit, just tell the loader to ignore the offending file or directory:
         | 
| 392 | 
            +
             | 
| 393 | 
            +
            ```ruby
         | 
| 394 | 
            +
            loader.ignore("#{__dir__}/generators")
         | 
| 395 | 
            +
            ```
         | 
| 396 | 
            +
             | 
| 397 | 
            +
            Otherwise, there's a flag to say the extra stuff is OK:
         | 
| 398 | 
            +
             | 
| 399 | 
            +
            ```ruby
         | 
| 400 | 
            +
            Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
         | 
| 401 | 
            +
            ```
         | 
| 402 | 
            +
             | 
| 403 | 
            +
            This method is idempotent when invoked from the same file, to support gems that want to reload (unlikely).
         | 
| 404 | 
            +
             | 
| 405 | 
            +
            If the entry point of your gem lives in a subdirectory of `lib` because it is reopening a namespace defined somewhere else, please use the generic API to setup the loader, and make sure you check the section [_Reopening third-party namespaces_](https://github.com/fxn/zeitwerk#reopening-third-party-namespaces) down below.
         | 
| 406 | 
            +
             | 
| 384 407 | 
             
            <a id="markdown-autoloading" name="autoloading"></a>
         | 
| 385 408 | 
             
            ### Autoloading
         | 
| 386 409 |  | 
| @@ -480,9 +503,14 @@ Reloading removes the currently loaded classes and modules and resets the loader | |
| 480 503 |  | 
| 481 504 | 
             
            It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent.
         | 
| 482 505 |  | 
| 483 | 
            -
             | 
| 506 | 
            +
            Reloading is not thread-safe:
         | 
| 507 | 
            +
             | 
| 508 | 
            +
            * You should not reload while another thread is reloading.
         | 
| 509 | 
            +
            * You should not autoload while another thread is reloading.
         | 
| 484 510 |  | 
| 485 | 
            -
             | 
| 511 | 
            +
            In order to reload in a thread-safe manner, frameworks need to implement some coordination. For example, a web framework that serves each request with its own thread may have a globally accessible read/write lock: When a request comes in, the framework acquires the lock for reading at the beginning, and releases it at the end. On the other hand, the code in the framework responsible for the call to `Zeitwerk::Loader#reload` needs to acquire the lock for writing.
         | 
| 512 | 
            +
             | 
| 513 | 
            +
            On reloading, client code has to update anything that would otherwise be storing a stale object. For example, if the routing layer of a web framework stores reloadable controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes.
         | 
| 486 514 |  | 
| 487 515 | 
             
            <a id="markdown-inflection" name="inflection"></a>
         | 
| 488 516 | 
             
            ### Inflection
         | 
| @@ -956,12 +984,48 @@ require "active_job" | |
| 956 984 | 
             
            require "active_job/queue_adapters"
         | 
| 957 985 |  | 
| 958 986 | 
             
            require "zeitwerk"
         | 
| 959 | 
            -
             | 
| 987 | 
            +
            # By passign the flag, we acknowledge the extra directory lib/active_job
         | 
| 988 | 
            +
            # has to be managed by the loader and no warning has to be issued for it.
         | 
| 989 | 
            +
            loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
         | 
| 960 990 | 
             
            loader.setup
         | 
| 961 991 | 
             
            ```
         | 
| 962 992 |  | 
| 963 993 | 
             
            With that, when Zeitwerk scans the file system and reaches the gem directories `lib/active_job` and `lib/active_job/queue_adapters`, it detects the corresponding modules already exist and therefore understands it does not have to manage them. The loader just descends into those directories. Eventually will reach `lib/active_job/queue_adapters/awesome_queue.rb`, and since `ActiveJob::QueueAdapters::AwesomeQueue` is unknown, Zeitwerk will manage it. Which is what happens regularly with the files in your gem. On reload, the namespaces are safe, won't be reloaded. The loader only reloads what it manages, which in this case is the adapter itself.
         | 
| 964 994 |  | 
| 995 | 
            +
            <a id="markdown-introspection" name="introspection"></a>
         | 
| 996 | 
            +
            ### Introspection
         | 
| 997 | 
            +
             | 
| 998 | 
            +
            The method `Zeitwerk::Loader#dirs` returns an array with the absolute paths of the root directories as strings:
         | 
| 999 | 
            +
             | 
| 1000 | 
            +
            ```ruby
         | 
| 1001 | 
            +
            loader = Zeitwerk::Loader.new
         | 
| 1002 | 
            +
            loader.push_dir(Pathname.new("/foo"))
         | 
| 1003 | 
            +
            loader.dirs # => ["/foo"]
         | 
| 1004 | 
            +
            ```
         | 
| 1005 | 
            +
             | 
| 1006 | 
            +
            This method accepts an optional `namespaces` keyword argument. If truthy, the method returns a hash table instead. Keys are the absolute paths of the root directories as strings. Values are their corresponding namespaces, class or module objects:
         | 
| 1007 | 
            +
             | 
| 1008 | 
            +
            ```ruby
         | 
| 1009 | 
            +
            loader = Zeitwerk::Loader.new
         | 
| 1010 | 
            +
            loader.push_dir(Pathname.new("/foo"))
         | 
| 1011 | 
            +
            loader.push_dir(Pathname.new("/bar"), namespace: Bar)
         | 
| 1012 | 
            +
            loader.dirs(namespaces: true) # => { "/foo" => Object, "/bar" => Bar }
         | 
| 1013 | 
            +
            ```
         | 
| 1014 | 
            +
             | 
| 1015 | 
            +
            These collections are read-only. Please add to them with `Zeitwerk::Loader#push_dir`.
         | 
| 1016 | 
            +
             | 
| 1017 | 
            +
            <a id="markdown-encodings" name="encodings"></a>
         | 
| 1018 | 
            +
            ### Encodings
         | 
| 1019 | 
            +
             | 
| 1020 | 
            +
            Zeitwerk supports projects whose files and file system are in UTF-8. The encoding of the file system can be checked this way:
         | 
| 1021 | 
            +
             | 
| 1022 | 
            +
            ```
         | 
| 1023 | 
            +
            % ruby -e "puts Encoding.find('filesystem')"
         | 
| 1024 | 
            +
            UTF-8
         | 
| 1025 | 
            +
            ```
         | 
| 1026 | 
            +
             | 
| 1027 | 
            +
            The test suite passes on Windows with codepage `Windows-1252` if all the involved absolute paths are ASCII. Other supersets of ASCII may work too, but you have to try.
         | 
| 1028 | 
            +
             | 
| 965 1029 | 
             
            <a id="markdown-rules-of-thumb" name="rules-of-thumb"></a>
         | 
| 966 1030 | 
             
            ### Rules of thumb
         | 
| 967 1031 |  | 
| @@ -1054,6 +1118,11 @@ With Zeitwerk, you just name things following conventions and done. Things are a | |
| 1054 1118 |  | 
| 1055 1119 | 
             
            Autoloading in Rails was based on `const_missing` up to Rails 5. That callback lacks fundamental information like the nesting or the resolution algorithm being used. Because of that, Rails autoloading was not able to match Ruby's semantics, and that introduced a [series of issues](https://guides.rubyonrails.org/v5.2/autoloading_and_reloading_constants.html#common-gotchas). Zeitwerk is based on a different technique and fixed Rails autoloading starting with Rails 6.
         | 
| 1056 1120 |  | 
| 1121 | 
            +
            <a id="markdown-awards" name="awards"></a>
         | 
| 1122 | 
            +
            ## Awards
         | 
| 1123 | 
            +
             | 
| 1124 | 
            +
            Zeitwerk has been awarded an "Outstanding Performance Award" Fukuoka Ruby Award 2022.
         | 
| 1125 | 
            +
             | 
| 1057 1126 | 
             
            <a id="markdown-thanks" name="thanks"></a>
         | 
| 1058 1127 | 
             
            ## Thanks
         | 
| 1059 1128 |  | 
    
        data/lib/zeitwerk/error.rb
    CHANGED
    
    
| @@ -0,0 +1,65 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Zeitwerk
         | 
| 4 | 
            +
              # @private
         | 
| 5 | 
            +
              class GemLoader < Loader
         | 
| 6 | 
            +
                # Users should not create instances directly, the public interface is
         | 
| 7 | 
            +
                # `Zeitwerk::Loader.for_gem`.
         | 
| 8 | 
            +
                private_class_method :new
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                # @private
         | 
| 11 | 
            +
                # @sig (String, bool) -> Zeitwerk::GemLoader
         | 
| 12 | 
            +
                def self._new(root_file, warn_on_extra_files:)
         | 
| 13 | 
            +
                  new(root_file, warn_on_extra_files: warn_on_extra_files)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # @sig (String, bool) -> void
         | 
| 17 | 
            +
                def initialize(root_file, warn_on_extra_files:)
         | 
| 18 | 
            +
                  super()
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  @tag                 = File.basename(root_file, ".rb")
         | 
| 21 | 
            +
                  @inflector           = GemInflector.new(root_file)
         | 
| 22 | 
            +
                  @root_file           = File.expand_path(root_file)
         | 
| 23 | 
            +
                  @lib                 = File.dirname(root_file)
         | 
| 24 | 
            +
                  @warn_on_extra_files = warn_on_extra_files
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  push_dir(@lib)
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                # @sig () -> void
         | 
| 30 | 
            +
                def setup
         | 
| 31 | 
            +
                  warn_on_extra_files if @warn_on_extra_files
         | 
| 32 | 
            +
                  super
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                private
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                # @sig () -> void
         | 
| 38 | 
            +
                def warn_on_extra_files
         | 
| 39 | 
            +
                  expected_namespace_dir = @root_file.delete_suffix(".rb")
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  ls(@lib) do |basename, abspath|
         | 
| 42 | 
            +
                    next if abspath == @root_file
         | 
| 43 | 
            +
                    next if abspath == expected_namespace_dir
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    basename_without_ext = basename.delete_suffix(".rb")
         | 
| 46 | 
            +
                    cname = inflector.camelize(basename_without_ext, abspath)
         | 
| 47 | 
            +
                    ftype = dir?(abspath) ? "directory" : "file"
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    warn(<<~EOS)
         | 
| 50 | 
            +
                      WARNING: Zeitwerk defines the constant #{cname} after the #{ftype}
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                          #{abspath}
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                      To prevent that, please configure the loader to ignore it:
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                          loader.ignore("\#{__dir__}/#{basename}")
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                      Otherwise, there is a flag to silence this warning:
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                          Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
         | 
| 61 | 
            +
                    EOS
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
            end
         | 
| @@ -5,18 +5,19 @@ require "securerandom" | |
| 5 5 |  | 
| 6 6 | 
             
            module Zeitwerk::Loader::Config
         | 
| 7 7 | 
             
              # Absolute paths of the root directories. Stored in a hash to preserve
         | 
| 8 | 
            -
              # order, easily handle duplicates,  | 
| 9 | 
            -
              #  | 
| 8 | 
            +
              # order, easily handle duplicates, have a fast lookup needed for detecting
         | 
| 9 | 
            +
              # nested paths, and store custom namespaces as values.
         | 
| 10 10 | 
             
              #
         | 
| 11 | 
            -
              #   "/Users/fxn/blog/app/assets"   =>  | 
| 12 | 
            -
              #   "/Users/fxn/blog/app/channels" =>  | 
| 11 | 
            +
              #   "/Users/fxn/blog/app/assets"   => Object,
         | 
| 12 | 
            +
              #   "/Users/fxn/blog/app/channels" => Object,
         | 
| 13 | 
            +
              #   "/Users/fxn/blog/adapters"     => ActiveJob::QueueAdapters,
         | 
| 13 14 | 
             
              #   ...
         | 
| 14 15 | 
             
              #
         | 
| 15 16 | 
             
              # This is a private collection maintained by the loader. The public
         | 
| 16 17 | 
             
              # interface for it is `push_dir` and `dirs`.
         | 
| 17 18 | 
             
              #
         | 
| 18 19 | 
             
              # @private
         | 
| 19 | 
            -
              # @sig Hash[String,  | 
| 20 | 
            +
              # @sig Hash[String, Module]
         | 
| 20 21 | 
             
              attr_reader :root_dirs
         | 
| 21 22 |  | 
| 22 23 | 
             
              # @sig #camelize
         | 
| @@ -131,18 +132,25 @@ module Zeitwerk::Loader::Config | |
| 131 132 |  | 
| 132 133 | 
             
              # Sets a tag for the loader, useful for logging.
         | 
| 133 134 | 
             
              #
         | 
| 134 | 
            -
              # @param tag [#to_s]
         | 
| 135 135 | 
             
              # @sig (#to_s) -> void
         | 
| 136 136 | 
             
              def tag=(tag)
         | 
| 137 137 | 
             
                @tag = tag.to_s
         | 
| 138 138 | 
             
              end
         | 
| 139 139 |  | 
| 140 | 
            -
              #  | 
| 141 | 
            -
              #  | 
| 140 | 
            +
              # If `namespaces` is falsey (default), returns an array with the absolute
         | 
| 141 | 
            +
              # paths of the root directories as strings. If truthy, returns a hash table
         | 
| 142 | 
            +
              # instead. Keys are the absolute paths of the root directories as strings,
         | 
| 143 | 
            +
              # values are their corresponding namespaces, class or module objects.
         | 
| 142 144 | 
             
              #
         | 
| 143 | 
            -
              #  | 
| 144 | 
            -
               | 
| 145 | 
            -
             | 
| 145 | 
            +
              # These are read-only collections, please add to them with `push_dir`.
         | 
| 146 | 
            +
              #
         | 
| 147 | 
            +
              # @sig () -> Array[String] | Hash[String, Module]
         | 
| 148 | 
            +
              def dirs(namespaces: false)
         | 
| 149 | 
            +
                if namespaces
         | 
| 150 | 
            +
                  root_dirs.clone
         | 
| 151 | 
            +
                else
         | 
| 152 | 
            +
                  root_dirs.keys
         | 
| 153 | 
            +
                end.freeze
         | 
| 146 154 | 
             
              end
         | 
| 147 155 |  | 
| 148 156 | 
             
              # You need to call this method before setup in order to be able to reload.
         | 
| @@ -15,18 +15,50 @@ module Zeitwerk::Loader::Helpers | |
| 15 15 |  | 
| 16 16 | 
             
              # @sig (String) { (String, String) -> void } -> void
         | 
| 17 17 | 
             
              def ls(dir)
         | 
| 18 | 
            -
                Dir. | 
| 18 | 
            +
                children = Dir.children(dir)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                # The order in which a directory is listed depends on the file system.
         | 
| 21 | 
            +
                #
         | 
| 22 | 
            +
                # Since client code may run in different platforms, it seems convenient to
         | 
| 23 | 
            +
                # order directory entries. This provides consistent eager loading across
         | 
| 24 | 
            +
                # platforms, for example.
         | 
| 25 | 
            +
                children.sort!
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                children.each do |basename|
         | 
| 19 28 | 
             
                  next if hidden?(basename)
         | 
| 20 29 |  | 
| 21 30 | 
             
                  abspath = File.join(dir, basename)
         | 
| 22 31 | 
             
                  next if ignored_paths.member?(abspath)
         | 
| 23 32 |  | 
| 33 | 
            +
                  if dir?(abspath)
         | 
| 34 | 
            +
                    next unless has_at_least_one_ruby_file?(abspath)
         | 
| 35 | 
            +
                  else
         | 
| 36 | 
            +
                    next unless ruby?(abspath)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 24 39 | 
             
                  # We freeze abspath because that saves allocations when passed later to
         | 
| 25 40 | 
             
                  # File methods. See #125.
         | 
| 26 41 | 
             
                  yield basename, abspath.freeze
         | 
| 27 42 | 
             
                end
         | 
| 28 43 | 
             
              end
         | 
| 29 44 |  | 
| 45 | 
            +
              # @sig (String) -> bool
         | 
| 46 | 
            +
              def has_at_least_one_ruby_file?(dir)
         | 
| 47 | 
            +
                to_visit = [dir]
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                while dir = to_visit.shift
         | 
| 50 | 
            +
                  ls(dir) do |_basename, abspath|
         | 
| 51 | 
            +
                    if dir?(abspath)
         | 
| 52 | 
            +
                      to_visit << abspath
         | 
| 53 | 
            +
                    else
         | 
| 54 | 
            +
                      return true
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                false
         | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
             | 
| 30 62 | 
             
              # @sig (String) -> bool
         | 
| 31 63 | 
             
              def ruby?(path)
         | 
| 32 64 | 
             
                path.end_with?(".rb")
         | 
| @@ -37,7 +69,7 @@ module Zeitwerk::Loader::Helpers | |
| 37 69 | 
             
                File.directory?(path)
         | 
| 38 70 | 
             
              end
         | 
| 39 71 |  | 
| 40 | 
            -
              # @sig String -> bool
         | 
| 72 | 
            +
              # @sig (String) -> bool
         | 
| 41 73 | 
             
              def hidden?(basename)
         | 
| 42 74 | 
             
                basename.start_with?(".")
         | 
| 43 75 | 
             
              end
         | 
    
        data/lib/zeitwerk/loader.rb
    CHANGED
    
    | @@ -13,6 +13,9 @@ module Zeitwerk | |
| 13 13 | 
             
                include Helpers
         | 
| 14 14 | 
             
                include Config
         | 
| 15 15 |  | 
| 16 | 
            +
                MUTEX = Mutex.new
         | 
| 17 | 
            +
                private_constant :MUTEX
         | 
| 18 | 
            +
             | 
| 16 19 | 
             
                # Maps absolute paths for which an autoload has been set ---and not
         | 
| 17 20 | 
             
                # executed--- to their corresponding parent class or module and constant
         | 
| 18 21 | 
             
                # name.
         | 
| @@ -144,15 +147,16 @@ module Zeitwerk | |
| 144 147 | 
             
                    end
         | 
| 145 148 |  | 
| 146 149 | 
             
                    to_unload.each do |cpath, (abspath, (parent, cname))|
         | 
| 147 | 
            -
                       | 
| 148 | 
            -
             | 
| 149 | 
            -
             | 
| 150 | 
            -
             | 
| 151 | 
            -
             | 
| 152 | 
            -
             | 
| 153 | 
            -
             | 
| 154 | 
            -
                         | 
| 155 | 
            -
             | 
| 150 | 
            +
                      unless on_unload_callbacks.empty?
         | 
| 151 | 
            +
                        begin
         | 
| 152 | 
            +
                          value = cget(parent, cname)
         | 
| 153 | 
            +
                        rescue ::NameError
         | 
| 154 | 
            +
                          # Perhaps the user deleted the constant by hand, or perhaps an
         | 
| 155 | 
            +
                          # autoload failed to define the expected constant but the user
         | 
| 156 | 
            +
                          # rescued the exception.
         | 
| 157 | 
            +
                        else
         | 
| 158 | 
            +
                          run_on_unload_callbacks(cpath, value, abspath)
         | 
| 159 | 
            +
                        end
         | 
| 156 160 | 
             
                      end
         | 
| 157 161 |  | 
| 158 162 | 
             
                      unload_cref(parent, cname)
         | 
| @@ -196,14 +200,12 @@ module Zeitwerk | |
| 196 200 | 
             
                # @raise [Zeitwerk::Error]
         | 
| 197 201 | 
             
                # @sig () -> void
         | 
| 198 202 | 
             
                def reload
         | 
| 199 | 
            -
                   | 
| 200 | 
            -
             | 
| 201 | 
            -
             | 
| 202 | 
            -
             | 
| 203 | 
            -
             | 
| 204 | 
            -
                   | 
| 205 | 
            -
                    raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
         | 
| 206 | 
            -
                  end
         | 
| 203 | 
            +
                  raise ReloadingDisabledError unless reloading_enabled?
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                  unload
         | 
| 206 | 
            +
                  recompute_ignored_paths
         | 
| 207 | 
            +
                  recompute_collapse_dirs
         | 
| 208 | 
            +
                  setup
         | 
| 207 209 | 
             
                end
         | 
| 208 210 |  | 
| 209 211 | 
             
                # Eager loads all files in the root directories, recursively. Files do not
         | 
| @@ -236,7 +238,7 @@ module Zeitwerk | |
| 236 238 | 
             
                          if cref = autoloads[abspath]
         | 
| 237 239 | 
             
                            cget(*cref)
         | 
| 238 240 | 
             
                          end
         | 
| 239 | 
            -
                        elsif  | 
| 241 | 
            +
                        elsif !root_dirs.key?(abspath)
         | 
| 240 242 | 
             
                          if collapse?(abspath)
         | 
| 241 243 | 
             
                            queue << [namespace, abspath]
         | 
| 242 244 | 
             
                          else
         | 
| @@ -289,10 +291,6 @@ module Zeitwerk | |
| 289 291 | 
             
                  # @sig #call | #debug | nil
         | 
| 290 292 | 
             
                  attr_accessor :default_logger
         | 
| 291 293 |  | 
| 292 | 
            -
                  # @private
         | 
| 293 | 
            -
                  # @sig Mutex
         | 
| 294 | 
            -
                  attr_accessor :mutex
         | 
| 295 | 
            -
             | 
| 296 294 | 
             
                  # This is a shortcut for
         | 
| 297 295 | 
             
                  #
         | 
| 298 296 | 
             
                  #   require "zeitwerk"
         | 
| @@ -304,10 +302,13 @@ module Zeitwerk | |
| 304 302 | 
             
                  # except that this method returns the same object in subsequent calls from
         | 
| 305 303 | 
             
                  # the same file, in the unlikely case the gem wants to be able to reload.
         | 
| 306 304 | 
             
                  #
         | 
| 307 | 
            -
                  #  | 
| 308 | 
            -
                   | 
| 305 | 
            +
                  # This method returns a subclass of Zeitwerk::Loader, but the exact type
         | 
| 306 | 
            +
                  # is private, client code can only rely on the interface.
         | 
| 307 | 
            +
                  #
         | 
| 308 | 
            +
                  # @sig (bool) -> Zeitwerk::GemLoader
         | 
| 309 | 
            +
                  def for_gem(warn_on_extra_files: true)
         | 
| 309 310 | 
             
                    called_from = caller_locations(1, 1).first.path
         | 
| 310 | 
            -
                    Registry.loader_for_gem(called_from)
         | 
| 311 | 
            +
                    Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
         | 
| 311 312 | 
             
                  end
         | 
| 312 313 |  | 
| 313 314 | 
             
                  # Broadcasts `eager_load` to all loaders.
         | 
| @@ -326,8 +327,6 @@ module Zeitwerk | |
| 326 327 | 
             
                  end
         | 
| 327 328 | 
             
                end
         | 
| 328 329 |  | 
| 329 | 
            -
                self.mutex = Mutex.new
         | 
| 330 | 
            -
             | 
| 331 330 | 
             
                private # -------------------------------------------------------------------------------------
         | 
| 332 331 |  | 
| 333 332 | 
             
                # @sig (String, Module) -> void
         | 
| @@ -338,7 +337,7 @@ module Zeitwerk | |
| 338 337 | 
             
                        basename.delete_suffix!(".rb")
         | 
| 339 338 | 
             
                        cname = inflector.camelize(basename, abspath).to_sym
         | 
| 340 339 | 
             
                        autoload_file(parent, cname, abspath)
         | 
| 341 | 
            -
                       | 
| 340 | 
            +
                      else
         | 
| 342 341 | 
             
                        # In a Rails application, `app/models/concerns` is a subdirectory of
         | 
| 343 342 | 
             
                        # `app/models`, but both of them are root directories.
         | 
| 344 343 | 
             
                        #
         | 
| @@ -466,7 +465,7 @@ module Zeitwerk | |
| 466 465 |  | 
| 467 466 | 
             
                # @sig (String) -> void
         | 
| 468 467 | 
             
                def raise_if_conflicting_directory(dir)
         | 
| 469 | 
            -
                   | 
| 468 | 
            +
                  MUTEX.synchronize do
         | 
| 470 469 | 
             
                    Registry.loaders.each do |loader|
         | 
| 471 470 | 
             
                      next if loader == self
         | 
| 472 471 | 
             
                      next if loader.ignores?(dir)
         | 
    
        data/lib/zeitwerk/registry.rb
    CHANGED
    
    | @@ -10,12 +10,11 @@ module Zeitwerk | |
| 10 10 | 
             
                  # @sig Array[Zeitwerk::Loader]
         | 
| 11 11 | 
             
                  attr_reader :loaders
         | 
| 12 12 |  | 
| 13 | 
            -
                  # Registers loaders  | 
| 14 | 
            -
                  # in case of reload.
         | 
| 13 | 
            +
                  # Registers gem loaders to let `for_gem` be idempotent in case of reload.
         | 
| 15 14 | 
             
                  #
         | 
| 16 15 | 
             
                  # @private
         | 
| 17 16 | 
             
                  # @sig Hash[String, Zeitwerk::Loader]
         | 
| 18 | 
            -
                  attr_reader : | 
| 17 | 
            +
                  attr_reader :gem_loaders_by_root_file
         | 
| 19 18 |  | 
| 20 19 | 
             
                  # Maps absolute paths to the loaders responsible for them.
         | 
| 21 20 | 
             
                  #
         | 
| @@ -77,7 +76,7 @@ module Zeitwerk | |
| 77 76 | 
             
                  # @sig (Zeitwerk::Loader) -> void
         | 
| 78 77 | 
             
                  def unregister_loader(loader)
         | 
| 79 78 | 
             
                    loaders.delete(loader)
         | 
| 80 | 
            -
                     | 
| 79 | 
            +
                    gem_loaders_by_root_file.delete_if { |_, l| l == loader }
         | 
| 81 80 | 
             
                    autoloads.delete_if { |_, l| l == loader }
         | 
| 82 81 | 
             
                    inceptions.delete_if { |_, (_, l)| l == loader }
         | 
| 83 82 | 
             
                  end
         | 
| @@ -87,14 +86,8 @@ module Zeitwerk | |
| 87 86 | 
             
                  #
         | 
| 88 87 | 
             
                  # @private
         | 
| 89 88 | 
             
                  # @sig (String) -> Zeitwerk::Loader
         | 
| 90 | 
            -
                  def loader_for_gem(root_file)
         | 
| 91 | 
            -
                     | 
| 92 | 
            -
                      Loader.new.tap do |loader|
         | 
| 93 | 
            -
                        loader.tag = File.basename(root_file, ".rb")
         | 
| 94 | 
            -
                        loader.inflector = GemInflector.new(root_file)
         | 
| 95 | 
            -
                        loader.push_dir(File.dirname(root_file))
         | 
| 96 | 
            -
                      end
         | 
| 97 | 
            -
                    end
         | 
| 89 | 
            +
                  def loader_for_gem(root_file, warn_on_extra_files:)
         | 
| 90 | 
            +
                    gem_loaders_by_root_file[root_file] ||= GemLoader._new(root_file, warn_on_extra_files: warn_on_extra_files)
         | 
| 98 91 | 
             
                  end
         | 
| 99 92 |  | 
| 100 93 | 
             
                  # @private
         | 
| @@ -137,9 +130,9 @@ module Zeitwerk | |
| 137 130 | 
             
                  end
         | 
| 138 131 | 
             
                end
         | 
| 139 132 |  | 
| 140 | 
            -
                @loaders | 
| 141 | 
            -
                @ | 
| 142 | 
            -
                @autoloads | 
| 143 | 
            -
                @inceptions | 
| 133 | 
            +
                @loaders                  = []
         | 
| 134 | 
            +
                @gem_loaders_by_root_file = {}
         | 
| 135 | 
            +
                @autoloads                = {}
         | 
| 136 | 
            +
                @inceptions               = {}
         | 
| 144 137 | 
             
              end
         | 
| 145 138 | 
             
            end
         | 
    
        data/lib/zeitwerk/version.rb
    CHANGED
    
    
    
        data/lib/zeitwerk.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: zeitwerk
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 2. | 
| 4 | 
            +
              version: 2.6.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Xavier Noria
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2022- | 
| 11 | 
            +
            date: 2022-09-30 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies: []
         | 
| 13 13 | 
             
            description: |2
         | 
| 14 14 | 
             
                  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
         | 
| @@ -26,6 +26,7 @@ files: | |
| 26 26 | 
             
            - lib/zeitwerk/error.rb
         | 
| 27 27 | 
             
            - lib/zeitwerk/explicit_namespace.rb
         | 
| 28 28 | 
             
            - lib/zeitwerk/gem_inflector.rb
         | 
| 29 | 
            +
            - lib/zeitwerk/gem_loader.rb
         | 
| 29 30 | 
             
            - lib/zeitwerk/inflector.rb
         | 
| 30 31 | 
             
            - lib/zeitwerk/kernel.rb
         | 
| 31 32 | 
             
            - lib/zeitwerk/loader.rb
         |