zeitwerk 2.4.1 → 2.5.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,78 +5,31 @@ require "securerandom"
5
5
 
6
6
  module Zeitwerk
7
7
  class Loader
8
+ require_relative "loader/helpers"
8
9
  require_relative "loader/callbacks"
9
- include Callbacks
10
- include RealModName
11
-
12
- # @sig String
13
- attr_reader :tag
14
-
15
- # @sig #camelize
16
- attr_accessor :inflector
17
-
18
- # @sig #call | #debug | nil
19
- attr_accessor :logger
10
+ require_relative "loader/config"
20
11
 
21
- # Absolute paths of the root directories. Stored in a hash to preserve
22
- # order, easily handle duplicates, and also be able to have a fast lookup,
23
- # needed for detecting nested paths.
24
- #
25
- # "/Users/fxn/blog/app/assets" => true,
26
- # "/Users/fxn/blog/app/channels" => true,
27
- # ...
28
- #
29
- # This is a private collection maintained by the loader. The public
30
- # interface for it is `push_dir` and `dirs`.
31
- #
32
- # @private
33
- # @sig Hash[String, true]
34
- attr_reader :root_dirs
12
+ include RealModName
13
+ include Callbacks
14
+ include Helpers
15
+ include Config
35
16
 
36
- # Absolute paths of files or directories that have to be preloaded.
17
+ # Keeps track of autoloads defined by the loader which have not been
18
+ # executed so far.
37
19
  #
38
- # @private
39
- # @sig Array[String]
40
- attr_reader :preloads
41
-
42
- # Absolute paths of files, directories, or glob patterns to be totally
43
- # ignored.
20
+ # This metadata helps us implement a few things:
44
21
  #
45
- # @private
46
- # @sig Set[String]
47
- attr_reader :ignored_glob_patterns
48
-
49
- # The actual collection of absolute file and directory names at the time the
50
- # ignored glob patterns were expanded. Computed on setup, and recomputed on
51
- # reload.
22
+ # 1. When autoloads are triggered, ensure they define the expected constant
23
+ # and invoke user callbacks. If reloading is enabled, remember cref and
24
+ # abspath for later unloading logic.
52
25
  #
53
- # @private
54
- # @sig Set[String]
55
- attr_reader :ignored_paths
56
-
57
- # Absolute paths of directories or glob patterns to be collapsed.
26
+ # 2. When unloading, remove autoloads that have not been executed.
58
27
  #
59
- # @private
60
- # @sig Set[String]
61
- attr_reader :collapse_glob_patterns
62
-
63
- # The actual collection of absolute directory names at the time the collapse
64
- # glob patterns were expanded. Computed on setup, and recomputed on reload.
28
+ # 3. Eager load with a recursive const_get, rather than a recursive require,
29
+ # for consistency with lazy loading.
65
30
  #
66
31
  # @private
67
- # @sig Set[String]
68
- attr_reader :collapse_dirs
69
-
70
- # Maps real absolute paths for which an autoload has been set ---and not
71
- # executed--- to their corresponding parent class or module and constant
72
- # name.
73
- #
74
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
75
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
76
- # ...
77
- #
78
- # @private
79
- # @sig Hash[String, [Module, Symbol]]
32
+ # @sig Zeitwerk::Autoloads
80
33
  attr_reader :autoloads
81
34
 
82
35
  # We keep track of autoloaded directories to remove them from the registry
@@ -93,8 +46,8 @@ module Zeitwerk
93
46
  #
94
47
  # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
95
48
  #
96
- # The cpath as key helps implementing unloadable_cpath? The real file name
97
- # is stored in order to be able to delete it from $LOADED_FEATURES, and the
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
98
51
  # pair [Module, Symbol] is used to remove_const the constant from the class
99
52
  # or module object.
100
53
  #
@@ -123,12 +76,6 @@ module Zeitwerk
123
76
  # @sig Hash[String, Array[String]]
124
77
  attr_reader :lazy_subdirs
125
78
 
126
- # Absolute paths of files or directories not to be eager loaded.
127
- #
128
- # @private
129
- # @sig Set[String]
130
- attr_reader :eager_load_exclusions
131
-
132
79
  # @private
133
80
  # @sig Mutex
134
81
  attr_reader :mutex
@@ -138,131 +85,21 @@ module Zeitwerk
138
85
  attr_reader :mutex2
139
86
 
140
87
  def initialize
141
- @initialized_at = Time.now
142
-
143
- @tag = SecureRandom.hex(3)
144
- @inflector = Inflector.new
145
- @logger = self.class.default_logger
146
-
147
- @root_dirs = {}
148
- @preloads = []
149
- @ignored_glob_patterns = Set.new
150
- @ignored_paths = Set.new
151
- @collapse_glob_patterns = Set.new
152
- @collapse_dirs = Set.new
153
- @autoloads = {}
154
- @autoloaded_dirs = []
155
- @to_unload = {}
156
- @lazy_subdirs = {}
157
- @eager_load_exclusions = Set.new
158
-
159
- # TODO: find a better name for these mutexes.
160
- @mutex = Mutex.new
161
- @mutex2 = Mutex.new
162
- @setup = false
163
- @eager_loaded = false
164
-
165
- @reloading_enabled = false
166
-
167
- Registry.register_loader(self)
168
- end
169
-
170
- # Sets a tag for the loader, useful for logging.
171
- #
172
- # @param tag [#to_s]
173
- # @sig (#to_s) -> void
174
- def tag=(tag)
175
- @tag = tag.to_s
176
- end
177
-
178
- # Absolute paths of the root directories. This is a read-only collection,
179
- # please push here via `push_dir`.
180
- #
181
- # @sig () -> Array[String]
182
- def dirs
183
- root_dirs.keys.freeze
184
- end
185
-
186
- # Pushes `path` to the list of root directories.
187
- #
188
- # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
189
- # the same process already manages that directory or one of its ascendants
190
- # or descendants.
191
- #
192
- # @raise [Zeitwerk::Error]
193
- # @sig (String | Pathname, Module) -> void
194
- def push_dir(path, namespace: Object)
195
- # Note that Class < Module.
196
- unless namespace.is_a?(Module)
197
- raise Error, "#{namespace.inspect} is not a class or module object, should be"
198
- end
199
-
200
- abspath = File.expand_path(path)
201
- if dir?(abspath)
202
- raise_if_conflicting_directory(abspath)
203
- root_dirs[abspath] = namespace
204
- else
205
- raise Error, "the root directory #{abspath} does not exist"
206
- end
207
- end
208
-
209
- # You need to call this method before setup in order to be able to reload.
210
- # There is no way to undo this, either you want to reload or you don't.
211
- #
212
- # @raise [Zeitwerk::Error]
213
- # @sig () -> void
214
- def enable_reloading
215
- mutex.synchronize do
216
- break if @reloading_enabled
217
-
218
- if @setup
219
- raise Error, "cannot enable reloading after setup"
220
- else
221
- @reloading_enabled = true
222
- end
223
- end
224
- end
88
+ super
225
89
 
226
- # @sig () -> bool
227
- def reloading_enabled?
228
- @reloading_enabled
229
- end
90
+ @autoloads = Autoloads.new
91
+ @autoloaded_dirs = []
92
+ @to_unload = {}
93
+ @lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
94
+ @mutex = Mutex.new
95
+ @mutex2 = Mutex.new
96
+ @setup = false
97
+ @eager_loaded = false
230
98
 
231
- # Files or directories to be preloaded instead of lazy loaded.
232
- #
233
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
234
- def preload(*paths)
235
- mutex.synchronize do
236
- expand_paths(paths).each do |abspath|
237
- preloads << abspath
238
- do_preload_abspath(abspath) if @setup
239
- end
240
- end
241
- end
242
-
243
- # Configure files, directories, or glob patterns to be totally ignored.
244
- #
245
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
246
- def ignore(*glob_patterns)
247
- glob_patterns = expand_paths(glob_patterns)
248
- mutex.synchronize do
249
- ignored_glob_patterns.merge(glob_patterns)
250
- ignored_paths.merge(expand_glob_patterns(glob_patterns))
251
- end
252
- end
253
-
254
- # Configure directories or glob patterns to be collapsed.
255
- #
256
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
257
- def collapse(*glob_patterns)
258
- glob_patterns = expand_paths(glob_patterns)
259
- mutex.synchronize do
260
- collapse_glob_patterns.merge(glob_patterns)
261
- collapse_dirs.merge(expand_glob_patterns(glob_patterns))
262
- end
99
+ Registry.register_loader(self)
263
100
  end
264
101
 
265
- # Sets autoloads in the root namespace and preloads files, if any.
102
+ # Sets autoloads in the root namespace.
266
103
  #
267
104
  # @sig () -> void
268
105
  def setup
@@ -272,7 +109,6 @@ module Zeitwerk
272
109
  actual_root_dirs.each do |root_dir, namespace|
273
110
  set_autoloads_in_dir(root_dir, namespace)
274
111
  end
275
- do_preload
276
112
 
277
113
  @setup = true
278
114
  end
@@ -285,7 +121,10 @@ module Zeitwerk
285
121
  # else, they are eligible for garbage collection, which would effectively
286
122
  # unload them.
287
123
  #
288
- # @private
124
+ # This method is public but undocumented. Main interface is `reload`, which
125
+ # means `unload` + `setup`. This one is avaiable to be used together with
126
+ # `unregister`, which is undocumented too.
127
+ #
289
128
  # @sig () -> void
290
129
  def unload
291
130
  mutex.synchronize do
@@ -297,21 +136,26 @@ module Zeitwerk
297
136
  # is enough.
298
137
  unloaded_files = Set.new
299
138
 
300
- autoloads.each do |realpath, (parent, cname)|
139
+ autoloads.each do |(parent, cname), abspath|
301
140
  if parent.autoload?(cname)
302
141
  unload_autoload(parent, cname)
303
142
  else
304
143
  # Could happen if loaded with require_relative. That is unsupported,
305
144
  # and the constant path would escape unloadable_cpath? This is just
306
145
  # defensive code to clean things up as much as we are able to.
307
- unload_cref(parent, cname) if cdef?(parent, cname)
308
- unloaded_files.add(realpath) if ruby?(realpath)
146
+ unload_cref(parent, cname) if cdef?(parent, cname)
147
+ unloaded_files.add(abspath) if ruby?(abspath)
309
148
  end
310
149
  end
311
150
 
312
- to_unload.each_value do |(realpath, (parent, cname))|
313
- unload_cref(parent, cname) if cdef?(parent, cname)
314
- unloaded_files.add(realpath) if ruby?(realpath)
151
+ to_unload.each do |cpath, (abspath, (parent, cname))|
152
+ unless on_unload_callbacks.empty?
153
+ value = parent.const_get(cname)
154
+ run_on_unload_callbacks(cpath, value, abspath)
155
+ end
156
+
157
+ unload_cref(parent, cname) if cdef?(parent, cname)
158
+ unloaded_files.add(abspath) if ruby?(abspath)
315
159
  end
316
160
 
317
161
  unless unloaded_files.empty?
@@ -335,7 +179,7 @@ module Zeitwerk
335
179
  lazy_subdirs.clear
336
180
 
337
181
  Registry.on_unload(self)
338
- ExplicitNamespace.unregister(self)
182
+ ExplicitNamespace.unregister_loader(self)
339
183
 
340
184
  @setup = false
341
185
  @eager_loaded = false
@@ -371,27 +215,29 @@ module Zeitwerk
371
215
  mutex.synchronize do
372
216
  break if @eager_loaded
373
217
 
218
+ log("eager load start") if logger
219
+
374
220
  queue = []
375
221
  actual_root_dirs.each do |root_dir, namespace|
376
- queue << [namespace, root_dir] unless eager_load_exclusions.member?(root_dir)
222
+ queue << [namespace, root_dir] unless excluded_from_eager_load?(root_dir)
377
223
  end
378
224
 
379
225
  while to_eager_load = queue.shift
380
226
  namespace, dir = to_eager_load
381
227
 
382
228
  ls(dir) do |basename, abspath|
383
- next if eager_load_exclusions.member?(abspath)
229
+ next if excluded_from_eager_load?(abspath)
384
230
 
385
231
  if ruby?(abspath)
386
- if cref = autoloads[File.realpath(abspath)]
387
- cref[0].const_get(cref[1], false)
232
+ if cref = autoloads.cref_for(abspath)
233
+ cget(*cref)
388
234
  end
389
235
  elsif dir?(abspath) && !root_dirs.key?(abspath)
390
- if collapse_dirs.member?(abspath)
236
+ if collapse?(abspath)
391
237
  queue << [namespace, abspath]
392
238
  else
393
239
  cname = inflector.camelize(basename, abspath)
394
- queue << [namespace.const_get(cname, false), abspath]
240
+ queue << [cget(namespace, cname), abspath]
395
241
  end
396
242
  end
397
243
  end
@@ -403,15 +249,9 @@ module Zeitwerk
403
249
  autoloaded_dirs.clear
404
250
 
405
251
  @eager_loaded = true
406
- end
407
- end
408
252
 
409
- # Let eager load ignore the given files or directories. The constants
410
- # defined in those files are still autoloadable.
411
- #
412
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
413
- def do_not_eager_load(*paths)
414
- mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
253
+ log("eager load end") if logger
254
+ end
415
255
  end
416
256
 
417
257
  # Says if the given constant path would be unloaded on reload. This
@@ -430,26 +270,13 @@ module Zeitwerk
430
270
  to_unload.keys.freeze
431
271
  end
432
272
 
433
- # Logs to `$stdout`, handy shortcut for debugging.
273
+ # This is a dangerous method.
434
274
  #
275
+ # @experimental
435
276
  # @sig () -> void
436
- def log!
437
- @logger = ->(msg) { puts msg }
438
- end
439
-
440
- # @private
441
- # @sig (String) -> bool
442
- def manages?(dir)
443
- dir = dir + "/"
444
- ignored_paths.each do |ignored_path|
445
- return false if dir.start_with?(ignored_path + "/")
446
- end
447
-
448
- root_dirs.each_key do |root_dir|
449
- return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
450
- end
451
-
452
- false
277
+ def unregister
278
+ Registry.unregister_loader(self)
279
+ ExplicitNamespace.unregister_loader(self)
453
280
  end
454
281
 
455
282
  # --- Class methods ---------------------------------------------------------------------------
@@ -499,19 +326,12 @@ module Zeitwerk
499
326
 
500
327
  private # -------------------------------------------------------------------------------------
501
328
 
502
- # @sig () -> Array[String]
503
- def actual_root_dirs
504
- root_dirs.reject do |root_dir, _namespace|
505
- !dir?(root_dir) || ignored_paths.member?(root_dir)
506
- end
507
- end
508
-
509
329
  # @sig (String, Module) -> void
510
330
  def set_autoloads_in_dir(dir, parent)
511
331
  ls(dir) do |basename, abspath|
512
332
  begin
513
333
  if ruby?(basename)
514
- basename[-3..-1] = ''
334
+ basename.delete_suffix!(".rb")
515
335
  cname = inflector.camelize(basename, abspath).to_sym
516
336
  autoload_file(parent, cname, abspath)
517
337
  elsif dir?(abspath)
@@ -521,9 +341,9 @@ module Zeitwerk
521
341
  # To resolve the ambiguity file name -> constant path this introduces,
522
342
  # the `app/models/concerns` directory is totally ignored as a namespace,
523
343
  # it counts only as root. The guard checks that.
524
- unless root_dirs.key?(abspath)
344
+ unless root_dir?(abspath)
525
345
  cname = inflector.camelize(basename, abspath).to_sym
526
- if collapse_dirs.member?(abspath)
346
+ if collapse?(abspath)
527
347
  set_autoloads_in_dir(abspath, parent)
528
348
  else
529
349
  autoload_subdir(parent, cname, abspath)
@@ -551,27 +371,28 @@ module Zeitwerk
551
371
 
552
372
  # @sig (Module, Symbol, String) -> void
553
373
  def autoload_subdir(parent, cname, subdir)
554
- if autoload_path = autoload_for?(parent, cname)
374
+ if autoload_path = autoloads.abspath_for(parent, cname)
555
375
  cpath = cpath(parent, cname)
556
376
  register_explicit_namespace(cpath) if ruby?(autoload_path)
557
377
  # We do not need to issue another autoload, the existing one is enough
558
378
  # no matter if it is for a file or a directory. Just remember the
559
379
  # subdirectory has to be visited if the namespace is used.
560
- (lazy_subdirs[cpath] ||= []) << subdir
380
+ lazy_subdirs[cpath] << subdir
561
381
  elsif !cdef?(parent, cname)
562
382
  # First time we find this namespace, set an autoload for it.
563
- (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
383
+ lazy_subdirs[cpath(parent, cname)] << subdir
564
384
  set_autoload(parent, cname, subdir)
565
385
  else
566
386
  # For whatever reason the constant that corresponds to this namespace has
567
387
  # already been defined, we have to recurse.
568
- set_autoloads_in_dir(subdir, parent.const_get(cname))
388
+ log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
389
+ set_autoloads_in_dir(subdir, cget(parent, cname))
569
390
  end
570
391
  end
571
392
 
572
393
  # @sig (Module, Symbol, String) -> void
573
394
  def autoload_file(parent, cname, file)
574
- if autoload_path = autoload_for?(parent, cname)
395
+ if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
575
396
  # First autoload for a Ruby file wins, just ignore subsequent ones.
576
397
  if ruby?(autoload_path)
577
398
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
@@ -598,163 +419,32 @@ module Zeitwerk
598
419
  autoloads.delete(dir)
599
420
  Registry.unregister_autoload(dir)
600
421
 
422
+ log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
423
+
601
424
  set_autoload(parent, cname, file)
602
425
  register_explicit_namespace(cpath(parent, cname))
603
426
  end
604
427
 
605
428
  # @sig (Module, Symbol, String) -> void
606
429
  def set_autoload(parent, cname, abspath)
607
- # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
608
- # real path to be able to delete it from $LOADED_FEATURES on unload, and to
609
- # be able to do a lookup later in Kernel#require for manual require calls.
610
- #
611
- # We freeze realpath because that saves allocations in Module#autoload.
612
- # See #125.
613
- realpath = File.realpath(abspath).freeze
614
- parent.autoload(cname, realpath)
430
+ autoloads.define(parent, cname, abspath)
431
+
615
432
  if logger
616
- if ruby?(realpath)
617
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
433
+ if ruby?(abspath)
434
+ log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
618
435
  else
619
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
436
+ log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
620
437
  end
621
438
  end
622
439
 
623
- autoloads[realpath] = [parent, cname]
624
- Registry.register_autoload(self, realpath)
440
+ Registry.register_autoload(self, abspath)
625
441
 
626
442
  # See why in the documentation of Zeitwerk::Registry.inceptions.
627
443
  unless parent.autoload?(cname)
628
- Registry.register_inception(cpath(parent, cname), realpath, self)
629
- end
630
- end
631
-
632
- # @sig (Module, Symbol) -> String?
633
- def autoload_for?(parent, cname)
634
- strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
635
- end
636
-
637
- # The autoload? predicate takes into account the ancestor chain of the
638
- # receiver, like const_defined? and other methods in the constants API do.
639
- #
640
- # For example, given
641
- #
642
- # class A
643
- # autoload :X, "x.rb"
644
- # end
645
- #
646
- # class B < A
647
- # end
648
- #
649
- # B.autoload?(:X) returns "x.rb".
650
- #
651
- # We need a way to strictly check in parent ignoring ancestors.
652
- #
653
- # @sig (Module, Symbol) -> String?
654
- if method(:autoload?).arity == 1
655
- def strict_autoload_path(parent, cname)
656
- parent.autoload?(cname) if cdef?(parent, cname)
657
- end
658
- else
659
- def strict_autoload_path(parent, cname)
660
- parent.autoload?(cname, false)
661
- end
662
- end
663
-
664
- # This method is called this way because I prefer `preload` to be the method
665
- # name to configure preloads in the public interface.
666
- #
667
- # @sig () -> void
668
- def do_preload
669
- preloads.each do |abspath|
670
- do_preload_abspath(abspath)
671
- end
672
- end
673
-
674
- # @sig (String) -> void
675
- def do_preload_abspath(abspath)
676
- if ruby?(abspath)
677
- do_preload_file(abspath)
678
- elsif dir?(abspath)
679
- do_preload_dir(abspath)
444
+ Registry.register_inception(cpath(parent, cname), abspath, self)
680
445
  end
681
446
  end
682
447
 
683
- # @sig (String) -> void
684
- def do_preload_dir(dir)
685
- ls(dir) do |_basename, abspath|
686
- do_preload_abspath(abspath)
687
- end
688
- end
689
-
690
- # @sig (String) -> bool
691
- def do_preload_file(file)
692
- log("preloading #{file}") if logger
693
- require file
694
- end
695
-
696
- # @sig (Module, Symbol) -> String
697
- def cpath(parent, cname)
698
- parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
699
- end
700
-
701
- # @sig (String) { (String, String) -> void } -> void
702
- def ls(dir)
703
- Dir.foreach(dir) do |basename|
704
- next if basename.start_with?(".")
705
-
706
- abspath = File.join(dir, basename)
707
- next if ignored_paths.member?(abspath)
708
-
709
- # We freeze abspath because that saves allocations when passed later to
710
- # File methods. See #125.
711
- yield basename, abspath.freeze
712
- end
713
- end
714
-
715
- # @sig (String) -> bool
716
- def ruby?(path)
717
- path.end_with?(".rb")
718
- end
719
-
720
- # @sig (String) -> bool
721
- def dir?(path)
722
- File.directory?(path)
723
- end
724
-
725
- # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
726
- def expand_paths(paths)
727
- paths.flatten.map! { |path| File.expand_path(path) }
728
- end
729
-
730
- # @sig (Array[String]) -> Array[String]
731
- def expand_glob_patterns(glob_patterns)
732
- # Note that Dir.glob works with regular file names just fine. That is,
733
- # glob patterns technically need no wildcards.
734
- glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
735
- end
736
-
737
- # @sig () -> void
738
- def recompute_ignored_paths
739
- ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
740
- end
741
-
742
- # @sig () -> void
743
- def recompute_collapse_dirs
744
- collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
745
- end
746
-
747
- # @sig (String) -> void
748
- def log(message)
749
- method_name = logger.respond_to?(:debug) ? :debug : :call
750
- logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
751
- end
752
-
753
- # @sig (Module, Symbol) -> bool
754
- def cdef?(parent, cname)
755
- parent.const_defined?(cname, false)
756
- end
757
-
758
448
  # @sig (String) -> void
759
449
  def register_explicit_namespace(cpath)
760
450
  ExplicitNamespace.register(cpath, self)
@@ -764,17 +454,33 @@ module Zeitwerk
764
454
  def raise_if_conflicting_directory(dir)
765
455
  self.class.mutex.synchronize do
766
456
  Registry.loaders.each do |loader|
767
- if loader != self && loader.manages?(dir)
768
- require "pp"
769
- raise Error,
770
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
771
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
772
- EOS
457
+ next if loader == self
458
+ next if loader.ignores?(dir)
459
+
460
+ dir = dir + "/"
461
+ loader.root_dirs.each do |root_dir, _namespace|
462
+ next if ignores?(root_dir)
463
+
464
+ root_dir = root_dir + "/"
465
+ if dir.start_with?(root_dir) || root_dir.start_with?(dir)
466
+ require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
467
+ raise Error,
468
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
469
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
470
+ EOS
471
+ end
773
472
  end
774
473
  end
775
474
  end
776
475
  end
777
476
 
477
+ # @sig (String, Object, String) -> void
478
+ def run_on_unload_callbacks(cpath, value, abspath)
479
+ # Order matters. If present, run the most specific one.
480
+ on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
481
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
482
+ end
483
+
778
484
  # @sig (Module, Symbol) -> void
779
485
  def unload_autoload(parent, cname)
780
486
  parent.__send__(:remove_const, cname)