zeitwerk 2.4.2 → 2.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,73 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "set"
4
- require "securerandom"
5
4
 
6
5
  module Zeitwerk
7
6
  class Loader
7
+ require_relative "loader/helpers"
8
8
  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
9
+ require_relative "loader/config"
35
10
 
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
11
+ include RealModName
12
+ include Callbacks
13
+ include Helpers
14
+ include Config
69
15
 
70
- # Maps real absolute paths for which an autoload has been set ---and not
16
+ # Maps absolute paths for which an autoload has been set ---and not
71
17
  # executed--- to their corresponding parent class or module and constant
72
18
  # name.
73
19
  #
@@ -93,8 +39,8 @@ module Zeitwerk
93
39
  #
94
40
  # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
95
41
  #
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
42
+ # The cpath as key helps implementing unloadable_cpath? The file name is
43
+ # stored in order to be able to delete it from $LOADED_FEATURES, and the
98
44
  # pair [Module, Symbol] is used to remove_const the constant from the class
99
45
  # or module object.
100
46
  #
@@ -123,15 +69,6 @@ module Zeitwerk
123
69
  # @sig Hash[String, Array[String]]
124
70
  attr_reader :lazy_subdirs
125
71
 
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
72
  # @private
136
73
  # @sig Mutex
137
74
  attr_reader :mutex
@@ -141,150 +78,21 @@ module Zeitwerk
141
78
  attr_reader :mutex2
142
79
 
143
80
  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
81
+ super
221
82
 
222
- if @setup
223
- raise Error, "cannot enable reloading after setup"
224
- else
225
- @reloading_enabled = true
226
- end
227
- end
228
- end
83
+ @autoloads = {}
84
+ @autoloaded_dirs = []
85
+ @to_unload = {}
86
+ @lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
87
+ @mutex = Mutex.new
88
+ @mutex2 = Mutex.new
89
+ @setup = false
90
+ @eager_loaded = false
229
91
 
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
92
+ Registry.register_loader(self)
285
93
  end
286
94
 
287
- # Sets autoloads in the root namespace and preloads files, if any.
95
+ # Sets autoloads in the root namespace.
288
96
  #
289
97
  # @sig () -> void
290
98
  def setup
@@ -294,7 +102,8 @@ module Zeitwerk
294
102
  actual_root_dirs.each do |root_dir, namespace|
295
103
  set_autoloads_in_dir(root_dir, namespace)
296
104
  end
297
- do_preload
105
+
106
+ on_setup_callbacks.each(&:call)
298
107
 
299
108
  @setup = true
300
109
  end
@@ -307,7 +116,10 @@ module Zeitwerk
307
116
  # else, they are eligible for garbage collection, which would effectively
308
117
  # unload them.
309
118
  #
310
- # @private
119
+ # This method is public but undocumented. Main interface is `reload`, which
120
+ # means `unload` + `setup`. This one is avaiable to be used together with
121
+ # `unregister`, which is undocumented too.
122
+ #
311
123
  # @sig () -> void
312
124
  def unload
313
125
  mutex.synchronize do
@@ -319,21 +131,32 @@ module Zeitwerk
319
131
  # is enough.
320
132
  unloaded_files = Set.new
321
133
 
322
- autoloads.each do |realpath, (parent, cname)|
134
+ autoloads.each do |abspath, (parent, cname)|
323
135
  if parent.autoload?(cname)
324
136
  unload_autoload(parent, cname)
325
137
  else
326
138
  # Could happen if loaded with require_relative. That is unsupported,
327
139
  # and the constant path would escape unloadable_cpath? This is just
328
140
  # 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)
141
+ unload_cref(parent, cname)
142
+ unloaded_files.add(abspath) if ruby?(abspath)
331
143
  end
332
144
  end
333
145
 
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)
146
+ to_unload.each do |cpath, (abspath, (parent, cname))|
147
+ # We have to check cdef? in this condition. Reason is, constants whose
148
+ # file does not define them have to be kept in to_unload as explained
149
+ # in the implementation of on_file_autoloaded.
150
+ #
151
+ # If the constant is not defined, on_unload should not be triggered
152
+ # for it.
153
+ if !on_unload_callbacks.empty? && cdef?(parent, cname)
154
+ value = parent.const_get(cname)
155
+ run_on_unload_callbacks(cpath, value, abspath)
156
+ end
157
+
158
+ unload_cref(parent, cname)
159
+ unloaded_files.add(abspath) if ruby?(abspath)
337
160
  end
338
161
 
339
162
  unless unloaded_files.empty?
@@ -357,7 +180,7 @@ module Zeitwerk
357
180
  lazy_subdirs.clear
358
181
 
359
182
  Registry.on_unload(self)
360
- ExplicitNamespace.unregister(self)
183
+ ExplicitNamespace.unregister_loader(self)
361
184
 
362
185
  @setup = false
363
186
  @eager_loaded = false
@@ -386,34 +209,39 @@ module Zeitwerk
386
209
  # Eager loads all files in the root directories, recursively. Files do not
387
210
  # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
388
211
  # are not eager loaded. You can opt-out specifically in specific files and
389
- # directories with `do_not_eager_load`.
212
+ # directories with `do_not_eager_load`, and that can be overridden passing
213
+ # `force: true`.
390
214
  #
391
- # @sig () -> void
392
- def eager_load
215
+ # @sig (true | false) -> void
216
+ def eager_load(force: false)
393
217
  mutex.synchronize do
394
218
  break if @eager_loaded
395
219
 
220
+ log("eager load start") if logger
221
+
222
+ honour_exclusions = !force
223
+
396
224
  queue = []
397
225
  actual_root_dirs.each do |root_dir, namespace|
398
- queue << [namespace, root_dir] unless eager_load_exclusions.member?(root_dir)
226
+ queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
399
227
  end
400
228
 
401
229
  while to_eager_load = queue.shift
402
230
  namespace, dir = to_eager_load
403
231
 
404
232
  ls(dir) do |basename, abspath|
405
- next if eager_load_exclusions.member?(abspath)
233
+ next if honour_exclusions && excluded_from_eager_load?(abspath)
406
234
 
407
235
  if ruby?(abspath)
408
- if cref = autoloads[File.realpath(abspath)]
409
- cref[0].const_get(cref[1], false)
236
+ if cref = autoloads[abspath]
237
+ cget(*cref)
410
238
  end
411
239
  elsif dir?(abspath) && !root_dirs.key?(abspath)
412
- if collapse_dirs.member?(abspath)
240
+ if collapse?(abspath)
413
241
  queue << [namespace, abspath]
414
242
  else
415
243
  cname = inflector.camelize(basename, abspath)
416
- queue << [namespace.const_get(cname, false), abspath]
244
+ queue << [cget(namespace, cname), abspath]
417
245
  end
418
246
  end
419
247
  end
@@ -425,15 +253,9 @@ module Zeitwerk
425
253
  autoloaded_dirs.clear
426
254
 
427
255
  @eager_loaded = true
428
- end
429
- end
430
256
 
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)) }
257
+ log("eager load end") if logger
258
+ end
437
259
  end
438
260
 
439
261
  # Says if the given constant path would be unloaded on reload. This
@@ -452,26 +274,13 @@ module Zeitwerk
452
274
  to_unload.keys.freeze
453
275
  end
454
276
 
455
- # Logs to `$stdout`, handy shortcut for debugging.
277
+ # This is a dangerous method.
456
278
  #
279
+ # @experimental
457
280
  # @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
281
+ def unregister
282
+ Registry.unregister_loader(self)
283
+ ExplicitNamespace.unregister_loader(self)
475
284
  end
476
285
 
477
286
  # --- Class methods ---------------------------------------------------------------------------
@@ -521,19 +330,12 @@ module Zeitwerk
521
330
 
522
331
  private # -------------------------------------------------------------------------------------
523
332
 
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
333
  # @sig (String, Module) -> void
532
334
  def set_autoloads_in_dir(dir, parent)
533
335
  ls(dir) do |basename, abspath|
534
336
  begin
535
337
  if ruby?(basename)
536
- basename[-3..-1] = ''
338
+ basename.delete_suffix!(".rb")
537
339
  cname = inflector.camelize(basename, abspath).to_sym
538
340
  autoload_file(parent, cname, abspath)
539
341
  elsif dir?(abspath)
@@ -543,9 +345,9 @@ module Zeitwerk
543
345
  # To resolve the ambiguity file name -> constant path this introduces,
544
346
  # the `app/models/concerns` directory is totally ignored as a namespace,
545
347
  # it counts only as root. The guard checks that.
546
- unless root_dirs.key?(abspath)
348
+ unless root_dir?(abspath)
547
349
  cname = inflector.camelize(basename, abspath).to_sym
548
- if collapse_dirs.member?(abspath)
350
+ if collapse?(abspath)
549
351
  set_autoloads_in_dir(abspath, parent)
550
352
  else
551
353
  autoload_subdir(parent, cname, abspath)
@@ -573,27 +375,28 @@ module Zeitwerk
573
375
 
574
376
  # @sig (Module, Symbol, String) -> void
575
377
  def autoload_subdir(parent, cname, subdir)
576
- if autoload_path = autoload_for?(parent, cname)
378
+ if autoload_path = autoload_path_set_by_me_for?(parent, cname)
577
379
  cpath = cpath(parent, cname)
578
380
  register_explicit_namespace(cpath) if ruby?(autoload_path)
579
381
  # We do not need to issue another autoload, the existing one is enough
580
382
  # no matter if it is for a file or a directory. Just remember the
581
383
  # subdirectory has to be visited if the namespace is used.
582
- (lazy_subdirs[cpath] ||= []) << subdir
384
+ lazy_subdirs[cpath] << subdir
583
385
  elsif !cdef?(parent, cname)
584
386
  # First time we find this namespace, set an autoload for it.
585
- (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
387
+ lazy_subdirs[cpath(parent, cname)] << subdir
586
388
  set_autoload(parent, cname, subdir)
587
389
  else
588
390
  # For whatever reason the constant that corresponds to this namespace has
589
391
  # already been defined, we have to recurse.
590
- set_autoloads_in_dir(subdir, parent.const_get(cname))
392
+ log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
393
+ set_autoloads_in_dir(subdir, cget(parent, cname))
591
394
  end
592
395
  end
593
396
 
594
397
  # @sig (Module, Symbol, String) -> void
595
398
  def autoload_file(parent, cname, file)
596
- if autoload_path = autoload_for?(parent, cname)
399
+ if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
597
400
  # First autoload for a Ruby file wins, just ignore subsequent ones.
598
401
  if ruby?(autoload_path)
599
402
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
@@ -620,163 +423,42 @@ module Zeitwerk
620
423
  autoloads.delete(dir)
621
424
  Registry.unregister_autoload(dir)
622
425
 
426
+ log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
427
+
623
428
  set_autoload(parent, cname, file)
624
429
  register_explicit_namespace(cpath(parent, cname))
625
430
  end
626
431
 
627
432
  # @sig (Module, Symbol, String) -> void
628
433
  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)
434
+ parent.autoload(cname, abspath)
435
+
637
436
  if logger
638
- if ruby?(realpath)
639
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
437
+ if ruby?(abspath)
438
+ log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
640
439
  else
641
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
440
+ log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
642
441
  end
643
442
  end
644
443
 
645
- autoloads[realpath] = [parent, cname]
646
- Registry.register_autoload(self, realpath)
444
+ autoloads[abspath] = [parent, cname]
445
+ Registry.register_autoload(self, abspath)
647
446
 
648
447
  # See why in the documentation of Zeitwerk::Registry.inceptions.
649
448
  unless parent.autoload?(cname)
650
- Registry.register_inception(cpath(parent, cname), realpath, self)
449
+ Registry.register_inception(cpath(parent, cname), abspath, self)
651
450
  end
652
451
  end
653
452
 
654
453
  # @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)
454
+ def autoload_path_set_by_me_for?(parent, cname)
455
+ if autoload_path = strict_autoload_path(parent, cname)
456
+ autoload_path if autoloads.key?(autoload_path)
457
+ else
458
+ Registry.inception?(cpath(parent, cname))
702
459
  end
703
460
  end
704
461
 
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
734
- end
735
- end
736
-
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
462
  # @sig (String) -> void
781
463
  def register_explicit_namespace(cpath)
782
464
  ExplicitNamespace.register(cpath, self)
@@ -786,17 +468,33 @@ module Zeitwerk
786
468
  def raise_if_conflicting_directory(dir)
787
469
  self.class.mutex.synchronize do
788
470
  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
471
+ next if loader == self
472
+ next if loader.ignores?(dir)
473
+
474
+ dir = dir + "/"
475
+ loader.root_dirs.each do |root_dir, _namespace|
476
+ next if ignores?(root_dir)
477
+
478
+ root_dir = root_dir + "/"
479
+ if dir.start_with?(root_dir) || root_dir.start_with?(dir)
480
+ require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
481
+ raise Error,
482
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
483
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
484
+ EOS
485
+ end
795
486
  end
796
487
  end
797
488
  end
798
489
  end
799
490
 
491
+ # @sig (String, Object, String) -> void
492
+ def run_on_unload_callbacks(cpath, value, abspath)
493
+ # Order matters. If present, run the most specific one.
494
+ on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
495
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
496
+ end
497
+
800
498
  # @sig (Module, Symbol) -> void
801
499
  def unload_autoload(parent, cname)
802
500
  parent.__send__(:remove_const, cname)
@@ -805,7 +503,13 @@ module Zeitwerk
805
503
 
806
504
  # @sig (Module, Symbol) -> void
807
505
  def unload_cref(parent, cname)
506
+ # Let's optimistically remove_const. The way we use it, this is going to
507
+ # succeed always if all is good.
808
508
  parent.__send__(:remove_const, cname)
509
+ rescue ::NameError
510
+ # There are a few edge scenarios in which this may happen. If the constant
511
+ # is gone, that is OK, anyway.
512
+ else
809
513
  log("#{cpath(parent, cname)} unloaded") if logger
810
514
  end
811
515
  end