zeitwerk 2.4.0 → 2.4.2

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: 682fa352a9d6d4bf68d07cf2cbfbb539de3cfec4e8af2f930894ab27c707ca93
4
- data.tar.gz: 1e7ce6157e6046cdb60f0f1b531f1f39fb97cad3537392114be5cf84dae672c9
3
+ metadata.gz: f45a7c3caf48f06e10dd513a7b074da7ec059fa3299751d361514aadc83d995e
4
+ data.tar.gz: c8c29793e95245b47b1283891791d2c20365b8c694b3dcdfb7daab0b74c16bdb
5
5
  SHA512:
6
- metadata.gz: e308baba1a8fbe4d7c64d7195c753d795a53caefa7264d2404117cb620f2081f6b0fc05c056dafb035b44430102e4472b7eb9fb3a4fd9d70aa124b7f9a3a8313
7
- data.tar.gz: 5139d5abaeed86e256493b592460b13b19fd7602f7c099f7017b55af9f16a91429e84074a4e31fe79bf28235e02cbba3055cc4bda579bbc7de2a391c5997a393
6
+ metadata.gz: 6194d326b268c9333ed9d2ac1c62b65756fa9af844c5fb7ad8272ecec33aa439015506758bcd329e421508facb4153e04e45da0efb0a2a42001a6f2e0a0d3b6a
7
+ data.tar.gz: 656009f40e777f641ff1dc80f54b63559514439538c73965aa9226b6c3e33c6be71c07eb30adfe7f4cd4b3be72aa6e42918392ba27167fb9502b9aed037f36d3
data/README.md CHANGED
@@ -25,6 +25,7 @@
25
25
  - [Zeitwerk::Inflector](#zeitwerkinflector)
26
26
  - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
27
27
  - [Custom inflector](#custom-inflector)
28
+ - [The on_load callback](#the-on_load-callback)
28
29
  - [Logging](#logging)
29
30
  - [Loader tag](#loader-tag)
30
31
  - [Ignoring parts of the project](#ignoring-parts-of-the-project)
@@ -151,7 +152,7 @@ require "active_job/queue_adapters"
151
152
  loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
152
153
  ```
153
154
 
154
- your adapter can be stored directly in that directory instead of the canonical `lib/active_job/queue_adapters`.
155
+ your adapter can be stored directly in that directory instead of the canonical `#{__dir__}/active_job/queue_adapters`.
155
156
 
156
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.
157
158
 
@@ -202,7 +203,7 @@ booking/actions/create.rb -> Booking::Create
202
203
  To make it work that way, configure Zeitwerk to collapse said directory:
203
204
 
204
205
  ```ruby
205
- loader.collapse("booking/actions")
206
+ loader.collapse("#{__dir__}/booking/actions")
206
207
  ```
207
208
 
208
209
  This method accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
@@ -212,7 +213,7 @@ You can pass directories and glob patterns. Glob patterns are expanded when they
212
213
  To illustrate usage of glob patterns, if `actions` in the example above is part of a standardized structure, you could use a wildcard:
213
214
 
214
215
  ```ruby
215
- loader.collapse("*/actions")
216
+ loader.collapse("#{__dir__}/*/actions")
216
217
  ```
217
218
 
218
219
  <a id="markdown-nested-root-directories" name="nested-root-directories"></a>
@@ -502,6 +503,52 @@ class MyGem::Inflector < Zeitwerk::GemInflector
502
503
  end
503
504
  ```
504
505
 
506
+ <a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
507
+ ### The on_load callback
508
+
509
+ 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
+
511
+ For example, let's imagine this class belongs to a Rails application:
512
+
513
+ ```ruby
514
+ class SomeApiClient
515
+ class << self
516
+ attr_accessor :endpoint
517
+ end
518
+ end
519
+ ```
520
+
521
+ With `on_load`, it is easy to schedule code at boot time that initializes `endpoint` according to the configuration:
522
+
523
+ ```ruby
524
+ # config/environments/development.rb
525
+ loader.on_load("SomeApiClient") do
526
+ SomeApiClient.endpoint = "https://api.dev"
527
+ end
528
+
529
+ # config/environments/production.rb
530
+ loader.on_load("SomeApiClient") do
531
+ SomeApiClient.endpoint = "https://api.prod"
532
+ end
533
+ ```
534
+
535
+ Uses cases:
536
+
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.
538
+ * Delaying the execution of the block until the class is loaded for performance.
539
+ * 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
+
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.
545
+
546
+ Multiple callbacks on the same target are supported, and they run in order of definition.
547
+
548
+ The block is executed once the loader has loaded the target. In particular, if the target was already loaded when the callback is defined, the block won't run. But if you reload and load the target again, then it will. Normally, you'll want to define `on_load` callbacks before `setup`.
549
+
550
+ Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
551
+
505
552
  <a id="markdown-logging" name="logging"></a>
506
553
  ### Logging
507
554
 
@@ -14,24 +14,22 @@ module Zeitwerk
14
14
  # the file system, to the loader responsible for them.
15
15
  #
16
16
  # @private
17
- # @return [{String => Zeitwerk::Loader}]
17
+ # @sig Hash[String, Zeitwerk::Loader]
18
18
  attr_reader :cpaths
19
19
 
20
20
  # @private
21
- # @return [Mutex]
21
+ # @sig Mutex
22
22
  attr_reader :mutex
23
23
 
24
24
  # @private
25
- # @return [TracePoint]
25
+ # @sig TracePoint
26
26
  attr_reader :tracer
27
27
 
28
28
  # Asserts `cpath` corresponds to an explicit namespace for which `loader`
29
29
  # is responsible.
30
30
  #
31
31
  # @private
32
- # @param cpath [String]
33
- # @param loader [Zeitwerk::Loader]
34
- # @return [void]
32
+ # @sig (String, Zeitwerk::Loader) -> void
35
33
  def register(cpath, loader)
36
34
  mutex.synchronize do
37
35
  cpaths[cpath] = loader
@@ -42,19 +40,22 @@ module Zeitwerk
42
40
  end
43
41
 
44
42
  # @private
45
- # @param loader [Zeitwerk::Loader]
46
- # @return [void]
43
+ # @sig (Zeitwerk::Loader) -> void
47
44
  def unregister(loader)
48
45
  cpaths.delete_if { |_cpath, l| l == loader }
49
46
  disable_tracer_if_unneeded
50
47
  end
51
48
 
49
+ private
50
+
51
+ # @sig () -> void
52
52
  def disable_tracer_if_unneeded
53
53
  mutex.synchronize do
54
54
  tracer.disable if cpaths.empty?
55
55
  end
56
56
  end
57
57
 
58
+ # @sig (TracePoint) -> void
58
59
  def tracepoint_class_callback(event)
59
60
  # If the class is a singleton class, we won't do anything with it so we
60
61
  # can bail out immediately. This is several orders of magnitude faster
@@ -2,16 +2,14 @@
2
2
 
3
3
  module Zeitwerk
4
4
  class GemInflector < Inflector
5
- # @param root_file [String]
5
+ # @sig (String) -> void
6
6
  def initialize(root_file)
7
7
  namespace = File.basename(root_file, ".rb")
8
8
  lib_dir = File.dirname(root_file)
9
9
  @version_file = File.join(lib_dir, namespace, "version.rb")
10
10
  end
11
11
 
12
- # @param basename [String]
13
- # @param abspath [String]
14
- # @return [String]
12
+ # @sig (String, String) -> String
15
13
  def camelize(basename, abspath)
16
14
  abspath == @version_file ? "VERSION" : super
17
15
  end
@@ -11,9 +11,7 @@ module Zeitwerk
11
11
  #
12
12
  # Takes into account hard-coded mappings configured with `inflect`.
13
13
  #
14
- # @param basename [String]
15
- # @param _abspath [String]
16
- # @return [String]
14
+ # @sig (String, String) -> String
17
15
  def camelize(basename, _abspath)
18
16
  overrides[basename] || basename.split('_').each(&:capitalize!).join
19
17
  end
@@ -30,8 +28,7 @@ module Zeitwerk
30
28
  # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
31
29
  # inflector.camelize("users_controller", abspath) # => "UsersController"
32
30
  #
33
- # @param inflections [{String => String}]
34
- # @return [void]
31
+ # @sig (Hash[String, String]) -> void
35
32
  def inflect(inflections)
36
33
  overrides.merge!(inflections)
37
34
  end
@@ -41,7 +38,7 @@ module Zeitwerk
41
38
  # Hard-coded basename to constant name user maps that override the default
42
39
  # inflection logic.
43
40
  #
44
- # @return [{String => String}]
41
+ # @sig () -> Hash[String, String]
45
42
  def overrides
46
43
  @overrides ||= {}
47
44
  end
@@ -19,8 +19,7 @@ module Kernel
19
19
  # already existing ancestor chains.
20
20
  alias_method :zeitwerk_original_require, :require
21
21
 
22
- # @param path [String]
23
- # @return [Boolean]
22
+ # @sig (String) -> true | false
24
23
  def require(path)
25
24
  if loader = Zeitwerk::Registry.loader_for(path)
26
25
  if path.end_with?(".rb")
@@ -29,6 +28,7 @@ module Kernel
29
28
  end
30
29
  else
31
30
  loader.on_dir_autoloaded(path)
31
+ true
32
32
  end
33
33
  else
34
34
  zeitwerk_original_require(path).tap do |required|
@@ -9,13 +9,13 @@ module Zeitwerk
9
9
  include Callbacks
10
10
  include RealModName
11
11
 
12
- # @return [String]
12
+ # @sig String
13
13
  attr_reader :tag
14
14
 
15
- # @return [#camelize]
15
+ # @sig #camelize
16
16
  attr_accessor :inflector
17
17
 
18
- # @return [#call, #debug, nil]
18
+ # @sig #call | #debug | nil
19
19
  attr_accessor :logger
20
20
 
21
21
  # Absolute paths of the root directories. Stored in a hash to preserve
@@ -30,20 +30,20 @@ module Zeitwerk
30
30
  # interface for it is `push_dir` and `dirs`.
31
31
  #
32
32
  # @private
33
- # @return [{String => true}]
33
+ # @sig Hash[String, true]
34
34
  attr_reader :root_dirs
35
35
 
36
36
  # Absolute paths of files or directories that have to be preloaded.
37
37
  #
38
38
  # @private
39
- # @return [<String>]
39
+ # @sig Array[String]
40
40
  attr_reader :preloads
41
41
 
42
42
  # Absolute paths of files, directories, or glob patterns to be totally
43
43
  # ignored.
44
44
  #
45
45
  # @private
46
- # @return [Set<String>]
46
+ # @sig Set[String]
47
47
  attr_reader :ignored_glob_patterns
48
48
 
49
49
  # The actual collection of absolute file and directory names at the time the
@@ -51,20 +51,20 @@ module Zeitwerk
51
51
  # reload.
52
52
  #
53
53
  # @private
54
- # @return [Set<String>]
54
+ # @sig Set[String]
55
55
  attr_reader :ignored_paths
56
56
 
57
57
  # Absolute paths of directories or glob patterns to be collapsed.
58
58
  #
59
59
  # @private
60
- # @return [Set<String>]
60
+ # @sig Set[String]
61
61
  attr_reader :collapse_glob_patterns
62
62
 
63
63
  # The actual collection of absolute directory names at the time the collapse
64
64
  # glob patterns were expanded. Computed on setup, and recomputed on reload.
65
65
  #
66
66
  # @private
67
- # @return [Set<String>]
67
+ # @sig Set[String]
68
68
  attr_reader :collapse_dirs
69
69
 
70
70
  # Maps real absolute paths for which an autoload has been set ---and not
@@ -76,7 +76,7 @@ module Zeitwerk
76
76
  # ...
77
77
  #
78
78
  # @private
79
- # @return [{String => (Module, Symbol)}]
79
+ # @sig Hash[String, [Module, Symbol]]
80
80
  attr_reader :autoloads
81
81
 
82
82
  # We keep track of autoloaded directories to remove them from the registry
@@ -86,7 +86,7 @@ module Zeitwerk
86
86
  # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
87
87
  #
88
88
  # @private
89
- # @return [<String>]
89
+ # @sig Array[String]
90
90
  attr_reader :autoloaded_dirs
91
91
 
92
92
  # Stores metadata needed for unloading. Its entries look like this:
@@ -102,7 +102,7 @@ module Zeitwerk
102
102
  # or eager loaded. Otherwise, the collection remains empty.
103
103
  #
104
104
  # @private
105
- # @return [{String => (String, (Module, Symbol))}]
105
+ # @sig Hash[String, [String, [Module, Symbol]]]
106
106
  attr_reader :to_unload
107
107
 
108
108
  # Maps constant paths of namespaces to arrays of corresponding directories.
@@ -120,21 +120,24 @@ module Zeitwerk
120
120
  # up the corresponding autoloads.
121
121
  #
122
122
  # @private
123
- # @return [{String => <String>}]
123
+ # @sig Hash[String, Array[String]]
124
124
  attr_reader :lazy_subdirs
125
125
 
126
126
  # Absolute paths of files or directories not to be eager loaded.
127
127
  #
128
128
  # @private
129
- # @return [Set<String>]
129
+ # @sig Set[String]
130
130
  attr_reader :eager_load_exclusions
131
131
 
132
+ # User-oriented callbacks to be fired when a constant is loaded.
133
+ attr_reader :on_load_callbacks
134
+
132
135
  # @private
133
- # @return [Mutex]
136
+ # @sig Mutex
134
137
  attr_reader :mutex
135
138
 
136
139
  # @private
137
- # @return [Mutex]
140
+ # @sig Mutex
138
141
  attr_reader :mutex2
139
142
 
140
143
  def initialize
@@ -155,6 +158,7 @@ module Zeitwerk
155
158
  @to_unload = {}
156
159
  @lazy_subdirs = {}
157
160
  @eager_load_exclusions = Set.new
161
+ @on_load_callbacks = {}
158
162
 
159
163
  # TODO: find a better name for these mutexes.
160
164
  @mutex = Mutex.new
@@ -170,7 +174,7 @@ module Zeitwerk
170
174
  # Sets a tag for the loader, useful for logging.
171
175
  #
172
176
  # @param tag [#to_s]
173
- # @return [void]
177
+ # @sig (#to_s) -> void
174
178
  def tag=(tag)
175
179
  @tag = tag.to_s
176
180
  end
@@ -178,7 +182,7 @@ module Zeitwerk
178
182
  # Absolute paths of the root directories. This is a read-only collection,
179
183
  # please push here via `push_dir`.
180
184
  #
181
- # @return [<String>]
185
+ # @sig () -> Array[String]
182
186
  def dirs
183
187
  root_dirs.keys.freeze
184
188
  end
@@ -189,10 +193,8 @@ module Zeitwerk
189
193
  # the same process already manages that directory or one of its ascendants
190
194
  # or descendants.
191
195
  #
192
- # @param path [<String, Pathname>]
193
- # @param namespace [Class, Module]
194
196
  # @raise [Zeitwerk::Error]
195
- # @return [void]
197
+ # @sig (String | Pathname, Module) -> void
196
198
  def push_dir(path, namespace: Object)
197
199
  # Note that Class < Module.
198
200
  unless namespace.is_a?(Module)
@@ -212,7 +214,7 @@ module Zeitwerk
212
214
  # There is no way to undo this, either you want to reload or you don't.
213
215
  #
214
216
  # @raise [Zeitwerk::Error]
215
- # @return [void]
217
+ # @sig () -> void
216
218
  def enable_reloading
217
219
  mutex.synchronize do
218
220
  break if @reloading_enabled
@@ -225,15 +227,14 @@ module Zeitwerk
225
227
  end
226
228
  end
227
229
 
228
- # @return [Boolean]
230
+ # @sig () -> bool
229
231
  def reloading_enabled?
230
232
  @reloading_enabled
231
233
  end
232
234
 
233
235
  # Files or directories to be preloaded instead of lazy loaded.
234
236
  #
235
- # @param paths [<String, Pathname, <String, Pathname>>]
236
- # @return [void]
237
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
237
238
  def preload(*paths)
238
239
  mutex.synchronize do
239
240
  expand_paths(paths).each do |abspath|
@@ -245,8 +246,7 @@ module Zeitwerk
245
246
 
246
247
  # Configure files, directories, or glob patterns to be totally ignored.
247
248
  #
248
- # @param paths [<String, Pathname, <String, Pathname>>]
249
- # @return [void]
249
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
250
250
  def ignore(*glob_patterns)
251
251
  glob_patterns = expand_paths(glob_patterns)
252
252
  mutex.synchronize do
@@ -257,8 +257,7 @@ module Zeitwerk
257
257
 
258
258
  # Configure directories or glob patterns to be collapsed.
259
259
  #
260
- # @param paths [<String, Pathname, <String, Pathname>>]
261
- # @return [void]
260
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
262
261
  def collapse(*glob_patterns)
263
262
  glob_patterns = expand_paths(glob_patterns)
264
263
  mutex.synchronize do
@@ -267,9 +266,27 @@ module Zeitwerk
267
266
  end
268
267
  end
269
268
 
269
+ # Configure a block to be invoked once a certain constant path is loaded.
270
+ # Supports multiple callbacks, and if there are many, they are executed in
271
+ # the order in which they were defined.
272
+ #
273
+ # loader.on_load("SomeApiClient") do
274
+ # SomeApiClient.endpoint = "https://api.dev"
275
+ # end
276
+ #
277
+ # @raise [TypeError]
278
+ # @sig (String) { () -> void } -> void
279
+ def on_load(cpath, &block)
280
+ raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String)
281
+
282
+ mutex.synchronize do
283
+ (on_load_callbacks[cpath] ||= []) << block
284
+ end
285
+ end
286
+
270
287
  # Sets autoloads in the root namespace and preloads files, if any.
271
288
  #
272
- # @return [void]
289
+ # @sig () -> void
273
290
  def setup
274
291
  mutex.synchronize do
275
292
  break if @setup
@@ -291,7 +308,7 @@ module Zeitwerk
291
308
  # unload them.
292
309
  #
293
310
  # @private
294
- # @return [void]
311
+ # @sig () -> void
295
312
  def unload
296
313
  mutex.synchronize do
297
314
  # We are going to keep track of the files that were required by our
@@ -354,7 +371,7 @@ module Zeitwerk
354
371
  # client code in the README of the project.
355
372
  #
356
373
  # @raise [Zeitwerk::Error]
357
- # @return [void]
374
+ # @sig () -> void
358
375
  def reload
359
376
  if reloading_enabled?
360
377
  unload
@@ -371,7 +388,7 @@ module Zeitwerk
371
388
  # are not eager loaded. You can opt-out specifically in specific files and
372
389
  # directories with `do_not_eager_load`.
373
390
  #
374
- # @return [void]
391
+ # @sig () -> void
375
392
  def eager_load
376
393
  mutex.synchronize do
377
394
  break if @eager_loaded
@@ -414,8 +431,7 @@ module Zeitwerk
414
431
  # Let eager load ignore the given files or directories. The constants
415
432
  # defined in those files are still autoloadable.
416
433
  #
417
- # @param paths [<String, Pathname, <String, Pathname>>]
418
- # @return [void]
434
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
419
435
  def do_not_eager_load(*paths)
420
436
  mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
421
437
  end
@@ -423,8 +439,7 @@ module Zeitwerk
423
439
  # Says if the given constant path would be unloaded on reload. This
424
440
  # predicate returns `false` if reloading is disabled.
425
441
  #
426
- # @param cpath [String]
427
- # @return [Boolean]
442
+ # @sig (String) -> bool
428
443
  def unloadable_cpath?(cpath)
429
444
  to_unload.key?(cpath)
430
445
  end
@@ -432,21 +447,20 @@ module Zeitwerk
432
447
  # Returns an array with the constant paths that would be unloaded on reload.
433
448
  # This predicate returns an empty array if reloading is disabled.
434
449
  #
435
- # @return [<String>]
450
+ # @sig () -> Array[String]
436
451
  def unloadable_cpaths
437
452
  to_unload.keys.freeze
438
453
  end
439
454
 
440
455
  # Logs to `$stdout`, handy shortcut for debugging.
441
456
  #
442
- # @return [void]
457
+ # @sig () -> void
443
458
  def log!
444
459
  @logger = ->(msg) { puts msg }
445
460
  end
446
461
 
447
462
  # @private
448
- # @param dir [String]
449
- # @return [Boolean]
463
+ # @sig (String) -> bool
450
464
  def manages?(dir)
451
465
  dir = dir + "/"
452
466
  ignored_paths.each do |ignored_path|
@@ -463,11 +477,11 @@ module Zeitwerk
463
477
  # --- Class methods ---------------------------------------------------------------------------
464
478
 
465
479
  class << self
466
- # @return [#call, #debug, nil]
480
+ # @sig #call | #debug | nil
467
481
  attr_accessor :default_logger
468
482
 
469
483
  # @private
470
- # @return [Mutex]
484
+ # @sig Mutex
471
485
  attr_accessor :mutex
472
486
 
473
487
  # This is a shortcut for
@@ -481,7 +495,7 @@ module Zeitwerk
481
495
  # except that this method returns the same object in subsequent calls from
482
496
  # the same file, in the unlikely case the gem wants to be able to reload.
483
497
  #
484
- # @return [Zeitwerk::Loader]
498
+ # @sig () -> Zeitwerk::Loader
485
499
  def for_gem
486
500
  called_from = caller_locations(1, 1).first.path
487
501
  Registry.loader_for_gem(called_from)
@@ -489,7 +503,7 @@ module Zeitwerk
489
503
 
490
504
  # Broadcasts `eager_load` to all loaders.
491
505
  #
492
- # @return [void]
506
+ # @sig () -> void
493
507
  def eager_load_all
494
508
  Registry.loaders.each(&:eager_load)
495
509
  end
@@ -497,7 +511,7 @@ module Zeitwerk
497
511
  # Returns an array with the absolute paths of the root directories of all
498
512
  # registered loaders. This is a read-only collection.
499
513
  #
500
- # @return [<String>]
514
+ # @sig () -> Array[String]
501
515
  def all_dirs
502
516
  Registry.loaders.flat_map(&:dirs).freeze
503
517
  end
@@ -507,16 +521,14 @@ module Zeitwerk
507
521
 
508
522
  private # -------------------------------------------------------------------------------------
509
523
 
510
- # @return [<String>]
524
+ # @sig () -> Array[String]
511
525
  def actual_root_dirs
512
526
  root_dirs.reject do |root_dir, _namespace|
513
527
  !dir?(root_dir) || ignored_paths.member?(root_dir)
514
528
  end
515
529
  end
516
530
 
517
- # @param dir [String]
518
- # @param parent [Module]
519
- # @return [void]
531
+ # @sig (String, Module) -> void
520
532
  def set_autoloads_in_dir(dir, parent)
521
533
  ls(dir) do |basename, abspath|
522
534
  begin
@@ -559,10 +571,7 @@ module Zeitwerk
559
571
  end
560
572
  end
561
573
 
562
- # @param parent [Module]
563
- # @param cname [Symbol]
564
- # @param subdir [String]
565
- # @return [void]
574
+ # @sig (Module, Symbol, String) -> void
566
575
  def autoload_subdir(parent, cname, subdir)
567
576
  if autoload_path = autoload_for?(parent, cname)
568
577
  cpath = cpath(parent, cname)
@@ -582,10 +591,7 @@ module Zeitwerk
582
591
  end
583
592
  end
584
593
 
585
- # @param parent [Module]
586
- # @param cname [Symbol]
587
- # @param file [String]
588
- # @return [void]
594
+ # @sig (Module, Symbol, String) -> void
589
595
  def autoload_file(parent, cname, file)
590
596
  if autoload_path = autoload_for?(parent, cname)
591
597
  # First autoload for a Ruby file wins, just ignore subsequent ones.
@@ -606,11 +612,10 @@ module Zeitwerk
606
612
  end
607
613
  end
608
614
 
609
- # @param dir [String] directory that would have autovivified a module
610
- # @param file [String] the file where the namespace is explicitly defined
611
- # @param parent [Module]
612
- # @param cname [Symbol]
613
- # @return [void]
615
+ # `dir` is the directory that would have autovivified a namespace. `file` is
616
+ # the file where we've found the namespace is explicitly defined.
617
+ #
618
+ # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
614
619
  def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
615
620
  autoloads.delete(dir)
616
621
  Registry.unregister_autoload(dir)
@@ -619,10 +624,7 @@ module Zeitwerk
619
624
  register_explicit_namespace(cpath(parent, cname))
620
625
  end
621
626
 
622
- # @param parent [Module]
623
- # @param cname [Symbol]
624
- # @param abspath [String]
625
- # @return [void]
627
+ # @sig (Module, Symbol, String) -> void
626
628
  def set_autoload(parent, cname, abspath)
627
629
  # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
628
630
  # real path to be able to delete it from $LOADED_FEATURES on unload, and to
@@ -649,9 +651,7 @@ module Zeitwerk
649
651
  end
650
652
  end
651
653
 
652
- # @param parent [Module]
653
- # @param cname [Symbol]
654
- # @return [String, nil]
654
+ # @sig (Module, Symbol) -> String?
655
655
  def autoload_for?(parent, cname)
656
656
  strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
657
657
  end
@@ -672,9 +672,7 @@ module Zeitwerk
672
672
  #
673
673
  # We need a way to strictly check in parent ignoring ancestors.
674
674
  #
675
- # @param parent [Module]
676
- # @param cname [Symbol]
677
- # @return [String, nil]
675
+ # @sig (Module, Symbol) -> String?
678
676
  if method(:autoload?).arity == 1
679
677
  def strict_autoload_path(parent, cname)
680
678
  parent.autoload?(cname) if cdef?(parent, cname)
@@ -688,15 +686,14 @@ module Zeitwerk
688
686
  # This method is called this way because I prefer `preload` to be the method
689
687
  # name to configure preloads in the public interface.
690
688
  #
691
- # @return [void]
689
+ # @sig () -> void
692
690
  def do_preload
693
691
  preloads.each do |abspath|
694
692
  do_preload_abspath(abspath)
695
693
  end
696
694
  end
697
695
 
698
- # @param abspath [String]
699
- # @return [void]
696
+ # @sig (String) -> void
700
697
  def do_preload_abspath(abspath)
701
698
  if ruby?(abspath)
702
699
  do_preload_file(abspath)
@@ -705,31 +702,25 @@ module Zeitwerk
705
702
  end
706
703
  end
707
704
 
708
- # @param dir [String]
709
- # @return [void]
705
+ # @sig (String) -> void
710
706
  def do_preload_dir(dir)
711
707
  ls(dir) do |_basename, abspath|
712
708
  do_preload_abspath(abspath)
713
709
  end
714
710
  end
715
711
 
716
- # @param file [String]
717
- # @return [Boolean]
712
+ # @sig (String) -> bool
718
713
  def do_preload_file(file)
719
714
  log("preloading #{file}") if logger
720
715
  require file
721
716
  end
722
717
 
723
- # @param parent [Module]
724
- # @param cname [Symbol]
725
- # @return [String]
718
+ # @sig (Module, Symbol) -> String
726
719
  def cpath(parent, cname)
727
720
  parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
728
721
  end
729
722
 
730
- # @param dir [String]
731
- # @yieldparam path [String, String]
732
- # @return [void]
723
+ # @sig (String) { (String, String) -> void } -> void
733
724
  def ls(dir)
734
725
  Dir.foreach(dir) do |basename|
735
726
  next if basename.start_with?(".")
@@ -743,57 +734,55 @@ module Zeitwerk
743
734
  end
744
735
  end
745
736
 
746
- # @param path [String]
747
- # @return [Boolean]
737
+ # @sig (String) -> bool
748
738
  def ruby?(path)
749
739
  path.end_with?(".rb")
750
740
  end
751
741
 
752
- # @param path [String]
753
- # @return [Boolean]
742
+ # @sig (String) -> bool
754
743
  def dir?(path)
755
744
  File.directory?(path)
756
745
  end
757
746
 
758
- # @param paths [<String, Pathname, <String, Pathname>>]
759
- # @return [<String>]
747
+ # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
760
748
  def expand_paths(paths)
761
749
  paths.flatten.map! { |path| File.expand_path(path) }
762
750
  end
763
751
 
764
- # @param glob_patterns [<String>]
765
- # @return [<String>]
752
+ # @sig (Array[String]) -> Array[String]
766
753
  def expand_glob_patterns(glob_patterns)
767
754
  # Note that Dir.glob works with regular file names just fine. That is,
768
755
  # glob patterns technically need no wildcards.
769
756
  glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
770
757
  end
771
758
 
772
- # @return [void]
759
+ # @sig () -> void
773
760
  def recompute_ignored_paths
774
761
  ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
775
762
  end
776
763
 
777
- # @return [void]
764
+ # @sig () -> void
778
765
  def recompute_collapse_dirs
779
766
  collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
780
767
  end
781
768
 
782
- # @param message [String]
783
- # @return [void]
769
+ # @sig (String) -> void
784
770
  def log(message)
785
771
  method_name = logger.respond_to?(:debug) ? :debug : :call
786
772
  logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
787
773
  end
788
774
 
775
+ # @sig (Module, Symbol) -> bool
789
776
  def cdef?(parent, cname)
790
777
  parent.const_defined?(cname, false)
791
778
  end
792
779
 
780
+ # @sig (String) -> void
793
781
  def register_explicit_namespace(cpath)
794
782
  ExplicitNamespace.register(cpath, self)
795
783
  end
796
784
 
785
+ # @sig (String) -> void
797
786
  def raise_if_conflicting_directory(dir)
798
787
  self.class.mutex.synchronize do
799
788
  Registry.loaders.each do |loader|
@@ -808,19 +797,15 @@ module Zeitwerk
808
797
  end
809
798
  end
810
799
 
811
- # @param parent [Module]
812
- # @param cname [Symbol]
813
- # @return [void]
800
+ # @sig (Module, Symbol) -> void
814
801
  def unload_autoload(parent, cname)
815
- parent.send(:remove_const, cname)
802
+ parent.__send__(:remove_const, cname)
816
803
  log("autoload for #{cpath(parent, cname)} removed") if logger
817
804
  end
818
805
 
819
- # @param parent [Module]
820
- # @param cname [Symbol]
821
- # @return [void]
806
+ # @sig (Module, Symbol) -> void
822
807
  def unload_cref(parent, cname)
823
- parent.send(:remove_const, cname)
808
+ parent.__send__(:remove_const, cname)
824
809
  log("#{cpath(parent, cname)} unloaded") if logger
825
810
  end
826
811
  end
@@ -4,26 +4,28 @@ module Zeitwerk::Loader::Callbacks
4
4
  # Invoked from our decorated Kernel#require when a managed file is autoloaded.
5
5
  #
6
6
  # @private
7
- # @param file [String]
8
- # @return [void]
7
+ # @sig (String) -> void
9
8
  def on_file_autoloaded(file)
10
- cref = autoloads.delete(file)
11
- to_unload[cpath(*cref)] = [file, cref] if reloading_enabled?
9
+ cref = autoloads.delete(file)
10
+ cpath = cpath(*cref)
11
+
12
+ to_unload[cpath] = [file, cref] if reloading_enabled?
12
13
  Zeitwerk::Registry.unregister_autoload(file)
13
14
 
14
15
  if logger && cdef?(*cref)
15
- log("constant #{cpath(*cref)} loaded from file #{file}")
16
+ log("constant #{cpath} loaded from file #{file}")
16
17
  elsif !cdef?(*cref)
17
- raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath(*cref)}, but didn't", cref.last)
18
+ raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
18
19
  end
20
+
21
+ run_on_load_callbacks(cpath)
19
22
  end
20
23
 
21
24
  # Invoked from our decorated Kernel#require when a managed directory is
22
25
  # autoloaded.
23
26
  #
24
27
  # @private
25
- # @param dir [String]
26
- # @return [void]
28
+ # @sig (String) -> void
27
29
  def on_dir_autoloaded(dir)
28
30
  # Module#autoload does not serialize concurrent requires, and we handle
29
31
  # directories ourselves, so the callback needs to account for concurrency.
@@ -39,9 +41,10 @@ module Zeitwerk::Loader::Callbacks
39
41
  mutex2.synchronize do
40
42
  if cref = autoloads.delete(dir)
41
43
  autovivified_module = cref[0].const_set(cref[1], Module.new)
42
- log("module #{autovivified_module.name} autovivified from directory #{dir}") if logger
44
+ cpath = autovivified_module.name
45
+ log("module #{cpath} autovivified from directory #{dir}") if logger
43
46
 
44
- to_unload[autovivified_module.name] = [dir, cref] if reloading_enabled?
47
+ to_unload[cpath] = [dir, cref] if reloading_enabled?
45
48
 
46
49
  # We don't unregister `dir` in the registry because concurrent threads
47
50
  # wouldn't find a loader associated to it in Kernel#require and would
@@ -50,6 +53,8 @@ module Zeitwerk::Loader::Callbacks
50
53
  autoloaded_dirs << dir
51
54
 
52
55
  on_namespace_loaded(autovivified_module)
56
+
57
+ run_on_load_callbacks(cpath)
53
58
  end
54
59
  end
55
60
  end
@@ -59,8 +64,7 @@ module Zeitwerk::Loader::Callbacks
59
64
  # subdirectories, we descend into them now.
60
65
  #
61
66
  # @private
62
- # @param namespace [Module]
63
- # @return [void]
67
+ # @sig (Module) -> void
64
68
  def on_namespace_loaded(namespace)
65
69
  if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
66
70
  subdirs.each do |subdir|
@@ -68,4 +72,15 @@ module Zeitwerk::Loader::Callbacks
68
72
  end
69
73
  end
70
74
  end
75
+
76
+ private
77
+
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
+
83
+ callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
84
+ callbacks.each(&:call) if callbacks
85
+ end
71
86
  end
@@ -7,8 +7,7 @@ module Zeitwerk::RealModName
7
7
  #
8
8
  # The name method can be overridden, hence the indirection in this method.
9
9
  #
10
- # @param mod [Class, Module]
11
- # @return [String, nil]
10
+ # @sig (Module) -> String?
12
11
  if UnboundMethod.method_defined?(:bind_call)
13
12
  def real_mod_name(mod)
14
13
  UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
@@ -7,14 +7,14 @@ module Zeitwerk
7
7
  # them from being garbage collected.
8
8
  #
9
9
  # @private
10
- # @return [<Zeitwerk::Loader>]
10
+ # @sig Array[Zeitwerk::Loader]
11
11
  attr_reader :loaders
12
12
 
13
13
  # Registers loaders created with `for_gem` to make the method idempotent
14
14
  # in case of reload.
15
15
  #
16
16
  # @private
17
- # @return [{String => Zeitwerk::Loader}]
17
+ # @sig Hash[String, Zeitwerk::Loader]
18
18
  attr_reader :loaders_managing_gems
19
19
 
20
20
  # Maps real paths to the loaders responsible for them.
@@ -23,7 +23,7 @@ module Zeitwerk
23
23
  # invoke callbacks and autovivify modules.
24
24
  #
25
25
  # @private
26
- # @return [{String => Zeitwerk::Loader}]
26
+ # @sig Hash[String, Zeitwerk::Loader]
27
27
  attr_reader :autoloads
28
28
 
29
29
  # This hash table addresses an edge case in which an autoload is ignored.
@@ -62,14 +62,13 @@ module Zeitwerk
62
62
  # end
63
63
  #
64
64
  # @private
65
- # @return [{String => (String, Zeitwerk::Loader)}]
65
+ # @sig Hash[String, [String, Zeitwerk::Loader]]
66
66
  attr_reader :inceptions
67
67
 
68
68
  # Registers a loader.
69
69
  #
70
70
  # @private
71
- # @param loader [Zeitwerk::Loader]
72
- # @return [void]
71
+ # @sig (Zeitwerk::Loader) -> void
73
72
  def register_loader(loader)
74
73
  loaders << loader
75
74
  end
@@ -78,8 +77,7 @@ module Zeitwerk
78
77
  # file. That is how Zeitwerk::Loader.for_gem is idempotent.
79
78
  #
80
79
  # @private
81
- # @param root_file [String]
82
- # @return [Zeitwerk::Loader]
80
+ # @sig (String) -> Zeitwerk::Loader
83
81
  def loader_for_gem(root_file)
84
82
  loaders_managing_gems[root_file] ||= begin
85
83
  Loader.new.tap do |loader|
@@ -91,32 +89,25 @@ module Zeitwerk
91
89
  end
92
90
 
93
91
  # @private
94
- # @param loader [Zeitwerk::Loader]
95
- # @param realpath [String]
96
- # @return [void]
92
+ # @sig (Zeitwerk::Loader, String) -> String
97
93
  def register_autoload(loader, realpath)
98
94
  autoloads[realpath] = loader
99
95
  end
100
96
 
101
97
  # @private
102
- # @param realpath [String]
103
- # @return [void]
98
+ # @sig (String) -> void
104
99
  def unregister_autoload(realpath)
105
100
  autoloads.delete(realpath)
106
101
  end
107
102
 
108
103
  # @private
109
- # @param cpath [String]
110
- # @param realpath [String]
111
- # @param loader [Zeitwerk::Loader]
112
- # @return [void]
104
+ # @sig (String, String, Zeitwerk::Loader) -> void
113
105
  def register_inception(cpath, realpath, loader)
114
106
  inceptions[cpath] = [realpath, loader]
115
107
  end
116
108
 
117
109
  # @private
118
- # @param cpath [String]
119
- # @return [String, nil]
110
+ # @sig (String) -> String?
120
111
  def inception?(cpath)
121
112
  if pair = inceptions[cpath]
122
113
  pair.first
@@ -124,15 +115,13 @@ module Zeitwerk
124
115
  end
125
116
 
126
117
  # @private
127
- # @param path [String]
128
- # @return [Zeitwerk::Loader, nil]
118
+ # @sig (String) -> Zeitwerk::Loader?
129
119
  def loader_for(path)
130
120
  autoloads[path]
131
121
  end
132
122
 
133
123
  # @private
134
- # @param loader [Zeitwerk::Loader]
135
- # @return [void]
124
+ # @sig (Zeitwerk::Loader) -> void
136
125
  def on_unload(loader)
137
126
  autoloads.delete_if { |_path, object| object == loader }
138
127
  inceptions.delete_if { |_cpath, (_path, object)| object == loader }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.4.0"
4
+ VERSION = "2.4.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-14 00:00:00.000000000 Z
11
+ date: 2020-11-27 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -41,7 +41,7 @@ metadata:
41
41
  changelog_uri: https://github.com/fxn/zeitwerk/blob/master/CHANGELOG.md
42
42
  source_code_uri: https://github.com/fxn/zeitwerk
43
43
  bug_tracker_uri: https://github.com/fxn/zeitwerk/issues
44
- post_install_message:
44
+ post_install_message:
45
45
  rdoc_options: []
46
46
  require_paths:
47
47
  - lib
@@ -57,7 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
57
  version: '0'
58
58
  requirements: []
59
59
  rubygems_version: 3.1.2
60
- signing_key:
60
+ signing_key:
61
61
  specification_version: 4
62
62
  summary: Efficient and thread-safe constant autoloader
63
63
  test_files: []