zeitwerk 2.4.1 → 2.5.0.beta3

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.
@@ -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)