zeitwerk 2.6.7 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
3
4
  require "set"
4
5
 
5
6
  module Zeitwerk
@@ -17,109 +18,119 @@ module Zeitwerk
17
18
  include Config
18
19
  include EagerLoad
19
20
 
20
- MUTEX = Mutex.new
21
+ MUTEX = Mutex.new #: Mutex
21
22
  private_constant :MUTEX
22
23
 
23
24
  # Maps absolute paths for which an autoload has been set ---and not
24
- # executed--- to their corresponding parent class or module and constant
25
- # name.
25
+ # executed--- to their corresponding Zeitwerk::Cref object.
26
26
  #
27
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
28
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
27
+ # "/Users/fxn/blog/app/models/user.rb" => #<Zeitwerk::Cref:... @mod=Object, @cname=:User, ...>,
28
+ # "/Users/fxn/blog/app/models/hotel/pricing.rb" => #<Zeitwerk::Cref:... @mod=Hotel, @cname=:Pricing, ...>,
29
29
  # ...
30
30
  #
31
- # @sig Hash[String, [Module, Symbol]]
31
+ #: Hash[String, Zeitwerk::Cref]
32
32
  attr_reader :autoloads
33
33
  internal :autoloads
34
34
 
35
+ # When the path passed to Module#autoload is in the stack of features being
36
+ # loaded at the moment, Ruby passes. For example, Module#autoload? returns
37
+ # `nil` even if the autoload has not been attempted. See
38
+ #
39
+ # https://bugs.ruby-lang.org/issues/21035
40
+ #
41
+ # We call these "inceptions".
42
+ #
43
+ # A common case is the entry point of gems managed by Zeitwerk. Their main
44
+ # file is normally required and, while doing so, the loader sets an autoload
45
+ # on the gem namespace. That autoload hits this edge case.
46
+ #
47
+ # There is some logic that neeeds to know if an autoload for a given
48
+ # constant already exists. We check Module#autoload? first, and fallback to
49
+ # the inceptions just in case.
50
+ #
51
+ # This map keeps track of pairs (cref, autoload_path) found by the loader.
52
+ # The object Zeitwerk::Registry.inceptions, on the other hand, acts as a
53
+ # global registry for them.
54
+ #
55
+ #: Zeitwerk::Cref::Map[String]
56
+ attr_reader :inceptions
57
+ internal :inceptions
58
+
35
59
  # We keep track of autoloaded directories to remove them from the registry
36
60
  # at the end of eager loading.
37
61
  #
38
62
  # Files are removed as they are autoloaded, but directories need to wait due
39
63
  # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
40
64
  #
41
- # @sig Array[String]
65
+ #: Array[String]
42
66
  attr_reader :autoloaded_dirs
43
67
  internal :autoloaded_dirs
44
68
 
45
- # Stores metadata needed for unloading. Its entries look like this:
46
- #
47
- # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
48
- #
49
- # The cpath as key helps implementing unloadable_cpath? The file name is
50
- # stored in order to be able to delete it from $LOADED_FEATURES, and the
51
- # pair [Module, Symbol] is used to remove_const the constant from the class
52
- # or module object.
69
+ # If reloading is enabled, this collection maps autoload paths to their
70
+ # autoloaded crefs.
53
71
  #
54
- # If reloading is enabled, this hash is filled as constants are autoloaded
55
- # or eager loaded. Otherwise, the collection remains empty.
72
+ # On unload, the autoload paths are passed to callbacks, files deleted from
73
+ # $LOADED_FEATURES, and the crefs are deleted.
56
74
  #
57
- # @sig Hash[String, [String, [Module, Symbol]]]
75
+ #: Hash[String, Zeitwerk::Cref]
58
76
  attr_reader :to_unload
59
77
  internal :to_unload
60
78
 
61
- # Maps namespace constant paths to their respective directories.
62
- #
63
- # For example, given this mapping:
64
- #
65
- # "Admin" => [
66
- # "/Users/fxn/blog/app/controllers/admin",
67
- # "/Users/fxn/blog/app/models/admin",
68
- # ...
69
- # ]
79
+ # Maps namespace crefs to the directories that conform the namespace.
70
80
  #
71
- # when `Admin` gets defined we know that it plays the role of a namespace
72
- # and that its children are spread over those directories. We'll visit them
73
- # to set up the corresponding autoloads.
81
+ # When these crefs get defined we know their children are spread over those
82
+ # directories. We'll visit them to set up the corresponding autoloads.
74
83
  #
75
- # @sig Hash[String, Array[String]]
84
+ #: Zeitwerk::Cref::Map[String]
76
85
  attr_reader :namespace_dirs
77
86
  internal :namespace_dirs
78
87
 
79
88
  # A shadowed file is a file managed by this loader that is ignored when
80
89
  # setting autoloads because its matching constant is already taken.
81
90
  #
82
- # This private set is populated as we descend. For example, if the loader
83
- # has only scanned the top-level, `shadowed_files` does not have shadowed
84
- # files that may exist deep in the project tree yet.
91
+ # This private set is populated lazily, as we descend. For example, if the
92
+ # loader has only scanned the top-level, `shadowed_files` does not have the
93
+ # shadowed files that may exist deep in the project tree.
85
94
  #
86
- # @sig Set[String]
95
+ #: Set[String]
87
96
  attr_reader :shadowed_files
88
97
  internal :shadowed_files
89
98
 
90
- # @sig Mutex
99
+ #: Mutex
91
100
  attr_reader :mutex
92
101
  private :mutex
93
102
 
94
- # @sig Mutex
95
- attr_reader :mutex2
96
- private :mutex2
103
+ #: Monitor
104
+ attr_reader :dirs_autoload_monitor
105
+ private :dirs_autoload_monitor
97
106
 
98
107
  def initialize
99
108
  super
100
109
 
101
110
  @autoloads = {}
111
+ @inceptions = Zeitwerk::Cref::Map.new
102
112
  @autoloaded_dirs = []
103
113
  @to_unload = {}
104
- @namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
114
+ @namespace_dirs = Zeitwerk::Cref::Map.new
105
115
  @shadowed_files = Set.new
106
- @mutex = Mutex.new
107
- @mutex2 = Mutex.new
108
116
  @setup = false
109
117
  @eager_loaded = false
110
118
 
111
- Registry.register_loader(self)
119
+ @mutex = Mutex.new
120
+ @dirs_autoload_monitor = Monitor.new
121
+
122
+ Registry.loaders.register(self)
112
123
  end
113
124
 
114
125
  # Sets autoloads in the root namespaces.
115
126
  #
116
- # @sig () -> void
127
+ #: () -> void
117
128
  def setup
118
129
  mutex.synchronize do
119
130
  break if @setup
120
131
 
121
132
  actual_roots.each do |root_dir, root_namespace|
122
- set_autoloads_in_dir(root_dir, root_namespace)
133
+ define_autoloads_for_dir(root_dir, root_namespace)
123
134
  end
124
135
 
125
136
  on_setup_callbacks.each(&:call)
@@ -139,7 +150,7 @@ module Zeitwerk
139
150
  # means `unload` + `setup`. This one is available to be used together with
140
151
  # `unregister`, which is undocumented too.
141
152
  #
142
- # @sig () -> void
153
+ #: () -> void
143
154
  def unload
144
155
  mutex.synchronize do
145
156
  raise SetupRequired unless @setup
@@ -152,32 +163,32 @@ module Zeitwerk
152
163
  # is enough.
153
164
  unloaded_files = Set.new
154
165
 
155
- autoloads.each do |abspath, (parent, cname)|
156
- if parent.autoload?(cname)
157
- unload_autoload(parent, cname)
166
+ autoloads.each do |abspath, cref|
167
+ if cref.autoload?
168
+ unload_autoload(cref)
158
169
  else
159
170
  # Could happen if loaded with require_relative. That is unsupported,
160
171
  # and the constant path would escape unloadable_cpath? This is just
161
172
  # defensive code to clean things up as much as we are able to.
162
- unload_cref(parent, cname)
173
+ unload_cref(cref)
163
174
  unloaded_files.add(abspath) if ruby?(abspath)
164
175
  end
165
176
  end
166
177
 
167
- to_unload.each do |cpath, (abspath, (parent, cname))|
178
+ to_unload.each do |abspath, cref|
168
179
  unless on_unload_callbacks.empty?
169
180
  begin
170
- value = cget(parent, cname)
181
+ value = cref.get
171
182
  rescue ::NameError
172
183
  # Perhaps the user deleted the constant by hand, or perhaps an
173
184
  # autoload failed to define the expected constant but the user
174
185
  # rescued the exception.
175
186
  else
176
- run_on_unload_callbacks(cpath, value, abspath)
187
+ run_on_unload_callbacks(cref, value, abspath)
177
188
  end
178
189
  end
179
190
 
180
- unload_cref(parent, cname)
191
+ unload_cref(cref)
181
192
  unloaded_files.add(abspath) if ruby?(abspath)
182
193
  end
183
194
 
@@ -202,8 +213,10 @@ module Zeitwerk
202
213
  namespace_dirs.clear
203
214
  shadowed_files.clear
204
215
 
205
- Registry.on_unload(self)
206
- ExplicitNamespace.__unregister_loader(self)
216
+ unregister_inceptions
217
+ unregister_explicit_namespaces
218
+
219
+ Registry.autoloads.unregister_loader(self)
207
220
 
208
221
  @setup = false
209
222
  @eager_loaded = false
@@ -216,8 +229,7 @@ module Zeitwerk
216
229
  # This method is not thread-safe, please see how this can be achieved by
217
230
  # client code in the README of the project.
218
231
  #
219
- # @raise [Zeitwerk::Error]
220
- # @sig () -> void
232
+ #: () -> void ! Zeitwerk::Error
221
233
  def reload
222
234
  raise ReloadingDisabledError unless reloading_enabled?
223
235
  raise SetupRequired unless @setup
@@ -228,35 +240,125 @@ module Zeitwerk
228
240
  setup
229
241
  end
230
242
 
243
+ # Returns a hash that maps the absolute paths of the managed files and
244
+ # directories to their respective expected constant paths.
245
+ #
246
+ #: () -> Hash[String, String]
247
+ def all_expected_cpaths
248
+ result = {}
249
+
250
+ actual_roots.each do |root_dir, root_namespace|
251
+ queue = [[root_dir, real_mod_name(root_namespace)]]
252
+
253
+ while (dir, cpath = queue.shift)
254
+ result[dir] = cpath
255
+
256
+ prefix = cpath == "Object" ? "" : cpath + "::"
257
+
258
+ ls(dir) do |basename, abspath, ftype|
259
+ if ftype == :file
260
+ basename.delete_suffix!(".rb")
261
+ result[abspath] = prefix + inflector.camelize(basename, abspath)
262
+ else
263
+ if collapse?(abspath)
264
+ queue << [abspath, cpath]
265
+ else
266
+ queue << [abspath, prefix + inflector.camelize(basename, abspath)]
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+
273
+ result
274
+ end
275
+
276
+ #: (String | Pathname) -> String?
277
+ def cpath_expected_at(path)
278
+ abspath = File.expand_path(path)
279
+
280
+ raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
281
+
282
+ return unless dir?(abspath) || ruby?(abspath)
283
+ return if ignored_path?(abspath)
284
+
285
+ paths = []
286
+
287
+ if ruby?(abspath)
288
+ basename = File.basename(abspath, ".rb")
289
+ return if hidden?(basename)
290
+
291
+ paths << [basename, abspath]
292
+ walk_up_from = File.dirname(abspath)
293
+ else
294
+ walk_up_from = abspath
295
+ end
296
+
297
+ root_namespace = nil
298
+
299
+ walk_up(walk_up_from) do |dir|
300
+ break if root_namespace = roots[dir]
301
+ return if ignored_path?(dir)
302
+
303
+ basename = File.basename(dir)
304
+ return if hidden?(basename)
305
+
306
+ paths << [basename, dir] unless collapse?(dir)
307
+ end
308
+
309
+ return unless root_namespace
310
+
311
+ if paths.empty?
312
+ real_mod_name(root_namespace)
313
+ else
314
+ cnames = paths.reverse_each.map { cname_for(_1, _2) }
315
+
316
+ if root_namespace == Object
317
+ cnames.join("::")
318
+ else
319
+ "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
320
+ end
321
+ end
322
+ end
323
+
231
324
  # Says if the given constant path would be unloaded on reload. This
232
325
  # predicate returns `false` if reloading is disabled.
233
326
  #
234
- # @sig (String) -> bool
327
+ # This is an undocumented method that I wrote to help transition from the
328
+ # classic autoloader in Rails. Its usage was removed from Rails in 7.0.
329
+ #
330
+ #: (String) -> bool
235
331
  def unloadable_cpath?(cpath)
236
- to_unload.key?(cpath)
332
+ unloadable_cpaths.include?(cpath)
237
333
  end
238
334
 
239
335
  # Returns an array with the constant paths that would be unloaded on reload.
240
336
  # This predicate returns an empty array if reloading is disabled.
241
337
  #
242
- # @sig () -> Array[String]
338
+ # This is an undocumented method that I wrote to help transition from the
339
+ # classic autoloader in Rails. Its usage was removed from Rails in 7.0.
340
+ #
341
+ #: () -> Array[String]
243
342
  def unloadable_cpaths
244
- to_unload.keys.freeze
343
+ to_unload.values.map(&:path)
245
344
  end
246
345
 
247
346
  # This is a dangerous method.
248
347
  #
249
348
  # @experimental
250
- # @sig () -> void
349
+ #: () -> void
251
350
  def unregister
351
+ unregister_inceptions
352
+ unregister_explicit_namespaces
353
+ Registry.loaders.unregister(self)
354
+ Registry.autoloads.unregister_loader(self)
252
355
  Registry.unregister_loader(self)
253
- ExplicitNamespace.__unregister_loader(self)
254
356
  end
255
357
 
256
358
  # The return value of this predicate is only meaningful if the loader has
257
359
  # scanned the file. This is the case in the spots where we use it.
258
360
  #
259
- # @sig (String) -> Boolean
361
+ #: (String) -> bool
260
362
  internal def shadowed_file?(file)
261
363
  shadowed_files.member?(file)
262
364
  end
@@ -264,12 +366,15 @@ module Zeitwerk
264
366
  # --- Class methods ---------------------------------------------------------------------------
265
367
 
266
368
  class << self
267
- # @sig #call | #debug | nil
369
+ include RealModName
370
+
371
+ #: call(String) -> void | debug(String) -> void | nil
268
372
  attr_accessor :default_logger
269
373
 
270
374
  # This is a shortcut for
271
375
  #
272
376
  # require "zeitwerk"
377
+ #
273
378
  # loader = Zeitwerk::Loader.new
274
379
  # loader.tag = File.basename(__FILE__, ".rb")
275
380
  # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
@@ -281,16 +386,45 @@ module Zeitwerk
281
386
  # This method returns a subclass of Zeitwerk::Loader, but the exact type
282
387
  # is private, client code can only rely on the interface.
283
388
  #
284
- # @sig (bool) -> Zeitwerk::GemLoader
389
+ #: (?warn_on_extra_files: boolish) -> Zeitwerk::GemLoader
285
390
  def for_gem(warn_on_extra_files: true)
286
391
  called_from = caller_locations(1, 1).first.path
287
- Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
392
+ Registry.loader_for_gem(called_from, namespace: Object, warn_on_extra_files: warn_on_extra_files)
393
+ end
394
+
395
+ # This is a shortcut for
396
+ #
397
+ # require "zeitwerk"
398
+ #
399
+ # loader = Zeitwerk::Loader.new
400
+ # loader.tag = namespace.name + "-" + File.basename(__FILE__, ".rb")
401
+ # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
402
+ # loader.push_dir(__dir__, namespace: namespace)
403
+ #
404
+ # except that this method returns the same object in subsequent calls from
405
+ # the same file, in the unlikely case the gem wants to be able to reload.
406
+ #
407
+ # This method returns a subclass of Zeitwerk::Loader, but the exact type
408
+ # is private, client code can only rely on the interface.
409
+ #
410
+ #: (Module) -> Zeitwerk::GemLoader
411
+ def for_gem_extension(namespace)
412
+ unless namespace.is_a?(Module) # Note that Class < Module.
413
+ raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
414
+ end
415
+
416
+ unless real_mod_name(namespace)
417
+ raise Zeitwerk::Error, "extending anonymous namespaces is unsupported"
418
+ end
419
+
420
+ called_from = caller_locations(1, 1).first.path
421
+ Registry.loader_for_gem(called_from, namespace: namespace, warn_on_extra_files: false)
288
422
  end
289
423
 
290
424
  # Broadcasts `eager_load` to all loaders. Those that have not been setup
291
425
  # are skipped.
292
426
  #
293
- # @sig () -> void
427
+ #: () -> void
294
428
  def eager_load_all
295
429
  Registry.loaders.each do |loader|
296
430
  begin
@@ -304,7 +438,7 @@ module Zeitwerk
304
438
  # Broadcasts `eager_load_namespace` to all loaders. Those that have not
305
439
  # been setup are skipped.
306
440
  #
307
- # @sig (Module) -> void
441
+ #: (Module) -> void
308
442
  def eager_load_namespace(mod)
309
443
  Registry.loaders.each do |loader|
310
444
  begin
@@ -318,51 +452,37 @@ module Zeitwerk
318
452
  # Returns an array with the absolute paths of the root directories of all
319
453
  # registered loaders. This is a read-only collection.
320
454
  #
321
- # @sig () -> Array[String]
455
+ #: () -> Array[String]
322
456
  def all_dirs
323
- Registry.loaders.flat_map(&:dirs).freeze
457
+ dirs = []
458
+ Registry.loaders.each do |loader|
459
+ dirs.concat(loader.dirs)
460
+ end
461
+ dirs.freeze
324
462
  end
325
463
  end
326
464
 
327
- # @sig (String, Module) -> void
328
- private def set_autoloads_in_dir(dir, parent)
329
- ls(dir) do |basename, abspath|
330
- begin
331
- if ruby?(basename)
332
- basename.delete_suffix!(".rb")
333
- cname = inflector.camelize(basename, abspath).to_sym
334
- autoload_file(parent, cname, abspath)
465
+ #: (String, Module) -> void
466
+ private def define_autoloads_for_dir(dir, parent)
467
+ ls(dir) do |basename, abspath, ftype|
468
+ if ftype == :file
469
+ basename.delete_suffix!(".rb")
470
+ cref = Cref.new(parent, cname_for(basename, abspath))
471
+ autoload_file(cref, abspath)
472
+ else
473
+ if collapse?(abspath)
474
+ define_autoloads_for_dir(abspath, parent)
335
475
  else
336
- if collapse?(abspath)
337
- set_autoloads_in_dir(abspath, parent)
338
- else
339
- cname = inflector.camelize(basename, abspath).to_sym
340
- autoload_subdir(parent, cname, abspath)
341
- end
476
+ cref = Cref.new(parent, cname_for(basename, abspath))
477
+ autoload_subdir(cref, abspath)
342
478
  end
343
- rescue ::NameError => error
344
- path_type = ruby?(abspath) ? "file" : "directory"
345
-
346
- raise NameError.new(<<~MESSAGE, error.name)
347
- #{error.message} inferred by #{inflector.class} from #{path_type}
348
-
349
- #{abspath}
350
-
351
- Possible ways to address this:
352
-
353
- * Tell Zeitwerk to ignore this particular #{path_type}.
354
- * Tell Zeitwerk to ignore one of its parent directories.
355
- * Rename the #{path_type} to comply with the naming conventions.
356
- * Modify the inflector to handle this case.
357
- MESSAGE
358
479
  end
359
480
  end
360
481
  end
361
482
 
362
- # @sig (Module, Symbol, String) -> void
363
- private def autoload_subdir(parent, cname, subdir)
364
- if autoload_path = autoload_path_set_by_me_for?(parent, cname)
365
- cpath = cpath(parent, cname)
483
+ #: (Zeitwerk::Cref, String) -> void
484
+ private def autoload_subdir(cref, subdir)
485
+ if autoload_path = autoload_path_set_by_me_for?(cref)
366
486
  if ruby?(autoload_path)
367
487
  # Scanning visited a Ruby file first, and now a directory for the same
368
488
  # constant has been found. This means we are dealing with an explicit
@@ -371,97 +491,110 @@ module Zeitwerk
371
491
  # Registering is idempotent, and we have to keep the autoload pointing
372
492
  # to the file. This may run again if more directories are found later
373
493
  # on, no big deal.
374
- register_explicit_namespace(cpath)
494
+ register_explicit_namespace(cref)
375
495
  end
376
496
  # If the existing autoload points to a file, it has to be preserved, if
377
497
  # not, it is fine as it is. In either case, we do not need to override.
378
498
  # Just remember the subdirectory conforms this namespace.
379
- namespace_dirs[cpath] << subdir
380
- elsif !cdef?(parent, cname)
499
+ namespace_dirs.get_or_set(cref) { [] } << subdir
500
+ elsif !cref.defined?
381
501
  # First time we find this namespace, set an autoload for it.
382
- namespace_dirs[cpath(parent, cname)] << subdir
383
- set_autoload(parent, cname, subdir)
502
+ namespace_dirs.get_or_set(cref) { [] } << subdir
503
+ define_autoload(cref, subdir)
384
504
  else
385
505
  # For whatever reason the constant that corresponds to this namespace has
386
506
  # already been defined, we have to recurse.
387
- log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
388
- set_autoloads_in_dir(subdir, cget(parent, cname))
507
+ log("the namespace #{cref} already exists, descending into #{subdir}") if logger
508
+ define_autoloads_for_dir(subdir, cref.get)
389
509
  end
390
510
  end
391
511
 
392
- # @sig (Module, Symbol, String) -> void
393
- private def autoload_file(parent, cname, file)
394
- if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
512
+ #: (Zeitwerk::Cref, String) -> void
513
+ private def autoload_file(cref, file)
514
+ if autoload_path = cref.autoload? || Registry.inceptions.registered?(cref)
395
515
  # First autoload for a Ruby file wins, just ignore subsequent ones.
396
516
  if ruby?(autoload_path)
397
517
  shadowed_files << file
398
518
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
399
519
  else
400
- promote_namespace_from_implicit_to_explicit(
401
- dir: autoload_path,
402
- file: file,
403
- parent: parent,
404
- cname: cname
405
- )
520
+ promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
406
521
  end
407
- elsif cdef?(parent, cname)
522
+ elsif cref.defined?
408
523
  shadowed_files << file
409
- log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
524
+ log("file #{file} is ignored because #{cref} is already defined") if logger
410
525
  else
411
- set_autoload(parent, cname, file)
526
+ define_autoload(cref, file)
412
527
  end
413
528
  end
414
529
 
415
530
  # `dir` is the directory that would have autovivified a namespace. `file` is
416
531
  # the file where we've found the namespace is explicitly defined.
417
532
  #
418
- # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
419
- private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
533
+ #: (dir: String, file: String, cref: Zeitwerk::Cref) -> void
534
+ private def promote_namespace_from_implicit_to_explicit(dir:, file:, cref:)
420
535
  autoloads.delete(dir)
421
- Registry.unregister_autoload(dir)
536
+ Registry.autoloads.unregister(dir)
422
537
 
423
- log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
538
+ log("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
424
539
 
425
- set_autoload(parent, cname, file)
426
- register_explicit_namespace(cpath(parent, cname))
540
+ # Order matters: When Module#const_added is triggered by the autoload, we
541
+ # don't want the namespace to be registered yet.
542
+ define_autoload(cref, file)
543
+ register_explicit_namespace(cref)
427
544
  end
428
545
 
429
- # @sig (Module, Symbol, String) -> void
430
- private def set_autoload(parent, cname, abspath)
431
- parent.autoload(cname, abspath)
546
+ #: (Zeitwerk::Cref, String) -> void
547
+ private def define_autoload(cref, abspath)
548
+ cref.autoload(abspath)
432
549
 
433
550
  if logger
434
551
  if ruby?(abspath)
435
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
552
+ log("autoload set for #{cref}, to be loaded from #{abspath}")
436
553
  else
437
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
554
+ log("autoload set for #{cref}, to be autovivified from #{abspath}")
438
555
  end
439
556
  end
440
557
 
441
- autoloads[abspath] = [parent, cname]
442
- Registry.register_autoload(self, abspath)
558
+ autoloads[abspath] = cref
559
+ Registry.autoloads.register(abspath, self)
443
560
 
444
- # See why in the documentation of Zeitwerk::Registry.inceptions.
445
- unless parent.autoload?(cname)
446
- Registry.register_inception(cpath(parent, cname), abspath, self)
447
- end
561
+ register_inception(cref, abspath) unless cref.autoload?
448
562
  end
449
563
 
450
- # @sig (Module, Symbol) -> String?
451
- private def autoload_path_set_by_me_for?(parent, cname)
452
- if autoload_path = strict_autoload_path(parent, cname)
564
+ #: (Zeitwerk::Cref) -> String?
565
+ private def autoload_path_set_by_me_for?(cref)
566
+ if autoload_path = cref.autoload?
453
567
  autoload_path if autoloads.key?(autoload_path)
454
568
  else
455
- Registry.inception?(cpath(parent, cname))
569
+ inceptions[cref]
456
570
  end
457
571
  end
458
572
 
459
- # @sig (String) -> void
460
- private def register_explicit_namespace(cpath)
461
- ExplicitNamespace.__register(cpath, self)
573
+ #: (Zeitwerk::Cref) -> void
574
+ private def register_explicit_namespace(cref)
575
+ Registry.explicit_namespaces.register(cref, self)
576
+ end
577
+
578
+ #: () -> void
579
+ private def unregister_explicit_namespaces
580
+ Registry.explicit_namespaces.unregister_loader(self)
581
+ end
582
+
583
+ #: (Zeitwerk::Cref, String) -> void
584
+ private def register_inception(cref, abspath)
585
+ inceptions[cref] = abspath
586
+ Registry.inceptions.register(cref, abspath)
587
+ end
588
+
589
+ #: () -> void
590
+ private def unregister_inceptions
591
+ inceptions.each_key do |cref|
592
+ Registry.inceptions.unregister(cref)
593
+ end
594
+ inceptions.clear
462
595
  end
463
596
 
464
- # @sig (String) -> void
597
+ #: (String) -> void
465
598
  private def raise_if_conflicting_directory(dir)
466
599
  MUTEX.synchronize do
467
600
  dir_slash = dir + "/"
@@ -479,36 +612,35 @@ module Zeitwerk
479
612
  raise Error,
480
613
  "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
481
614
  " which is already managed by\n\n#{loader.pretty_inspect}\n"
482
- EOS
483
615
  end
484
616
  end
485
617
  end
486
618
  end
487
619
  end
488
620
 
489
- # @sig (String, Object, String) -> void
490
- private def run_on_unload_callbacks(cpath, value, abspath)
621
+ #: (String, top, String) -> void
622
+ private def run_on_unload_callbacks(cref, value, abspath)
491
623
  # Order matters. If present, run the most specific one.
492
- on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
493
- on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
624
+ on_unload_callbacks[cref.path]&.each { |c| c.call(value, abspath) }
625
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cref.path, value, abspath) }
494
626
  end
495
627
 
496
- # @sig (Module, Symbol) -> void
497
- private def unload_autoload(parent, cname)
498
- crem(parent, cname)
499
- log("autoload for #{cpath(parent, cname)} removed") if logger
628
+ #: (Zeitwerk::Cref) -> void
629
+ private def unload_autoload(cref)
630
+ cref.remove
631
+ log("autoload for #{cref} removed") if logger
500
632
  end
501
633
 
502
- # @sig (Module, Symbol) -> void
503
- private def unload_cref(parent, cname)
634
+ #: (Zeitwerk::Cref) -> void
635
+ private def unload_cref(cref)
504
636
  # Let's optimistically remove_const. The way we use it, this is going to
505
637
  # succeed always if all is good.
506
- crem(parent, cname)
638
+ cref.remove
507
639
  rescue ::NameError
508
640
  # There are a few edge scenarios in which this may happen. If the constant
509
641
  # is gone, that is OK, anyway.
510
642
  else
511
- log("#{cpath(parent, cname)} unloaded") if logger
643
+ log("#{cref} unloaded") if logger
512
644
  end
513
645
  end
514
646
  end