pax_global_header00006660000000000000000000000064135702401130014506gustar00rootroot0000000000000052 comment=652477c70a3385dd06251e40ea530b3026beb59c zeitwerk-2.2.2/000077500000000000000000000000001357024011300133555ustar00rootroot00000000000000zeitwerk-2.2.2/.gitignore000066400000000000000000000000341357024011300153420ustar00rootroot00000000000000Gemfile.lock test/tmp *.gem zeitwerk-2.2.2/.travis.yml000066400000000000000000000002521357024011300154650ustar00rootroot00000000000000language: ruby rvm: - 2.4.4 - 2.4 - 2.5 - 2.6 - ruby-head - jruby-head - truffleruby matrix: allow_failures: - rvm: jruby-head - rvm: truffleruby zeitwerk-2.2.2/CHANGELOG.md000066400000000000000000000161121357024011300151670ustar00rootroot00000000000000# CHANGELOG ## 2.2.2 (29 November 2019) * `Zeitwerk::NameError#name` has the name of the missing constant now. ## 2.2.1 (1 November 2019) * Zeitwerk raised `NameError` when a managed file did not define its expected constant. Now, it raises `Zeitwerk::NameError` instead, so it is possible for client code to distinguish that mismatch from a regular `NameError`. Regarding backwards compatibility, `Zeitwerk::NameError` is a subclass of `NameError`. ## 2.2.0 (9 October 2019) * The default inflectors have API to override how to camelize selected basenames: ```ruby loader.inflector.inflect "mysql_adapter" => "MySQLAdapter" ``` This addresses a common pattern, which is to use the basic inflectors with a few straightforward exceptions typically configured in a hash table or `case` expression. You no longer have to define a custom inflector if that is all you need. * Documentation improvements. ## 2.1.10 (6 September 2019) * Raises `Zeitwerk::NameError` with a better error message when a managed file or directory has a name that yields an invalid constant name when inflected. `Zeitwerk::NameError` is a subclass of `NameError`. ## 2.1.9 (16 July 2019) * Preloading is soft-deprecated. The use case it was thought for is no longer. Please, if you have a legit use case for it, drop me a line. * Root directory conflict detection among loaders takes ignored directories into account. * Supports classes and modules with overridden `name` methods. * Documentation improvements. ## 2.1.8 (29 June 2019) * Fixes eager loading nested root directories. The new approach in 2.1.7 introduced a regression. ## 2.1.7 (29 June 2019) * Prevent the inflector from deleting parts un multiword constants whose capitalization is the same. For example, `point_2d` should be inflected as `Point2d`, rather than `Point`. While the inflector is frozen, this seems to be just wrong, and the refinement should be backwards compatible, since those constants were not usable. * Make eager loading consistent with auto loading with regard to detecting namespaces that do not define the matching constant. * Documentation improvements. ## 2.1.6 (30 April 2019) * Fixed: If an eager load exclusion contained an autoload for a namespace also present in other branches that had to be eager loaded, they could be skipped. * `loader.log!` is a convenient shortcut to get traces to `$stdout`. * Allocates less strings. ## 2.1.5 (24 April 2019) * Failed autoloads raise `NameError` as always, but with a more user-friendly message instead of the original generic one from Ruby. * Eager loading uses `const_get` now rather than `require`. A file that does not define the expected constant could be eager loaded, but not autoloaded, which would be inconsistent. Thanks to @casperisfine for reporting this one and help testing the alternative. ## 2.1.4 (23 April 2019) * Supports deletion of root directories in disk after they've been configured. `push_dir` requires root directories to exist to prevent misconfigurations, but after that Zeitwerk no longer assumes they exist. This might be convenient if you removed one in a web application while a server was running. ## 2.1.3 (22 April 2019) * Documentation improvements. * Internal work. ## 2.1.2 (11 April 2019) * Calling `reload` with reloading disabled raises `Zeitwerk::ReloadingDisabledError`. ## 2.1.1 (10 April 2019) * Internal performance work. ## 2.1.0 (9 April 2019) * `loaded_cpaths` is gone, you can ask if a constant path is going to be unloaded instead with `loader.to_unload?(cpath)`. Thanks to this refinement, Zeitwerk is able to consume even less memory. (Change included in a minor upgrade because the introspection API is not documented, and it still isn't, needs some time to settle down). ## 2.0.0 (7 April 2019) * Reloading is disabled by default. In order to be able to reload you need to opt-in by calling `loader.enable_reloading` before setup. The motivation for this breaking change is twofold. On one hand, this is a design decision at the interface/usage level that reflects that the majority of use cases for Zeitwerk do not need reloading. On the other hand, if reloading is not enabled, Zeitwerk is able to use less memory. Notably, this is more optimal for large web applications in production. ## 1.4.3 (26 March 2019) * Faster reload. If you're using `bootsnap`, requires at least version 1.4.2. ## 1.4.2 (23 March 2019) * Includes an optimization. ## 1.4.1 (23 March 2019) * Fixes concurrent autovivifications. ## 1.4.0 (19 March 2019) * Trace point optimization for singleton classes by @casperisfine. See the use case, explanation, and patch in [#24](https://github.com/fxn/zeitwerk/pull/24). * `Zeitwerk::Loader#do_not_eager_load` provides a way to have autoloadable files and directories that should be skipped when eager loading. ## 1.3.4 (14 March 2019) * Files shadowed by previous occurrences defining the same constant path were being correctly skipped when autoloading, but not when eager loading. This has been fixed. This mimicks what happens when there are two files in `$LOAD_PATH` with the same relative name, only the first one is loaded by `require`. ## 1.3.3 (12 March 2019) * Bug fix by @casperisfine: If the superclass or one of the ancestors of an explicit namespace `N` has an autoload set for constant `C`, and `n/c.rb` exists, the autoload for `N::C` proper could be missed. ## 1.3.2 (6 March 2019) * Improved documentation. * Zeitwerk creates at most one trace point per process, instead of one per loader. This is more performant when there are multiple gems managed by Zeitwerk. ## 1.3.1 (23 February 2019) * After module vivification, the tracer could trigger one unnecessary autoload walk. ## 1.3.0 (21 February 2019) * In addition to callables, loggers can now also be any object that responds to `debug`, which accepts one string argument. ## 1.2.0 (14 February 2019) * Use `pretty_print` in the exception message for conflicting directories. ## 1.2.0.beta (14 February 2019) * Two different loaders cannot be managing the same files. Now, `Zeitwerk::Loader#push_dir` raises `Zeitwerk::ConflictingDirectory` if it detects a conflict. ## 1.1.0 (14 February 2019) * New class attribute `Zeitwerk::Loader.default_logger`, inherited by newly instantiated loaders. Default is `nil`. * Traces include the loader tag in the prefix to easily distinguish them. * Loaders now have a tag. ## 1.0.0 (12 February 2019) * Documentation improvements. ## 1.0.0.beta3 (4 February 2019) * Documentation improvements. * `Zeitwerk::Loader#ignore` accepts glob patterns. * New read-only introspection method `Zeitwerk::Loader.all_dirs`. * New read-only introspection method `Zeitwerk::Loader#dirs`. * New introspection predicate `Zeitwerk::Loader#loaded?(cpath)`. ## 1.0.0.beta2 (22 January 2019) * `do_not_eager_load` has been removed, please use `ignore` to opt-out. * Documentation improvements. * Pronunciation section in the README, linking to sample audio file. * All logged messages have a "Zeitwerk:" prefix for easy grepping. * On reload, the logger also traces constants and autoloads removed. ## 1.0.0.beta (18 January 2019) * Initial beta release. zeitwerk-2.2.2/Gemfile000066400000000000000000000001601357024011300146450ustar00rootroot00000000000000source 'https://rubygems.org' gemspec gem "rake" gem "minitest" gem "minitest-focus" gem "minitest-reporters" zeitwerk-2.2.2/MIT-LICENSE000066400000000000000000000020451357024011300150120ustar00rootroot00000000000000Copyright (c) 2019–ω Xavier Noria Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. zeitwerk-2.2.2/PROJECT_RULES.md000066400000000000000000000102371357024011300157620ustar00rootroot00000000000000# Project rules ## Notation It is very important that all the source code uses systematically the following naming conventions. ### Variables for constants * `cname`: A constant name, for example `:User`. Must be a symbol. * `cpath`: A constant path, for example `"User"` or `"Hotel::Pricing"`. Must be a string. * `cref`: A constant reference represented as an array of two elements. The first one a class or module object, and the second one a constant name as a symbol. For example `[Admin, :UsersController]`. ### Variables for paths You should pick always the most specific option: * `file`: Absolute path of a file. * `dir`: Absolute path of a directory. * `abspath`: Absolute path of a file or directory. * `realpath`: Absolute real path of a file or directory. Note that Zeitwerk does not deal with file or directory objects, only with paths. For brevity, we exploit this fact to adopt the convention `file`/`dir` instead of `filename`/`dirname` or somesuch. ## Paths * The only relative file names allowed in the project come from users. For example, public methods like `push_dir` should understand relative paths. * As soon as a relative file name comes from outside, it has to be converted to an absolute file name right away. * Internally, you have to use exclusively absolute file names. In particular, any `autoload` or `require` calls have to be issued using absolute paths to avoid `$LOAD_PATH` walks. * It is forbidden to do any sort of directory lookups resolving relative file names. * The only directory walks allowed are the one needed to set autoloads. One pass, and as lazy as possible (do not descend into subdirectories until necessary). * File and directory names should be kept as entered as much as possible so that logging prints what the user expects. Convert to real paths only in code that needs coordination with `Kernel#require`. ## Class and module names * Classes and modules may override the `name` method, therefore we cannot assume it returns their original constant path. Always use the helper `real_mod_name` on classes and modules coming from the user. ## Types * All methods should have a documented signature. * Use the most concise type always. Use a set when a set is the best choice, use `Module` when a class or module object is the natural data type (rather than its name). * Use always symbols for constant names. * Use always strings for constant paths. * Use always strings for paths, not pathnames. Pathnames are only accepted coming from the user, but internally everything is strings. ## Public interface definition Documented public methods conform the public interface. In particular: * Public methods tagged as `@private` do not belong to the public interface. * Undocumented public methods do not belong to the public interface. They are probably exploratory and may change or be deleted without warning. These are private interface in practice. * Undocumented public methods can be used in the Rails integration. We control both repositories, and Rails usage may help refine the actual public interface. Any release can change the private interface, including patch releases. ## Documentation Try to word the documentation in terms of classes, modules, and namespaces. Do that with extra care to avoid introducing leaking metaphors. We sacrifice there a bit of precision in order to communicate better. Some Ruby programmers do not have a deep understanding of constants, so better avoid being pedantic for didactic purposes. Those in the know understand what the documentation really says. ## Performance Zeitwerk is infrastructure, should have minimal cost both in speed and memory usage. Be extra careful, allocate as less as possible, store as less as possible. Use always absolute file names for `autoload` and `require`. Log always using this pattern: ```ruby log(message) if logger ``` to avoid unnecessary calls, and unnecessary computed values in the message. Some projects may have hundreds of root directories and hundreds of thousands of files, please remember that. However, do not write ugly code. Ugly code should be extremely justified in terms of performance. Instead, keep it simple, write simple performant code that reads well and is idiomatic. zeitwerk-2.2.2/README.md000066400000000000000000000630001357024011300146330ustar00rootroot00000000000000# Zeitwerk [![Gem Version](https://img.shields.io/gem/v/zeitwerk.svg?style=for-the-badge)](https://rubygems.org/gems/zeitwerk) [![Build Status](https://img.shields.io/travis/com/fxn/zeitwerk/master?style=for-the-badge)](https://travis-ci.com/fxn/zeitwerk) - [Introduction](#introduction) - [Synopsis](#synopsis) - [File structure](#file-structure) - [Implicit namespaces](#implicit-namespaces) - [Explicit namespaces](#explicit-namespaces) - [Nested root directories](#nested-root-directories) - [Usage](#usage) - [Setup](#setup) - [Autoloading](#autoloading) - [Eager loading](#eager-loading) - [Reloading](#reloading) - [Inflection](#inflection) - [Zeitwerk::Inflector](#zeitwerkinflector) - [Zeitwerk::GemInflector](#zeitwerkgeminflector) - [Custom inflector](#custom-inflector) - [Logging](#logging) - [Loader tag](#loader-tag) - [Ignoring parts of the project](#ignoring-parts-of-the-project) - [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions) - [Use case: The adapter pattern](#use-case-the-adapter-pattern) - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files) - [Edge cases](#edge-cases) - [Rules of thumb](#rules-of-thumb) - [Autoloading, explicit namespaces, and debuggers](#autoloading-explicit-namespaces-and-debuggers) - [Pronunciation](#pronunciation) - [Supported Ruby versions](#supported-ruby-versions) - [Testing](#testing) - [Motivation](#motivation) - [Thanks](#thanks) - [License](#license) ## Introduction Zeitwerk is an efficient and thread-safe code loader for Ruby. Given a [conventional file structure](#file-structure), Zeitwerk is able to load your project's classes and modules on demand (autoloading), or upfront (eager loading). You don't need to write `require` calls for your own files, rather, you can streamline your programming knowing that your classes and modules are available everywhere. This feature is efficient, thread-safe, and matches Ruby's semantics for constants. Zeitwerk is also able to reload code, which may be handy while developing web applications. Coordination is needed to reload in a thread-safe manner. The documentation below explains how to do this. The gem is designed so that any project, gem dependency, application, etc. can have their own independent loader, coexisting in the same process, managing their own project trees, and independent of each other. Each loader has its own configuration, inflector, and optional logger. Internally, Zeitwerk issues `require` calls exclusively using absolute file names, so there are no costly file system lookups in `$LOAD_PATH`. Technically, the directories managed by Zeitwerk do not even need to be in `$LOAD_PATH`. Furthermore, Zeitwerk does only one single scan of the project tree, and it descends into subdirectories lazily, only if their namespaces are used. ## Synopsis Main interface for gems: ```ruby # lib/my_gem.rb (main file) require "zeitwerk" loader = Zeitwerk::Loader.for_gem loader.setup # ready! module MyGem # ... end loader.eager_load # optionally ``` Main generic interface: ```ruby loader = Zeitwerk::Loader.new loader.push_dir(...) loader.setup # ready! ``` The `loader` variable can go out of scope. Zeitwerk keeps a registry with all of them, and so the object won't be garbage collected. You can reload if you want to: ```ruby loader = Zeitwerk::Loader.new loader.push_dir(...) loader.enable_reloading # you need to opt-in before setup loader.setup ... loader.reload ``` and you can eager load all the code: ```ruby loader.eager_load ``` It is also possible to broadcast `eager_load` to all instances: ```ruby Zeitwerk::Loader.eager_load_all ``` ## File structure To have a file structure Zeitwerk can work with, just name files and directories after the name of the classes and modules they define: ``` lib/my_gem.rb -> MyGem lib/my_gem/foo.rb -> MyGem::Foo lib/my_gem/bar_baz.rb -> MyGem::BarBaz lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo ``` Every directory configured with `push_dir` acts as root namespace. There can be several of them. For example, given ```ruby loader.push_dir(Rails.root.join("app/models")) loader.push_dir(Rails.root.join("app/controllers")) ``` Zeitwerk understands that their respective files and subdirectories belong to the root namespace: ``` app/models/user.rb -> User app/controllers/admin/users_controller.rb -> Admin::UsersController ``` ### Implicit namespaces Directories without a matching Ruby file get modules autovivified automatically by Zeitwerk. For example, in ``` app/controllers/admin/users_controller.rb -> Admin::UsersController ``` `Admin` is autovivified as a module on demand, you do not need to define an `Admin` class or module in an `admin.rb` file explicitly. ### Explicit namespaces Classes and modules that act as namespaces can also be explicitly defined, though. For instance, consider ``` app/models/hotel.rb -> Hotel app/models/hotel/pricing.rb -> Hotel::Pricing ``` There, `app/models/hotel.rb` defines `Hotel`, and thus Zeitwerk does not autovivify a module. The classes and modules from the namespace are already available in the body of the class or module defining it: ```ruby class Hotel < ApplicationRecord include Pricing # works ... end ``` An explicit namespace must be managed by one single loader. Loaders that reopen namespaces owned by other projects are responsible for loading their constants before setup. ### Nested root directories Root directories should not be ideally nested, but Zeitwerk supports them because in Rails, for example, both `app/models` and `app/models/concerns` belong to the autoload paths. Zeitwerk detects nested root directories, and treats them as roots only. In the example above, `concerns` is not considered to be a namespace below `app/models`. For example, the file: ``` app/models/concerns/geolocatable.rb ``` should define `Geolocatable`, not `Concerns::Geolocatable`. ## Usage ### Setup Loaders are ready to load code right after calling `setup` on them: ```ruby loader.setup ``` This method is synchronized and idempotent. Customization should generally be done before that call. In particular, in the generic interface you may set the root directories from which you want to load files: ```ruby loader.push_dir(...) loader.push_dir(...) loader.setup ``` The loader returned by `Zeitwerk::Loader.for_gem` has the directory of the caller pushed, normally that is the absolute path of `lib`. In that sense, `for_gem` can be used also by projects with a gem structure, even if they are not technically gems. That is, you don't need a gemspec or anything. If the main module of a library 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: ```ruby # lib/my_gem.rb (main file) require "zeitwerk" loader = Zeitwerk::Loader.for_gem loader.setup module MyGem # Since the setup has been performed, at this point we are already able # to reference project constants, in this case MyGem::MyLogger. include MyLogger end ``` Zeitwerk works internally only with absolute paths to avoid costly file searches in `$LOAD_PATH`. Indeed, the root directories do not even need to belong to `$LOAD_PATH`, everything just works by design if they don't. ### Autoloading After `setup`, you are able to reference classes and modules from the project without issuing `require` calls for them. They are all available everywhere, autoloading loads them on demand. This works even if the reference to the class or module is first hit in client code, outside your project. Let's revisit the example above: ```ruby # lib/my_gem.rb (main file) require "zeitwerk" loader = Zeitwerk::Loader.for_gem loader.setup module MyGem include MyLogger # (*) end ``` That works, and there is no `require "my_gem/my_logger"`. When `(*)` is reached, Zeitwerk seamlessly autoloads `MyGem::MyLogger`. If autoloading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`. ### Eager loading Zeitwerk instances are able to eager load their managed files: ```ruby loader.eager_load ``` That skips [ignored files and directories](#ignoring-parts-of-the-project), and you can also tell Zeitwerk that certain files or directories are autoloadable, but should not be eager loaded: ```ruby db_adapters = "#{__dir__}/my_gem/db_adapters" loader.do_not_eager_load(db_adapters) loader.setup loader.eager_load # won't eager load the database adapters ``` In gems, the method needs to be invoked after the main namespace has been defined, as shown in [Synopsis](https://github.com/fxn/zeitwerk#synopsis). Eager loading is synchronized and idempotent. If eager loading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`. If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances: ```ruby Zeitwerk::Loader.eager_load_all ``` This may be handy in top-level services, like web applications. Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded. ### Reloading Zeitwerk is able to reload code, but you need to enable this feature: ```ruby loader = Zeitwerk::Loader.new loader.push_dir(...) loader.enable_reloading # you need to opt-in before setup loader.setup ... loader.reload ``` There is no way to undo this, either you want to reload or you don't. Enabling reloading after setup raises `Zeitwerk::Error`. Attempting to reload without having it enabled raises `Zeitwerk::ReloadingDisabledError`. Generally speaking, reloading is useful while developing running services like web applications. Gems that implement regular libraries, so to speak, or services running in testing or production environments, won't normally have a use case for reloading. If reloading is not enabled, Zeitwerk is able to use less memory. Reloading removes the currently loaded classes and modules and resets the loader so that it will pick whatever is in the file system now. It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent. In order for reloading to be thread-safe, you need to implement some coordination. For example, a web framework that serves each request with its own thread may have a globally accessible RW lock. When a request comes in, the framework acquires the lock for reading at the beginning, and the code in the framework that calls `loader.reload` needs to acquire the lock for writing. 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 controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes. ### Inflection Each individual loader needs an inflector to figure out which constant path would a given file or directory map to. Zeitwerk ships with two basic inflectors. #### Zeitwerk::Inflector This is a very basic inflector that converts snake case to camel case: ``` user -> User users_controller -> UsersController html_parser -> HtmlParser ``` The camelize logic can be overridden easily for individual basenames: ```ruby loader.inflector.inflect( "html_parser" => "HTMLParser", "mysql_adapter" => "MySQLAdapter" ) ``` The `inflect` method can be invoked several times if you prefer this other style: ```ruby loader.inflector.inflect "html_parser" => "HTMLParser" loader.inflector.inflect "mysql_adapter" => "MySQLAdapter" ``` Overrides need to be configured before calling `setup`. There are no inflection rules or global configuration that can affect this inflector. It is deterministic. Loaders instantiated with `Zeitwerk::Loader.new` have an inflector of this type, independent of each other. #### Zeitwerk::GemInflector This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`. Loaders instantiated with `Zeitwerk::Loader.for_gem` have an inflector of this type, independent of each other. #### Custom inflector The inflectors that ship with Zeitwerk are deterministic and simple. But you can configure your own: ```ruby # frozen_string_literal: true class MyInflector < Zeitwerk::Inflector def camelize(basename, abspath) if basename =~ /\Ahtml_(.*)/ "HTML" + super($1, abspath) else super end end end ``` The first argument, `basename`, is a string with the basename of the file or directory to be inflected. In the case of a file, without extension. In the case of a directory, without trailing slash. The inflector needs to return this basename inflected. Therefore, a simple constant name without colons. The second argument, `abspath`, is a string with the absolute path to the file or directory in case you need it to decide how to inflect the basename. Paths to directories don't have trailing slashes. Then, assign the inflector: ```ruby loader.inflector = MyInflector.new ``` This needs to be done before calling `setup`. If a custom inflector definition in a gem takes too much space in the main file, you can extract it. For example, this is a simple pattern: ```ruby # lib/my_gem/inflector.rb module MyGem class Inflector < Zeitwerk::GemInflector ... end end # lib/my_gem.rb require "zeitwerk" require_relative "my_gem/inflector" loader = Zeitwerk::Loader.for_gem loader.inflector = MyGem::Inflector.new(__FILE__) loader.setup module MyGem # ... end ``` Since `MyGem` is referenced before the namespace is defined in the main file, it is important to use this style: ```ruby # Correct, effectively defines MyGem. module MyGem class Inflector < Zeitwerk::GemInflector # ... end end ``` instead of: ```ruby # Raises uninitialized constant MyGem (NameError). class MyGem::Inflector < Zeitwerk::GemInflector # ... end ``` ### Logging Zeitwerk is silent by default, but you can ask loaders to trace their activity. Logging is meant just for troubleshooting, shouldn't normally be enabled. The `log!` method is a quick shortcut to let the loader log to `$stdout`: ``` loader.log! ``` If you want more control, a logger can be configured as a callable ```ruby loader.logger = method(:puts) loader.logger = ->(msg) { ... } ``` as well as anything that responds to `debug`: ```ruby loader.logger = Logger.new($stderr) loader.logger = Rails.logger ``` In both cases, the corresponding methods are going to be passed exactly one argument with the message to be logged. It is also possible to set a global default this way: ```ruby Zeitwerk::Loader.default_logger = method(:puts) ``` If there is a logger configured, you'll see traces when autoloads are set, files loaded, and modules autovivified. While reloading, removed autoloads and unloaded objects are also traced. As a curiosity, if your project has namespaces you'll notice in the traces Zeitwerk sets autoloads for _directories_. That's a technique used to be able to descend into subdirectories on demand, avoiding that way unnecessary tree walks. #### Loader tag Loaders have a tag that is printed in traces in order to be able to distinguish them in globally logged activity: ``` Zeitwerk@9fa54b: autoload set for User, to be loaded from ... ``` By default, a random tag like the one above is assigned, but you can change it: ``` loader.tag = "grep_me" ``` The tag of a loader returned by `for_gem` is the basename of the root file without extension: ``` Zeitwerk@my_gem: constant MyGem::Foo loaded from ... ``` ### Ignoring parts of the project Zeitwerk ignores automatically any file or directory whose name starts with a dot, and any files that do not have extension ".rb". However, sometimes it might still be convenient to tell Zeitwerk to completely ignore some particular Ruby file or directory. That is possible with `ignore`, which accepts an arbitrary number of strings or `Pathname` objects, and also an array of them. You can ignore file names, directory names, and glob patterns. Glob patterns are expanded when they are added and again on each reload. Let's see some use cases. #### Use case: Files that do not follow the conventions Let's suppose that your gem decorates something in `Kernel`: ```ruby # lib/my_gem/core_ext/kernel.rb Kernel.module_eval do # ... end ``` That file does not define a constant path after the path name and you need to tell Zeitwerk: ```ruby kernel_ext = "#{__dir__}/my_gem/core_ext/kernel.rb" loader.ignore(kernel_ext) loader.setup ``` You can also ignore the whole directory: ```ruby core_ext = "#{__dir__}/my_gem/core_ext" loader.ignore(core_ext) loader.setup ``` #### Use case: The adapter pattern Another use case for ignoring files is the adapter pattern. Let's imagine your project talks to databases, supports several, and has adapters for each one of them. Those adapters may have top-level `require` calls that load their respective drivers: ```ruby # my_gem/db_adapters/postgresql.rb require "pg" ``` but you don't want your users to install them all, only the one they are going to use. On the other hand, if your code is eager loaded by you or a parent project (with `Zeitwerk::Loader.eager_load_all`), those `require` calls are going to be executed. Ignoring the adapters prevents that: ```ruby db_adapters = "#{__dir__}/my_gem/db_adapters" loader.ignore(db_adapters) loader.setup ``` The chosen adapter, then, has to be loaded by hand somehow: ```ruby require "my_gem/db_adapters/#{config[:db_adapter]}" ``` Note that since the directory is ignored, the required adapter can instantiate another loader to manage its subtree, if desired. Such loader would coexist with the main one just fine. #### Use case: Test files mixed with implementation files There are project layouts that put implementation files and test files together. To ignore the test files, you can use a glob pattern like this: ```ruby tests = "#{__dir__}/**/*_test.rb" loader.ignore(tests) loader.setup ``` ### Edge cases A class or module that acts as a namespace: ```ruby # trip.rb class Trip include Geolocation end # trip/geolocation.rb module Trip::Geolocation ... end ``` has to be defined with the `class` or `module` keywords, as in the example above. For technical reasons, raw constant assignment is not supported: ```ruby # trip.rb Trip = Class.new { ... } # NOT SUPPORTED Trip = Struct.new { ... } # NOT SUPPORTED ``` This only affects explicit namespaces, those idioms work well for any other ordinary class or module. ### Rules of thumb 1. Different loaders should manage different directory trees. It is an error condition to configure overlapping root directories in different loaders. 2. Think the mere existence of a file is effectively like writing a `require` call for them, which is executed on demand (autoload) or upfront (eager load). 3. In that line, if two loaders manage files that translate to the same constant in the same namespace, the first one wins, the rest are ignored. Similar to what happens with `require` and `$LOAD_PATH`, only the first occurrence matters. 4. Projects that reopen a namespace defined by some dependency have to ensure said namespace is loaded before setup. That is, the project has to make sure it reopens, rather than define. This is often accomplished just loading the dependency. 5. Objects stored in reloadable constants should not be cached in places that are not reloaded. For example, non-reloadable classes should not subclass a reloadable class, or mixin a reloadable module. Otherwise, after reloading, those classes or module objects would become stale. Referring to constants in dynamic places like method calls or lambdas is fine. 6. In a given process, ideally, there should be at most one loader with reloading enabled. Technically, you can have more, but it may get tricky if one refers to constants managed by the other one. Do that only if you know what you are doing. ### Autoloading, explicit namespaces, and debuggers As of this writing, Zeitwerk is unable to autoload classes or modules that belong to [explicit namespaces](#explicit-namespaces) inside debugger sessions. You'll get a `NameError`. The root cause is that debuggers set trace points, and Zeitwerk does too to support explicit namespaces. A debugger session happens inside a trace point handler, and Ruby does not invoke other handlers from within a running handler. Therefore, the code that manages explicit namespaces in Zeitwerk does not get called by the interpreter. See [this issue](https://github.com/deivid-rodriguez/byebug/issues/564#issuecomment-499413606) for further details. As a workaround, you can eager load. Zeitwerk tries hard to succeed or fail consistently both autoloading and eager loading, so switching to eager loading should not introduce any interference in your debugging logic, generally speaking. ## Pronunciation "Zeitwerk" is pronounced [this way](http://share.hashref.com/zeitwerk/zeitwerk_pronunciation.mp3). ## Supported Ruby versions Zeitwerk works with MRI 2.4.4 and above. ## Testing In order to run the test suite of Zeitwerk, `cd` into the project root and execute ``` bin/test ``` To run one particular suite, pass its file name as an argument: ``` bin/test test/lib/zeitwerk/test_eager_load.rb ``` Furthermore, the project has a development dependency on [`minitest-focus`](https://github.com/seattlerb/minitest-focus). To run an individual test mark it with `focus`: ```ruby focus test "capitalizes the first letter" do assert_equal "User", camelize("user") end ``` and run `bin/test`. ## Motivation Since `require` has global side-effects, and there is no static way to verify that you have issued the `require` calls for code that your file depends on, in practice it is very easy to forget some. That introduces bugs that depend on the load order. Zeitwerk provides a way to forget about `require` in your own code, just name things following conventions and done. On the other hand, autoloading in Rails is based on `const_missing`, which lacks fundamental information like the nesting and the resolution algorithm that was being used. Because of that, Rails autoloading is not able to match Ruby's semantics and that introduces a series of gotchas. The original goal of this project was to bring a better autoloading mechanism for Rails 6. ## Thanks I'd like to thank [@matthewd](https://github.com/matthewd) for the discussions we've had about this topic in the past years, I learned a couple of tricks used in Zeitwerk from him. Also, would like to thank [@Shopify](https://github.com/Shopify), [@rafaelfranca](https://github.com/rafaelfranca), and [@dylanahsmith](https://github.com/dylanahsmith), for sharing [this PoC](https://github.com/Shopify/autoload_reloader). The technique Zeitwerk uses to support explicit namespaces was copied from that project. Jean Boussier ([@casperisfine](https://github.com/casperisfine), [@byroot](https://github.com/byroot)) deserves special mention. Jean migrated autoloading in Shopify when Zeitwerk integration in Rails was yet unreleased. His work and positive attitude have been outstanding, and thanks to his feedback the interface and performance of Zeitwerk are way, way better. Kudos man ❤️. Finally, many thanks to [@schurig](https://github.com/schurig) for recording an [audio file](http://share.hashref.com/zeitwerk/zeitwerk_pronunciation.mp3) with the pronunciation of "Zeitwerk" in perfect German. 💯 ## License Released under the MIT License, Copyright (c) 2019–ω Xavier Noria. zeitwerk-2.2.2/Rakefile000066400000000000000000000002251357024011300150210ustar00rootroot00000000000000require 'rake/testtask' task :default => :test Rake::TestTask.new do |t| t.test_files = Dir.glob('test/lib/**/test_*.rb') t.libs << "test" end zeitwerk-2.2.2/bin/000077500000000000000000000000001357024011300141255ustar00rootroot00000000000000zeitwerk-2.2.2/bin/test000077500000000000000000000001361357024011300150320ustar00rootroot00000000000000#!/bin/bash if [[ -z $1 ]]; then bundle exec rake else bundle exec rake TEST="$1" fi zeitwerk-2.2.2/extras/000077500000000000000000000000001357024011300146635ustar00rootroot00000000000000zeitwerk-2.2.2/extras/zeitwerk_pronunciation.mp3000066400000000000000000002511631357024011300221300ustar00rootroot00000000000000ID3vTSSLogic Pro X 10.4.4COMhengiTunNORM 00000537 000004F7 00001508 00001441 000003C6 000003C6 000082B0 00008327 00000392 00000392COMengiTunSMPB 00000000 00000210 000008E8 0000000000015888 00000000 000121CC 00000000 00000000 00000000 00000000 00000000 00000000@7@7@7@7@7@7@7@7@7@7@7@7@7@7@7@7@7@7@7@7@7@7@7>@Jc H0 AIhDy֙X,B$D ZMI7l`1`D LȮKȦ!!E (&bg"::+L"ȮEc w"Tb+ur+tr+u髑m|bCȒC@2k011D(,@=0"#Q: - SH@pg00E4M)',!xq])sJOLiL#y<$қx_sҚdW?3 @@͐</{pen:@ =;E4?6{=W%0X }/ H& Xc h'n%ܱ؉:62ޣHywm(as=8??؎5z9}mM@Oǒ<f4~.@x&ƈ/)އDC>Qr' g;L[XFjJQ>φ`U|8OY"i^]<,77"yw^@S9Gbb [1F|bBrX[MY>hVuz`1{uknԱ1k^?_^ck{׷uook1\ͩ|)@R;O{p`in@ Ú6cOYH|SF"NQѥKG8OTvDqMz[7`_ٶhշ󘸃zגُXXyV%lbe13XJ ֈ%3D!>@* .s^"̗J HO`:CStt*|x*6C>˔D^ŽsT5b7ď]3E>_'q|z6*tZ.5}^& m$ܳi'wMV{9F5;|lcֱ]xݩ{_?0ioߟ+XU+Y1\<5s 1F_Kί,5L$W11P9.:87 1lhD6뇦bv^gl;0e_e$ ]-gXzwWlkqf=&zo/#ꕧX"äJُSE˻1[X@s:oKp2Pmn@ =Û>@a78 11w'WK(29$oR e4iI#JҊRB8w0Sq?#Ⱖ.Xi \z1RB@ N%ADG(H+baڨj1pzᬣװGarS&05? Vv5y;2VYǺP5]Y'<&b76N4rid>G&k ef*u8 3j_l2aDsR\^c "1"H UbXkvʸpҜ޵kYW~_<]'o~ikF)ءvz\VVӿW9O̞>շE ˯,Ԑ*+&%Sqz#_ 1 '+,yU5dgo@A&a@Qj;T ÈQQ(A~JnX6&d}T$%QUw1cpK|2ƝQ`b#ElWP+Y7xMW,(pojj\¶ecf]ͬKXo}jM/|b5S9\͋gw3Lo&inټM[x߮ŋ=h{5P' YeטSG7,NP/"`DV76QUrA WoW)]p~\1B, tfbc}wcNDgmsx6mx4WֳןSo5]}f֖/q\cxf|Ln&)}ַ3MRl]"EjX)w K@;o{ppmn@ mHA2'ӛs3p3T Ѐ%2͐b.&3Dt[:4XBD? KtK^v=d.%[cSnS;DX!LU\ X®"_{M¦7niO=ͱz_8kU>w_\g깷Ʒ}g-w5)mkWViZDkxq=<{Zh?5$7˹ j$l @~+,HAh0Lù aNoV-6CqxrMQ"Yi3{T޺Ìx;abܱWves ֑XQ5XsjB]+ƶkizxOjSooLzWT?: Uj7=V"˟nkuYi&ZZ=5P)Ly@HQ P).)dR҆$XVH5j!wj+𓚈ޭ̊q^bF-ISfH>jAqףVs)IrάцTBTZE5t^~7}0ȊKs͛f[ .|Gj[t^u K_|ch4y8:\߈U ncԘ~3zѺ4]y~zee+M7Yc܉n,=yy_tTְ}b,1F;z? @B;o{p`mn6@ MÈɸ95U EDVeB%%ø=|.lI3}A-N'Eǜnj:2ϘP'j&FEsLfw%TH.;ͮ &-`_+;knצvMhى|otsZԛֹƯB-ғ_J[)LuC,K9{ŦL–Wanxך _]phW! !r엶 S9JaBE,_93FJKLE[XdV*1$Ea e^F+L f) akjgnfnza^zyj|4W|G#aŻ8fǚi=1%fi#U?1}5ܳgx1}c6z7[ڦk]ŸrgXzn׾k|ԟ?XR7ծ%q}/6{4[dUd`rBRRLn1}lS-q]GhʊF۞P?:b4`aRs2-GdU,C=#WY uLO20."b&0QK6eԙ6LA]Zs5:"K6IAKRlwdeH):jY.ԥ:: j2kZL MAIR0 5uX`KAbC! %MRtn5BlER'ԉrRJDT1T]0>+̋0LG%Y}s&ˆ咺ZKZ_3'ќ/i3:n@Yi\QigB˭Z&lYlwZ4&ɲӪV5nZɦhNB | 1ho;<NݖDsE+`TBܞ- g{?{RɽT,/}yc3PPe>Eh)A%sB8hpx080Ǘ6D/yLAq@4ϖCB tY6PQ&D, ˃-|,[ m.+H1r#>|a'Af(AFl] #La6Zy&)$Pf/Yf!mB`ta: zfO,% F/WՉ-`P `ك.7nM*R}A uׂV*7NwY@J2bpd 4JqcRX&8%LbFZTdIUfI\M˗"4PFR딹oV19^bƣmn o;oTT7>RpIe~F.^{S:JHm 6 2~1f_1,F @`1zLaL"a!CH-cf xP}4{deKQYP7U`^ X9ey+02B6d( eŏ5 yclF1\ jFQ$k U&9a4.\ IC$HB@ 0l0l9cl#d7# -\eAf `x\\" D j&8Éa <cC " 0R)h"2q,3d"fbEN$b|Pd#eKݝO!\hDF@pB)!`4b2*kSRr`yA$\ț(uQ L&chR81Gl;\*.sa3`$h\$T !m". 0N0p~0)شG% kT1H @]6  XyKAYa!7s(.Bt#&@GK-:^x! @E$52L1 %!T[MȺ 7ɘp 3=]pv9~8bmxK$ oHX{I)i݊f z ZWn_"X0͍ !:@>YRe5v=5fg{" P& v`#_}*fWawigv/&One~_½xi9]{jw0\@¥J(S֪&a* Ͽ8ggr^cznkx~1M6%Հq7mˏ}L$,=q$bނہ"lWȩ 3cCHd0FTbM]bY"A OdD Ru$c anb\KユW.=LR1)U,Wo{1g+or=cXKTIE |` xQXp h^(MM)#ax6j Cur~ r-r[vjbJ#o0uiOт?E 0, 'QLU2d Li.͌c:ي2!~#`5Λ+&Q)ㄡjOyg0`shqӓ$ `[@ٟ $E@J$ӣ,j `c* j?e!KPESVI@;/|{_1 Y?́W 'b?T !ib:8؁. 'u-U,r k7w4AqR-$f d1.8O%-Pwߠ",ĶXf LvcqnY(C*Y6x r\ 99`Ps ;DXbψ$ұK6n:Me$Ṗ #yءYss~s<7ajX4 ʹ\\) O X%Jg!} .ZXw%ľB|׏Cfj5;SݽIS mC4n ?bJRCЮ}Jd$!1X,EQ@ⷋԁP8@ *\b#$ A0D~c7 b4m|d(t.+siҘdj( `Dŕ1 0 DT1dFV$A$ ¡ &,2P9 LrW]:b 3dn>2^%OICUv0|-Vz=,;^HF5.Yy8;߹fiJ_)SaBజA" ǒR!`ءXSs%:Kwg}-6 TT5NMP3X` -:4`,@ Ҡc@{׽ȖB˹EwV흿'cuhgcbbMng1zGq *`esxpj @x^#NMf@axg06$-U%##v{kw MD)JyMԢMjIaqjtq:/A=rJDBҥa@ leߑBKr$LL20vLS,Tǟ((0t0-rFaa^*d`)T(`'(0B 8Y&3^l6\2zJ2ix&RU qGѤ?2Idy^N[#t,u+jf[_)\7R " :86!#{ƶ#|UHGG(d Ó:NK Pv$;Jb/!܊Ybr܎ {Y|P#)6# D#Ex+'h]A;eh!f_urrNT1oNrV%?;Yd&2!oV7erAfܻ{Vwg%3Ӛ]mXees,lj(PАwEP h !G|SȞB#BփDdE ,YђNPqĬ-"Dd!$ 4S)\r@Q  2R )…@ DAA(F" @Ā;i`;YU7 Wr!ep$ %qP[%k۔nJZ?r)˵kԿS+72&%@3NGg{jk~OHG2SWنMH #5nnn$K\>Gh ך\ӟ L{\z`0 /XXw"pQT}ٱ(CՊTYSoيιRa^YW:cOS޿` qQp @)^$YD c-[e@x MgnFӚ0 x5bu3gi="r8 [.8.$dB %bLLBV:4^4cC $4cFp.hR0׈ K91M4C(jBPGH Y1: ɼ4a `9}Ly`@!kx`ApJ~\0H6)1"B\wV5Kv~u61e\Ε—nUUO.K.]S%pʶ9e˝g ;y{ Ks׮x ғlEZ"Trsq5X`9HysYLvȫO$]pSy&y)ea—0IYGk^EpaJc$/:/9QI<8ӿ`Jٶjv F;Ub~+ٗv=e{ SB(R M~0$tZ[9Z1dXRZ~_FN q_d̡gҸ@Q JbxEP2`Xo $rԃ -J D0ł!б`d O.j7MZD4PfaPP&@( !U"`Ea!CqI$X{]leP"81gwkF_Z)SC/Ri# z4K&agQ9VYع6/jyek[j W_TbAArY4x/~w:*^oɝ& $"Uz8vqb!Xu%r9}ca،j$@N)chc,I@`NS̆@ȸ5LTxjuS8nKVY1)S8jpaggG` mO8p 0=^g2 dp8`₺!kU2gl -Z7D 49odgNҦ0aKXm4^,ņk9]9FXu(Q!14 .9Gr7 P#T/B~"'\hB8]1ia*ì_ iA̲R %`S}IoU Wk-aVU`6;B5z$37fE #ז?9–̳.5S)mY5k%Yk.c|-oǘ缻_e] +{u*Ep=[(\ilU&P"PZ޿e然)l~G:>$Rz:mLR:ꖝ9)oS&K).*e)vKRSNMЀD~?̖ft37(SdVve޽l7=iⰰOnQ  Ɓ#fikiI4r0fp@.X]@j T`St8 H.@@R) 1a?20@` +) c#QXX(T ^D@ 28X/ = c0u@R  %]r?"Ȣ @=nP3 d(bi 8 2fYp \W{kkK!UfgemVC-ږ;.@rq%!ekmoǗ˘7 vRtDIr_:e20p#+M'Htvj-Xfm\y7dPN|V&$`fBXn2d۳oHyeBoeYۙ?́I/YPEBvཋPب BY}` a{p=\'2 m!=q@s pŁ<`DOPU0bŔ# y8  ̤ͣ~ h!&9f&0Rf`3I:'yZP܃εDL- `i:e`Po3TCp ("0<  H PQ9qӚ2nMe<5w(QX+lInXM1a-1(ˬ:X9@ K^YQvd ݅)m.ҽt.V6x_L)ܼ)2;+^NoyqsbQeġ(6 4O._̣N F8xuiz˹daNm{$M]ѕ5g,YFk%ؙnƮODܦ*4tP'8I#^VXRBUэ0޴jqBAccǬ` eMo`)Y `+bA@jH& 2,44bUF0 lL7H.hqOvz[38C@1`pC # 09M2"Ώ F80ac0#9 \։!'aP 35r<%V 쪤Q7X>%3k]IbeݖK Vpw}\ .)a w(UƫecSZISԖ?SUonbU޷I ]}feܿWKg rߕs&Uv^dq<֛6o(!1:WNv+WTqgH(kL[-UwI5MYzn#'^k1iC, |T:`g+n%2}{ OvXt6Ê! a+PKChz `fq  0FQG" p#@`DE Pd28oc,B !`e($ "! X 05@$: C AA0")m@$&h@" \("toF$  ʍф6  d0GA8ƀPH=P׍$BEtѤ _>@RTsHt Z.)"0!JfynlK3>hu*U&7)qxP+a?dȴ2z;3Rb6y&ܲjdvOM Za>fT\]!ס&+ùJDe5cǍ;+Lkye>t֧0_X;8Z5o=m20l<^72C;Z{ZCPTkkZrթ{?z}ލۚ,$/4MEgu!U-IlMxɂ_OM܈$BBƆe~̻֞–_)<~g 8epKՕK6$`/Xe QingT|\-8̯YmHa,+ʣvԔgڥ$T.2􍞧PnƮgi):>J̾o0fET/'1Żwr>&@8}wkq Ei٘EYQ  -Yt'`AxB8((č`H' d@Ӭ rR-Ld؜mExb{"vWvY5-/3O,F)?p*I;j;U1>WF7;Ga$H-BsS Bzؕu{X[\W8ZŃ4L>bx71UB8AX㐃YX 8 @Eϧa.hdӈ&!.hV2P`ԸLd7#byFfa QI?./+N2~d6`V)QxlGk<΀P`c`p-61WA\AY&O(tKCcS  Ľ(BY2cT3vn%]0yfzw-Ais/etQqZgى\=$./R_R,F\Eɦq#~^Ӗ{UHk:DK>mQF(zbCJZ` ՐNVx2 ɨ&:xdC%0,39`bfH(1\ c4A;`@Gx`4@͠ .H h@AE \ 0 $RH @0# 1hP k]4D P`jd.! D  L @6 &̀Ž(Jm;bPZb W `89:1x,``$IL f!`"Aʄ5"ؤD", >@l6hrd451r M_NnS9"Eqg͉Jp%AܬDe/__J3+麾Z,3Eo_i-=<H%B/ T?ka:0Td9TlXũz9i Q35eqLie)DU֟\S`@O<>< b0#2#"$8LH *+w&UGRYz%qi-*= ,DJQӇLBl̅5G3煴}l0K\!l@L)`0ЁX0Z[CJ(%99qfb+aqq $<8Lym`=2V]!>.ՈO5u+]u8s#v&_jRC҇VK-Ie%6wbwۙ ;[o ¦ 1W/ֵ9$\M>LTtUHX@ɍDa&: c*?TE *22Ze dN( 8} <&#r0Ih2@ y.*dJpB0VZ:L ' ![`!rS(lܺ\tH,$d1rxLd( B@  -˄bLЉu BC iSMB!2ZI21Ui8,j`dfb,lV[5x>bs>1kP& s-IeVpq`jlLn!1(ua lLķ/pSBmN"HE4SAsh94=eJÁdRkj8qQ7--srx7fT/Ȫ$̰s5/m =m ƜZ'yPU7SQ:qR*Vp = a71G\7W4(M0$@kE[`$PvC%z>$O)k#M$\`UD8C,>[(i--6%a i:JyٚUm7k-jdzz[}nת道ӛ /T כ:)]gURݟ2ɟxjaRK5;jR֫J$8@I,Xʦ^%M۵zV,}=ʮ9w;NZ>6wZ߭cR;@DɌ85Lp#T8T1H^' D ; 7q 6Qy҆bw2(6C )-cG` FMX{ro8\%I4 aʬ"A=pDC#{"lt!"Z2$׆$e喷4>URuq ޞX:K3!"Z {rXUO7PxtBt 'hg'2CG?ݫCd/"$(Ɓ$Ta_T#4`C<G@KDWQ/ʅaWF#5aT@J݌q)>CW_c٢-0LᲹ8*uWR4}qok65KFZ90Dv)L'$sas0%!qbB O 7 @)B-VYUx,@$"ȝ #Cmyc1! Kme SU'ufsKR,wOY=KP$tgQaܵ,ڊR8<܇:joq\c])IH4ITAJf}/A͇tg,fRSSv7f:ƒ)KER*뒖 fZGv +ϋ$aB|l [G`ePT-tx.q':T":X AeaJxGwR@ȩ6Q  9I ȶ3D,H2ʄaڦi_$':FcmV8>m+z(kvBy!u%Jfp܂;K8жD#xQhp$m^5}e-2ErQ:R\џ׫OWcf-s`] łȄ.` .̃{ro,\$ .̡9U,X)u|azvv AcSÖ+VU Jl$A%Qi73-jJ/Iv^hP,ng7EO utͯ0 g2 Ή[3O =4OԠV;#wGF踓0Gp-C-Z+TMΔb e#~X!)%yX!Ct!TNW 9jʹbz}3Eg!*կ|vhYiYfJͺW65Oomֺ&pPk PgPc@V88(!Tr, ]^!@ $ar|`ܷA5 g8 k \2b5pW. ZjShrIR8/\Vk\&8P:C +e%58 0t{ָtTDM| 1p3H?޷ gb'I2EJeo?YxB1ВкbYfRӮuΓe \ȕͪ]%X%]Ʌ2.414|Ys3+gfH er5  YsUlM@\W%Mf֥3c^& K 2\$[ULrXF!p[9u*p(Z14Ò2ܧ9e(8{W.wֿٔ<ǵqL0-_BeZb{fH &i&vl4CS$ 2ih\C,P ^.1J bX&D5j,' M<7Im+ Ƃ**f^R(i\Lȁ<p$KFx֚-稊 `7YOA02!E&-k]H4&l4L ]ouIuuO7Zk1@ @ s ( ` A(̆*Ä!l9¢ñbЉ2%$ؔ3q|MH0)"SZ nIɹu@c"r EmBZnYq 5MzUv0 6,> 7j3B3|7?T71$B1$1HT1$>0A!(Àc5%P==̬2 |P6a`1eb3%,jc,@`@FZF<$0lĠWe Rȟ @yc¦ .B *Ĝ:ZJW$ӜNS׵z00k;*k<0o9g(wZj~}O1)vJ[}{+xeV-aIb).u>݈w`|#fa&50B`0, 5DPV86_$.kU%tsf]蚒J_Zj}(6H(v_gO3I{3LXV右VW'Q[y3zZK,<{{WgEGJ` dL{pXsL\'a0uͽp0E!DrY3.2#2?50TCP0 0'cpw0i< E|6hv$ Le2XaL$42sXAFPٌAPɀ HdH/PL1$VW 䩓#D1l\}Z@Kl2`֘7@s(ITy! JS1ra C5ju1eq_qrv\Vp>AYc_j˳=\2+buh<\0)D  l K p@=lBhS1P0C18241 @C&dz#`QX`|QXD"D?|4Z 1 0"c bh`` ɁtSJ@Hp2+" p(0 -3Z1 %o/xފS%O@nelׇ&O`ͦ/ pBaP hф !HKXdjwH,r\/Ju42xS*J #|f5texoy] 1c1AI;-A$%S d0(ʇ 0M6zD2 b0ç\b04&#:ho 71dAbhS0p9rF@ҝ"jj^cCa2fD`RWG"a A `&`RR0pd3 s2`NILO1[D @ń 4c%E2qD3̎2*1 X@J* 4BsL,0hĀFȤ00`4t! ZK`0\3DX r@MAMB0J rUtк,/7742I!< tK^s.: E*tL%EZktԝLЄ\)0VaمCAh7 vp .x `@E$!U(.%Dv1 *$o2+j #MaX;SyKO5I6f=G6F;5û7Í3) H`u4W` yJ܋r'8sO\( ,s op08*T3, !0 0 2=C 01 N0 YaaMJXQdhAPoc T1$ǀ"BL5 @$f0.b b(^bAt3 €d,P AŢ"1#-..j .Blfk7l@`A` laz|aD 600:)U1$'62Vx`BB eQ2!#:B,JW@30T0аX 0kT U+YB(1,A$0i3dB-; G}b_k)E3^J7JV^ys]<9ϩ{Β+9"~ʼkYL7cΧ[Z޻Nn-YS[c7h:4Q01242V>C _M;eɄa b0@[0d >sC6P, ʢ "0p#PFDD `p(B`P 0@!KQt`K>BQ rx/"FZꋗ9w IgKʡQi0dLEӤvׁiLiL`,B0 Ⱙs_U,of# _^2 Ÿ&yّ\ϖ,i},qxi7a gVlPD,$ʂCLv!6x\M/2`l 1P10PpLaEE9PdAR@AA#R ,Ejrw_!10*FA@QB(zh6ɶ".boĶ%.DD[ yI/(ݐQڕ7(ƓRfaȍ.TMe7TNԾsֱ<1ǻ?Ϻ^o8/w7#hQr,홆`p +0Env.qhY\LQ/U@R  @z[+bE3AA@oLrb;VPR#€ WO"cR蝩L~Q-ij)6IZETpa53;S٦寖kvnrjR&!$T~!%"b+ڝTncbi8iɝH>0L A`  Yɐ'@kAiӉPbq(Vk%n,6!lD| ]iXuԉl귬?:|\m.`g;:48 %%EXGnϠ0SP40 @0CP0c P0 F00- `>6S5SIF336 P:b@1 P@ʜWwH4AaQRLeKE@Nrܪ301Cu|21.Wၡ`p4# \fB0XM-(鄅[%`Q]BBU'QS!H@gFA Bf,6qLml(3H ۛլ5ng{)҈~݉eUP`bG+@JW'[_2Y k2 K'y=tȁ!Š>P 1lׁ1 =ō1%`b\р @8"g$ A9L8ʗ5 3j_)ZRPos[$p+8h{i&@Zc23Q]}$eMLb`@R+DfDw'b>$ pE00 P0g#4Cd6>E5mg&IAtpoflT+E@ᙶm11&Y2syL$'xO%PDfJ$bB TtǕHI" p! .DQA߯%9sXD"N טd \A*DF Ռ,c?8DƊ \%-W5Ziu*ǿy꛷)LJ8ܯS%}nŬmVZ?R/w>t<>\1B4ƢP83\0(2.L_L 4 Bπ—iB ! τ9 YB]0.Cp c_Z'D_VY׏c#{4 UL?̆?R+*.ч"h(Nd ǏdBY 9 g[ɎL.E/g>AIɺX dT5]ylԮV7)5,R;z%՚ )vw~2W?z˿沫Д4 ȄO[TTV ̌ Lx?LL<L> 'Ll @TI>9͜@ ƀ`˓&CŨevg2 @?Ij H>"^Sa֍1"(9ַ ͉kI`o]tfuk Yʍ1C7yv!)RA!3};~c҂ɊV `UL%!Sx 7a Q&e޹IXa1ax 0u(h <& pqwtAdA#W[-p$BJG"Ksx^ ␗9?2h:0pTq^ X% bbAaܣQpsI9Y hDmb jDpRi&h$N*ݝ]Kd+S^ٗW]z VIeٓ! Ei!Cٌ(@jR#ZI4eV7s#ᕬ`8 $(XƱ_^Cud2<7r-̄$%јfլ;s\ \տzjmN{V%wX<2浞_ouqc(`Aۃpiqw/^>Au' 5U0102X0c 1 0(0J#pB0ps0-h1R@&yYaBq@0(i8* P#2`$20g0o3L  Ҫk5g-U~+]*IK$uf / &eQ$ b+f9Aj,+b E$NXY"L 5L363($\;Ag"%ҙ6R.R]Z Zkl/Y#JHtEF!xP0i% N0jN "Km]6U*k@[핤+ xJtZ{C:=D.K(fU60q;xyYkm<(o./kT}5νkH˗/Q(0ҐZcāc/IDhFrtR6CҀi  h3TZD`tApebɚsK/+/3hdip"(b M7/BቹDP2' VuFl`BA9N]h)P11Y3- ffE=MWAW~!A)F. h%PT5$F`p``v`HfRbPY61AY!   gLyO=4X t^00@-۵6^S*97q}4zhrrH @RXC-| l.EkQSSS:kt5d5ܷnl]s0Ӌ:U*\ w=` FZ L'Y*a] '80}jd ~0(AT 0AF_ D85 hѠfJH8 @,xB00*| ,l3`F> 5 -lAAh!@4N`b c DY`!Xr*"A`"?2 9P ` q("pĄ8Y,>1z2"; A#! Dju35[ Ghv ZlBAނhnx<8:d(CSC#LQc@D@T-/k /D Rd2kU˧j_KbMz: h  @4g   ^-b+PTA& pA!:]a> ؾ@.3Ry42t4tGp@prI ƋsXXܗLn@L<#9Rzב\C?L33d10 !)i C@Qq!#1@,7BF* $1Q0y`pQdn֦_nOo+U,&GK䯄n8T¥`j J`@P(#@IwiXZRH%k%Z+@@P@Mܧ(:ULu5xg@]ه!՚jz;-nJW!w_gg.f1txˏ ƂBL y!b$^0@HTEh0 5a4 1vS Ąa13Kr8Q酨*.Sִ.Yߩ\~s;s5oQX]5~4oc>VaA(mS`sFp 8po/^eA mº=x2\c `B`YȄAࡂ@LC(ո2b iXM7QIštE/@B{3])) J`vr2f,.b'Ən@odTsqxgqgûppV})N{,v=G=Fr?wh:`:g'pa$q*Q0#9păqVIcZbS7!C0aध,gnj=*qGoc>'O o f_j-$xz2޸}kvZk&5ͳ"_YnJ}^<4i0h]fcJa8&`\#PBXJ(!Pe,(8 @+;:2wI5蓊 1°1a@!qEdpl )x: Z F2\"ٽ3pxY$4C%CщO4ظ)EcL$&!CV=?68*sLTF7FF и*aqAIFפ `A !`J?=B3Hڂ×*& SEUHYz˜ -(:\ |,L5'̫mTM:> 01.cbKFfTt(bh#a?0. ej\f[& $kQ"Dh8 <ːB{hODH0`v(yPTOu`|dm MH+ 3f$*#GpplQO0QFXO7IȇO,Ix7 S`A so?imKPƒ˃S<[@VCEϚL\1Ѵd4c#&:$1鋋@A&c(x1Aي!8B:bл08=rL0"|1!]Ohc:O\Ũ-JV 7BeSKl;Q&R,=mԂ^(znucpoc4|F'>y:cjHV]zt@P@zso^^A Mê)0(*C(SaQf6LD|1D čAE42c:`t&sF, D4H5u>A.j{/T <hT̘~\X#\`Wb=Ui}2K6*b65eFzQ=Q)T4TҘIsPVmbh7m6狘*YxZϼKXi;,N6wXu3NΐS1"3g&LeT\dyV) !80f^Q%020@(Z6VYF@iZ@2XKOi&Y L&Ġe-B<ܖC6#DO 8%a8r]Dݘ%hT53&+_'7tf\h[-SB'' p&%f)._s)_ V֞Ay鏲XEj (Z\jthCLJ-M ILyY|)K6Z길iݩɈj3 'UrH4K>.6C&.%B~J:"A0.،x ȌTTbx٬α⒩ǚi吗9g=uYz~0Mp7 Sn L 8dUɀ@†i#x1h~Y\ w  r5ePT ѕ0x \i+.IUK2|18' %+ B\`J44'خhC F©hO"R20hDb CՎ,BXIGŐR撑kG"eiy{O}3@Đ@kprsMnAiMWH ) Y5 `51(SqHL4(]0xb`is N/FP$&L f$% XU 9*0 G-HlVEƓ؈AlJv&'B7rb%IC `= Ƨ*LOR%aj'c`盆ޑ&Q71sPUf+e: Dtn͏;hтS6/2thCC& ɄD&! %lI$ TÐ1СqPqEaA 4@U8\@ @Bp#=kUtL3Qr`F<>^4MV?dɅЌ7Is%"N<D"L:Lfdb^$ K󓦋2QrM"xQÆMZB$LU[nmzG޷VdGQFXnaYhh&?1cDP=Dh(n PHm_d@DDG5XƲhnLM~YLS@סLq&ą= # JBFCʥ.W/@˙ !-,>s& I5jCAJ>ejoSSvV_@B@pRsTnL+)Ax0M# e8Ј .61*"pptYQ sCM/B p!ᚨ 8 TC9sXetŌ֋K͈$MPS18eЭHi rEj-D/l g2 v:fhj]21ES&t(3TԶfzi%Z,TKEdԊRv 97!<6XdQ鉂&M0,bp8ae)B`)MCC$Ę!23Aש|ʥkʤ\ ʯeb܋yۊEdkb>0YvENGc"djf ]ff":ϨG 9d@-*b"lMi6 D " ytZ,HȤLKҙ-LO-NLfN()'l?]nD#R:e`( @$`Fl1\#H dnZ Xp2A Ȳ&L[# Yv"\a<ӟŞR,1y~bK2E( m%h!+eli~Zc7LZG;rdCv@inO{jֹ+ú;W[psHnj@ mþH!2"L 5j(g0 d&- 8",qA&0pqef0hFP 00@8ThJ[ fumЭɅ75I@ faDQwOj+'it~uqo:~H!aYYp&&~kOr;Ohruԍ"/ltoW` I ='B 9'bqG`੪.dbn2yPUT"`D e05;*vvX$RA ͼ(LPHrG*@xia› pH((8("2LYV)iŕ!bh{݌efb>n;~ާ_?qu1P[ M(ŠDɅQ@L̇ \X*g"2pgUXFKPcM˥zn]Rɧ}WeUf#⟉rלA!-AuKô1p{SW6,\JYŞICP2?  fA˺:..Us3T9nS_Cs9Od5TꭃLn"2AsŗH]DL3VD2ɿ8 bKK ƒRk@!r,Ml&BCZ[?r#t/{$y]ƢKL$HCbbаTTdaBDX. zRp\pTA `~xhs5z:sezIyfK&DIjk~>g@B>[p2sGn@hàHA!0P37 lM10ᨰYeB@"APpF!A!`J,1Yy{Zţic P*{+ |Es=61j;="Q|Jk5y:؟>j덷z8@Hx$LP4xA%2Aج0$+JeXaKϒ9BB<@ #zd% 8zXE%ŸI[lqgžux 8Tpƍ d 9"dG*PH40pH: !( aP酇ja5cPӆP)0g5 cJ̮dlw~.w'DlDsh!^dc dkT!Mj$Q`p@hD`P ( /0BI.D jM169rqIy aoB~9.>,j4>҈2;]K|R'MQCpRL0t"Q\(kpɲoJn@hß-2<(C"̨-0h#C=4TN R <10 *[Ì5FpXDkqa" Q N]I \kaf,}8T뽮{d@IheCc#D V|+M|8R4bL"d3sYM'"K7.;LΒuR ̛!A Ե +RMC;gtTw쮅^ڵl.ԃ{.Fh&f$p񘎞 PP #i:A ΰ]@ʺE5ZBC.zUjsBӅ)X峯B{)DhE˘\w828\\YIL,qdB$内T، &9 D"R*-yru8)..@_a5厞eg yf *E K`0y ZX 4%DiQ$2!7ѡbPCpoHn@ Mz-2Rx&,̆!3ģ!. `6XBL1`Ġ@9hDcBDA`Ru-W\KoYwŜTmYuunu%3x!B%AV"Bwg}`ܱCP@,Hdv3C-òVmy1WX)!쭽DTVIsLU5}_W<qU?s\sP!1w١(8 a0 0EIIcAB3gdj_T2" *Mu%avj)ʒ=#A.so+@ gȈ A(#2*#ǒ Ä$ьҹh* 30h^:5ˤ;i8*&JXY;櫿f'cp`TD ߅G(م PAŃJ" ZS;Hv@c YiMӼR-{ٴ a}JW$bB lDb#)1IN#$ 9Eǘ,1<\c9SȆb9s9\chȓn&K, b;hs)'+5/%pJX h 9@4Ɇٌk c~]G-6ơdvܴElEW˸65$3/G}l7maJVm=yC5Xެl'{ٽt|LCXl\9 VZNA/t[_]UnfrX爸|#Wq Ýq[l|}}|t*bqFOdb(uwQC@ S EHn4DȬ0D9kf%^H4-`դo^f–rJ̀_ĎH)mȚuc؄kÄࡐ#bj؛ DL@:FC9p )Nnj*(TUiд;FjAe}f?G}mG3->dkhO2 օsLu1,B((:IbCEBY*N$V =#" 96@BSw;feaS<52gэ '"Zd.AESх4 1ȱ C #G_G;">nZgq £ ft2+9 \2l4.r6Q^0Pd jm;[J$941E;0,°C/fr%i6]&H(t.5/.,I#-:jXJ:c\.NskeGDڨd}=Ϳs}|8w+<j?j9"a4 \X`Pl2aF(H"L eI6D1% Ú4P™2 P A'222˥HU{m-iֻgtqŶa\9ydM_vg)tʆ:|M6C%߳:w<<@=yCprsHn@emuH-4ߙ9sc*9Lb1 c$hT sU)Ս0#47>x }Z Th-ݖᙺwcAWXMJxJ"rU~h(h9rH.ZP#\Wx#Fظ7%e$[q2mhB<_sJ{w+SN]/q/5W_|FOG3s"iڙM-L g@NL E Аh,ԃAhHTmqɪ_aj13MZ &: 7yKG孥VyOHߧg* LG`C@.4\T)XJ, z Vx9;c`BhPr (ERYRHL=mG eqOpWJL{҆J}jfefF}q1u}F@ײ!p21 />3ȣl:2Aaǥ%@ /T0ERdd2 TJVyیNmߚtĶ=,E,arL@ FRt(MqyaYǤL*RRf(°Y #W^je5' -圅C/:- N~;leGQ'gҩ|p&+}~>oc.~j7?/q&ӿ `Q$` 0T ?@.ެH*g!ZqA801Ź qe*aO_@ *v)rG_-%JE(SkXvǝZEbat WDbce4o:$ܱcmY7oe6Iu.5ޖ?WnSۿwr͵U1_^ꏮ9w_]8<@og,^*[IN_EfJ`vKg*׍0U40Ma$^S[8TCs\bI1iM'}$yWCϯAD5Pl|1N_Mt>sn5sR1Xʞl3Nٽ5=\ԶlG@ې=y[pro+n@ --4$Cy1R>GYzFG!C$MZJzm2]Ut+oҏۉܖ?ˡR.95S}5;~^; \hdDgX괙I- L͐3>,5LL%aSe!:%քT ;'RY Ycoc9ne}øwNm뺕!9Ĺ}q[ooٿkw]⦅lAf$2cN2C1H,7-Q\15^C$Senλ I2mYXyU-IR9y)#iAʼnR̸6l@\zM4K9g \B7bϚ(x\yMKu-9Eq/Ntum\؞1o4):*g*q9', 52`#O63?02Q!s_)S*1OR)juS66BI÷ @L"fLS2J|R_`Rfsg9vv7r^ٙ4&n{olG5OsXeuGV )u2K)4 >8!,@f.  ƵQk>o4,fGѼKtKkWc@I W:ñV1-Xy2kN{ lk}ځǷ^i.׮5krޫϴoA[7 g{.[ro2~NLgw)\^ro=ٜK@F9A%PȌ AI 3HbG &:}Za}ɦ#K1%]k4Lch5MMz}v#`dL<<;x[SY@x1yC$?"7`@O4+Krr[~nJHyu9o n$& h80k-kj3u'?'JU'9Z>mVgb,vEU]tl-ꛪw||ɫg?pwc6ѠRN HAFA& Vr*;:ڨ0nBQ-1^h{[U5uO}ه,b;FZMDd4V/M3&ݪ\<]̱[p\yM󋸶/xfb}=vy9nx_u7e_V.>];g{+~l]g-mXls{Ae}7q{>k=}ߙ^M[Z??|u>ٕ)`@Vg{ g4vqZg7}z>焼 nǤ0a$",L4$ 46z;1 %ၟIfU1a?NL^InUw-nͪ RFϘsA}ƾCg\ƷyvgYymKStwI[Sx8okZf-slٵ |n~Ǯbmoag61Z+Oo_?y.BԀ p  <R41@CKW0 F%dVX!x+{0s32%%4wPjbʭͺI]Ra$ZpW')Gw $fվ%H[,\Apc3{j{XsǤ R}I/\Vj5&@?kפbO}ZL__پ+iǧ׽LR7Mbrc$E0@Y DD] Da(6ZΝx ֥nPW}c3+$dtJ.rCѪMNJ%wr=R ۵ {6h pק8yg[m[~ƫOIyJ<56_W7\:bͳ}Ҕ)o━k8JRkJ^7{zk{ҟ9)McY1JR@Ӑ:8Kppen ;A6-CIcD!B"1e]%t/pXg 0*U1.>.7r\?iiiar455NӔM^"4 8BHҊ"H 5hE"HR*&(YT "Y[k"ȥ-&jHBP 1JK !g%(T(YK[,^-$H5f%J1g?1zU cT1e-bdH-J "#(A(M :6Dy`" =D( :dzIlF"7AJ5Q!%ZrCMrNECx/XSr\XpX&ke)" BH1YT B`HR)%g%[RT(e+UB $H)Z""&D[),H"D}I$H1!B cPf1CPjR(JRVD$MJ_RD%YI$(Pzb ,?T(PcPDRT*'s(_@$7&` @X NQ\=Epzeitwerk-2.2.2/lib/000077500000000000000000000000001357024011300141235ustar00rootroot00000000000000zeitwerk-2.2.2/lib/zeitwerk.rb000066400000000000000000000005711357024011300163170ustar00rootroot00000000000000# frozen_string_literal: true module Zeitwerk require_relative "zeitwerk/real_mod_name" require_relative "zeitwerk/loader" require_relative "zeitwerk/registry" require_relative "zeitwerk/explicit_namespace" require_relative "zeitwerk/inflector" require_relative "zeitwerk/gem_inflector" require_relative "zeitwerk/kernel" require_relative "zeitwerk/error" end zeitwerk-2.2.2/lib/zeitwerk/000077500000000000000000000000001357024011300157675ustar00rootroot00000000000000zeitwerk-2.2.2/lib/zeitwerk/error.rb000066400000000000000000000002151357024011300174430ustar00rootroot00000000000000module Zeitwerk class Error < StandardError end class ReloadingDisabledError < Error end class NameError < ::NameError end end zeitwerk-2.2.2/lib/zeitwerk/explicit_namespace.rb000066400000000000000000000050661357024011300221600ustar00rootroot00000000000000module Zeitwerk # Centralizes the logic for the trace point used to detect the creation of # explicit namespaces, needed to descend into matching subdirectories right # after the constant has been defined. # # The implementation assumes an explicit namespace is managed by one loader. # Loaders that reopen namespaces owned by other projects are responsible for # loading their constant before setup. This is documented. module ExplicitNamespace # :nodoc: all class << self include RealModName # Maps constant paths that correspond to explicit namespaces according to # the file system, to the loader responsible for them. # # @private # @return [{String => Zeitwerk::Loader}] attr_reader :cpaths # @private # @return [Mutex] attr_reader :mutex # @private # @return [TracePoint] attr_reader :tracer # Asserts `cpath` corresponds to an explicit namespace for which `loader` # is responsible. # # @private # @param cpath [String] # @param loader [Zeitwerk::Loader] # @return [void] def register(cpath, loader) mutex.synchronize do cpaths[cpath] = loader # We check enabled? because, looking at the C source code, enabling an # enabled tracer does not seem to be a simple no-op. tracer.enable unless tracer.enabled? end end # @private # @param loader [Zeitwerk::Loader] # @return [void] def unregister(loader) cpaths.delete_if { |_cpath, l| l == loader } disable_tracer_if_unneeded end def disable_tracer_if_unneeded mutex.synchronize do tracer.disable if cpaths.empty? end end def tracepoint_class_callback(event) # If the class is a singleton class, we won't do anything with it so we # can bail out immediately. This is several orders of magnitude faster # than accessing its name. return if event.self.singleton_class? # Note that it makes sense to compute the hash code unconditionally, # because the trace point is disabled if cpaths is empty. if loader = cpaths.delete(real_mod_name(event.self)) loader.on_namespace_loaded(event.self) disable_tracer_if_unneeded end end end @cpaths = {} @mutex = Mutex.new # We go through a method instead of defining a block mainly to have a better # label when profiling. @tracer = TracePoint.new(:class, &method(:tracepoint_class_callback)) end end zeitwerk-2.2.2/lib/zeitwerk/gem_inflector.rb000066400000000000000000000007721357024011300211370ustar00rootroot00000000000000# frozen_string_literal: true module Zeitwerk class GemInflector < Inflector # @param root_file [String] def initialize(root_file) namespace = File.basename(root_file, ".rb") lib_dir = File.dirname(root_file) @version_file = File.join(lib_dir, namespace, "version.rb") end # @param basename [String] # @param abspath [String] # @return [String] def camelize(basename, abspath) abspath == @version_file ? "VERSION" : super end end end zeitwerk-2.2.2/lib/zeitwerk/inflector.rb000066400000000000000000000027011357024011300203010ustar00rootroot00000000000000# frozen_string_literal: true module Zeitwerk class Inflector # Very basic snake case -> camel case conversion. # # inflector = Zeitwerk::Inflector.new # inflector.camelize("post", ...) # => "Post" # inflector.camelize("users_controller", ...) # => "UsersController" # inflector.camelize("api", ...) # => "Api" # # Takes into account hard-coded mappings configured with `inflect`. # # @param basename [String] # @param _abspath [String] # @return [String] def camelize(basename, _abspath) overrides[basename] || basename.split('_').map!(&:capitalize).join end # Configures hard-coded inflections: # # inflector = Zeitwerk::Inflector.new # inflector.inflect( # "html_parser" => "HTMLParser", # "mysql_adapter" => "MySQLAdapter" # ) # # inflector.camelize("html_parser", abspath) # => "HTMLParser" # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter" # inflector.camelize("users_controller", abspath) # => "UsersController" # # @param inflections [{String => String}] # @return [void] def inflect(inflections) overrides.merge!(inflections) end private # Hard-coded basename to constant name user maps that override the default # inflection logic. # # @return [{String => String}] def overrides @overrides ||= {} end end end zeitwerk-2.2.2/lib/zeitwerk/kernel.rb000066400000000000000000000016441357024011300176010ustar00rootroot00000000000000# frozen_string_literal: true module Kernel module_function # We cannot decorate with prepend + super because Kernel has already been # included in Object, and changes in ancestors don't get propagated into # already existing ancestor chains. alias_method :zeitwerk_original_require, :require # @param path [String] # @return [Boolean] def require(path) if loader = Zeitwerk::Registry.loader_for(path) if path.end_with?(".rb") zeitwerk_original_require(path).tap do |required| loader.on_file_autoloaded(path) if required end else loader.on_dir_autoloaded(path) end else zeitwerk_original_require(path).tap do |required| if required realpath = $LOADED_FEATURES.last if loader = Zeitwerk::Registry.loader_for(realpath) loader.on_file_autoloaded(realpath) end end end end end end zeitwerk-2.2.2/lib/zeitwerk/loader.rb000066400000000000000000000561301357024011300175670ustar00rootroot00000000000000# frozen_string_literal: true require "set" require "securerandom" module Zeitwerk class Loader require_relative "loader/callbacks" include Callbacks include RealModName # @return [String] attr_reader :tag # @return [#camelize] attr_accessor :inflector # @return [#call, #debug, nil] attr_accessor :logger # Absolute paths of the root directories. Stored in a hash to preserve # order, easily handle duplicates, and also be able to have a fast lookup, # needed for detecting nested paths. # # "/Users/fxn/blog/app/assets" => true, # "/Users/fxn/blog/app/channels" => true, # ... # # This is a private collection maintained by the loader. The public # interface for it is `push_dir` and `dirs`. # # @private # @return [{String => true}] attr_reader :root_dirs # Absolute paths of files or directories that have to be preloaded. # # @private # @return [] attr_reader :preloads # Absolute paths of files, directories, of glob patterns to be totally # ignored. # # @private # @return [Set] attr_reader :ignored_glob_patterns # The actual collection of absolute file and directory names at the time the # ignored glob patterns were expanded. Computed on setup, and recomputed on # reload. # # @private # @return [Set] attr_reader :ignored_paths # Maps real absolute paths for which an autoload has been set ---and not # executed--- to their corresponding parent class or module and constant # name. # # "/Users/fxn/blog/app/models/user.rb" => [Object, :User], # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing] # ... # # @private # @return [{String => (Module, Symbol)}] attr_reader :autoloads # We keep track of autoloaded directories to remove them from the registry # at the end of eager loading. # # Files are removed as they are autoloaded, but directories need to wait due # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded). # # @private # @return [] attr_reader :autoloaded_dirs # Stores metadata needed for unloading. Its entries look like this: # # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]] # # The cpath as key helps implementing unloadable_cpath? The real file name # is stored in order to be able to delete it from $LOADED_FEATURES, and the # pair [Module, Symbol] is used to remove_const the constant from the class # or module object. # # If reloading is enabled, this hash is filled as constants are autoloaded # or eager loaded. Otherwise, the collection remains empty. # # @private # @return [{String => (String, (Module, Symbol))}] attr_reader :to_unload # Maps constant paths of namespaces to arrays of corresponding directories. # # For example, given this mapping: # # "Admin" => [ # "/Users/fxn/blog/app/controllers/admin", # "/Users/fxn/blog/app/models/admin", # ... # ] # # when `Admin` gets defined we know that it plays the role of a namespace and # that its children are spread over those directories. We'll visit them to set # up the corresponding autoloads. # # @private # @return [{String => }] attr_reader :lazy_subdirs # Absolute paths of files or directories not to be eager loaded. # # @private # @return [Set] attr_reader :eager_load_exclusions # @private # @return [Mutex] attr_reader :mutex # @private # @return [Mutex] attr_reader :mutex2 def initialize @initialized_at = Time.now @tag = SecureRandom.hex(3) @inflector = Inflector.new @logger = self.class.default_logger @root_dirs = {} @preloads = [] @ignored_glob_patterns = Set.new @ignored_paths = Set.new @autoloads = {} @autoloaded_dirs = [] @to_unload = {} @lazy_subdirs = {} @eager_load_exclusions = Set.new # TODO: find a better name for these mutexes. @mutex = Mutex.new @mutex2 = Mutex.new @setup = false @eager_loaded = false @reloading_enabled = false Registry.register_loader(self) end # Sets a tag for the loader, useful for logging. # # @return [void] def tag=(tag) @tag = tag.to_s end # Absolute paths of the root directories. This is a read-only collection, # please push here via `push_dir`. # # @return [] def dirs root_dirs.keys.freeze end # Pushes `path` to the list of root directories. # # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in # the same process already manages that directory or one of its ascendants # or descendants. # # @param path [] # @raise [Zeitwerk::Error] # @return [void] def push_dir(path) abspath = File.expand_path(path) if dir?(abspath) raise_if_conflicting_directory(abspath) root_dirs[abspath] = true else raise Error, "the root directory #{abspath} does not exist" end end # You need to call this method before setup in order to be able to reload. # There is no way to undo this, either you want to reload or you don't. # # @raise [Zeitwerk::Error] # @return [void] def enable_reloading mutex.synchronize do break if @reloading_enabled if @setup raise Error, "cannot enable reloading after setup" else @reloading_enabled = true end end end # @return [Boolean] def reloading_enabled? @reloading_enabled end # Files or directories to be preloaded instead of lazy loaded. # # @param paths [>] # @return [void] def preload(*paths) mutex.synchronize do expand_paths(paths).each do |abspath| preloads << abspath do_preload_abspath(abspath) if @setup end end end # Configure files, directories, or glob patterns to be totally ignored. # # @param paths [>] # @return [void] def ignore(*glob_patterns) glob_patterns = expand_paths(glob_patterns) mutex.synchronize do ignored_glob_patterns.merge(glob_patterns) ignored_paths.merge(expand_glob_patterns(glob_patterns)) end end # Sets autoloads in the root namespace and preloads files, if any. # # @return [void] def setup mutex.synchronize do break if @setup actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) } do_preload @setup = true end end # Removes loaded constants and configured autoloads. # # The objects the constants stored are no longer reachable through them. In # addition, since said objects are normally not referenced from anywhere # else, they are eligible for garbage collection, which would effectively # unload them. # # @private # @return [void] def unload mutex.synchronize do # We are going to keep track of the files that were required by our # autoloads to later remove them from $LOADED_FEATURES, thus making them # loadable by Kernel#require again. # # Directories are not stored in $LOADED_FEATURES, keeping track of files # is enough. unloaded_files = Set.new autoloads.each do |realpath, (parent, cname)| if parent.autoload?(cname) unload_autoload(parent, cname) else # Could happen if loaded with require_relative. That is unsupported, # and the constant path would escape unloadable_cpath? This is just # defensive code to clean things up as much as we are able to. unload_cref(parent, cname) if cdef?(parent, cname) unloaded_files.add(realpath) if ruby?(realpath) end end to_unload.each_value do |(realpath, (parent, cname))| unload_cref(parent, cname) if cdef?(parent, cname) unloaded_files.add(realpath) if ruby?(realpath) end unless unloaded_files.empty? # Bootsnap decorates Kernel#require to speed it up using a cache and # this optimization does not check if $LOADED_FEATURES has the file. # # To make it aware of changes, the gem defines singleton methods in # $LOADED_FEATURES: # # https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb # # Rails applications may depend on bootsnap, so for unloading to work # in that setting it is preferable that we restrict our API choice to # one of those methods. $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) } end autoloads.clear autoloaded_dirs.clear to_unload.clear lazy_subdirs.clear Registry.on_unload(self) ExplicitNamespace.unregister(self) @setup = false end end # Unloads all loaded code, and calls setup again so that the loader is able # to pick any changes in the file system. # # This method is not thread-safe, please see how this can be achieved by # client code in the README of the project. # # @raise [Zeitwerk::Error] # @return [void] def reload if reloading_enabled? unload recompute_ignored_paths setup else raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup" end end # Eager loads all files in the root directories, recursively. Files do not # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files # are not eager loaded. You can opt-out specifically in specific files and # directories with `do_not_eager_load`. # # @return [void] def eager_load mutex.synchronize do break if @eager_loaded queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) } queue.map! { |dir| [Object, dir] } while to_eager_load = queue.shift namespace, dir = to_eager_load ls(dir) do |basename, abspath| next if eager_load_exclusions.member?(abspath) if ruby?(abspath) if cref = autoloads[File.realpath(abspath)] cref[0].const_get(cref[1], false) end elsif dir?(abspath) && !root_dirs.key?(abspath) cname = inflector.camelize(basename, abspath) queue << [namespace.const_get(cname, false), abspath] end end end autoloaded_dirs.each do |autoloaded_dir| Registry.unregister_autoload(autoloaded_dir) end autoloaded_dirs.clear @eager_loaded = true end end # Let eager load ignore the given files or directories. The constants # defined in those files are still autoloadable. # # @param paths [>] # @return [void] def do_not_eager_load(*paths) mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) } end # Says if the given constant path would be unloaded on reload. This # predicate returns `false` if reloading is disabled. # # @param cpath [String] # @return [Boolean] def unloadable_cpath?(cpath) to_unload.key?(cpath) end # Returns an array with the constant paths that would be unloaded on reload. # This predicate returns an empty array if reloading is disabled. # # @return [] def unloadable_cpaths to_unload.keys.freeze end # Logs to `$stdout`, handy shortcut for debugging. # # @return [void] def log! @logger = ->(msg) { puts msg } end # @private # @param dir [String] # @return [Boolean] def manages?(dir) dir = dir + "/" ignored_paths.each do |ignored_path| return false if dir.start_with?(ignored_path + "/") end root_dirs.each_key do |root_dir| return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/") end false end # --- Class methods --------------------------------------------------------------------------- class << self # @return [#call, #debug, nil] attr_accessor :default_logger # @private # @return [Mutex] attr_accessor :mutex # This is a shortcut for # # require "zeitwerk" # loader = Zeitwerk::Loader.new # loader.tag = File.basename(__FILE__, ".rb") # loader.inflector = Zeitwerk::GemInflector.new # loader.push_dir(__dir__) # # 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. # # @return [Zeitwerk::Loader] def for_gem called_from = caller_locations(1, 1).first.path Registry.loader_for_gem(called_from) end # Broadcasts `eager_load` to all loaders. # # @return [void] def eager_load_all Registry.loaders.each(&:eager_load) end # Returns an array with the absolute paths of the root directories of all # registered loaders. This is a read-only collection. # # @return [] def all_dirs Registry.loaders.flat_map(&:dirs).freeze end end self.mutex = Mutex.new private # ------------------------------------------------------------------------------------- # @return [] def actual_root_dirs root_dirs.keys.delete_if do |root_dir| !dir?(root_dir) || ignored_paths.member?(root_dir) end end # @param dir [String] # @param parent [Module] # @return [void] def set_autoloads_in_dir(dir, parent) ls(dir) do |basename, abspath| begin if ruby?(basename) basename.slice!(-3, 3) cname = inflector.camelize(basename, abspath).to_sym autoload_file(parent, cname, abspath) elsif dir?(abspath) # In a Rails application, `app/models/concerns` is a subdirectory of # `app/models`, but both of them are root directories. # # To resolve the ambiguity file name -> constant path this introduces, # the `app/models/concerns` directory is totally ignored as a namespace, # it counts only as root. The guard checks that. unless root_dirs.key?(abspath) cname = inflector.camelize(basename, abspath).to_sym autoload_subdir(parent, cname, abspath) end end rescue ::NameError => error path_type = ruby?(abspath) ? "file" : "directory" raise NameError.new(<<~MESSAGE, error.name) #{error.message} inferred by #{inflector.class} from #{path_type} #{abspath} Possible ways to address this: * Tell Zeitwerk to ignore this particular #{path_type}. * Tell Zeitwerk to ignore one of its parent directories. * Rename the #{path_type} to comply with the naming conventions. * Modify the inflector to handle this case. MESSAGE end end end # @param parent [Module] # @param cname [Symbol] # @param subdir [String] # @return [void] def autoload_subdir(parent, cname, subdir) if autoload_path = autoload_for?(parent, cname) cpath = cpath(parent, cname) register_explicit_namespace(cpath) if ruby?(autoload_path) # We do not need to issue another autoload, the existing one is enough # no matter if it is for a file or a directory. Just remember the # subdirectory has to be visited if the namespace is used. (lazy_subdirs[cpath] ||= []) << subdir elsif !cdef?(parent, cname) # First time we find this namespace, set an autoload for it. (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir set_autoload(parent, cname, subdir) else # For whatever reason the constant that corresponds to this namespace has # already been defined, we have to recurse. set_autoloads_in_dir(subdir, parent.const_get(cname)) end end # @param parent [Module] # @param cname [Symbol] # @param file [String] # @return [void] def autoload_file(parent, cname, file) if autoload_path = autoload_for?(parent, cname) # First autoload for a Ruby file wins, just ignore subsequent ones. if ruby?(autoload_path) log("file #{file} is ignored because #{autoload_path} has precedence") if logger else promote_namespace_from_implicit_to_explicit( dir: autoload_path, file: file, parent: parent, cname: cname ) end elsif cdef?(parent, cname) log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger else set_autoload(parent, cname, file) end end # @param dir [String] directory that would have autovivified a module # @param file [String] the file where the namespace is explicitly defined # @param parent [Module] # @param cname [Symbol] # @return [void] def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:) autoloads.delete(dir) Registry.unregister_autoload(dir) set_autoload(parent, cname, file) register_explicit_namespace(cpath(parent, cname)) end # @param parent [Module] # @param cname [Symbol] # @param abspath [String] # @return [void] def set_autoload(parent, cname, abspath) # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the # real path to be able to delete it from $LOADED_FEATURES on unload, and to # be able to do a lookup later in Kernel#require for manual require calls. realpath = File.realpath(abspath) parent.autoload(cname, realpath) if logger if ruby?(realpath) log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}") else log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}") end end autoloads[realpath] = [parent, cname] Registry.register_autoload(self, realpath) # See why in the documentation of Zeitwerk::Registry.inceptions. unless parent.autoload?(cname) Registry.register_inception(cpath(parent, cname), realpath, self) end end # @param parent [Module] # @param cname [Symbol] # @return [String, nil] def autoload_for?(parent, cname) strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname)) end # The autoload? predicate takes into account the ancestor chain of the # receiver, like const_defined? and other methods in the constants API do. # # For example, given # # class A # autoload :X, "x.rb" # end # # class B < A # end # # B.autoload?(:X) returns "x.rb". # # We need a way to strictly check in parent ignoring ancestors. # # @param parent [Module] # @param cname [Symbol] # @return [String, nil] if method(:autoload?).arity == 1 def strict_autoload_path(parent, cname) parent.autoload?(cname) if cdef?(parent, cname) end else def strict_autoload_path(parent, cname) parent.autoload?(cname, false) end end # This method is called this way because I prefer `preload` to be the method # name to configure preloads in the public interface. # # @return [void] def do_preload preloads.each do |abspath| do_preload_abspath(abspath) end end # @param abspath [String] # @return [void] def do_preload_abspath(abspath) if ruby?(abspath) do_preload_file(abspath) elsif dir?(abspath) do_preload_dir(abspath) end end # @param dir [String] # @return [void] def do_preload_dir(dir) ls(dir) do |_basename, abspath| do_preload_abspath(abspath) end end # @param file [String] # @return [Boolean] def do_preload_file(file) log("preloading #{file}") if logger require file end # @param parent [Module] # @param cname [Symbol] # @return [String] def cpath(parent, cname) parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}" end # @param dir [String] # @yieldparam path [String, String] # @return [void] def ls(dir) Dir.foreach(dir) do |basename| next if basename.start_with?(".") abspath = File.join(dir, basename) yield basename, abspath unless ignored_paths.member?(abspath) end end # @param path [String] # @return [Boolean] def ruby?(path) path.end_with?(".rb") end # @param path [String] # @return [Boolean] def dir?(path) File.directory?(path) end # @param paths [>] # @return [] def expand_paths(paths) paths.flatten.map! { |path| File.expand_path(path) } end # @param glob_patterns [] # @return [] def expand_glob_patterns(glob_patterns) # Note that Dir.glob works with regular file names just fine. That is, # glob patterns technically need no wildcards. glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) } end # @return [void] def recompute_ignored_paths ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns)) end # @param message [String] # @return [void] def log(message) method_name = logger.respond_to?(:debug) ? :debug : :call logger.send(method_name, "Zeitwerk@#{tag}: #{message}") end def cdef?(parent, cname) parent.const_defined?(cname, false) end def register_explicit_namespace(cpath) ExplicitNamespace.register(cpath, self) end def raise_if_conflicting_directory(dir) self.class.mutex.synchronize do Registry.loaders.each do |loader| if loader != self && loader.manages?(dir) require "pp" raise Error, "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \ " which is already managed by\n\n#{loader.pretty_inspect}\n" EOS end end end end # @param parent [Module] # @param cname [Symbol] # @return [void] def unload_autoload(parent, cname) parent.send(:remove_const, cname) log("autoload for #{cpath(parent, cname)} removed") if logger end # @param parent [Module] # @param cname [Symbol] # @return [void] def unload_cref(parent, cname) parent.send(:remove_const, cname) log("#{cpath(parent, cname)} unloaded") if logger end end end zeitwerk-2.2.2/lib/zeitwerk/loader/000077500000000000000000000000001357024011300172355ustar00rootroot00000000000000zeitwerk-2.2.2/lib/zeitwerk/loader/callbacks.rb000066400000000000000000000052131357024011300215020ustar00rootroot00000000000000module Zeitwerk::Loader::Callbacks include Zeitwerk::RealModName # Invoked from our decorated Kernel#require when a managed file is autoloaded. # # @private # @param file [String] # @return [void] def on_file_autoloaded(file) cref = autoloads.delete(file) to_unload[cpath(*cref)] = [file, cref] if reloading_enabled? Zeitwerk::Registry.unregister_autoload(file) if logger && cdef?(*cref) log("constant #{cpath(*cref)} loaded from file #{file}") elsif !cdef?(*cref) raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath(*cref)}, but didn't", cref.last) end end # Invoked from our decorated Kernel#require when a managed directory is # autoloaded. # # @private # @param dir [String] # @return [void] def on_dir_autoloaded(dir) # Module#autoload does not serialize concurrent requires, and we handle # directories ourselves, so the callback needs to account for concurrency. # # Multi-threading would introduce a race condition here in which thread t1 # autovivifies the module, and while autoloads for its children are being # set, thread t2 autoloads the same namespace. # # Without the mutex and subsequent delete call, t2 would reset the module. # That not only would reassign the constant (undesirable per se) but, worse, # the module object created by t2 wouldn't have any of the autoloads for its # children, since t1 would have correctly deleted its lazy_subdirs entry. mutex2.synchronize do if cref = autoloads.delete(dir) autovivified_module = cref[0].const_set(cref[1], Module.new) log("module #{autovivified_module.name} autovivified from directory #{dir}") if logger to_unload[autovivified_module.name] = [dir, cref] if reloading_enabled? # We don't unregister `dir` in the registry because concurrent threads # wouldn't find a loader associated to it in Kernel#require and would # try to require the directory. Instead, we are going to keep track of # these to be able to unregister later if eager loading. autoloaded_dirs << dir on_namespace_loaded(autovivified_module) end end end # Invoked when a class or module is created or reopened, either from the # tracer or from module autovivification. If the namespace has matching # subdirectories, we descend into them now. # # @private # @param namespace [Module] # @return [void] def on_namespace_loaded(namespace) if subdirs = lazy_subdirs.delete(real_mod_name(namespace)) subdirs.each do |subdir| set_autoloads_in_dir(subdir, namespace) end end end end zeitwerk-2.2.2/lib/zeitwerk/real_mod_name.rb000066400000000000000000000011671357024011300211030ustar00rootroot00000000000000module Zeitwerk::RealModName UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name) private_constant :UNBOUND_METHOD_MODULE_NAME # Returns the real name of the class or module, as set after the first # constant to which it was assigned (or nil). # # The name method can be overridden, hence the indirection in this method. # # @param mod [Class, Module] # @return [String, nil] if UnboundMethod.method_defined?(:bind_call) def real_mod_name(mod) UNBOUND_METHOD_MODULE_NAME.bind_call(mod) end else def real_mod_name(mod) UNBOUND_METHOD_MODULE_NAME.bind(mod).call end end end zeitwerk-2.2.2/lib/zeitwerk/registry.rb000066400000000000000000000105401357024011300201640ustar00rootroot00000000000000# frozen_string_literal: true module Zeitwerk module Registry # :nodoc: all class << self # Keeps track of all loaders. Useful to broadcast messages and to prevent # them from being garbage collected. # # @private # @return [] attr_reader :loaders # Registers loaders created with `for_gem` to make the method idempotent # in case of reload. # # @private # @return [{String => Zeitwerk::Loader}] attr_reader :loaders_managing_gems # Maps real paths to the loaders responsible for them. # # This information is used by our decorated `Kernel#require` to be able to # invoke callbacks and autovivify modules. # # @private # @return [{String => Zeitwerk::Loader}] attr_reader :autoloads # This hash table addresses an edge case in which an autoload is ignored. # # For example, let's suppose we want to autoload in a gem like this: # # # lib/my_gem.rb # loader = Zeitwerk::Loader.new # loader.push_dir(__dir__) # loader.setup # # module MyGem # end # # if you require "my_gem", as Bundler would do, this happens while setting # up autoloads: # # 1. Object.autoload?(:MyGem) returns `nil` because the autoload for # the constant is issued by Zeitwerk while the same file is being # required. # 2. The constant `MyGem` is undefined while setup runs. # # Therefore, a directory `lib/my_gem` would autovivify a module according to # the existing information. But that would be wrong. # # To overcome this fundamental limitation, we keep track of the constant # paths that are in this situation ---in the example above, "MyGem"--- and # take this collection into account for the autovivification logic. # # Note that you cannot generally address this by moving the setup code # below the constant definition, because we want libraries to be able to # use managed constants in the module body: # # module MyGem # include MyConcern # end # # @private # @return [{String => (String, Zeitwerk::Loader)}] attr_reader :inceptions # Registers a loader. # # @private # @param loader [Zeitwerk::Loader] # @return [void] def register_loader(loader) loaders << loader end # This method returns always a loader, the same instance for the same root # file. That is how Zeitwerk::Loader.for_gem is idempotent. # # @private # @param root_file [String] # @return [Zeitwerk::Loader] def loader_for_gem(root_file) loaders_managing_gems[root_file] ||= begin Loader.new.tap do |loader| loader.tag = File.basename(root_file, ".rb") loader.inflector = GemInflector.new(root_file) loader.push_dir(File.dirname(root_file)) end end end # @private # @param loader [Zeitwerk::Loader] # @param realpath [String] # @return [void] def register_autoload(loader, realpath) autoloads[realpath] = loader end # @private # @param realpath [String] # @return [void] def unregister_autoload(realpath) autoloads.delete(realpath) end # @private # @param cpath [String] # @param realpath [String] # @param loader [Zeitwerk::Loader] # @return [void] def register_inception(cpath, realpath, loader) inceptions[cpath] = [realpath, loader] end # @private # @param cpath [String] # @return [String, nil] def inception?(cpath) if pair = inceptions[cpath] pair.first end end # @private # @param path [String] # @return [Zeitwerk::Loader, nil] def loader_for(path) autoloads[path] end # @private # @param loader [Zeitwerk::Loader] # @return [void] def on_unload(loader) autoloads.delete_if { |_path, object| object == loader } inceptions.delete_if { |_cpath, (_path, object)| object == loader } end end @loaders = [] @loaders_managing_gems = {} @autoloads = {} @inceptions = {} end end zeitwerk-2.2.2/lib/zeitwerk/version.rb000066400000000000000000000001071357024011300177770ustar00rootroot00000000000000# frozen_string_literal: true module Zeitwerk VERSION = "2.2.2" end zeitwerk-2.2.2/test/000077500000000000000000000000001357024011300143345ustar00rootroot00000000000000zeitwerk-2.2.2/test/lib/000077500000000000000000000000001357024011300151025ustar00rootroot00000000000000zeitwerk-2.2.2/test/lib/test_gem_inflector.rb000066400000000000000000000015401357024011300213030ustar00rootroot00000000000000require "test_helper" class TestGemInflector < LoaderTest def with_setup files = [ ["lib/my_gem.rb", <<-EOS], loader = Zeitwerk::Loader.for_gem loader.enable_reloading loader.setup module MyGem end EOS ["lib/my_gem/foo.rb", "MyGem::Foo = true"], ["lib/my_gem/version.rb", "MyGem::VERSION = '1.0.0'"], ["lib/my_gem/ns/version.rb", "MyGem::Ns::Version = true"] ] with_files(files) do require "./lib/my_gem" yield end end test "the constant for my_gem/version.rb is inflected as VERSION" do with_setup { assert_equal "1.0.0", MyGem::VERSION } end test "other possible version.rb are inflected normally" do with_setup { assert MyGem::Ns::Version } end test "works as expected for other files" do with_setup { assert MyGem::Foo } end end zeitwerk-2.2.2/test/lib/test_inflector.rb000066400000000000000000000020371357024011300204550ustar00rootroot00000000000000require "test_helper" class TestInflector < Minitest::Test def camelize(str) Zeitwerk::Inflector.new.camelize(str, nil) end test "capitalizes the first letter" do assert_equal "User", camelize("user") end test "camelizes snake case basenames" do assert_equal "UsersController", camelize("users_controller") end test "supports segments that do not capitalize" do assert_equal "Point3dValue", camelize("point_3d_value") end test "knows nothing about acronyms" do assert_equal "HtmlParser", camelize("html_parser") end test "returns inflections defined using the inflect method" do inflections = { "html_parser" => "HTMLParser", "csv_controller" => "CSVController", "mysql_adapter" => "MySQLAdapter" } inflector = Zeitwerk::Inflector.new inflector.inflect(inflections) inflections.each do |basename, cname| assert_equal cname, inflector.camelize(basename, nil) end assert_equal "UsersController", inflector.camelize("users_controller", nil) end end zeitwerk-2.2.2/test/lib/test_real_mod_name.rb000066400000000000000000000023031357024011300212460ustar00rootroot00000000000000require "test_helper" class TestRealModName < Minitest::Test include Zeitwerk::RealModName test "returns nil for anonymous classes and modules" do [Class.new, Module.new].each do |mod| assert_nil real_mod_name(mod) end end test "returns nil for anonymous classes and modules that override #name" do [Class.new, Module.new].each do |mod| def mod.name; "X"; end assert_equal "X", mod.name assert_nil real_mod_name(mod) end end test "returns the name of regular classes an modules" do on_teardown do remove_const :C, from: self.class remove_const :M, from: self.class end C = Class.new M = Module.new [C, M].each do |mod| assert_equal mod.name, real_mod_name(mod) end end test "returns the real name of class and modules that override #name" do on_teardown do remove_const :C, from: self.class remove_const :M, from: self.class end C = Class.new { def self.name; "X"; end } M = Module.new { def self.name; "X"; end } [[C, "#{self.class}::C"], [M, "#{self.class}::M"]].each do |mod, real| assert_equal "X", mod.name assert_equal real, real_mod_name(mod) end end end zeitwerk-2.2.2/test/lib/zeitwerk/000077500000000000000000000000001357024011300167465ustar00rootroot00000000000000zeitwerk-2.2.2/test/lib/zeitwerk/test_all_dirs.rb000066400000000000000000000012631357024011300221250ustar00rootroot00000000000000require "test_helper" class TestAllDirs < LoaderTest test "returns an empty array if no loaders are instantiated" do assert_empty Zeitwerk::Loader.all_dirs end test "returns an empty array if there are loaders but they have no root dirs" do 2.times { new_loader } assert_empty Zeitwerk::Loader.all_dirs end test "returns the root directories of the registered loaders" do files = [ ["loaderA/a.rb", "A = true"], ["loaderB/b.rb", "B = true"] ] with_files(files) do new_loader(dirs: "loaderA") new_loader(dirs: "loaderB") assert_equal ["#{Dir.pwd}/loaderA", "#{Dir.pwd}/loaderB"], Zeitwerk::Loader.all_dirs end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_ancestors.rb000066400000000000000000000023001357024011300223260ustar00rootroot00000000000000require "test_helper" # The following properties are not supported by the classic Rails autoloader. class TestAncestors < LoaderTest test "autoloads a constant from an ancestor" do files = [ ["a.rb", "class A; end"], ["a/x.rb", "class A::X; end"], ["b.rb", "class B < A; end"], ["c.rb", "class C < B; end"] ] with_setup(files) do assert C::X end end test "autoloads a constant from an ancenstor, even if present above" do files = [ ["a.rb", "class A; X = :A; end"], ["b.rb", "class B < A; end"], ["b/x.rb", "class B; X = :B; end"], ["c.rb", "class C < B; end"] ] with_setup(files) do assert_equal :A, A::X assert_equal :B, C::X end end # See https://github.com/rails/rails/issues/28997. test "autoloads a constant from an ancestor that has some nesting going on" do files = [ ["test_class.rb", "class TestClass; include IncludeModule; end"], ["include_module.rb", "module IncludeModule; include ContainerModule; end"], ["container_module/child_class.rb", "class ContainerModule::ChildClass; end"] ] with_setup(files) do assert TestClass::ChildClass end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_autovivification.rb000066400000000000000000000015141357024011300237160ustar00rootroot00000000000000require "test_helper" class TestAutovivification < LoaderTest test "autoloads a simple constant in an autovivified module" do files = [["admin/x.rb", "Admin::X = true"]] with_setup(files) do assert_kind_of Module, Admin assert Admin::X end end test "autovivifies several levels in a row" do files = [["foo/bar/baz/woo.rb", "Foo::Bar::Baz::Woo = true"]] with_setup(files) do assert Foo::Bar::Baz::Woo end end test "autoloads several constants from the same namespace" do files = [ ["app/models/admin/hotel.rb", "class Admin::Hotel; end"], ["app/controllers/admin/hotels_controller.rb", "class Admin::HotelsController; end"] ] with_setup(files, dirs: %w(app/models app/controllers)) do assert Admin::Hotel assert Admin::HotelsController end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_callbacks.rb000066400000000000000000000024131357024011300222510ustar00rootroot00000000000000require "test_helper" class TestCallbacks < LoaderTest test "autoloading a file triggers on_file_autoloaded" do def loader.on_file_autoloaded(file) if file == File.realpath("x.rb") $on_file_autoloaded_called = true end super end files = [["x.rb", "X = true"]] with_setup(files) do $on_file_autoloaded_called = false assert X assert $on_file_autoloaded_called end end test "requiring an autoloadable file triggers on_file_autoloaded" do def loader.on_file_autoloaded(file) if file == File.realpath("y.rb") $on_file_autoloaded_called = true end super end files = [ ["x.rb", "X = true"], ["y.rb", "Y = X"] ] with_setup(files, load_path: ".") do $on_file_autoloaded_called = false require "y" assert Y assert $on_file_autoloaded_called end end test "autoloading a directory triggers on_dir_autoloaded" do def loader.on_dir_autoloaded(dir) if dir == File.realpath("m") $on_dir_autoloaded_called = true end super end files = [["m/x.rb", "M::X = true"]] with_setup(files) do $on_dir_autoloaded_called = false assert M::X assert $on_dir_autoloaded_called end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_concurrency.rb000066400000000000000000000014101357024011300226600ustar00rootroot00000000000000require "test_helper" class TestConcurrency < LoaderTest test "constant definition is synchronized" do files = [["m.rb", <<-EOS]] module M sleep 0.5 def self.works? true end end EOS with_setup(files) do t = Thread.new { M } assert M.works? t.join end end test "module autovivification" do $test_admin_const_set_calls = 0 files = [["admin/v2/user.rb", "class Admin::V2::User; end"]] with_setup(files) do assert Admin def Admin.const_set(cname, mod) $test_admin_const_set_calls += 1 sleep 0.5 super end Array.new(2) { Thread.new { Admin::V2 } }.each(&:join) assert_equal 1, $test_admin_const_set_calls end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_conflicting_directory.rb000066400000000000000000000045411357024011300247210ustar00rootroot00000000000000require "test_helper" class TestConflictingDirectory < LoaderTest def dir __dir__ end def parent File.expand_path("..", dir) end def existing_loader @existing_loader ||= new_loader(setup: false) end def loader @loader ||= new_loader(setup: false) end def conflicting_directory_message(dir) require "pp" "loader\n\n#{loader.pretty_inspect}\n\nwants to manage directory #{dir}," \ " which is already managed by\n\n#{existing_loader.pretty_inspect}\n" end test "raises if an existing loader manages the same root dir" do existing_loader.push_dir(dir) e = assert_raises(Zeitwerk::Error) { loader.push_dir(dir) } assert_equal conflicting_directory_message(dir), e.message end test "raises if an existing loader manages a parent directory" do existing_loader.push_dir(parent) e = assert_raises(Zeitwerk::Error) { loader.push_dir(dir) } assert_equal conflicting_directory_message(dir), e.message end test "raises if an existing loader manages a subdirectory" do existing_loader.push_dir(dir) e = assert_raises(Zeitwerk::Error) { loader.push_dir(parent) } assert_equal conflicting_directory_message(parent), e.message end test "does not raise if an existing loader manages a directory with a matching prefix" do files = [["foo/x.rb", "X = 1"], ["foobar/y.rb", "Y = 1"]] with_files(files) do existing_loader.push_dir("foo") assert loader.push_dir("foobar") end end test "does not raise if an existing loader ignores the directory (dir)" do existing_loader.push_dir(parent) existing_loader.ignore(dir) assert loader.push_dir(dir) end test "does not raise if an existing loader ignores the directory (glob pattern)" do existing_loader.push_dir(parent) existing_loader.ignore("#{parent}/*") assert loader.push_dir(dir) end test "raises if an existing loader ignores a directory with a matching prefix" do files = [["foo/x.rb", "X = 1"], ["foobar/y.rb", "Y = 1"]] with_files(files) do ignored = File.expand_path("foo") conflicting_dir = File.expand_path("foobar") existing_loader.push_dir(".") existing_loader.ignore(ignored) e = assert_raises(Zeitwerk::Error) { loader.push_dir(conflicting_dir) } assert_equal conflicting_directory_message(conflicting_dir), e.message end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_eager_load.rb000066400000000000000000000160031357024011300224140ustar00rootroot00000000000000require "test_helper" require "fileutils" class TestEagerLoad < LoaderTest test "eager loads independent files" do loaders = [loader, new_loader(setup: false)] $tel0 = $tel1 = false files = [ ["lib0/app0.rb", "module App0; end"], ["lib0/app0/foo.rb", "class App0::Foo; $tel0 = true; end"], ["lib1/app1/foo.rb", "class App1::Foo; end"], ["lib1/app1/foo/bar/baz.rb", "class App1::Foo::Bar::Baz; $tel1 = true; end"] ] with_files(files) do loaders[0].push_dir("lib0") loaders[0].setup loaders[1].push_dir("lib1") loaders[1].setup Zeitwerk::Loader.eager_load_all assert $tel0 assert $tel1 end end test "eager loads dependent loaders" do loaders = [loader, new_loader(setup: false)] $tel0 = $tel1 = false files = [ ["lib0/app0.rb", <<-EOS], module App0 App1 end EOS ["lib0/app0/foo.rb", <<-EOS], class App0::Foo $tel0 = App1::Foo end EOS ["lib1/app1/foo.rb", <<-EOS], class App1::Foo App0 end EOS ["lib1/app1/foo/bar/baz.rb", <<-EOS] class App1::Foo::Bar::Baz $tel1 = App0::Foo end EOS ] with_files(files) do loaders[0].push_dir("lib0") loaders[0].setup loaders[1].push_dir("lib1") loaders[1].setup Zeitwerk::Loader.eager_load_all assert $tel0 assert $tel1 end end test "eager loads gems" do on_teardown do remove_const :MyGem delete_loaded_feature "my_gem.rb" delete_loaded_feature "my_gem/foo.rb" delete_loaded_feature "my_gem/foo/bar.rb" delete_loaded_feature "my_gem/foo/baz.rb" end $my_gem_foo_bar_eager_loaded = false files = [ ["my_gem.rb", <<-EOS], $for_gem_test_loader = Zeitwerk::Loader.for_gem $for_gem_test_loader.setup class MyGem Foo::Baz # autoloads fine end $for_gem_test_loader.eager_load EOS ["my_gem/foo.rb", "class MyGem::Foo; end"], ["my_gem/foo/bar.rb", "class MyGem::Foo::Bar; end; $my_gem_foo_bar_eager_loaded = true"], ["my_gem/foo/baz.rb", "class MyGem::Foo::Baz; end"], ] with_files(files) do with_load_path(".") do require "my_gem" assert $my_gem_foo_bar_eager_loaded end end end [false, true].each do |enable_reloading| test "we can opt-out of entire root directories, and still autoload (enable_autoloading #{enable_reloading})" do on_teardown do remove_const :Foo delete_loaded_feature "foo.rb" end $test_eager_load_eager_loaded_p = false files = [["foo.rb", "Foo = true; $test_eager_load_eager_loaded_p = true"]] with_files(files) do loader = new_loader(dirs: ".", enable_reloading: enable_reloading) loader.do_not_eager_load(".") loader.eager_load assert !$test_eager_load_eager_loaded_p assert Foo end end test "we can opt-out of sudirectories, and still autoload (enable_autoloading #{enable_reloading})" do on_teardown do remove_const :Foo delete_loaded_feature "foo.rb" remove_const :DbAdapters delete_loaded_feature "db_adapters/mysql_adapter.rb" end $test_eager_load_eager_loaded_p = false files = [ ["db_adapters/mysql_adapter.rb", <<-EOS], module DbAdapters::MysqlAdapter end $test_eager_load_eager_loaded_p = true EOS ["foo.rb", "Foo = true"] ] with_files(files) do loader = new_loader(dirs: ".", enable_reloading: enable_reloading) loader.do_not_eager_load("db_adapters") loader.eager_load assert Foo assert !$test_eager_load_eager_loaded_p assert DbAdapters::MysqlAdapter end end test "we can opt-out of files, and still autoload (enable_autoloading #{enable_reloading})" do on_teardown do remove_const :Foo delete_loaded_feature "foo.rb" remove_const :Bar delete_loaded_feature "bar.rb" end $test_eager_load_eager_loaded_p = false files = [ ["foo.rb", "Foo = true"], ["bar.rb", "Bar = true; $test_eager_load_eager_loaded_p = true"] ] with_files(files) do loader = new_loader(dirs: ".", enable_reloading: enable_reloading) loader.do_not_eager_load("bar.rb") loader.eager_load assert Foo assert !$test_eager_load_eager_loaded_p assert Bar end end test "opt-ed out root directories sharing a namespace don't prevent autoload (enable_autoloading #{enable_reloading})" do on_teardown do remove_const :Ns delete_loaded_feature "ns/foo.rb" delete_loaded_feature "ns/bar.rb" end $test_eager_load_eager_loaded_p = false files = [ ["lazylib/ns/foo.rb", "module Ns::Foo; end"], ["eagerlib/ns/bar.rb", "module Ns::Bar; $test_eager_load_eager_loaded_p = true; end"] ] with_files(files) do loader = new_loader(dirs: %w(lazylib eagerlib), enable_reloading: enable_reloading) loader.do_not_eager_load('lazylib') loader.eager_load assert $test_eager_load_eager_loaded_p end end test "opt-ed out subdirectories don't prevent autoloading shared namespaces (enable_autoloading #{enable_reloading})" do on_teardown do remove_const :Ns delete_loaded_feature "ns/foo.rb" delete_loaded_feature "ns/bar.rb" end $test_eager_load_eager_loaded_p = false files = [ ["lazylib/ns/foo.rb", "module Ns::Foo; end"], ["eagerlib/ns/bar.rb", "module Ns::Bar; $test_eager_load_eager_loaded_p = true; end"] ] with_files(files) do loader = new_loader(dirs: %w(lazylib eagerlib), enable_reloading: enable_reloading) loader.do_not_eager_load('lazylib/namespace') loader.eager_load assert $test_eager_load_eager_loaded_p end end end test "eager loading skips repeated files" do $test_eager_loaded_file = nil files = [ ["a/foo.rb", "Foo = 1; $test_eager_loaded_file = :a"], ["b/foo.rb", "Foo = 1; $test_eager_loaded_file = :b"] ] with_files(files) do la = new_loader(dirs: "a") lb = new_loader(dirs: "b") la.eager_load lb.eager_load assert_equal :a, $test_eager_loaded_file end end test "eager loading skips files that would map to already loaded constants" do on_teardown { remove_const :X } $test_eager_loaded_file = false files = [["x.rb", "X = 1; $test_eager_loaded_file = true"]] ::X = 1 with_setup(files) do loader.eager_load assert !$test_eager_loaded_file end end test "eager loading works with symbolic links" do files = [["real/x.rb", "X = true"]] with_files(files) do FileUtils.ln_s("real", "symlink") loader.push_dir("symlink") loader.setup loader.eager_load assert_nil Object.autoload?(:X) end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_exceptions.rb000066400000000000000000000047561357024011300225270ustar00rootroot00000000000000require "test_helper" class TestExceptions < LoaderTest test "raises NameError if the expected constant is not defined" do files = [["typo.rb", "TyPo = 1"]] with_setup(files) do typo_rb = File.realpath("typo.rb") error = assert_raises(Zeitwerk::NameError) { Typo } assert_equal "expected file #{typo_rb} to define constant Typo, but didn't", error.message assert_equal :Typo, error.name end end test "eager loading raises NameError if files do not define the expected constants" do on_teardown do remove_const :X # should be unnecessary, but $LOADED_FEATURES.reject! redefines it remove_const :Y end files = [["x.rb", "Y = 1"]] with_setup(files) do x_rb = File.realpath("x.rb") error = assert_raises(Zeitwerk::NameError) { loader.eager_load } assert_equal "expected file #{x_rb} to define constant X, but didn't", error.message assert_equal :X, error.name end end test "eager loading raises NameError if a namespace has not been loaded yet" do on_teardown do remove_const :CLI delete_loaded_feature 'cli/x.rb' end files = [["cli/x.rb", "module CLI; X = 1; end"]] with_setup(files) do cli_x_rb = File.realpath("cli/x.rb") error = assert_raises(Zeitwerk::NameError) { loader.eager_load } assert_equal "expected file #{cli_x_rb} to define constant Cli::X, but didn't", error.message assert_equal :X, error.name end end test "raises if the file does" do files = [["raises.rb", "Raises = 1; raise 'foo'"]] with_setup(files, rm: false) do assert_raises(RuntimeError, "foo") { Raises } end end test "raises Zeitwerk::NameError if the inflector returns an invalid constant name for a file" do files = [["foo-bar.rb", "FooBar = 1"]] error = assert_raises Zeitwerk::NameError do with_setup(files) {} end assert_equal :"Foo-bar", error.name assert_includes error.message, "wrong constant name Foo-bar" assert_includes error.message, "Tell Zeitwerk to ignore this particular file." end test "raises Zeitwerk::NameError if the inflector returns an invalid constant name for a directory" do files = [["foo-bar/baz.rb", "FooBar::Baz = 1"]] error = assert_raises Zeitwerk::NameError do with_setup(files) {} end assert_equal :"Foo-bar", error.name assert_includes error.message, "wrong constant name Foo-bar" assert_includes error.message, "Tell Zeitwerk to ignore this particular directory." end end zeitwerk-2.2.2/test/lib/zeitwerk/test_explicit_namespace.rb000066400000000000000000000107061357024011300241730ustar00rootroot00000000000000require "test_helper" class TestExplicitNamespace < LoaderTest test "explicit namespaces are loaded correctly" do files = [ ["app/models/hotel.rb", "class Hotel; X = 1; end"], ["app/models/hotel/pricing.rb", "class Hotel::Pricing; end"] ] with_setup(files, dirs: "app/models") do assert_kind_of Class, Hotel assert Hotel::X assert Hotel::Pricing end end test "explicit namespaces are loaded correctly even if #name is overridden" do files = [ ["app/models/hotel.rb", <<~RUBY], class Hotel def self.name "X" end end RUBY ["app/models/hotel/pricing.rb", "class Hotel::Pricing; end"] ] with_setup(files, dirs: "app/models") do assert Hotel::Pricing end end test "explicit namespaces managed by different instances" do files = [ ["a/m.rb", "module M; end"], ["a/m/n.rb", "M::N = true"], ["b/x.rb", "module X; end"], ["b/x/y.rb", "X::Y = true"], ] with_files(files) do new_loader(dirs: "a") new_loader(dirs: "b") assert M::N assert X::Y end end test "autoloads are set correctly, even if there are autoloads for the same cname in the superclass" do files = [ ["a.rb", "class A; end"], ["a/x.rb", "A::X = :A"], ["b.rb", "class B < A; end"], ["b/x.rb", "B::X = :B"] ] with_setup(files) do assert_kind_of Class, A assert_kind_of Class, B assert_equal :B, B::X end end test "autoloads are set correctly, even if there are autoloads for the same cname in a module prepended to the superclass" do files = [ ["m/x.rb", "M::X = :M"], ["a.rb", "class A; prepend M; end"], ["b.rb", "class B < A; end"], ["b/x.rb", "B::X = :B"] ] with_setup(files) do assert_kind_of Class, A assert_kind_of Class, B assert_equal :B, B::X end end test "autoloads are set correctly, even if there are autoloads for the same cname in other ancestors" do files = [ ["m/x.rb", "M::X = :M"], ["a.rb", "class A; include M; end"], ["b.rb", "class B < A; end"], ["b/x.rb", "B::X = :B"] ] with_setup(files) do assert_kind_of Class, A assert_kind_of Class, B assert_equal :B, B::X end end # As of this writing, a tracer on the :class event does not seem to have any # performance penalty in an ordinary code base. But I prefer to precisely # control that we use a tracer only if needed in case this issue # # https://bugs.ruby-lang.org/issues/14104 # # goes forward. def tracer Zeitwerk::ExplicitNamespace.tracer end test "the tracer starts disabled" do assert !tracer.enabled? end test "simple autoloading does not enable the tracer" do files = [["x.rb", "X = true"]] with_setup(files) do assert !tracer.enabled? assert X assert !tracer.enabled? end end test "autovivification does not enable the tracer" do files = [["foo/bar.rb", "module Foo::Bar; end"]] with_setup(files) do assert !tracer.enabled? assert Foo::Bar assert !tracer.enabled? end end test "explicit namespaces enable the tracer until loaded" do files = [ ["hotel.rb", "class Hotel; end"], ["hotel/pricing.rb", "class Hotel::Pricing; end"] ] with_setup(files) do assert tracer.enabled? assert Hotel assert !tracer.enabled? assert Hotel::Pricing assert !tracer.enabled? end end test "the tracer is enabled until everything is loaded" do files = [ ["a/m.rb", "module M; end"], ["a/m/n.rb", "M::N = true"], ["b/x.rb", "module X; end"], ["b/x/y.rb", "X::Y = true"], ] with_files(files) do new_loader(dirs: "a") assert tracer.enabled? new_loader(dirs: "b") assert tracer.enabled? assert M assert tracer.enabled? assert X assert !tracer.enabled? end end # This is a regression test. test "the tracer handles singleton classes" do files = [ ["hotel.rb", <<-EOS], class Hotel class << self def x 1 end end end EOS ["hotel/pricing.rb", "class Hotel::Pricing; end"], ["car.rb", "class Car; end"], ["car/pricing.rb", "class Car::Pricing; end"], ] with_setup(files) do assert tracer.enabled? assert_equal 1, Hotel.x assert tracer.enabled? end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_for_gem.rb000066400000000000000000000051051357024011300217510ustar00rootroot00000000000000require "test_helper" class TestForGem < LoaderTest test "sets things correctly" do files = [ ["my_gem.rb", <<-EOS], $for_gem_test_loader = Zeitwerk::Loader.for_gem $for_gem_test_loader.enable_reloading $for_gem_test_loader.setup class MyGem end EOS ["my_gem/foo.rb", "class MyGem::Foo; end"], ["my_gem/foo/bar.rb", "class MyGem::Foo::Bar; end"] ] with_files(files) do with_load_path(".") do assert require "my_gem" # what bundler is going to do assert MyGem::Foo::Bar $for_gem_test_loader.unload assert !Object.const_defined?(:MyGem) $for_gem_test_loader.setup assert MyGem::Foo::Bar end end end test "is idempotent" do files = [ ["my_gem.rb", <<-EOS], $for_gem_test_zs << Zeitwerk::Loader.for_gem $for_gem_test_zs.last.enable_reloading $for_gem_test_zs.last.setup class MyGem end EOS ["my_gem/foo.rb", "class MyGem::Foo; end"] ] with_files(files) do with_load_path(".") do $for_gem_test_zs = [] assert require "my_gem" # what bundler is going to do assert MyGem::Foo $for_gem_test_zs.first.unload assert !Object.const_defined?(:MyGem) $for_gem_test_zs.first.setup assert MyGem::Foo assert_equal 2, $for_gem_test_zs.size assert_same $for_gem_test_zs.first, $for_gem_test_zs.last end end end test "configures the gem inflector by default" do on_teardown do remove_const :MyGem delete_loaded_feature "my_gem.rb" end files = [ ["my_gem.rb", <<-EOS], $for_gem_test_loader = Zeitwerk::Loader.for_gem $for_gem_test_loader.setup class MyGem end EOS ["my_gem/foo.rb", "class MyGem::Foo; end"] ] with_files(files) do with_load_path(".") do require "my_gem" assert_instance_of Zeitwerk::GemInflector, $for_gem_test_loader.inflector end end end test "configures the basename of the root file as loader name" do on_teardown do remove_const :MyGem delete_loaded_feature "my_gem.rb" end files = [ ["my_gem.rb", <<-EOS], $for_gem_test_loader = Zeitwerk::Loader.for_gem $for_gem_test_loader.setup class MyGem end EOS ["my_gem/foo.rb", "class MyGem::Foo; end"] ] with_files(files) do with_load_path(".") do require "my_gem" assert_equal "my_gem", $for_gem_test_loader.tag end end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_ignore.rb000066400000000000000000000057641357024011300216310ustar00rootroot00000000000000require "test_helper" require "set" class TestIgnore < LoaderTest test "ignored root directories are ignored" do files = [["x.rb", "X = true"]] with_files(files) do loader.push_dir(".") loader.ignore(".") assert_empty loader.autoloads assert_raises(NameError) { ::X } end end test "ignored files are ignored" do files = [ ["x.rb", "X = true"], ["y.rb", "Y = true"] ] with_files(files) do loader.push_dir(".") loader.ignore("y.rb") loader.setup assert_equal 1, loader.autoloads.size assert ::X assert_raises(NameError) { ::Y } end end test "ignored directories are ignored" do files = [ ["x.rb", "X = true"], ["m/a.rb", "M::A = true"], ["m/b.rb", "M::B = true"], ["m/c.rb", "M::C = true"] ] with_files(files) do loader.push_dir(".") loader.ignore("m") loader.setup assert_equal 1, loader.autoloads.size assert ::X assert_raises(NameError) { ::M } end end test "ignored files are not eager loaded" do files = [ ["x.rb", "X = true"], ["y.rb", "Y = true"] ] with_files(files) do loader.push_dir(".") loader.ignore("y.rb") loader.setup loader.eager_load assert ::X assert_raises(NameError) { ::Y } end end test "ignored directories are not eager loaded" do files = [ ["x.rb", "X = true"], ["m/a.rb", "M::A = true"], ["m/b.rb", "M::B = true"], ["m/c.rb", "M::C = true"] ] with_files(files) do loader.push_dir(".") loader.ignore("m") loader.setup loader.eager_load assert ::X assert_raises(NameError) { ::M } end end test "supports several arguments" do a = "#{Dir.pwd}/a.rb" b = "#{Dir.pwd}/b.rb" loader.ignore(a, b) assert_equal [a, b].to_set, loader.ignored_glob_patterns end test "supports an array" do a = "#{Dir.pwd}/a.rb" b = "#{Dir.pwd}/b.rb" loader.ignore([a, b]) assert_equal [a, b].to_set, loader.ignored_glob_patterns end test "supports glob patterns" do files = [ ["admin/user.rb", "class Admin::User; end"], ["admin/user_test.rb", "class Admin::UserTest < Minitest::Test; end"] ] with_files(files) do loader.push_dir(".") loader.ignore("**/*_test.rb") loader.setup assert Admin::User assert_raises(NameError) { Admin::UserTest } end end test "ignored paths are recomputed on reload" do files = [ ["user.rb", "class User; end"], ["user_test.rb", "class UserTest < Minitest::Test; end"], ] with_files(files) do loader.push_dir(".") loader.ignore("*_test.rb") loader.setup assert User assert_raises(NameError) { UserTest } File.write("post.rb", "class Post; end") File.write("post_test.rb", "class PostTest < Minitest::Test; end") loader.reload assert Post assert_raises(NameError) { PostTest } end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_logging.rb000066400000000000000000000106441357024011300217650ustar00rootroot00000000000000require "test_helper" class TestLogging < LoaderTest def setup super loader.logger = method(:print) end def teardown Zeitwerk::Loader.default_logger = nil loader.logger = nil super end def tagged_message(message) "Zeitwerk@#{loader.tag}: #{message}" end def assert_logged(expected) case expected when String assert_output(tagged_message(expected)) { yield } when Regexp assert_output(/#{tagged_message(expected)}/) { yield } end end test "log! just prints to $stdout" do loader.logger = nil # make sure we are setting something loader.log! message = "test log!" assert_logged(/#{message}\n/) { loader.send(:log, message) } end test "accepts objects that respond to :call" do logger = Object.new def logger.call(message) print message end loader.logger = logger message = "test message :call" assert_logged(message) { loader.send(:log, message) } end test "accepts objects that respond to :debug" do logger = Object.new def logger.debug(message) print message end loader.logger = logger message = "test message :debug" assert_logged(message) { loader.send(:log, message) } end test "new loaders get assigned the default global logger" do assert_nil Zeitwerk::Loader.new.logger Zeitwerk::Loader.default_logger = Object.new assert_same Zeitwerk::Loader.default_logger, Zeitwerk::Loader.new.logger end test "logs loaded files" do files = [["x.rb", "X = true"]] with_files(files) do with_load_path(".") do assert_logged(/constant X loaded from file #{File.realpath("x.rb")}/) do loader.push_dir(".") loader.setup assert X end end end end test "logs required managed files" do files = [["x.rb", "X = true"]] with_files(files) do with_load_path(".") do assert_logged(/constant X loaded from file #{File.realpath("x.rb")}/) do loader.push_dir(".") loader.setup assert require "x" end end end end test "logs autovivified modules" do files = [["admin/user.rb", "class Admin::User; end"]] with_files(files) do with_load_path(".") do assert_logged(/module Admin autovivified from directory #{File.realpath("admin")}/) do loader.push_dir(".") loader.setup assert Admin end end end end test "logs autoload configured for files" do files = [["x.rb", "X = true"]] with_files(files) do assert_logged("autoload set for X, to be loaded from #{File.realpath("x.rb")}") do loader.push_dir(".") loader.setup end end end test "logs autoload configured for directories" do files = [["admin/user.rb", "class Admin::User; end"]] with_files(files) do assert_logged("autoload set for Admin, to be autovivified from #{File.realpath("admin")}") do loader.push_dir(".") loader.setup end end end test "logs preloads" do files = [["x.rb", "X = true"]] with_files(files) do loader.push_dir(".") loader.preload("x.rb") assert_logged(/preloading #{File.realpath("x.rb")}/) do loader.setup end end end test "logs unloads for autoloads" do files = [["x.rb", "X = true"]] with_files(files) do assert_logged(/autoload for X removed/) do loader.push_dir(".") loader.setup loader.reload end end end test "logs unloads for loaded objects" do files = [["x.rb", "X = true"]] with_files(files) do assert_logged(/X unloaded/) do loader.push_dir(".") loader.setup assert X loader.reload end end end test "logs files shadowed by autoloads" do files = [ ["a/foo.rb", "Foo = :a"], ["b/foo.rb", "Foo = :b"] ] with_files(files) do new_loader(dirs: "a") assert_logged(%r(file .*?/b/foo\.rb is ignored because .*?/a/foo\.rb has precedence)) do loader.push_dir("b") loader.setup end end end test "eager loading skips files that would map to already loaded constants" do on_teardown { remove_const :X } ::X = 1 files = [["x.rb", "X = 1"]] with_files(files) do loader.push_dir(".") assert_logged(%r(file .*?/x\.rb is ignored because X is already defined)) do loader.setup end end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_multiple.rb000066400000000000000000000021401357024011300221620ustar00rootroot00000000000000require "test_helper" class TestMultiple < LoaderTest test "multiple independent loaders" do files = [ ["lib0/app0.rb", "module App0; end"], ["lib0/app0/foo.rb", "class App0::Foo; end"], ["lib1/app1/foo.rb", "class App1::Foo; end"], ["lib1/app1/foo/bar/baz.rb", "class App1::Foo::Bar::Baz; end"] ] with_files(files) do new_loader(dirs: "lib0") new_loader(dirs: "lib1") assert App0::Foo assert App1::Foo::Bar::Baz end end test "multiple dependent loaders" do files = [ ["lib0/app0.rb", <<-EOS], module App0 App1 end EOS ["lib0/app0/foo.rb", <<-EOS], class App0::Foo App1::Foo end EOS ["lib1/app1/foo.rb", <<-EOS], class App1::Foo App0 end EOS ["lib1/app1/foo/bar/baz.rb", <<-EOS] class App1::Foo::Bar::Baz App0::Foo end EOS ] with_files(files) do new_loader(dirs: "lib0") new_loader(dirs: "lib1") assert App0::Foo assert App1::Foo::Bar::Baz end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_nested_root_directories.rb000066400000000000000000000023041357024011300252520ustar00rootroot00000000000000require "test_helper" class TestNestedRootDirectories < LoaderTest test "nested root directories do not autovivify modules" do files = [["app/models/concerns/pricing.rb", "module Pricing; end"]] with_setup(files, dirs: %w(app/models app/models/concerns)) do assert_raises(NameError) { Concerns } end end test "nested root directories are ignored even if there is a matching file" do files = [ ["app/models/hotel.rb", "class Hotel; include GeoLoc; end"], ["app/models/concerns/geo_loc.rb", "module GeoLoc; end"], ["app/models/concerns.rb", "module Concerns; end"] ] with_setup(files, dirs: %w(app/models app/models/concerns)) do assert Concerns assert Hotel end end test "eager loading handles nested root directories correctly" do $airplane_eager_loaded = $locatable_eager_loaded = false files = [ ["airplane.rb", "class Airplane; $airplane_eager_loaded = true; end"], ["concerns/locatable.rb", "module Locatable; $locatable_eager_loaded = true; end"] ] with_setup(files, dirs: [".", "concerns"]) do loader.eager_load assert $airplane_eager_loaded assert $locatable_eager_loaded end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_preload.rb000066400000000000000000000022461357024011300217640ustar00rootroot00000000000000require "test_helper" class TestPreload < LoaderTest def preloads @preloads ||= ["a.rb", "m/n"] end def assert_preload $a_preloaded = $b_preoloaded = $c_preloaded = $d_preloaded = false files = [ ["a.rb", "A = 1; $a_preloaded = true"], ["m/n/b.rb", "M::N::B = 1; $b_preloaded = true"], ["m/c.rb", "M::C = 1; $c_preloaded = true"], ["d.rb", "D = 1; $d_preloaded = true"] ] with_files(files) do loader.push_dir(".") yield # preload here loader.setup assert $a_preloaded assert $b_preloaded assert !$c_preloaded assert !$d_preloaded end end test "preloads files and directories (multiple args)" do assert_preload do loader.preload(*preloads) end end test "preloads files and directories (array)" do assert_preload do loader.preload(preloads) end end test "preloads files and directories (multiple calls)" do assert_preload do loader.preload(preloads.first) loader.preload(preloads.last) end end test "preloads files after setup too" do assert_preload do loader.setup loader.preload(preloads) end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_push_dir.rb000066400000000000000000000013031357024011300221440ustar00rootroot00000000000000require "test_helper" require "pathname" class TesPushDir < LoaderTest test "accepts dirs as strings and stores their absolute paths" do loader.push_dir(".") assert loader.root_dirs == { Dir.pwd => true } assert loader.dirs.include?(Dir.pwd) end test "accepts dirs as pathnames and stores their absolute paths" do loader.push_dir(Pathname.new(".")) assert loader.root_dirs == { Dir.pwd => true } assert loader.dirs.include?(Dir.pwd) end test "raises on non-existing directories" do dir = File.expand_path("non-existing") e = assert_raises(Zeitwerk::Error) { loader.push_dir(dir) } assert_equal "the root directory #{dir} does not exist", e.message end end zeitwerk-2.2.2/test/lib/zeitwerk/test_reloading.rb000066400000000000000000000066111357024011300223020ustar00rootroot00000000000000require "test_helper" require "fileutils" class TestReloading < LoaderTest test "enabling reloading after setup raises" do e = assert_raises(Zeitwerk::Error) do loader = Zeitwerk::Loader.new loader.setup loader.enable_reloading end assert_equal "cannot enable reloading after setup", e.message end test "enabling reloading is idempotent, even after setup" do assert loader.reloading_enabled? # precondition loader.setup loader.enable_reloading # should not raise assert loader.reloading_enabled? end test "reloading works if the flag is set" do files = [ ["x.rb", "X = 1"], # top-level ["y.rb", "module Y; end"], # explicit namespace ["y/a.rb", "Y::A = 1"], ["z/a.rb", "Z::A = 1"] # implicit namespace ] with_setup(files) do assert_equal 1, X assert_equal 1, Y::A assert_equal 1, Z::A y_object_id = Y.object_id z_object_id = Z.object_id File.write("x.rb", "X = 2") File.write("y/a.rb", "Y::A = 2") File.write("z/a.rb", "Z::A = 2") loader.reload assert_equal 2, X assert_equal 2, Y::A assert_equal 2, Z::A assert Y.object_id != y_object_id assert Z.object_id != z_object_id assert_equal 2, X end end test "reloading raises if the flag is not set" do e = assert_raises(Zeitwerk::ReloadingDisabledError) do loader = Zeitwerk::Loader.new loader.setup loader.reload end assert_equal "can't reload, please call loader.enable_reloading before setup", e.message end test "if reloading is disabled, autoloading metadata shrinks while autoloading (performance test)" do on_teardown do remove_const :X delete_loaded_feature "x.rb" remove_const :Y delete_loaded_feature "y.rb" delete_loaded_feature "y/a.rb" remove_const :Z delete_loaded_feature "z/a.rb" end files = [ ["x.rb", "X = 1"], ["y.rb", "module Y; end"], ["y/a.rb", "Y::A = 1"], ["z/a.rb", "Z::A = 1"] ] with_files(files) do loader = new_loader(dirs: ".", enable_reloading: false) assert !loader.autoloads.empty? assert_equal 1, X assert_equal 1, Y::A assert_equal 1, Z::A assert loader.autoloads.empty? assert loader.to_unload.empty? end end test "if reloading is disabled, autoloading metadata shrinks while eager loading (performance test)" do on_teardown do remove_const :X delete_loaded_feature "x.rb" remove_const :Y delete_loaded_feature "y.rb" delete_loaded_feature "y/a.rb" remove_const :Z delete_loaded_feature "z/a.rb" end files = [ ["x.rb", "X = 1"], ["y.rb", "module Y; end"], ["y/a.rb", "Y::A = 1"], ["z/a.rb", "Z::A = 1"] ] with_files(files) do loader = new_loader(dirs: ".", enable_reloading: false) assert !loader.autoloads.empty? assert !Zeitwerk::Registry.autoloads.empty? loader.eager_load assert loader.autoloads.empty? assert Zeitwerk::Registry.autoloads.empty? assert loader.to_unload.empty? end end test "reloading supports deleted root directories" do files = [["a/x.rb", "X = 1"], ["b/y.rb", "Y = 1"]] with_setup(files, dirs: %w(a b)) do assert X assert Y FileUtils.rm_rf("b") loader.reload assert X end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_require_interaction.rb000066400000000000000000000134531357024011300244130ustar00rootroot00000000000000require "test_helper" require "pathname" class TestRequireInteraction < LoaderTest def assert_required(str) assert_equal true, require(str) end def assert_not_required(str) assert_equal false, require(str) end test "our decorated require returns true or false as expected" do on_teardown do remove_const :User delete_loaded_feature "user.rb" end files = [["user.rb", "class User; end"]] with_files(files) do with_load_path(".") do assert_required "user" assert_not_required "user" end end end test "our decorated require returns true or false as expected (Pathname)" do on_teardown do remove_const :User delete_loaded_feature "user.rb" end files = [["user.rb", "class User; end"]] pathname_for_user = Pathname.new("user") with_files(files) do with_load_path(".") do assert_required pathname_for_user assert_not_required pathname_for_user end end end test "autoloading makes require idempotent even with a relative path" do files = [["user.rb", "class User; end"]] with_setup(files, load_path: ".") do assert User assert_not_required "user" end end test "a required top-level file is still detected as autoloadable" do files = [["user.rb", "class User; end"]] with_setup(files, load_path: ".") do assert_required "user" loader.unload assert !Object.const_defined?(:User, false) loader.setup assert User end end test "a required top-level file is still detected as autoloadable (Pathname)" do files = [["user.rb", "class User; end"]] with_setup(files, load_path: ".") do assert_required Pathname.new("user") assert User loader.unload assert !Object.const_defined?(:User, false) loader.setup assert User end end test "a required top-level file is still detected as unloadable (require_relative)" do files = [["user.rb", "class User; end"]] with_setup(files) do assert_equal true, require_relative("../../tmp/user") loader.unload assert !Object.const_defined?(:User, false) loader.setup assert User end end test "require autovivifies as needed" do files = [ ["app/models/admin/user.rb", "class Admin::User; end"], ["app/controllers/admin/users_controller.rb", "class Admin::UsersController; end"] ] dirs = %w(app/models app/controllers) with_setup(files, dirs: dirs, load_path: dirs) do assert_required "admin/user" assert Admin::User assert Admin::UsersController loader.unload assert !Object.const_defined?(:Admin) end end test "require works well with explicit namespaces" do files = [ ["hotel.rb", "class Hotel; X = true; end"], ["hotel/pricing.rb", "class Hotel::Pricing; end"] ] with_setup(files, load_path: ".") do assert_required "hotel/pricing" assert Hotel::Pricing assert Hotel::X end end test "you can autoload yourself in a required file" do files = [ ["my_gem.rb", <<-EOS], loader = Zeitwerk::Loader.new loader.push_dir(__dir__) loader.enable_reloading loader.setup module MyGem; end EOS ["my_gem/foo.rb", "class MyGem::Foo; end"] ] with_files(files) do with_load_path(Dir.pwd) do assert_required "my_gem" end end end test "does not autovivify while loading an explicit namespace, constant is not yet defined - file first" do files = [ ["hotel.rb", <<-EOS], loader = Zeitwerk::Loader.new loader.push_dir(__dir__) loader.enable_reloading loader.setup Hotel.name class Hotel end EOS ["hotel/pricing.rb", "class Hotel::Pricing; end"] ] with_files(files) do iter = ->(dir, &block) do if dir == Dir.pwd block.call("hotel.rb") block.call("hotel") end end Dir.stub :foreach, iter do e = assert_raises(NameError) do with_load_path(Dir.pwd) do assert_required "hotel" end end assert_match %r/Hotel/, e.message end end end test "does not autovivify while loading an explicit namespace, constant is not yet defined - file last" do files = [ ["hotel.rb", <<-EOS], loader = Zeitwerk::Loader.new loader.push_dir(__dir__) loader.enable_reloading loader.setup Hotel.name class Hotel end EOS ["hotel/pricing.rb", "class Hotel::Pricing; end"] ] with_files(files) do iter = ->(dir, &block) do if dir == Dir.pwd block.call("hotel") block.call("hotel.rb") end end Dir.stub :foreach, iter do e = assert_raises(NameError) do with_load_path(Dir.pwd) do assert_required "hotel" end end assert_match %r/Hotel/, e.message end end end test "symlinks in autoloaded files set by Zeitwerk" do files = [["real/app/models/user.rb", "class User; end"]] with_files(files) do FileUtils.ln_s("real", "symlink") loader.push_dir("symlink/app/models") loader.setup with_load_path("symlink/app/models") do assert User assert_not_required "user" loader.reload assert_required "user" end end end test "symlinks in autoloaded files resolved by Ruby" do files = [["real/app/models/user.rb", "class User; end"]] with_files(files) do FileUtils.ln_s("real", "symlink") loader.push_dir("symlink/app/models") loader.setup with_load_path("symlink/app/models") do assert_required "user" loader.reload assert_required "user" end end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_ruby_compatibility.rb000066400000000000000000000220371357024011300242500ustar00rootroot00000000000000require "test_helper" require "pathname" class TestRubyCompatibility < LoaderTest # We decorate Kernel#require in lib/zeitwerk/kernel.rb to be able to log # autoloads and to record what has been autoloaded so far. test "autoload calls Kernel#require" do files = [["x.rb", "X = true"]] with_files(files) do loader.push_dir(".") loader.setup $trc_require_has_been_called = false $trc_autoload_path = File.expand_path("x.rb") begin Kernel.module_eval do alias_method :trc_original_require, :require def require(path) $trc_require_has_been_called = true if path == $trc_autoload_path trc_original_require(path) end end assert X assert $trc_require_has_been_called ensure Kernel.module_eval do remove_method :require define_method :require, instance_method(:trc_original_require) remove_method :trc_original_require end end end end # Zeitwerk has to be called as soon as explicit namespaces are defined, to be # able to configure autoloads for their children before the class or module # body is interpreted. If explicit namespaces are found, Zeitwerk sets a trace # point on the :class event with that purpose. # # This is key because the body could reference child constants at the # top-level, mixins are a common use case. test "TracePoint emits :class events" do on_teardown do @tp.disable remove_const :C, from: self.class end called = false @tp = TracePoint.new(:class) { called = true } @tp.enable class C; end assert called end # We configure autoloads on directories to autovivify modules on demand, and # lazily descend to set autoloads for their children. This is more efficient, # specially for large code bases. test "you can set autoloads on directories" do files = ["admin/users_controller.rb", "class UsersController; end"] with_setup(files) do assert_equal "#{Dir.pwd}/admin", Object.autoload?(:Admin) end end # While unloading constants we leverage this property to avoid lookups in # $LOADED_FEATURES for strings that we know are not going to be there. test "directories are not included in $LOADED_FEATURES" do with_files([]) do FileUtils.mkdir("admin") loader.push_dir(".") loader.setup assert Admin assert !$LOADED_FEATURES.include?(File.realpath("admin")) end end # We exploit this one to simplify the detection of explicit namespaces. # # Let's suppose `Admin` is an explicit namespace and scanning finds first a # directory called `admin`. We set at that point an autoload for `Admin` and # that will require that directory. If later on, scanning finds `admin.rb`, we # just set the autoload again, and change the target file. # # This way, we do not need to keep state or do an a posteriori pass, can set # autoloads linearly as scanning progresses. test "an autoload can be overridden" do on_teardown { remove_const :X } files = [ ["x0/x.rb", "X = 0"], ["x1/x.rb", "X = 1"] ] with_files(files) do Object.autoload(:X, File.expand_path("x0/x.rb")) Object.autoload(:X, File.expand_path("x1/x.rb")) assert_equal 1, X end end # I believe Zeitwerk does not exploit this one now. Let's leave it here to # keep track of undocumented corner cases anyway. test "const_defined? is true for autoloads and does not load the file, if the file exists" do on_teardown { remove_const :X } files = [["x.rb", "$const_defined_does_not_trigger_autoload = false; X = true"]] with_files(files) do $const_defined_does_not_trigger_autoload = true Object.autoload(:X, File.expand_path("x.rb")) assert Object.const_defined?(:X, false) assert $const_defined_does_not_trigger_autoload end end # Unloading removes autoloads by calling remove_const. It is convenient that # remove_const does not execute the autoload because it would be surprising, # and slower, that those unused files got loaded precisely while unloading. test "remove_const does not trigger an autoload" do files = [["x.rb", "$remove_const_does_not_trigger_autoload = false; X = 1"]] with_files(files) do $remove_const_does_not_trigger_autoload = true Object.autoload(:X, File.expand_path("x.rb")) remove_const :X assert $remove_const_does_not_trigger_autoload end end # Zeitwerk uses this property when unloading to be able to differentiate when # it is removing and autoload, and when it is unloading an actual loaded # object. test "autoloading removes the autoload configuration in the parent" do on_teardown do remove_const :X delete_loaded_feature "x.rb" end files = [["x.rb", "X = true"]] with_files(files) do Object.autoload(:X, File.expand_path("x.rb")) assert Object.autoload?(:X) assert X assert !Object.autoload?(:X) end end # We use remove_const to delete autoload configurations while unloading. # Otherwise, the configured files or directories could become stale. test "autoload configuration can be deleted with remove_const" do files = [["x.rb", "X = true"]] with_files(files) do Object.autoload(:X, File.expand_path("x.rb")) assert Object.autoload?(:X) remove_const :X assert !Object.autoload?(:X) end end # Thanks to this the code that unloads can just blindly issue remove_const # calls without catching exceptions. test "remove_const works on constants with an autoload even if the file did not define them" do on_teardown do remove_const :Foo remove_const :NOT_FOO delete_loaded_feature "foo.rb" end files = [["foo.rb", "NOT_FOO = 1"]] with_files(files) do with_load_path(Dir.pwd) do begin Object.autoload(:Foo, "foo") assert_raises(NameError) { Foo } end end end end # This edge case justifies the need for the inceptions collection in the # registry. test "an autoload on yourself is ignored" do files = [["foo.rb", <<-EOS]] Object.autoload(:Foo, __FILE__) $trc_inception = !Object.autoload?(:Foo) Foo = 1 EOS with_files(files) do loader.push_dir(".") loader.setup with_load_path do $trc_inception = false require "foo" end assert $trc_inception end end # Same as above, adding some depth. test "an autoload on a file being required at some point up in the call chain is also ignored" do files = [ ["foo.rb", <<-EOS], require 'bar' Foo = 1 EOS ["bar.rb", <<-EOS] Bar = true Object.autoload(:Foo, File.realpath('foo.rb')) $trc_inception = !Object.autoload?(:Foo) EOS ] with_files(files) do loader.push_dir(".") loader.setup with_load_path do $trc_inception = false require "foo" end assert $trc_inception end end # This is why we issue a lazy_subdirs.delete call in the tracer block, to # ignore events triggered by reopenings. test "tracing :class calls you back on creation and on reopening" do on_teardown do @tracer.disable remove_const :C, from: self.class remove_const :M, from: self.class end traced = [] @tracer = TracePoint.trace(:class) do |tp| traced << tp.self end 2.times do class C; end module M; end end assert_equal [C, M, C, M], traced end # Computing hash codes is costly and we want the tracer to be as efficient as # possible. The callback doesn't short-circuit anonymous classes and modules # because Class.new and Module.new do not trigger it, but if in the future # they do we could benchmark if we should change event.self.name before the # deletion call. test "trace points on the :class events don't get called on Class.new and Module.new" do on_teardown { @tracer.disable } $tracer_for_anonymous_class_and_modules_called = false @tracer = TracePoint.trace(:class) { $tracer_for_anonymous_class_and_modules_called = true } Class.new Module.new assert !$tracer_for_anonymous_class_and_modules_called end # If the user issues a require call with a Pathname object for a path that is # autoloadable, we are able to autoload because $LOADED_FEATURES.last returns # the real path as a string and loader_for is able to find its loader. During # unloading, we find and delete strings in $LOADED_FEATURES too. # # This is not a hard requirement, we could work around it if $LOADED_FEATURES # stored pathnames. But the code is simpler if this property holds. test "required pathnames end up as strings in $LOADED_FEATURES" do on_teardown do remove_const :X $LOADED_FEATURES.pop end files = [["x.rb", "X = 1"]] with_files(files) do with_load_path(".") do assert_equal true, require(Pathname.new("x")) assert_equal 1, X assert_equal File.realpath("x.rb"), $LOADED_FEATURES.last end end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_shadowed.rb000066400000000000000000000016411357024011300221320ustar00rootroot00000000000000require "test_helper" class TestShadowed < LoaderTest test "does not autoload from a shadowed file" do on_teardown { remove_const :X } ::X = 1 files = [["x.rb", "X = 2"]] with_setup(files) do assert_equal 1, ::X loader.reload assert_equal 1, ::X end end test "autoloads from a shadowed implicit namespace" do on_teardown { remove_const :M } mod = Module.new ::M = mod files = [["m/x.rb", "M::X = true"]] with_setup(files) do assert M::X loader.reload assert_same mod, M assert M::X end end test "autoloads from a shadowed explicit namespace" do on_teardown { remove_const :M } mod = Module.new ::M = mod files = [ ["m.rb", "class M; end"], ["m/x.rb", "M::X = true"] ] with_setup(files) do assert M::X loader.reload assert_same mod, M assert M::X end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_sti_old_school_workaround.rb000066400000000000000000000031371357024011300256150ustar00rootroot00000000000000require "test_helper" # Rails applications are expected to preload tree leafs in STIs. Using requires # is the old school way to address this and it is somewhat tricky. Let's have a # test to make sure the circularity works. class TestOldSchoolWorkaroundSTI < LoaderTest def files [ ["a.rb", <<-EOS], class A require 'b' end $test_sti_loaded << 'A' EOS ["b.rb", <<-EOS], class B < A require 'c' end $test_sti_loaded << 'B' EOS ["c.rb", <<-EOS], class C < B require 'd1' require 'd2' end $test_sti_loaded << 'C' EOS ["d1.rb", "class D1 < C; end; $test_sti_loaded << 'D1'"], ["d2.rb", "class D2 < C; end; $test_sti_loaded << 'D2'"] ] end def with_setup original_verbose = $VERBOSE $VERBOSE = nil # To avoid circular require warnings. $test_sti_loaded = [] super(files, load_path: ".") do yield end ensure $VERBOSE = original_verbose end def assert_all_loaded assert_equal %w(A B C D1 D2), $test_sti_loaded.sort end test "loading the root loads everything" do with_setup do assert A assert_all_loaded end end test "loading a root child loads everything" do with_setup do assert B assert_all_loaded end end test "loading an intermediate descendant loads everything" do with_setup do assert C assert_all_loaded end end test "loading a leaf loads everything" do with_setup do assert D1 assert_all_loaded end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_top_level.rb000066400000000000000000000033341357024011300223260ustar00rootroot00000000000000require "test_helper" class TestTopLevel < LoaderTest test "autoloads a simple constant in a top-level file" do files = [["x.rb", "X = true"]] with_setup(files) do assert X end end test "autoloads a simple class in a top-level file" do files = [["app/models/user.rb", "class User; end"]] with_setup(files, dirs: "app/models") do assert User end end test "autoloads several top-level classes" do files = [ ["app/models/user.rb", "class User; end"], ["app/controllers/users_controller.rb", "class UsersController; User; end"] ] with_setup(files, dirs: %w(app/models app/controllers)) do assert UsersController end end test "autoloads only the first of multiple occurrences" do files = [ ["app/models/user.rb", "User = :model"], ["app/decorators/user.rb", "User = :decorator"], ] with_setup(files, dirs: %w(app/models app/decorators)) do assert_equal :model, User end end test "anything other than Ruby and visible directories is ignored" do files = [ ["x.txt", ""], # Programmer notes ["x.lua", ""], # Lua files for Redis ["x.yaml", ""], # Included configuration ["x.json", ""], # Included configuration ["x.erb", ""], # Included template ["x.jpg", ""], # Included image ["x.rb~", ""], # Emacs auto backup ["#x.rb#", ""], # Emacs auto save [".filename.swp", ""], # Vim swap file ["4913", ""], # May be created by Vim [".idea/workspace.xml", ""] # RubyMine ] with_setup(files) do assert_empty loader.autoloads end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_unload.rb000066400000000000000000000062141357024011300216170ustar00rootroot00000000000000require "test_helper" class TestUnload < LoaderTest test "unload removes all autoloaded constants" do files = [ ["user.rb", "class User; end"], ["admin/root.rb", "class Admin::Root; end"] ] with_setup(files) do assert User assert Admin::Root admin = Admin loader.unload assert !Object.const_defined?(:User) assert !Object.const_defined?(:Admin) assert !admin.const_defined?(:Root) end end test "unload removes autoloaded constants, even if #name is overridden" do files = [["x.rb", <<~RUBY]] module X def self.name "Y" end end RUBY with_setup(files) do assert X loader.unload assert !Object.const_defined?(:X) end end test "unload removes non-executed autoloads" do files = [["x.rb", "X = true"]] with_setup(files) do # This does not autolaod, see the compatibility test. assert Object.const_defined?(:X) loader.unload assert !Object.const_defined?(:X) end end test "unload clears internal caches" do files = [ ["app/user.rb", "class User; end"], ["app/api/v1/users_controller.rb", "class Api::V1::UsersController; end"], ["app/admin/root.rb", "class Admin::Root; end"], ["lib/user.rb", "class User; end"] ] with_setup(files, dirs: %w(app lib)) do assert User assert Api::V1::UsersController assert !loader.autoloads.empty? assert !loader.autoloaded_dirs.empty? assert !loader.to_unload.empty? assert !loader.lazy_subdirs.empty? loader.unload assert loader.autoloads.empty? assert loader.autoloaded_dirs.empty? assert loader.to_unload.empty? assert loader.lazy_subdirs.empty? end end test "unload does not assume autoloaded constants are still there" do files = [["x.rb", "X = true"]] with_setup(files) do assert X assert remove_const(:X) # user removed the constant by hand loader.unload # should not raise end end test "already existing namespaces are not reset" do on_teardown do remove_const :ActiveStorage delete_loaded_feature "active_storage.rb" end files = [ ["lib/active_storage.rb", "module ActiveStorage; end"], ["app/models/active_storage/blob.rb", "class ActiveStorage::Blob; end"] ] with_files(files) do with_load_path("lib") do require "active_storage" loader.push_dir("app/models") loader.setup assert ActiveStorage::Blob loader.unload assert ActiveStorage end end end test "unload clears explicit namespaces associated" do files = [ ["a/m.rb", "module M; end"], ["a/m/n.rb", "M::N = true"], ["b/x.rb", "module X; end"], ["b/x/y.rb", "X::Y = true"], ] with_files(files) do la = new_loader(dirs: "a") assert Zeitwerk::ExplicitNamespace.cpaths["M"] == la lb = new_loader(dirs: "b") assert Zeitwerk::ExplicitNamespace.cpaths["X"] == lb la.unload assert_nil Zeitwerk::ExplicitNamespace.cpaths["M"] assert Zeitwerk::ExplicitNamespace.cpaths["X"] == lb end end end zeitwerk-2.2.2/test/lib/zeitwerk/test_unloadable_cpath.rb000066400000000000000000000033561357024011300236260ustar00rootroot00000000000000require "test_helper" require "set" class TestUnloadableCpath < LoaderTest test "a loader that has loading nothing, has nothing to unload" do files = [["x.rb", "X = true"]] with_setup(files) do assert_empty loader.unloadable_cpaths assert !loader.unloadable_cpath?("X") end end test "a loader that loaded some stuff has that stuff to be unloaded if reloading is enabled" do files = [ ["m/x.rb", "M::X = true"], ["m/y.rb", "M::Y = true"], ["z.rb", "Z = true"] ] with_setup(files) do assert M::X assert_equal %w(M M::X), loader.unloadable_cpaths assert loader.unloadable_cpath?("M") assert loader.unloadable_cpath?("M::X") assert !loader.unloadable_cpath?("M::Y") assert !loader.unloadable_cpath?("Z") end end test "unloadable_cpaths returns actual constant paths even if #name is overridden" do files = [["m.rb", <<~RUBY], ["m/c.rb", "M::C = true"]] module M def self.name "X" end end RUBY with_setup(files) do assert M::C assert loader.unloadable_cpath?("M::C") end end test "a loader that loaded some stuff has nothing to unload if reloading is disabled" do on_teardown do remove_const :M delete_loaded_feature "m/x.rb" delete_loaded_feature "m/y.rb" remove_const :Z delete_loaded_feature "z.rb" end files = [ ["m/x.rb", "M::X = true"], ["m/y.rb", "M::Y = true"], ["z.rb", "Z = true"] ] with_files(files) do loader = new_loader(dirs: ".", enable_reloading: false) assert M::X assert M::Y assert Z assert_empty loader.unloadable_cpaths assert loader.to_unload.empty? end end end zeitwerk-2.2.2/test/support/000077500000000000000000000000001357024011300160505ustar00rootroot00000000000000zeitwerk-2.2.2/test/support/delete_loaded_feature.rb000066400000000000000000000002331357024011300226600ustar00rootroot00000000000000module DeleteLoadedFeature def delete_loaded_feature(path) $LOADED_FEATURES.delete_if do |realpath| realpath.end_with?(path) end end end zeitwerk-2.2.2/test/support/loader_test.rb000066400000000000000000000037301357024011300207050ustar00rootroot00000000000000class LoaderTest < Minitest::Test TMP_DIR = File.expand_path("../tmp", __dir__) attr_reader :loader def setup @loader = new_loader(setup: false) end # We enable reloading in the reloaders of the test suite to have a robust # cleanup of constants. # # There are gems that allow you to run tests in forked processes and you do # not need to care, but JRuby does not support forking, and I prefer to be # ready for the day in which Zeitwerk runs on JRuby. def new_loader(dirs: [], enable_reloading: true, setup: true) Zeitwerk::Loader.new.tap do |loader| Array(dirs).each do |dir| loader.push_dir(dir) end loader.enable_reloading if enable_reloading loader.setup if setup end end def reset_constants Zeitwerk::Registry.loaders.each(&:unload) end def reset_registry Zeitwerk::Registry.loaders.clear Zeitwerk::Registry.loaders_managing_gems.clear end def reset_explicit_namespace Zeitwerk::ExplicitNamespace.cpaths.clear Zeitwerk::ExplicitNamespace.tracer.disable end def teardown reset_constants reset_registry reset_explicit_namespace end def mkdir_test FileUtils.rm_rf(TMP_DIR) FileUtils.mkdir_p(TMP_DIR) end def with_files(files, rm: true) mkdir_test Dir.chdir(TMP_DIR) do files.each do |fname, contents| FileUtils.mkdir_p(File.dirname(fname)) File.write(fname, contents) end begin yield ensure mkdir_test if rm end end end def with_load_path(dirs = loader.dirs) Array(dirs).each { |dir| $LOAD_PATH.push(dir) } yield ensure Array(dirs).each { |dir| $LOAD_PATH.delete(dir) } end def with_setup(files, dirs: ".", load_path: nil, rm: true) with_files(files, rm: rm) do Array(dirs).each { |dir| loader.push_dir(dir) } loader.setup if load_path with_load_path(load_path) { yield } else yield end end end end zeitwerk-2.2.2/test/support/on_teardown.rb000066400000000000000000000001721357024011300207140ustar00rootroot00000000000000module OnTeardown def on_teardown define_singleton_method(:teardown) do yield super() end end end zeitwerk-2.2.2/test/support/remove_const.rb000066400000000000000000000001511357024011300210750ustar00rootroot00000000000000module RemoveConst def remove_const(cname, from: Object) from.send(:remove_const, cname) end end zeitwerk-2.2.2/test/support/test_macro.rb000066400000000000000000000002321357024011300205320ustar00rootroot00000000000000module TestMacro def test(description, &block) method_name = "test_#{description}".gsub(/\W/, "_") define_method(method_name, &block) end end zeitwerk-2.2.2/test/test_helper.rb000066400000000000000000000007101357024011300171750ustar00rootroot00000000000000require "minitest/autorun" require "minitest/focus" require "minitest/reporters" Minitest::Reporters.use!(Minitest::Reporters::DefaultReporter.new) require "zeitwerk" require "support/test_macro" require "support/delete_loaded_feature" require "support/loader_test" require "support/remove_const" require "support/on_teardown" Minitest::Test.class_eval do extend TestMacro include DeleteLoadedFeature include RemoveConst include OnTeardown end zeitwerk-2.2.2/zeitwerk.gemspec000066400000000000000000000013741357024011300165730ustar00rootroot00000000000000require_relative "lib/zeitwerk/version" Gem::Specification.new do |spec| spec.name = "zeitwerk" spec.summary = "Efficient and thread-safe constant autoloader" spec.description = <<-EOS Zeitwerk implements constant autoloading with Ruby semantics. Each gem and application may have their own independent autoloader, with its own configuration, inflector, and logger. Supports autoloading, preloading, reloading, and eager loading. EOS spec.author = "Xavier Noria" spec.email = 'fxn@hashref.com' spec.license = "MIT" spec.homepage = "https://github.com/fxn/zeitwerk" spec.files = Dir["README.md", "MIT-LICENSE", "lib/**/*.rb"] spec.version = Zeitwerk::VERSION spec.required_ruby_version = ">= 2.4.4" end