zeitwerk 2.4.2 → 2.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,69 +5,16 @@ 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
20
-
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
10
+ require_relative "loader/config"
35
11
 
36
- # Absolute paths of files or directories that have to be preloaded.
37
- #
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.
44
- #
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.
52
- #
53
- # @private
54
- # @sig Set[String]
55
- attr_reader :ignored_paths
56
-
57
- # Absolute paths of directories or glob patterns to be collapsed.
58
- #
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.
65
- #
66
- # @private
67
- # @sig Set[String]
68
- attr_reader :collapse_dirs
12
+ include RealModName
13
+ include Callbacks
14
+ include Helpers
15
+ include Config
69
16
 
70
- # Maps real absolute paths for which an autoload has been set ---and not
17
+ # Maps absolute paths for which an autoload has been set ---and not
71
18
  # executed--- to their corresponding parent class or module and constant
72
19
  # name.
73
20
  #
@@ -93,8 +40,8 @@ module Zeitwerk
93
40
  #
94
41
  # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
95
42
  #
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
43
+ # The cpath as key helps implementing unloadable_cpath? The file name is
44
+ # stored in order to be able to delete it from $LOADED_FEATURES, and the
98
45
  # pair [Module, Symbol] is used to remove_const the constant from the class
99
46
  # or module object.
100
47
  #
@@ -123,15 +70,6 @@ module Zeitwerk
123
70
  # @sig Hash[String, Array[String]]
124
71
  attr_reader :lazy_subdirs
125
72
 
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
- # User-oriented callbacks to be fired when a constant is loaded.
133
- attr_reader :on_load_callbacks
134
-
135
73
  # @private
136
74
  # @sig Mutex
137
75
  attr_reader :mutex
@@ -141,150 +79,21 @@ module Zeitwerk
141
79
  attr_reader :mutex2
142
80
 
143
81
  def initialize
144
- @initialized_at = Time.now
145
-
146
- @tag = SecureRandom.hex(3)
147
- @inflector = Inflector.new
148
- @logger = self.class.default_logger
149
-
150
- @root_dirs = {}
151
- @preloads = []
152
- @ignored_glob_patterns = Set.new
153
- @ignored_paths = Set.new
154
- @collapse_glob_patterns = Set.new
155
- @collapse_dirs = Set.new
156
- @autoloads = {}
157
- @autoloaded_dirs = []
158
- @to_unload = {}
159
- @lazy_subdirs = {}
160
- @eager_load_exclusions = Set.new
161
- @on_load_callbacks = {}
162
-
163
- # TODO: find a better name for these mutexes.
164
- @mutex = Mutex.new
165
- @mutex2 = Mutex.new
166
- @setup = false
167
- @eager_loaded = false
168
-
169
- @reloading_enabled = false
170
-
171
- Registry.register_loader(self)
172
- end
173
-
174
- # Sets a tag for the loader, useful for logging.
175
- #
176
- # @param tag [#to_s]
177
- # @sig (#to_s) -> void
178
- def tag=(tag)
179
- @tag = tag.to_s
180
- end
181
-
182
- # Absolute paths of the root directories. This is a read-only collection,
183
- # please push here via `push_dir`.
184
- #
185
- # @sig () -> Array[String]
186
- def dirs
187
- root_dirs.keys.freeze
188
- end
189
-
190
- # Pushes `path` to the list of root directories.
191
- #
192
- # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
193
- # the same process already manages that directory or one of its ascendants
194
- # or descendants.
195
- #
196
- # @raise [Zeitwerk::Error]
197
- # @sig (String | Pathname, Module) -> void
198
- def push_dir(path, namespace: Object)
199
- # Note that Class < Module.
200
- unless namespace.is_a?(Module)
201
- raise Error, "#{namespace.inspect} is not a class or module object, should be"
202
- end
203
-
204
- abspath = File.expand_path(path)
205
- if dir?(abspath)
206
- raise_if_conflicting_directory(abspath)
207
- root_dirs[abspath] = namespace
208
- else
209
- raise Error, "the root directory #{abspath} does not exist"
210
- end
211
- end
212
-
213
- # You need to call this method before setup in order to be able to reload.
214
- # There is no way to undo this, either you want to reload or you don't.
215
- #
216
- # @raise [Zeitwerk::Error]
217
- # @sig () -> void
218
- def enable_reloading
219
- mutex.synchronize do
220
- break if @reloading_enabled
82
+ super
221
83
 
222
- if @setup
223
- raise Error, "cannot enable reloading after setup"
224
- else
225
- @reloading_enabled = true
226
- end
227
- end
228
- end
84
+ @autoloads = {}
85
+ @autoloaded_dirs = []
86
+ @to_unload = {}
87
+ @lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
88
+ @mutex = Mutex.new
89
+ @mutex2 = Mutex.new
90
+ @setup = false
91
+ @eager_loaded = false
229
92
 
230
- # @sig () -> bool
231
- def reloading_enabled?
232
- @reloading_enabled
233
- end
234
-
235
- # Files or directories to be preloaded instead of lazy loaded.
236
- #
237
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
238
- def preload(*paths)
239
- mutex.synchronize do
240
- expand_paths(paths).each do |abspath|
241
- preloads << abspath
242
- do_preload_abspath(abspath) if @setup
243
- end
244
- end
245
- end
246
-
247
- # Configure files, directories, or glob patterns to be totally ignored.
248
- #
249
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
250
- def ignore(*glob_patterns)
251
- glob_patterns = expand_paths(glob_patterns)
252
- mutex.synchronize do
253
- ignored_glob_patterns.merge(glob_patterns)
254
- ignored_paths.merge(expand_glob_patterns(glob_patterns))
255
- end
256
- end
257
-
258
- # Configure directories or glob patterns to be collapsed.
259
- #
260
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
261
- def collapse(*glob_patterns)
262
- glob_patterns = expand_paths(glob_patterns)
263
- mutex.synchronize do
264
- collapse_glob_patterns.merge(glob_patterns)
265
- collapse_dirs.merge(expand_glob_patterns(glob_patterns))
266
- end
267
- end
268
-
269
- # Configure a block to be invoked once a certain constant path is loaded.
270
- # Supports multiple callbacks, and if there are many, they are executed in
271
- # the order in which they were defined.
272
- #
273
- # loader.on_load("SomeApiClient") do
274
- # SomeApiClient.endpoint = "https://api.dev"
275
- # end
276
- #
277
- # @raise [TypeError]
278
- # @sig (String) { () -> void } -> void
279
- def on_load(cpath, &block)
280
- raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String)
281
-
282
- mutex.synchronize do
283
- (on_load_callbacks[cpath] ||= []) << block
284
- end
93
+ Registry.register_loader(self)
285
94
  end
286
95
 
287
- # Sets autoloads in the root namespace and preloads files, if any.
96
+ # Sets autoloads in the root namespace.
288
97
  #
289
98
  # @sig () -> void
290
99
  def setup
@@ -294,7 +103,8 @@ module Zeitwerk
294
103
  actual_root_dirs.each do |root_dir, namespace|
295
104
  set_autoloads_in_dir(root_dir, namespace)
296
105
  end
297
- do_preload
106
+
107
+ on_setup_callbacks.each(&:call)
298
108
 
299
109
  @setup = true
300
110
  end
@@ -307,7 +117,10 @@ module Zeitwerk
307
117
  # else, they are eligible for garbage collection, which would effectively
308
118
  # unload them.
309
119
  #
310
- # @private
120
+ # This method is public but undocumented. Main interface is `reload`, which
121
+ # means `unload` + `setup`. This one is avaiable to be used together with
122
+ # `unregister`, which is undocumented too.
123
+ #
311
124
  # @sig () -> void
312
125
  def unload
313
126
  mutex.synchronize do
@@ -319,21 +132,26 @@ module Zeitwerk
319
132
  # is enough.
320
133
  unloaded_files = Set.new
321
134
 
322
- autoloads.each do |realpath, (parent, cname)|
135
+ autoloads.each do |abspath, (parent, cname)|
323
136
  if parent.autoload?(cname)
324
137
  unload_autoload(parent, cname)
325
138
  else
326
139
  # Could happen if loaded with require_relative. That is unsupported,
327
140
  # and the constant path would escape unloadable_cpath? This is just
328
141
  # defensive code to clean things up as much as we are able to.
329
- unload_cref(parent, cname) if cdef?(parent, cname)
330
- unloaded_files.add(realpath) if ruby?(realpath)
142
+ unload_cref(parent, cname)
143
+ unloaded_files.add(abspath) if ruby?(abspath)
331
144
  end
332
145
  end
333
146
 
334
- to_unload.each_value do |(realpath, (parent, cname))|
335
- unload_cref(parent, cname) if cdef?(parent, cname)
336
- unloaded_files.add(realpath) if ruby?(realpath)
147
+ to_unload.each do |cpath, (abspath, (parent, cname))|
148
+ unless on_unload_callbacks.empty?
149
+ value = parent.const_get(cname)
150
+ run_on_unload_callbacks(cpath, value, abspath)
151
+ end
152
+
153
+ unload_cref(parent, cname)
154
+ unloaded_files.add(abspath) if ruby?(abspath)
337
155
  end
338
156
 
339
157
  unless unloaded_files.empty?
@@ -357,7 +175,7 @@ module Zeitwerk
357
175
  lazy_subdirs.clear
358
176
 
359
177
  Registry.on_unload(self)
360
- ExplicitNamespace.unregister(self)
178
+ ExplicitNamespace.unregister_loader(self)
361
179
 
362
180
  @setup = false
363
181
  @eager_loaded = false
@@ -386,34 +204,39 @@ module Zeitwerk
386
204
  # Eager loads all files in the root directories, recursively. Files do not
387
205
  # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
388
206
  # are not eager loaded. You can opt-out specifically in specific files and
389
- # directories with `do_not_eager_load`.
207
+ # directories with `do_not_eager_load`, and that can be overridden passing
208
+ # `force: true`.
390
209
  #
391
- # @sig () -> void
392
- def eager_load
210
+ # @sig (true | false) -> void
211
+ def eager_load(force: false)
393
212
  mutex.synchronize do
394
213
  break if @eager_loaded
395
214
 
215
+ log("eager load start") if logger
216
+
217
+ honour_exclusions = !force
218
+
396
219
  queue = []
397
220
  actual_root_dirs.each do |root_dir, namespace|
398
- queue << [namespace, root_dir] unless eager_load_exclusions.member?(root_dir)
221
+ queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
399
222
  end
400
223
 
401
224
  while to_eager_load = queue.shift
402
225
  namespace, dir = to_eager_load
403
226
 
404
227
  ls(dir) do |basename, abspath|
405
- next if eager_load_exclusions.member?(abspath)
228
+ next if honour_exclusions && excluded_from_eager_load?(abspath)
406
229
 
407
230
  if ruby?(abspath)
408
- if cref = autoloads[File.realpath(abspath)]
409
- cref[0].const_get(cref[1], false)
231
+ if cref = autoloads[abspath]
232
+ cget(*cref)
410
233
  end
411
234
  elsif dir?(abspath) && !root_dirs.key?(abspath)
412
- if collapse_dirs.member?(abspath)
235
+ if collapse?(abspath)
413
236
  queue << [namespace, abspath]
414
237
  else
415
238
  cname = inflector.camelize(basename, abspath)
416
- queue << [namespace.const_get(cname, false), abspath]
239
+ queue << [cget(namespace, cname), abspath]
417
240
  end
418
241
  end
419
242
  end
@@ -425,15 +248,9 @@ module Zeitwerk
425
248
  autoloaded_dirs.clear
426
249
 
427
250
  @eager_loaded = true
428
- end
429
- end
430
251
 
431
- # Let eager load ignore the given files or directories. The constants
432
- # defined in those files are still autoloadable.
433
- #
434
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
435
- def do_not_eager_load(*paths)
436
- mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
252
+ log("eager load end") if logger
253
+ end
437
254
  end
438
255
 
439
256
  # Says if the given constant path would be unloaded on reload. This
@@ -452,26 +269,13 @@ module Zeitwerk
452
269
  to_unload.keys.freeze
453
270
  end
454
271
 
455
- # Logs to `$stdout`, handy shortcut for debugging.
272
+ # This is a dangerous method.
456
273
  #
274
+ # @experimental
457
275
  # @sig () -> void
458
- def log!
459
- @logger = ->(msg) { puts msg }
460
- end
461
-
462
- # @private
463
- # @sig (String) -> bool
464
- def manages?(dir)
465
- dir = dir + "/"
466
- ignored_paths.each do |ignored_path|
467
- return false if dir.start_with?(ignored_path + "/")
468
- end
469
-
470
- root_dirs.each_key do |root_dir|
471
- return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
472
- end
473
-
474
- false
276
+ def unregister
277
+ Registry.unregister_loader(self)
278
+ ExplicitNamespace.unregister_loader(self)
475
279
  end
476
280
 
477
281
  # --- Class methods ---------------------------------------------------------------------------
@@ -521,19 +325,12 @@ module Zeitwerk
521
325
 
522
326
  private # -------------------------------------------------------------------------------------
523
327
 
524
- # @sig () -> Array[String]
525
- def actual_root_dirs
526
- root_dirs.reject do |root_dir, _namespace|
527
- !dir?(root_dir) || ignored_paths.member?(root_dir)
528
- end
529
- end
530
-
531
328
  # @sig (String, Module) -> void
532
329
  def set_autoloads_in_dir(dir, parent)
533
330
  ls(dir) do |basename, abspath|
534
331
  begin
535
332
  if ruby?(basename)
536
- basename[-3..-1] = ''
333
+ basename.delete_suffix!(".rb")
537
334
  cname = inflector.camelize(basename, abspath).to_sym
538
335
  autoload_file(parent, cname, abspath)
539
336
  elsif dir?(abspath)
@@ -543,9 +340,9 @@ module Zeitwerk
543
340
  # To resolve the ambiguity file name -> constant path this introduces,
544
341
  # the `app/models/concerns` directory is totally ignored as a namespace,
545
342
  # it counts only as root. The guard checks that.
546
- unless root_dirs.key?(abspath)
343
+ unless root_dir?(abspath)
547
344
  cname = inflector.camelize(basename, abspath).to_sym
548
- if collapse_dirs.member?(abspath)
345
+ if collapse?(abspath)
549
346
  set_autoloads_in_dir(abspath, parent)
550
347
  else
551
348
  autoload_subdir(parent, cname, abspath)
@@ -573,27 +370,28 @@ module Zeitwerk
573
370
 
574
371
  # @sig (Module, Symbol, String) -> void
575
372
  def autoload_subdir(parent, cname, subdir)
576
- if autoload_path = autoload_for?(parent, cname)
373
+ if autoload_path = autoload_path_set_by_me_for?(parent, cname)
577
374
  cpath = cpath(parent, cname)
578
375
  register_explicit_namespace(cpath) if ruby?(autoload_path)
579
376
  # We do not need to issue another autoload, the existing one is enough
580
377
  # no matter if it is for a file or a directory. Just remember the
581
378
  # subdirectory has to be visited if the namespace is used.
582
- (lazy_subdirs[cpath] ||= []) << subdir
379
+ lazy_subdirs[cpath] << subdir
583
380
  elsif !cdef?(parent, cname)
584
381
  # First time we find this namespace, set an autoload for it.
585
- (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
382
+ lazy_subdirs[cpath(parent, cname)] << subdir
586
383
  set_autoload(parent, cname, subdir)
587
384
  else
588
385
  # For whatever reason the constant that corresponds to this namespace has
589
386
  # already been defined, we have to recurse.
590
- set_autoloads_in_dir(subdir, parent.const_get(cname))
387
+ log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
388
+ set_autoloads_in_dir(subdir, cget(parent, cname))
591
389
  end
592
390
  end
593
391
 
594
392
  # @sig (Module, Symbol, String) -> void
595
393
  def autoload_file(parent, cname, file)
596
- if autoload_path = autoload_for?(parent, cname)
394
+ if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
597
395
  # First autoload for a Ruby file wins, just ignore subsequent ones.
598
396
  if ruby?(autoload_path)
599
397
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
@@ -620,163 +418,42 @@ module Zeitwerk
620
418
  autoloads.delete(dir)
621
419
  Registry.unregister_autoload(dir)
622
420
 
421
+ log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
422
+
623
423
  set_autoload(parent, cname, file)
624
424
  register_explicit_namespace(cpath(parent, cname))
625
425
  end
626
426
 
627
427
  # @sig (Module, Symbol, String) -> void
628
428
  def set_autoload(parent, cname, abspath)
629
- # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
630
- # real path to be able to delete it from $LOADED_FEATURES on unload, and to
631
- # be able to do a lookup later in Kernel#require for manual require calls.
632
- #
633
- # We freeze realpath because that saves allocations in Module#autoload.
634
- # See #125.
635
- realpath = File.realpath(abspath).freeze
636
- parent.autoload(cname, realpath)
429
+ parent.autoload(cname, abspath)
430
+
637
431
  if logger
638
- if ruby?(realpath)
639
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
432
+ if ruby?(abspath)
433
+ log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
640
434
  else
641
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
435
+ log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
642
436
  end
643
437
  end
644
438
 
645
- autoloads[realpath] = [parent, cname]
646
- Registry.register_autoload(self, realpath)
439
+ autoloads[abspath] = [parent, cname]
440
+ Registry.register_autoload(self, abspath)
647
441
 
648
442
  # See why in the documentation of Zeitwerk::Registry.inceptions.
649
443
  unless parent.autoload?(cname)
650
- Registry.register_inception(cpath(parent, cname), realpath, self)
444
+ Registry.register_inception(cpath(parent, cname), abspath, self)
651
445
  end
652
446
  end
653
447
 
654
448
  # @sig (Module, Symbol) -> String?
655
- def autoload_for?(parent, cname)
656
- strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
657
- end
658
-
659
- # The autoload? predicate takes into account the ancestor chain of the
660
- # receiver, like const_defined? and other methods in the constants API do.
661
- #
662
- # For example, given
663
- #
664
- # class A
665
- # autoload :X, "x.rb"
666
- # end
667
- #
668
- # class B < A
669
- # end
670
- #
671
- # B.autoload?(:X) returns "x.rb".
672
- #
673
- # We need a way to strictly check in parent ignoring ancestors.
674
- #
675
- # @sig (Module, Symbol) -> String?
676
- if method(:autoload?).arity == 1
677
- def strict_autoload_path(parent, cname)
678
- parent.autoload?(cname) if cdef?(parent, cname)
679
- end
680
- else
681
- def strict_autoload_path(parent, cname)
682
- parent.autoload?(cname, false)
683
- end
684
- end
685
-
686
- # This method is called this way because I prefer `preload` to be the method
687
- # name to configure preloads in the public interface.
688
- #
689
- # @sig () -> void
690
- def do_preload
691
- preloads.each do |abspath|
692
- do_preload_abspath(abspath)
693
- end
694
- end
695
-
696
- # @sig (String) -> void
697
- def do_preload_abspath(abspath)
698
- if ruby?(abspath)
699
- do_preload_file(abspath)
700
- elsif dir?(abspath)
701
- do_preload_dir(abspath)
702
- end
703
- end
704
-
705
- # @sig (String) -> void
706
- def do_preload_dir(dir)
707
- ls(dir) do |_basename, abspath|
708
- do_preload_abspath(abspath)
709
- end
710
- end
711
-
712
- # @sig (String) -> bool
713
- def do_preload_file(file)
714
- log("preloading #{file}") if logger
715
- require file
716
- end
717
-
718
- # @sig (Module, Symbol) -> String
719
- def cpath(parent, cname)
720
- parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
721
- end
722
-
723
- # @sig (String) { (String, String) -> void } -> void
724
- def ls(dir)
725
- Dir.foreach(dir) do |basename|
726
- next if basename.start_with?(".")
727
-
728
- abspath = File.join(dir, basename)
729
- next if ignored_paths.member?(abspath)
730
-
731
- # We freeze abspath because that saves allocations when passed later to
732
- # File methods. See #125.
733
- yield basename, abspath.freeze
449
+ def autoload_path_set_by_me_for?(parent, cname)
450
+ if autoload_path = strict_autoload_path(parent, cname)
451
+ autoload_path if autoloads.key?(autoload_path)
452
+ else
453
+ Registry.inception?(cpath(parent, cname))
734
454
  end
735
455
  end
736
456
 
737
- # @sig (String) -> bool
738
- def ruby?(path)
739
- path.end_with?(".rb")
740
- end
741
-
742
- # @sig (String) -> bool
743
- def dir?(path)
744
- File.directory?(path)
745
- end
746
-
747
- # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
748
- def expand_paths(paths)
749
- paths.flatten.map! { |path| File.expand_path(path) }
750
- end
751
-
752
- # @sig (Array[String]) -> Array[String]
753
- def expand_glob_patterns(glob_patterns)
754
- # Note that Dir.glob works with regular file names just fine. That is,
755
- # glob patterns technically need no wildcards.
756
- glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
757
- end
758
-
759
- # @sig () -> void
760
- def recompute_ignored_paths
761
- ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
762
- end
763
-
764
- # @sig () -> void
765
- def recompute_collapse_dirs
766
- collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
767
- end
768
-
769
- # @sig (String) -> void
770
- def log(message)
771
- method_name = logger.respond_to?(:debug) ? :debug : :call
772
- logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
773
- end
774
-
775
- # @sig (Module, Symbol) -> bool
776
- def cdef?(parent, cname)
777
- parent.const_defined?(cname, false)
778
- end
779
-
780
457
  # @sig (String) -> void
781
458
  def register_explicit_namespace(cpath)
782
459
  ExplicitNamespace.register(cpath, self)
@@ -786,17 +463,33 @@ module Zeitwerk
786
463
  def raise_if_conflicting_directory(dir)
787
464
  self.class.mutex.synchronize do
788
465
  Registry.loaders.each do |loader|
789
- if loader != self && loader.manages?(dir)
790
- require "pp"
791
- raise Error,
792
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
793
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
794
- EOS
466
+ next if loader == self
467
+ next if loader.ignores?(dir)
468
+
469
+ dir = dir + "/"
470
+ loader.root_dirs.each do |root_dir, _namespace|
471
+ next if ignores?(root_dir)
472
+
473
+ root_dir = root_dir + "/"
474
+ if dir.start_with?(root_dir) || root_dir.start_with?(dir)
475
+ require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
476
+ raise Error,
477
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
478
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
479
+ EOS
480
+ end
795
481
  end
796
482
  end
797
483
  end
798
484
  end
799
485
 
486
+ # @sig (String, Object, String) -> void
487
+ def run_on_unload_callbacks(cpath, value, abspath)
488
+ # Order matters. If present, run the most specific one.
489
+ on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
490
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
491
+ end
492
+
800
493
  # @sig (Module, Symbol) -> void
801
494
  def unload_autoload(parent, cname)
802
495
  parent.__send__(:remove_const, cname)
@@ -805,7 +498,13 @@ module Zeitwerk
805
498
 
806
499
  # @sig (Module, Symbol) -> void
807
500
  def unload_cref(parent, cname)
501
+ # Let's optimistically remove_const. The way we use it, this is going to
502
+ # succeed always if all is good.
808
503
  parent.__send__(:remove_const, cname)
504
+ rescue ::NameError
505
+ # There are a few edge scenarios in which this may happen. If the constant
506
+ # is gone, that is OK, anyway.
507
+ else
809
508
  log("#{cpath(parent, cname)} unloaded") if logger
810
509
  end
811
510
  end