zeitwerk 2.4.2 → 2.5.0

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
10
+ require_relative "loader/config"
14
11
 
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
35
-
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
12
+ include RealModName
13
+ include Callbacks
14
+ include Helpers
15
+ include Config
48
16
 
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.
17
+ # Keeps track of autoloads defined by the loader which have not been
18
+ # executed so far.
52
19
  #
53
- # @private
54
- # @sig Set[String]
55
- attr_reader :ignored_paths
56
-
57
- # Absolute paths of directories or glob patterns to be collapsed.
20
+ # This metadata helps us implement a few things:
58
21
  #
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.
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.
65
25
  #
66
- # @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.
26
+ # 2. When unloading, remove autoloads that have not been executed.
73
27
  #
74
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
75
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
76
- # ...
28
+ # 3. Eager load with a recursive const_get, rather than a recursive require,
29
+ # for consistency with lazy loading.
77
30
  #
78
31
  # @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,15 +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
- # User-oriented callbacks to be fired when a constant is loaded.
133
- attr_reader :on_load_callbacks
134
-
135
79
  # @private
136
80
  # @sig Mutex
137
81
  attr_reader :mutex
@@ -141,150 +85,21 @@ module Zeitwerk
141
85
  attr_reader :mutex2
142
86
 
143
87
  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
221
-
222
- if @setup
223
- raise Error, "cannot enable reloading after setup"
224
- else
225
- @reloading_enabled = true
226
- end
227
- end
228
- end
229
-
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
88
+ super
246
89
 
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
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
268
98
 
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
99
+ Registry.register_loader(self)
285
100
  end
286
101
 
287
- # Sets autoloads in the root namespace and preloads files, if any.
102
+ # Sets autoloads in the root namespace.
288
103
  #
289
104
  # @sig () -> void
290
105
  def setup
@@ -294,7 +109,8 @@ module Zeitwerk
294
109
  actual_root_dirs.each do |root_dir, namespace|
295
110
  set_autoloads_in_dir(root_dir, namespace)
296
111
  end
297
- do_preload
112
+
113
+ on_setup_callbacks.each(&:call)
298
114
 
299
115
  @setup = true
300
116
  end
@@ -307,7 +123,10 @@ module Zeitwerk
307
123
  # else, they are eligible for garbage collection, which would effectively
308
124
  # unload them.
309
125
  #
310
- # @private
126
+ # This method is public but undocumented. Main interface is `reload`, which
127
+ # means `unload` + `setup`. This one is avaiable to be used together with
128
+ # `unregister`, which is undocumented too.
129
+ #
311
130
  # @sig () -> void
312
131
  def unload
313
132
  mutex.synchronize do
@@ -319,21 +138,26 @@ module Zeitwerk
319
138
  # is enough.
320
139
  unloaded_files = Set.new
321
140
 
322
- autoloads.each do |realpath, (parent, cname)|
141
+ autoloads.each do |(parent, cname), abspath|
323
142
  if parent.autoload?(cname)
324
143
  unload_autoload(parent, cname)
325
144
  else
326
145
  # Could happen if loaded with require_relative. That is unsupported,
327
146
  # and the constant path would escape unloadable_cpath? This is just
328
147
  # 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)
148
+ unload_cref(parent, cname)
149
+ unloaded_files.add(abspath) if ruby?(abspath)
331
150
  end
332
151
  end
333
152
 
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)
153
+ to_unload.each do |cpath, (abspath, (parent, cname))|
154
+ unless on_unload_callbacks.empty?
155
+ value = parent.const_get(cname)
156
+ run_on_unload_callbacks(cpath, value, abspath)
157
+ end
158
+
159
+ unload_cref(parent, cname)
160
+ unloaded_files.add(abspath) if ruby?(abspath)
337
161
  end
338
162
 
339
163
  unless unloaded_files.empty?
@@ -357,7 +181,7 @@ module Zeitwerk
357
181
  lazy_subdirs.clear
358
182
 
359
183
  Registry.on_unload(self)
360
- ExplicitNamespace.unregister(self)
184
+ ExplicitNamespace.unregister_loader(self)
361
185
 
362
186
  @setup = false
363
187
  @eager_loaded = false
@@ -386,34 +210,39 @@ module Zeitwerk
386
210
  # Eager loads all files in the root directories, recursively. Files do not
387
211
  # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
388
212
  # are not eager loaded. You can opt-out specifically in specific files and
389
- # directories with `do_not_eager_load`.
213
+ # directories with `do_not_eager_load`, and that can be overridden passing
214
+ # `force: true`.
390
215
  #
391
- # @sig () -> void
392
- def eager_load
216
+ # @sig (true | false) -> void
217
+ def eager_load(force: false)
393
218
  mutex.synchronize do
394
219
  break if @eager_loaded
395
220
 
221
+ log("eager load start") if logger
222
+
223
+ honour_exclusions = !force
224
+
396
225
  queue = []
397
226
  actual_root_dirs.each do |root_dir, namespace|
398
- queue << [namespace, root_dir] unless eager_load_exclusions.member?(root_dir)
227
+ queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
399
228
  end
400
229
 
401
230
  while to_eager_load = queue.shift
402
231
  namespace, dir = to_eager_load
403
232
 
404
233
  ls(dir) do |basename, abspath|
405
- next if eager_load_exclusions.member?(abspath)
234
+ next if honour_exclusions && excluded_from_eager_load?(abspath)
406
235
 
407
236
  if ruby?(abspath)
408
- if cref = autoloads[File.realpath(abspath)]
409
- cref[0].const_get(cref[1], false)
237
+ if cref = autoloads.cref_for(abspath)
238
+ cget(*cref)
410
239
  end
411
240
  elsif dir?(abspath) && !root_dirs.key?(abspath)
412
- if collapse_dirs.member?(abspath)
241
+ if collapse?(abspath)
413
242
  queue << [namespace, abspath]
414
243
  else
415
244
  cname = inflector.camelize(basename, abspath)
416
- queue << [namespace.const_get(cname, false), abspath]
245
+ queue << [cget(namespace, cname), abspath]
417
246
  end
418
247
  end
419
248
  end
@@ -425,15 +254,9 @@ module Zeitwerk
425
254
  autoloaded_dirs.clear
426
255
 
427
256
  @eager_loaded = true
428
- end
429
- end
430
257
 
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)) }
258
+ log("eager load end") if logger
259
+ end
437
260
  end
438
261
 
439
262
  # Says if the given constant path would be unloaded on reload. This
@@ -452,26 +275,13 @@ module Zeitwerk
452
275
  to_unload.keys.freeze
453
276
  end
454
277
 
455
- # Logs to `$stdout`, handy shortcut for debugging.
278
+ # This is a dangerous method.
456
279
  #
280
+ # @experimental
457
281
  # @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
282
+ def unregister
283
+ Registry.unregister_loader(self)
284
+ ExplicitNamespace.unregister_loader(self)
475
285
  end
476
286
 
477
287
  # --- Class methods ---------------------------------------------------------------------------
@@ -521,19 +331,12 @@ module Zeitwerk
521
331
 
522
332
  private # -------------------------------------------------------------------------------------
523
333
 
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
334
  # @sig (String, Module) -> void
532
335
  def set_autoloads_in_dir(dir, parent)
533
336
  ls(dir) do |basename, abspath|
534
337
  begin
535
338
  if ruby?(basename)
536
- basename[-3..-1] = ''
339
+ basename.delete_suffix!(".rb")
537
340
  cname = inflector.camelize(basename, abspath).to_sym
538
341
  autoload_file(parent, cname, abspath)
539
342
  elsif dir?(abspath)
@@ -543,9 +346,9 @@ module Zeitwerk
543
346
  # To resolve the ambiguity file name -> constant path this introduces,
544
347
  # the `app/models/concerns` directory is totally ignored as a namespace,
545
348
  # it counts only as root. The guard checks that.
546
- unless root_dirs.key?(abspath)
349
+ unless root_dir?(abspath)
547
350
  cname = inflector.camelize(basename, abspath).to_sym
548
- if collapse_dirs.member?(abspath)
351
+ if collapse?(abspath)
549
352
  set_autoloads_in_dir(abspath, parent)
550
353
  else
551
354
  autoload_subdir(parent, cname, abspath)
@@ -573,27 +376,28 @@ module Zeitwerk
573
376
 
574
377
  # @sig (Module, Symbol, String) -> void
575
378
  def autoload_subdir(parent, cname, subdir)
576
- if autoload_path = autoload_for?(parent, cname)
379
+ if autoload_path = autoloads.abspath_for(parent, cname)
577
380
  cpath = cpath(parent, cname)
578
381
  register_explicit_namespace(cpath) if ruby?(autoload_path)
579
382
  # We do not need to issue another autoload, the existing one is enough
580
383
  # no matter if it is for a file or a directory. Just remember the
581
384
  # subdirectory has to be visited if the namespace is used.
582
- (lazy_subdirs[cpath] ||= []) << subdir
385
+ lazy_subdirs[cpath] << subdir
583
386
  elsif !cdef?(parent, cname)
584
387
  # First time we find this namespace, set an autoload for it.
585
- (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
388
+ lazy_subdirs[cpath(parent, cname)] << subdir
586
389
  set_autoload(parent, cname, subdir)
587
390
  else
588
391
  # For whatever reason the constant that corresponds to this namespace has
589
392
  # already been defined, we have to recurse.
590
- set_autoloads_in_dir(subdir, parent.const_get(cname))
393
+ log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
394
+ set_autoloads_in_dir(subdir, cget(parent, cname))
591
395
  end
592
396
  end
593
397
 
594
398
  # @sig (Module, Symbol, String) -> void
595
399
  def autoload_file(parent, cname, file)
596
- if autoload_path = autoload_for?(parent, cname)
400
+ if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
597
401
  # First autoload for a Ruby file wins, just ignore subsequent ones.
598
402
  if ruby?(autoload_path)
599
403
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
@@ -620,163 +424,32 @@ module Zeitwerk
620
424
  autoloads.delete(dir)
621
425
  Registry.unregister_autoload(dir)
622
426
 
427
+ log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
428
+
623
429
  set_autoload(parent, cname, file)
624
430
  register_explicit_namespace(cpath(parent, cname))
625
431
  end
626
432
 
627
433
  # @sig (Module, Symbol, String) -> void
628
434
  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)
435
+ autoloads.define(parent, cname, abspath)
436
+
637
437
  if logger
638
- if ruby?(realpath)
639
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
438
+ if ruby?(abspath)
439
+ log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
640
440
  else
641
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
441
+ log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
642
442
  end
643
443
  end
644
444
 
645
- autoloads[realpath] = [parent, cname]
646
- Registry.register_autoload(self, realpath)
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)
651
- end
652
- end
653
-
654
- # @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)
449
+ Registry.register_inception(cpath(parent, cname), abspath, self)
702
450
  end
703
451
  end
704
452
 
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
453
  # @sig (String) -> void
781
454
  def register_explicit_namespace(cpath)
782
455
  ExplicitNamespace.register(cpath, self)
@@ -786,17 +459,33 @@ module Zeitwerk
786
459
  def raise_if_conflicting_directory(dir)
787
460
  self.class.mutex.synchronize do
788
461
  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
462
+ next if loader == self
463
+ next if loader.ignores?(dir)
464
+
465
+ dir = dir + "/"
466
+ loader.root_dirs.each do |root_dir, _namespace|
467
+ next if ignores?(root_dir)
468
+
469
+ root_dir = root_dir + "/"
470
+ if dir.start_with?(root_dir) || root_dir.start_with?(dir)
471
+ require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
472
+ raise Error,
473
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
474
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
475
+ EOS
476
+ end
795
477
  end
796
478
  end
797
479
  end
798
480
  end
799
481
 
482
+ # @sig (String, Object, String) -> void
483
+ def run_on_unload_callbacks(cpath, value, abspath)
484
+ # Order matters. If present, run the most specific one.
485
+ on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
486
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
487
+ end
488
+
800
489
  # @sig (Module, Symbol) -> void
801
490
  def unload_autoload(parent, cname)
802
491
  parent.__send__(:remove_const, cname)
@@ -805,7 +494,13 @@ module Zeitwerk
805
494
 
806
495
  # @sig (Module, Symbol) -> void
807
496
  def unload_cref(parent, cname)
497
+ # Let's optimistically remove_const. The way we use it, this is going to
498
+ # succeed always if all is good.
808
499
  parent.__send__(:remove_const, cname)
500
+ rescue ::NameError
501
+ # There are a few edge scenarios in which this may happen. If the constant
502
+ # is gone, that is OK, anyway.
503
+ else
809
504
  log("#{cpath(parent, cname)} unloaded") if logger
810
505
  end
811
506
  end