zeitwerk 2.4.2 → 2.5.0

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: c85bb1028371436b80651c897670fef7158dcca7a916b10f6cb8584684c44442
4
+ data.tar.gz: c07fec548ab787daa28b10260beb022d7375aff90a506daa9bfecdc2f5bd6e5c
5
5
  SHA512:
6
- metadata.gz: 6194d326b268c9333ed9d2ac1c62b65756fa9af844c5fb7ad8272ecec33aa439015506758bcd329e421508facb4153e04e45da0efb0a2a42001a6f2e0a0d3b6a
7
- data.tar.gz: 656009f40e777f641ff1dc80f54b63559514439538c73965aa9226b6c3e33c6be71c07eb30adfe7f4cd4b3be72aa6e42918392ba27167fb9502b9aed037f36d3
6
+ metadata.gz: d01e5091fa868cdc80c4dd6eb82cc3ace4e21e0704aa2128e0ef5098a52140884daa62e9da862490104e31d393e06b33e29170e6d3f6fd3e44aa5111b25a06f1
7
+ data.tar.gz: 055d861fe2440f465277fbfce4c2bd9b7460d38f14dc5435449c6655d1be97f7e24fb7154b64102267c020e45d52f0bff21fbc7fc35eec0063b2dac2a92197bd
data/README.md CHANGED
@@ -3,45 +3,61 @@
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)
22
+ - [Testing compliance](#testing-compliance)
17
23
  - [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)
24
+ - [Setup](#setup)
25
+ - [Generic](#generic)
26
+ - [for_gem](#for_gem)
27
+ - [Autoloading](#autoloading)
28
+ - [Eager loading](#eager-loading)
29
+ - [Eager load exclusions](#eager-load-exclusions)
30
+ - [Global eager load](#global-eager-load)
31
+ - [Reloading](#reloading)
32
+ - [Inflection](#inflection)
33
+ - [Zeitwerk::Inflector](#zeitwerkinflector)
34
+ - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
35
+ - [Custom inflector](#custom-inflector)
36
+ - [Callbacks](#callbacks)
37
+ - [The on_setup callback](#the-on_setup-callback)
28
38
  - [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)
39
+ - [The on_unload callback](#the-on_unload-callback)
40
+ - [Technical details](#technical-details)
41
+ - [Logging](#logging)
42
+ - [Loader tag](#loader-tag)
43
+ - [Ignoring parts of the project](#ignoring-parts-of-the-project)
44
+ - [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
45
+ - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
46
+ - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
47
+ - [Edge cases](#edge-cases)
48
+ - [Beware of circular dependencies](#beware-of-circular-dependencies)
49
+ - [Reopening third-party namespaces](#reopening-third-party-namespaces)
50
+ - [Rules of thumb](#rules-of-thumb)
51
+ - [Debuggers](#debuggers)
52
+ - [debug.rb](#debugrb)
53
+ - [Break](#break)
54
+ - [Byebug](#byebug)
41
55
  - [Pronunciation](#pronunciation)
42
56
  - [Supported Ruby versions](#supported-ruby-versions)
43
57
  - [Testing](#testing)
44
58
  - [Motivation](#motivation)
59
+ - [Kernel#require is brittle](#kernelrequire-is-brittle)
60
+ - [Rails autoloading was brittle](#rails-autoloading-was-brittle)
45
61
  - [Thanks](#thanks)
46
62
  - [License](#license)
47
63
 
@@ -117,6 +133,9 @@ Zeitwerk::Loader.eager_load_all
117
133
  <a id="markdown-file-structure" name="file-structure"></a>
118
134
  ## File structure
119
135
 
136
+ <a id="markdown-the-idea-file-paths-match-constant-paths" name="the-idea-file-paths-match-constant-paths"></a>
137
+ ### The idea: File paths match constant paths
138
+
120
139
  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
140
 
122
141
  ```
@@ -126,25 +145,67 @@ lib/my_gem/bar_baz.rb -> MyGem::BarBaz
126
145
  lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
127
146
  ```
128
147
 
129
- Every directory configured with `push_dir` acts as root namespace. There can be several of them. For example, given
148
+ 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.
149
+
150
+ <a id="markdown-inner-simple-constants" name="inner-simple-constants"></a>
151
+ ### Inner simple constants
152
+
153
+ While a simple constant like `HttpCrawler::MAX_RETRIES` can be defined in its own file:
130
154
 
131
155
  ```ruby
132
- loader.push_dir(Rails.root.join("app/models"))
133
- loader.push_dir(Rails.root.join("app/controllers"))
156
+ # http_crawler/max_retries.rb
157
+ HttpCrawler::MAX_RETRIES = 10
134
158
  ```
135
159
 
136
- Zeitwerk understands that their respective files and subdirectories belong to the root namespace:
160
+ that is not required, you can also define it the regular way:
137
161
 
162
+ ```ruby
163
+ # http_crawler.rb
164
+ class HttpCrawler
165
+ MAX_RETRIES = 10
166
+ end
138
167
  ```
139
- app/models/user.rb -> User
140
- app/controllers/admin/users_controller.rb -> Admin::UsersController
168
+
169
+ The first example needs a custom [inflection](https://github.com/fxn/zeitwerk#inflection) rule:
170
+
171
+ ```ruby
172
+ loader.inflector.inflect("max_retries" => "MAX_RETRIES")
173
+ ```
174
+
175
+ Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
176
+
177
+ In the second example, no custom rule is needed.
178
+
179
+ <a id="markdown-root-directories-and-root-namespaces" name="root-directories-and-root-namespaces"></a>
180
+ ### Root directories and root namespaces
181
+
182
+ Every directory configured with `push_dir` is called a _root directory_, and they represent _root namespaces_.
183
+
184
+ <a id="markdown-the-default-root-namespace-is-object" name="the-default-root-namespace-is-object"></a>
185
+ #### The default root namespace is `Object`
186
+
187
+ By default, the namespace associated to a root directory is the top-level one: `Object`.
188
+
189
+ For example, given
190
+
191
+ ```ruby
192
+ loader.push_dir("#{__dir__}/models")
193
+ loader.push_dir("#{__dir__}/serializers"))
194
+ ```
195
+
196
+ these are the expected classes and modules being defined by these files:
197
+
198
+ ```
199
+ models/user.rb -> User
200
+ serializers/user_serializer.rb -> UserSerializer
141
201
  ```
142
202
 
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.
203
+ <a id="markdown-custom-root-namespaces" name="custom-root-namespaces"></a>
204
+ #### Custom root namespaces
144
205
 
145
- For example, Active Job queue adapters have to define a constant after their name in `ActiveJob::QueueAdapters`.
206
+ 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
207
 
147
- So, if you declare
208
+ For example, given:
148
209
 
149
210
  ```ruby
150
211
  require "active_job"
@@ -152,9 +213,26 @@ require "active_job/queue_adapters"
152
213
  loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
153
214
  ```
154
215
 
155
- your adapter can be stored directly in that directory instead of the canonical `#{__dir__}/active_job/queue_adapters`.
216
+ a file defining `ActiveJob::QueueAdapters::MyQueueAdapter` does not need the conventional parent directories, you can (and have to) store the file directly below `adapters`:
217
+
218
+ ```
219
+ adapters/my_queue_adapter.rb -> ActiveJob::QueueAdapters::MyQueueAdapter
220
+ ```
221
+
222
+ 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.
223
+
224
+ <a id="markdown-nested-root-directories" name="nested-root-directories"></a>
225
+ #### Nested root directories
226
+
227
+ 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.
228
+
229
+ 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:
230
+
231
+ ```
232
+ app/models/concerns/geolocatable.rb
233
+ ```
156
234
 
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.
235
+ should define `Geolocatable`, not `Concerns::Geolocatable`.
158
236
 
159
237
  <a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
160
238
  ### Implicit namespaces
@@ -216,18 +294,22 @@ To illustrate usage of glob patterns, if `actions` in the example above is part
216
294
  loader.collapse("#{__dir__}/*/actions")
217
295
  ```
218
296
 
219
- <a id="markdown-nested-root-directories" name="nested-root-directories"></a>
220
- ### Nested root directories
297
+ <a id="markdown-testing-compliance" name="testing-compliance"></a>
298
+ ### Testing compliance
221
299
 
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.
300
+ When a managed file is loaded, Zeitwerk verifies the expected constant is defined. If it is not, `Zeitwerk::NameError` is raised.
223
301
 
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:
302
+ So, an easy way to ensure compliance in the test suite is to eager load the project:
225
303
 
304
+ ```ruby
305
+ begin
306
+ loader.eager_load(force: true)
307
+ rescue Zeitwerk::NameError => e
308
+ flunk e.message
309
+ else
310
+ assert true
311
+ end
226
312
  ```
227
- app/models/concerns/geolocatable.rb
228
- ```
229
-
230
- should define `Geolocatable`, not `Concerns::Geolocatable`.
231
313
 
232
314
  <a id="markdown-usage" name="usage"></a>
233
315
  ## Usage
@@ -331,7 +413,16 @@ Zeitwerk instances are able to eager load their managed files:
331
413
  loader.eager_load
332
414
  ```
333
415
 
334
- 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:
416
+ That skips [ignored files and directories](#ignoring-parts-of-the-project).
417
+
418
+ 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).
419
+
420
+ Eager loading is synchronized and idempotent.
421
+
422
+ <a id="markdown-eager-load-exclusions" name="eager-load-exclusions"></a>
423
+ #### Eager load exclusions
424
+
425
+ You can tell Zeitwerk that certain files or directories are autoloadable, but should not be eager loaded:
335
426
 
336
427
  ```ruby
337
428
  db_adapters = "#{__dir__}/my_gem/db_adapters"
@@ -340,13 +431,20 @@ loader.setup
340
431
  loader.eager_load # won't eager load the database adapters
341
432
  ```
342
433
 
343
- 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).
434
+ However, that can be overridden with `force`:
344
435
 
345
- Eager loading is synchronized and idempotent.
436
+ ```ruby
437
+ loader.eager_load(force: true) # database adapters are eager loaded
438
+ ```
439
+
440
+ Which may be handy if the project eager loads in the test suite to [ensure project layour compliance](#testing-compliance).
346
441
 
347
- If eager loading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
442
+ The `force` flag does not affect ignored files and directories, those are still ignored.
348
443
 
349
- If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
444
+ <a id="markdown-global-eager-load" name="global-eager-load"></a>
445
+ #### Global eager load
446
+
447
+ If you want to eager load yourself and all dependencies that use Zeitwerk, you can broadcast the `eager_load` call to all instances:
350
448
 
351
449
  ```ruby
352
450
  Zeitwerk::Loader.eager_load_all
@@ -356,6 +454,8 @@ This may be handy in top-level services, like web applications.
356
454
 
357
455
  Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
358
456
 
457
+ This method does not accept the `force` flag, since in general it wouldn't be a good idea to force eager loading in 3rd party code.
458
+
359
459
  <a id="markdown-reloading" name="reloading"></a>
360
460
  ### Reloading
361
461
 
@@ -387,11 +487,13 @@ On reloading, client code has to update anything that would otherwise be storing
387
487
  <a id="markdown-inflection" name="inflection"></a>
388
488
  ### Inflection
389
489
 
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.
490
+ 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
491
 
392
492
  <a id="markdown-zeitwerkinflector" name="zeitwerkinflector"></a>
393
493
  #### Zeitwerk::Inflector
394
494
 
495
+ Each loader instantiated with `Zeitwerk::Loader.new` has an inflector of this type by default.
496
+
395
497
  This is a very basic inflector that converts snake case to camel case:
396
498
 
397
499
  ```
@@ -418,16 +520,16 @@ loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
418
520
 
419
521
  Overrides need to be configured before calling `setup`.
420
522
 
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.
523
+ 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
524
 
425
525
  <a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
426
526
  #### Zeitwerk::GemInflector
427
527
 
528
+ Each loader instantiated with `Zeitwerk::Loader.for_gem` has an inflector of this type by default.
529
+
428
530
  This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
429
531
 
430
- Loaders instantiated with `Zeitwerk::Loader.for_gem` have an inflector of this type, independent of each other.
532
+ 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
533
 
432
534
  <a id="markdown-custom-inflector" name="custom-inflector"></a>
433
535
  #### Custom inflector
@@ -503,8 +605,26 @@ class MyGem::Inflector < Zeitwerk::GemInflector
503
605
  end
504
606
  ```
505
607
 
608
+ <a id="markdown-callbacks" name="callbacks"></a>
609
+ ### Callbacks
610
+
611
+ <a id="markdown-the-on_setup-callback" name="the-on_setup-callback"></a>
612
+ #### The on_setup callback
613
+
614
+ The `on_setup` callback is fired on setup and on each reload:
615
+
616
+ ```ruby
617
+ loader.on_setup do
618
+ # Ready to autoload here.
619
+ end
620
+ ```
621
+
622
+ Multiple `on_setup` callbacks are supported, and they run in order of definition.
623
+
624
+ If `setup` was already executed, the callback is fired immediately.
625
+
506
626
  <a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
507
- ### The on_load callback
627
+ #### The on_load callback
508
628
 
509
629
  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
630
 
@@ -522,26 +642,23 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
522
642
 
523
643
  ```ruby
524
644
  # config/environments/development.rb
525
- loader.on_load("SomeApiClient") do
526
- SomeApiClient.endpoint = "https://api.dev"
645
+ loader.on_load("SomeApiClient") do |klass, _abspath|
646
+ klass.endpoint = "https://api.dev"
527
647
  end
528
648
 
529
649
  # config/environments/production.rb
530
- loader.on_load("SomeApiClient") do
531
- SomeApiClient.endpoint = "https://api.prod"
650
+ loader.on_load("SomeApiClient") do |klass, _abspath|
651
+ klass.endpoint = "https://api.prod"
532
652
  end
533
653
  ```
534
654
 
535
- Uses cases:
655
+ Some uses cases:
536
656
 
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.
657
+ * 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
658
  * Delaying the execution of the block until the class is loaded for performance.
539
659
  * 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
660
 
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.
661
+ `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
662
 
546
663
  Multiple callbacks on the same target are supported, and they run in order of definition.
547
664
 
@@ -549,6 +666,68 @@ The block is executed once the loader has loaded the target. In particular, if t
549
666
 
550
667
  Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
551
668
 
669
+ It is also possible to be called when any constant managed by the loader is loaded:
670
+
671
+ ```ruby
672
+ loader.on_load do |cpath, value, abspath|
673
+ # ...
674
+ end
675
+ ```
676
+
677
+ 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.
678
+
679
+ Multiple callbacks like these are supported, and they run in order of definition.
680
+
681
+ 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.
682
+
683
+ If both types of callbacks are defined, the specific ones run first.
684
+
685
+ Since `on_load` callbacks are executed right after files are loaded, even if the loading context seems to be far away, in practice **the block is subject to [circular dependencies](#beware-of-circular-dependencies)**. As a rule of thumb, as far as loading order and its interdependencies is concerned, you have to program as if the block was executed at the bottom of the file just loaded.
686
+
687
+ <a id="markdown-the-on_unload-callback" name="the-on_unload-callback"></a>
688
+ #### The on_unload callback
689
+
690
+ 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.
691
+
692
+ 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:
693
+
694
+ ```ruby
695
+ loader.on_unload("Country") do |klass, _abspath|
696
+ klass.clear_cache
697
+ end
698
+ ```
699
+
700
+ `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.
701
+
702
+ `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.
703
+
704
+ Multiple callbacks on the same target are supported, and they run in order of definition.
705
+
706
+ Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
707
+
708
+ It is also possible to be called when any constant managed by the loader is unloaded:
709
+
710
+ ```ruby
711
+ loader.on_unload do |cpath, value, abspath|
712
+ # ...
713
+ end
714
+ ```
715
+
716
+ 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.
717
+
718
+ Multiple callbacks like these are supported, and they run in order of definition.
719
+
720
+ If both types of callbacks are defined, the specific ones run first.
721
+
722
+ <a id="markdown-technical-details" name="technical-details"></a>
723
+ #### Technical details
724
+
725
+ 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?
726
+
727
+ 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.
728
+
729
+ 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.
730
+
552
731
  <a id="markdown-logging" name="logging"></a>
553
732
  ### Logging
554
733
 
@@ -717,6 +896,26 @@ Trip = Struct.new { ... } # NOT SUPPORTED
717
896
 
718
897
  This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
719
898
 
899
+ <a id="markdown-beware-of-circular-dependencies" name="beware-of-circular-dependencies"></a>
900
+ ### Beware of circular dependencies
901
+
902
+ In Ruby, you can't have certain top-level circular dependencies. Take for example:
903
+
904
+ ```ruby
905
+ # c.rb
906
+ class C < D
907
+ end
908
+
909
+ # d.rb
910
+ class D
911
+ C
912
+ end
913
+ ```
914
+
915
+ In order to define `C`, you need to load `D`. However, the body of `D` refers to `C`.
916
+
917
+ Circular dependencies like those do not work in plain Ruby, and therefore do not work in projects managed by Zeitwerk either.
918
+
720
919
  <a id="markdown-reopening-third-party-namespaces" name="reopening-third-party-namespaces"></a>
721
920
  ### Reopening third-party namespaces
722
921
 
@@ -771,6 +970,11 @@ With that, when Zeitwerk scans the file system and reaches the gem directories `
771
970
  <a id="markdown-debuggers" name="debuggers"></a>
772
971
  ### Debuggers
773
972
 
973
+ <a id="markdown-debugrb" name="debugrb"></a>
974
+ #### debug.rb
975
+
976
+ The new [debug.rb](https://github.com/ruby/debug) gem and Zeitwerk seem to be compatible, as far as I can tell. This is the new debugger that is going to ship with Ruby 3.1.
977
+
774
978
  <a id="markdown-break" name="break"></a>
775
979
  #### Break
776
980
 
@@ -789,7 +993,11 @@ Zeitwerk and [Byebug](https://github.com/deivid-rodriguez/byebug) are incompatib
789
993
  <a id="markdown-supported-ruby-versions" name="supported-ruby-versions"></a>
790
994
  ## Supported Ruby versions
791
995
 
792
- Zeitwerk works with MRI 2.4.4 and above.
996
+ Zeitwerk works with CRuby 2.5 and above.
997
+
998
+ On TruffleRuby all is good except for thread-safety. Right now, in TruffleRuby `Module#autoload` does not block threads accessing a constant that is being autoloaded. CRuby prevents such access to avoid concurrent threads from seeing partial evaluations of the corresponding file. Zeitwerk inherits autoloading thread-safety from this property. This is not an issue if your project gets eager loaded, or if you lazy load in single-threaded environments. (See https://github.com/oracle/truffleruby/issues/2431.)
999
+
1000
+ JRuby 9.3.0.0 is almost there. As of this writing, the test suite of Zeitwerk passes on JRuby except for three tests. (See https://github.com/jruby/jruby/issues/6781.)
793
1001
 
794
1002
  <a id="markdown-testing" name="testing"></a>
795
1003
  ## Testing
@@ -820,9 +1028,19 @@ and run `bin/test`.
820
1028
  <a id="markdown-motivation" name="motivation"></a>
821
1029
  ## Motivation
822
1030
 
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.
1031
+ <a id="markdown-kernelrequire-is-brittle" name="kernelrequire-is-brittle"></a>
1032
+ ### Kernel#require is brittle
1033
+
1034
+ 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.
1035
+
1036
+ 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.
1037
+
1038
+ 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.
1039
+
1040
+ <a id="markdown-rails-autoloading-was-brittle" name="rails-autoloading-was-brittle"></a>
1041
+ ### Rails autoloading was brittle
824
1042
 
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.
1043
+ 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
1044
 
827
1045
  <a id="markdown-thanks" name="thanks"></a>
828
1046
  ## 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