zeitwerk 2.4.2 → 2.5.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f45a7c3caf48f06e10dd513a7b074da7ec059fa3299751d361514aadc83d995e
4
- data.tar.gz: c8c29793e95245b47b1283891791d2c20365b8c694b3dcdfb7daab0b74c16bdb
3
+ metadata.gz: 4012f73de9387546f477e8254668d450c00310666bde922b90a6b2be8d4b9740
4
+ data.tar.gz: 7c403df9ea53494c2eb1a01b3fb81f70ce1a6488a420728f134c7c32ab4cb444
5
5
  SHA512:
6
- metadata.gz: 6194d326b268c9333ed9d2ac1c62b65756fa9af844c5fb7ad8272ecec33aa439015506758bcd329e421508facb4153e04e45da0efb0a2a42001a6f2e0a0d3b6a
7
- data.tar.gz: 656009f40e777f641ff1dc80f54b63559514439538c73965aa9226b6c3e33c6be71c07eb30adfe7f4cd4b3be72aa6e42918392ba27167fb9502b9aed037f36d3
6
+ metadata.gz: f2222619050df7f4271a73b2abdc258d541249ed34a75738afb1661ba17460dc2a0c53690f33a85e33b5925e28a6e2d292fa9d12711722261ae517d6d38f7520
7
+ data.tar.gz: 77679e246700480f751e0bce8373d5fe648a78f41a2b8bd97cc0d9c0bdff52651bd64be59ad56809b2785539fd7439f3b9ad554d9ddb8f668fbca9b187d51a24
data/README.md CHANGED
@@ -3,45 +3,56 @@
3
3
 
4
4
 
5
5
  [![Gem Version](https://img.shields.io/gem/v/zeitwerk.svg?style=for-the-badge)](https://rubygems.org/gems/zeitwerk)
6
- [![Build Status](https://img.shields.io/travis/com/fxn/zeitwerk/master?style=for-the-badge)](https://travis-ci.com/fxn/zeitwerk)
6
+ [![Build Status](https://img.shields.io/github/workflow/status/fxn/zeitwerk/CI?event=push&style=for-the-badge)](https://github.com/fxn/zeitwerk/actions?query=event%3Apush)
7
7
 
8
8
  <!-- TOC -->
9
9
 
10
10
  - [Introduction](#introduction)
11
11
  - [Synopsis](#synopsis)
12
12
  - [File structure](#file-structure)
13
- - [Implicit namespaces](#implicit-namespaces)
14
- - [Explicit namespaces](#explicit-namespaces)
15
- - [Collapsing directories](#collapsing-directories)
13
+ - [The idea: File paths match constant paths](#the-idea-file-paths-match-constant-paths)
14
+ - [Inner simple constants](#inner-simple-constants)
15
+ - [Root directories and root namespaces](#root-directories-and-root-namespaces)
16
+ - [The default root namespace is `Object`](#the-default-root-namespace-is-object)
17
+ - [Custom root namespaces](#custom-root-namespaces)
16
18
  - [Nested root directories](#nested-root-directories)
19
+ - [Implicit namespaces](#implicit-namespaces)
20
+ - [Explicit namespaces](#explicit-namespaces)
21
+ - [Collapsing directories](#collapsing-directories)
17
22
  - [Usage](#usage)
18
- - [Setup](#setup)
19
- - [Generic](#generic)
20
- - [for_gem](#for_gem)
21
- - [Autoloading](#autoloading)
22
- - [Eager loading](#eager-loading)
23
- - [Reloading](#reloading)
24
- - [Inflection](#inflection)
25
- - [Zeitwerk::Inflector](#zeitwerkinflector)
26
- - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
27
- - [Custom inflector](#custom-inflector)
23
+ - [Setup](#setup)
24
+ - [Generic](#generic)
25
+ - [for_gem](#for_gem)
26
+ - [Autoloading](#autoloading)
27
+ - [Eager loading](#eager-loading)
28
+ - [Reloading](#reloading)
29
+ - [Inflection](#inflection)
30
+ - [Zeitwerk::Inflector](#zeitwerkinflector)
31
+ - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
32
+ - [Custom inflector](#custom-inflector)
33
+ - [Callbacks](#callbacks)
34
+ - [The on_setup callback](#the-on_setup-callback)
28
35
  - [The on_load callback](#the-on_load-callback)
29
- - [Logging](#logging)
30
- - [Loader tag](#loader-tag)
31
- - [Ignoring parts of the project](#ignoring-parts-of-the-project)
32
- - [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
33
- - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
34
- - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
35
- - [Edge cases](#edge-cases)
36
- - [Reopening third-party namespaces](#reopening-third-party-namespaces)
37
- - [Rules of thumb](#rules-of-thumb)
38
- - [Debuggers](#debuggers)
39
- - [Break](#break)
40
- - [Byebug](#byebug)
36
+ - [The on_unload callback](#the-on_unload-callback)
37
+ - [Technical details](#technical-details)
38
+ - [Logging](#logging)
39
+ - [Loader tag](#loader-tag)
40
+ - [Ignoring parts of the project](#ignoring-parts-of-the-project)
41
+ - [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
42
+ - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
43
+ - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
44
+ - [Edge cases](#edge-cases)
45
+ - [Reopening third-party namespaces](#reopening-third-party-namespaces)
46
+ - [Rules of thumb](#rules-of-thumb)
47
+ - [Debuggers](#debuggers)
48
+ - [Break](#break)
49
+ - [Byebug](#byebug)
41
50
  - [Pronunciation](#pronunciation)
42
51
  - [Supported Ruby versions](#supported-ruby-versions)
43
52
  - [Testing](#testing)
44
53
  - [Motivation](#motivation)
54
+ - [Kernel#require is brittle](#kernelrequire-is-brittle)
55
+ - [Rails autoloading was brittle](#rails-autoloading-was-brittle)
45
56
  - [Thanks](#thanks)
46
57
  - [License](#license)
47
58
 
@@ -117,6 +128,9 @@ Zeitwerk::Loader.eager_load_all
117
128
  <a id="markdown-file-structure" name="file-structure"></a>
118
129
  ## File structure
119
130
 
131
+ <a id="markdown-the-idea-file-paths-match-constant-paths" name="the-idea-file-paths-match-constant-paths"></a>
132
+ ### The idea: File paths match constant paths
133
+
120
134
  To have a file structure Zeitwerk can work with, just name files and directories after the name of the classes and modules they define:
121
135
 
122
136
  ```
@@ -126,25 +140,67 @@ lib/my_gem/bar_baz.rb -> MyGem::BarBaz
126
140
  lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
127
141
  ```
128
142
 
129
- Every directory configured with `push_dir` acts as root namespace. There can be several of them. For example, given
143
+ You can tune that a bit by [collapsing directories](#collapsing-directories), or by [ignoring parts of the project](#ignoring-parts-of-the-project), but that is the main idea.
144
+
145
+ <a id="markdown-inner-simple-constants" name="inner-simple-constants"></a>
146
+ ### Inner simple constants
147
+
148
+ While a simple constant like `HttpCrawler::MAX_RETRIES` can be defined in its own file:
130
149
 
131
150
  ```ruby
132
- loader.push_dir(Rails.root.join("app/models"))
133
- loader.push_dir(Rails.root.join("app/controllers"))
151
+ # http_crawler/max_retries.rb
152
+ HttpCrawler::MAX_RETRIES = 10
134
153
  ```
135
154
 
136
- Zeitwerk understands that their respective files and subdirectories belong to the root namespace:
155
+ that is not required, you can also define it the regular way:
137
156
 
157
+ ```ruby
158
+ # http_crawler.rb
159
+ class HttpCrawler
160
+ MAX_RETRIES = 10
161
+ end
138
162
  ```
139
- app/models/user.rb -> User
140
- app/controllers/admin/users_controller.rb -> Admin::UsersController
163
+
164
+ The first example needs a custom [inflection](https://github.com/fxn/zeitwerk#inflection) rule:
165
+
166
+ ```ruby
167
+ loader.inflector.inflect("max_retries" => "MAX_RETRIES")
168
+ ```
169
+
170
+ Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
171
+
172
+ In the second example, no custom rule is needed.
173
+
174
+ <a id="markdown-root-directories-and-root-namespaces" name="root-directories-and-root-namespaces"></a>
175
+ ### Root directories and root namespaces
176
+
177
+ Every directory configured with `push_dir` is called a _root directory_, and they represent _root namespaces_.
178
+
179
+ <a id="markdown-the-default-root-namespace-is-object" name="the-default-root-namespace-is-object"></a>
180
+ #### The default root namespace is `Object`
181
+
182
+ By default, the namespace associated to a root directory is the top-level one: `Object`.
183
+
184
+ For example, given
185
+
186
+ ```ruby
187
+ loader.push_dir("#{__dir__}/models")
188
+ loader.push_dir("#{__dir__}/serializers"))
141
189
  ```
142
190
 
143
- Alternatively, you can associate a custom namespace to a root directory by passing a class or module object in the optional `namespace` keyword argument.
191
+ these are the expected classes and modules being defined by these files:
144
192
 
145
- For example, Active Job queue adapters have to define a constant after their name in `ActiveJob::QueueAdapters`.
193
+ ```
194
+ models/user.rb -> User
195
+ serializers/user_serializer.rb -> UserSerializer
196
+ ```
197
+
198
+ <a id="markdown-custom-root-namespaces" name="custom-root-namespaces"></a>
199
+ #### Custom root namespaces
200
+
201
+ While `Object` is by far the most common root namespace, you can associate a different one to a particular root directory. The method `push_dir` accepts a class or module object in the optional `namespace` keyword argument.
146
202
 
147
- So, if you declare
203
+ For example, given:
148
204
 
149
205
  ```ruby
150
206
  require "active_job"
@@ -152,9 +208,26 @@ require "active_job/queue_adapters"
152
208
  loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
153
209
  ```
154
210
 
155
- your adapter can be stored directly in that directory instead of the canonical `#{__dir__}/active_job/queue_adapters`.
211
+ a file defining `ActiveJob::QueueAdapters::MyQueueAdapter` does not need the conventional parent directories, you can (and have to) store the file directly below `adapters`:
212
+
213
+ ```
214
+ adapters/my_queue_adapter.rb -> ActiveJob::QueueAdapters::MyQueueAdapter
215
+ ```
216
+
217
+ Please, note that the given root namespace must be non-reloadable, though autoloaded constants in that namespace can be. That is, if you associate `app/api` with an existing `Api` module, that module should not be reloadable. However, if the project defines and autoloads the class `Api::Deliveries`, that one can be reloaded.
218
+
219
+ <a id="markdown-nested-root-directories" name="nested-root-directories"></a>
220
+ #### Nested root directories
156
221
 
157
- Please, note that the given namespace must be non-reloadable, though autoloaded constants in that namespace can be. That is, if you associate `app/api` with an existing `Api` module, that module should not be reloadable. However, if the project defines and autoloads the class `Api::V2::Deliveries`, that one can be reloaded.
222
+ 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.
223
+
224
+ 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:
225
+
226
+ ```
227
+ app/models/concerns/geolocatable.rb
228
+ ```
229
+
230
+ should define `Geolocatable`, not `Concerns::Geolocatable`.
158
231
 
159
232
  <a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
160
233
  ### Implicit namespaces
@@ -216,19 +289,6 @@ To illustrate usage of glob patterns, if `actions` in the example above is part
216
289
  loader.collapse("#{__dir__}/*/actions")
217
290
  ```
218
291
 
219
- <a id="markdown-nested-root-directories" name="nested-root-directories"></a>
220
- ### Nested root directories
221
-
222
- 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.
223
-
224
- 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:
225
-
226
- ```
227
- app/models/concerns/geolocatable.rb
228
- ```
229
-
230
- should define `Geolocatable`, not `Concerns::Geolocatable`.
231
-
232
292
  <a id="markdown-usage" name="usage"></a>
233
293
  ## Usage
234
294
 
@@ -387,11 +447,13 @@ On reloading, client code has to update anything that would otherwise be storing
387
447
  <a id="markdown-inflection" name="inflection"></a>
388
448
  ### Inflection
389
449
 
390
- 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.
450
+ 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, and you can define your own.
391
451
 
392
452
  <a id="markdown-zeitwerkinflector" name="zeitwerkinflector"></a>
393
453
  #### Zeitwerk::Inflector
394
454
 
455
+ Each loader instantiated with `Zeitwerk::Loader.new` has an inflector of this type by default.
456
+
395
457
  This is a very basic inflector that converts snake case to camel case:
396
458
 
397
459
  ```
@@ -418,16 +480,16 @@ loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
418
480
 
419
481
  Overrides need to be configured before calling `setup`.
420
482
 
421
- There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
422
-
423
- Loaders instantiated with `Zeitwerk::Loader.new` have an inflector of this type, independent of each other.
483
+ The inflectors of different loaders are independent of each other. There are no global inflection rules or global configuration that can affect this inflector. It is deterministic.
424
484
 
425
485
  <a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
426
486
  #### Zeitwerk::GemInflector
427
487
 
488
+ Each loader instantiated with `Zeitwerk::Loader.for_gem` has an inflector of this type by default.
489
+
428
490
  This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
429
491
 
430
- Loaders instantiated with `Zeitwerk::Loader.for_gem` have an inflector of this type, independent of each other.
492
+ The inflectors of different loaders are independent of each other. There are no global inflection rules or global configuration that can affect this inflector. It is deterministic.
431
493
 
432
494
  <a id="markdown-custom-inflector" name="custom-inflector"></a>
433
495
  #### Custom inflector
@@ -503,8 +565,26 @@ class MyGem::Inflector < Zeitwerk::GemInflector
503
565
  end
504
566
  ```
505
567
 
568
+ <a id="markdown-callbacks" name="callbacks"></a>
569
+ ### Callbacks
570
+
571
+ <a id="markdown-the-on_setup-callback" name="the-on_setup-callback"></a>
572
+ #### The on_setup callback
573
+
574
+ The `on_setup` callback is fired on setup and on each reload:
575
+
576
+ ```ruby
577
+ loader.on_setup do
578
+ # Ready to autoload here.
579
+ end
580
+ ```
581
+
582
+ Multiple `on_setup` callbacks are supported, and they run in order of definition.
583
+
584
+ If `setup` was already executed, the callback is fired immediately.
585
+
506
586
  <a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
507
- ### The on_load callback
587
+ #### The on_load callback
508
588
 
509
589
  The usual place to run something when a file is loaded is the file itself. However, sometimes you'd like to be called, and this is possible with the `on_load` callback.
510
590
 
@@ -522,26 +602,23 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
522
602
 
523
603
  ```ruby
524
604
  # config/environments/development.rb
525
- loader.on_load("SomeApiClient") do
526
- SomeApiClient.endpoint = "https://api.dev"
605
+ loader.on_load("SomeApiClient") do |klass, _abspath|
606
+ klass.endpoint = "https://api.dev"
527
607
  end
528
608
 
529
609
  # config/environments/production.rb
530
- loader.on_load("SomeApiClient") do
531
- SomeApiClient.endpoint = "https://api.prod"
610
+ loader.on_load("SomeApiClient") do |klass, _abspath|
611
+ klass.endpoint = "https://api.prod"
532
612
  end
533
613
  ```
534
614
 
535
- Uses cases:
615
+ Some uses cases:
536
616
 
537
- * Doing something with an autoloadable class or module in a Rails application during initialization, in a way that plays well with reloading. As in the previous example.
617
+ * Doing something with a reloadable class or module in a Rails application during initialization, in a way that plays well with reloading. As in the previous example.
538
618
  * Delaying the execution of the block until the class is loaded for performance.
539
619
  * Delaying the execution of the block until the class is loaded because it follows the adapter pattern and better not to load the class if the user does not need it.
540
- * Etc.
541
620
 
542
- However, let me stress that the easiest way to accomplish that is to write whatever you have to do in the actual target file. `on_load` use cases are edgy, use it only if appropriate.
543
-
544
- `on_load` receives the name of the target class or module as a string. The given block is executed every time its corresponding file is loaded. That includes reloads.
621
+ `on_load` gets a target constant path as a string (e.g., "User", or "Service::NotificationsGateway"). When fired, its block receives the stored value, and the absolute path to the corresponding file or directory as a string. The callback is executed every time the target is loaded. That includes reloads.
545
622
 
546
623
  Multiple callbacks on the same target are supported, and they run in order of definition.
547
624
 
@@ -549,6 +626,66 @@ The block is executed once the loader has loaded the target. In particular, if t
549
626
 
550
627
  Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
551
628
 
629
+ It is also possible to be called when any constant managed by the loader is loaded:
630
+
631
+ ```ruby
632
+ loader.on_load do |cpath, value, abspath|
633
+ # ...
634
+ end
635
+ ```
636
+
637
+ The block gets the constant path as a string (e.g., "User", or "Foo::VERSION"), the value it stores (e.g., the class object stored in `User`, or "2.5.0"), and the absolute path to the corresponding file or directory as a string.
638
+
639
+ Multiple callbacks like these are supported, and they run in order of definition.
640
+
641
+ There are use cases for this last catch-all callback, but they are rare. If you just need to understand how things are being loaded for debugging purposes, please remember that `Zeitwerk::Loader#log!` logs plenty of information.
642
+
643
+ If both types of callbacks are defined, the specific ones run first.
644
+
645
+ <a id="markdown-the-on_unload-callback" name="the-on_unload-callback"></a>
646
+ #### The on_unload callback
647
+
648
+ When reloading is enabled, you may occasionally need to execute something before a certain autoloaded class or module is unloaded. The `on_unload` callback allows you to do that.
649
+
650
+ For example, let's imagine that a `Country` class fetches a list of countries and caches them when it is loaded. You might want to clear that cache if unloaded:
651
+
652
+ ```ruby
653
+ loader.on_unload("Country") do |klass, _abspath|
654
+ klass.clear_cache
655
+ end
656
+ ```
657
+
658
+ `on_unload` gets a target constant path as a string (e.g., "User", or "Service::NotificationsGateway"). When fired, its block receives the stored value, and the absolute path to the corresponding file or directory as a string. The callback is executed every time the target is unloaded.
659
+
660
+ `on_unload` blocks are executed before the class is unloaded, but in the middle of unloading, which happens in an unspecified order. Therefore, **that callback should not refer to any reloadable constant because there is no guarantee the constant works there**. Those blocks should rely on objects only, as in the example above, or regular constants not managed by the loader. This remark is transitive, applies to any methods invoked within the block.
661
+
662
+ Multiple callbacks on the same target are supported, and they run in order of definition.
663
+
664
+ Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
665
+
666
+ It is also possible to be called when any constant managed by the loader is unloaded:
667
+
668
+ ```ruby
669
+ loader.on_unload do |cpath, value, abspath|
670
+ # ...
671
+ end
672
+ ```
673
+
674
+ The block gets the constant path as a string (e.g., "User", or "Foo::VERSION"), the value it stores (e.g., the class object stored in `User`, or "2.5.0"), and the absolute path to the corresponding file or directory as a string.
675
+
676
+ Multiple callbacks like these are supported, and they run in order of definition.
677
+
678
+ If both types of callbacks are defined, the specific ones run first.
679
+
680
+ <a id="markdown-technical-details" name="technical-details"></a>
681
+ #### Technical details
682
+
683
+ Zeitwerk uses the word "unload" to ease communication and for symmetry with `on_load`. However, in Ruby you cannot unload things for real. So, when does `on_unload` technically happen?
684
+
685
+ When unloading, Zeitwerk issues `Module#remove_const` calls. Classes and modules are no longer reachable through their constants, and `on_unload` callbacks are executed right before those calls.
686
+
687
+ Technically, though, the objects themselves are still alive, but if everything is used as expected and they are not stored in any non-reloadable place (don't do that), they are ready for garbage collection, which is when the real unloading happens.
688
+
552
689
  <a id="markdown-logging" name="logging"></a>
553
690
  ### Logging
554
691
 
@@ -820,9 +957,19 @@ and run `bin/test`.
820
957
  <a id="markdown-motivation" name="motivation"></a>
821
958
  ## Motivation
822
959
 
823
- 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.
960
+ <a id="markdown-kernelrequire-is-brittle" name="kernelrequire-is-brittle"></a>
961
+ ### Kernel#require is brittle
962
+
963
+ 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.
964
+
965
+ Also, if the project has namespaces, setting things up and getting client code to load things in a consistent way needs discipline. For example, `require "foo/bar"` may define `Foo`, instead of reopen it. That may be a broken window, giving place to superclass mismatches or partially-defined namespaces.
966
+
967
+ With Zeitwerk, you just name things following conventions and done. Things are available everywhere, and descend is always orderly. Without effort and without broken windows.
968
+
969
+ <a id="markdown-rails-autoloading-was-brittle" name="rails-autoloading-was-brittle"></a>
970
+ ### Rails autoloading was brittle
824
971
 
825
- 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.
972
+ Autoloading in Rails was based on `const_missing` up to Rails 5. That callback lacks fundamental information like the nesting or the resolution algorithm being used. Because of that, Rails autoloading was not able to match Ruby's semantics, and that introduced a [series of issues](https://guides.rubyonrails.org/v5.2/autoloading_and_reloading_constants.html#common-gotchas). Zeitwerk is based on a different technique and fixed Rails autoloading starting with Rails 6.
826
973
 
827
974
  <a id="markdown-thanks" name="thanks"></a>
828
975
  ## Thanks
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk
4
+ # @private
5
+ class Autoloads
6
+ # Maps crefs for which an autoload has been defined to the corresponding
7
+ # absolute path.
8
+ #
9
+ # [Object, :User] => "/Users/fxn/blog/app/models/user.rb"
10
+ # [Object, :Hotel] => "/Users/fxn/blog/app/models/hotel"
11
+ # ...
12
+ #
13
+ # This colection is transient, callbacks delete its entries as autoloads get
14
+ # executed.
15
+ #
16
+ # @sig Hash[[Module, Symbol], String]
17
+ attr_reader :c2a
18
+
19
+ # This is the inverse of c2a, for inverse lookups.
20
+ #
21
+ # @sig Hash[String, [Module, Symbol]]
22
+ attr_reader :a2c
23
+
24
+ # @sig () -> void
25
+ def initialize
26
+ @c2a = {}
27
+ @a2c = {}
28
+ end
29
+
30
+ # @sig (Module, Symbol, String) -> void
31
+ def define(parent, cname, abspath)
32
+ parent.autoload(cname, abspath)
33
+ cref = [parent, cname]
34
+ c2a[cref] = abspath
35
+ a2c[abspath] = cref
36
+ end
37
+
38
+ # @sig () { () -> [[Module, Symbol], String] } -> void
39
+ def each(&block)
40
+ c2a.each(&block)
41
+ end
42
+
43
+ # @sig (Module, Symbol) -> String?
44
+ def abspath_for(parent, cname)
45
+ c2a[[parent, cname]]
46
+ end
47
+
48
+ # @sig (String) -> [Module, Symbol]?
49
+ def cref_for(abspath)
50
+ a2c[abspath]
51
+ end
52
+
53
+ # @sig (String) -> [Module, Symbol]?
54
+ def delete(abspath)
55
+ cref = a2c.delete(abspath)
56
+ c2a.delete(cref)
57
+ cref
58
+ end
59
+
60
+ # @sig () -> void
61
+ def clear
62
+ c2a.clear
63
+ a2c.clear
64
+ end
65
+
66
+ # @sig () -> bool
67
+ def empty?
68
+ c2a.empty? && a2c.empty?
69
+ end
70
+ end
71
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk
2
4
  class Error < StandardError
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk
2
4
  # Centralizes the logic for the trace point used to detect the creation of
3
5
  # explicit namespaces, needed to descend into matching subdirectories right
@@ -41,7 +43,7 @@ module Zeitwerk
41
43
 
42
44
  # @private
43
45
  # @sig (Zeitwerk::Loader) -> void
44
- def unregister(loader)
46
+ def unregister_loader(loader)
45
47
  cpaths.delete_if { |_cpath, l| l == loader }
46
48
  disable_tracer_if_unneeded
47
49
  end
@@ -62,8 +64,14 @@ module Zeitwerk
62
64
  # than accessing its name.
63
65
  return if event.self.singleton_class?
64
66
 
65
- # Note that it makes sense to compute the hash code unconditionally,
66
- # because the trace point is disabled if cpaths is empty.
67
+ # It might be tempting to return if name.nil?, to avoid the computation
68
+ # of a hash code and delete call. But Ruby does not trigger the :class
69
+ # event on Class.new or Module.new, so that would incur in an extra call
70
+ # for nothing.
71
+ #
72
+ # On the other hand, if we were called, cpaths is not empty. Otherwise
73
+ # the tracer is disabled. So we do need to go ahead with the hash code
74
+ # computation and delete call.
67
75
  if loader = cpaths.delete(real_mod_name(event.self))
68
76
  loader.on_namespace_loaded(event.self)
69
77
  disable_tracer_if_unneeded
@@ -3,7 +3,7 @@
3
3
  module Kernel
4
4
  module_function
5
5
 
6
- # We are going to decorate Kerner#require with two goals.
6
+ # We are going to decorate Kernel#require with two goals.
7
7
  #
8
8
  # First, by intercepting Kernel#require calls, we are able to autovivify
9
9
  # modules on required directories, and also do internal housekeeping when
@@ -12,7 +12,8 @@ module Kernel
12
12
  # On the other hand, if you publish a new version of a gem that is now managed
13
13
  # by Zeitwerk, client code can reference directly your classes and modules and
14
14
  # should not require anything. But if someone has legacy require calls around,
15
- # they will work as expected, and in a compatible way.
15
+ # they will work as expected, and in a compatible way. This feature is by now
16
+ # EXPERIMENTAL and UNDOCUMENTED.
16
17
  #
17
18
  # We cannot decorate with prepend + super because Kernel has already been
18
19
  # included in Object, and changes in ancestors don't get propagated into
@@ -33,9 +34,9 @@ module Kernel
33
34
  else
34
35
  zeitwerk_original_require(path).tap do |required|
35
36
  if required
36
- realpath = $LOADED_FEATURES.last
37
- if loader = Zeitwerk::Registry.loader_for(realpath)
38
- loader.on_file_autoloaded(realpath)
37
+ abspath = $LOADED_FEATURES.last
38
+ if loader = Zeitwerk::Registry.loader_for(abspath)
39
+ loader.on_file_autoloaded(abspath)
39
40
  end
40
41
  end
41
42
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk::Loader::Callbacks
2
4
  include Zeitwerk::RealModName
3
5
 
@@ -18,7 +20,7 @@ module Zeitwerk::Loader::Callbacks
18
20
  raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
19
21
  end
20
22
 
21
- run_on_load_callbacks(cpath)
23
+ run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
22
24
  end
23
25
 
24
26
  # Invoked from our decorated Kernel#require when a managed directory is
@@ -54,7 +56,7 @@ module Zeitwerk::Loader::Callbacks
54
56
 
55
57
  on_namespace_loaded(autovivified_module)
56
58
 
57
- run_on_load_callbacks(cpath)
59
+ run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
58
60
  end
59
61
  end
60
62
  end
@@ -75,12 +77,13 @@ module Zeitwerk::Loader::Callbacks
75
77
 
76
78
  private
77
79
 
78
- # @sig (String) -> void
79
- def run_on_load_callbacks(cpath)
80
- # Very common, do not even compute a hash code.
81
- return if on_load_callbacks.empty?
82
-
80
+ # @sig (String, Object) -> void
81
+ def run_on_load_callbacks(cpath, value, abspath)
82
+ # Order matters. If present, run the most specific one.
83
83
  callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
84
- callbacks.each(&:call) if callbacks
84
+ callbacks&.each { |c| c.call(value, abspath) }
85
+
86
+ callbacks = on_load_callbacks[:ANY]
87
+ callbacks&.each { |c| c.call(cpath, value, abspath) }
85
88
  end
86
89
  end