zeitwerk 2.4.2 → 2.5.0

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
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