zeitwerk 2.6.8 → 2.7.5

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