zeitwerk 2.2.2 → 2.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,13 +9,13 @@ module Zeitwerk
9
9
  include Callbacks
10
10
  include RealModName
11
11
 
12
- # @return [String]
12
+ # @sig String
13
13
  attr_reader :tag
14
14
 
15
- # @return [#camelize]
15
+ # @sig #camelize
16
16
  attr_accessor :inflector
17
17
 
18
- # @return [#call, #debug, nil]
18
+ # @sig #call | #debug | nil
19
19
  attr_accessor :logger
20
20
 
21
21
  # Absolute paths of the root directories. Stored in a hash to preserve
@@ -30,20 +30,20 @@ module Zeitwerk
30
30
  # interface for it is `push_dir` and `dirs`.
31
31
  #
32
32
  # @private
33
- # @return [{String => true}]
33
+ # @sig Hash[String, true]
34
34
  attr_reader :root_dirs
35
35
 
36
36
  # Absolute paths of files or directories that have to be preloaded.
37
37
  #
38
38
  # @private
39
- # @return [<String>]
39
+ # @sig Array[String]
40
40
  attr_reader :preloads
41
41
 
42
- # Absolute paths of files, directories, of glob patterns to be totally
42
+ # Absolute paths of files, directories, or glob patterns to be totally
43
43
  # ignored.
44
44
  #
45
45
  # @private
46
- # @return [Set<String>]
46
+ # @sig Set[String]
47
47
  attr_reader :ignored_glob_patterns
48
48
 
49
49
  # The actual collection of absolute file and directory names at the time the
@@ -51,9 +51,22 @@ module Zeitwerk
51
51
  # reload.
52
52
  #
53
53
  # @private
54
- # @return [Set<String>]
54
+ # @sig Set[String]
55
55
  attr_reader :ignored_paths
56
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
69
+
57
70
  # Maps real absolute paths for which an autoload has been set ---and not
58
71
  # executed--- to their corresponding parent class or module and constant
59
72
  # name.
@@ -63,7 +76,7 @@ module Zeitwerk
63
76
  # ...
64
77
  #
65
78
  # @private
66
- # @return [{String => (Module, Symbol)}]
79
+ # @sig Hash[String, [Module, Symbol]]
67
80
  attr_reader :autoloads
68
81
 
69
82
  # We keep track of autoloaded directories to remove them from the registry
@@ -73,7 +86,7 @@ module Zeitwerk
73
86
  # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
74
87
  #
75
88
  # @private
76
- # @return [<String>]
89
+ # @sig Array[String]
77
90
  attr_reader :autoloaded_dirs
78
91
 
79
92
  # Stores metadata needed for unloading. Its entries look like this:
@@ -89,7 +102,7 @@ module Zeitwerk
89
102
  # or eager loaded. Otherwise, the collection remains empty.
90
103
  #
91
104
  # @private
92
- # @return [{String => (String, (Module, Symbol))}]
105
+ # @sig Hash[String, [String, [Module, Symbol]]]
93
106
  attr_reader :to_unload
94
107
 
95
108
  # Maps constant paths of namespaces to arrays of corresponding directories.
@@ -107,21 +120,24 @@ module Zeitwerk
107
120
  # up the corresponding autoloads.
108
121
  #
109
122
  # @private
110
- # @return [{String => <String>}]
123
+ # @sig Hash[String, Array[String]]
111
124
  attr_reader :lazy_subdirs
112
125
 
113
126
  # Absolute paths of files or directories not to be eager loaded.
114
127
  #
115
128
  # @private
116
- # @return [Set<String>]
129
+ # @sig Set[String]
117
130
  attr_reader :eager_load_exclusions
118
131
 
132
+ # User-oriented callbacks to be fired when a constant is loaded.
133
+ attr_reader :on_load_callbacks
134
+
119
135
  # @private
120
- # @return [Mutex]
136
+ # @sig Mutex
121
137
  attr_reader :mutex
122
138
 
123
139
  # @private
124
- # @return [Mutex]
140
+ # @sig Mutex
125
141
  attr_reader :mutex2
126
142
 
127
143
  def initialize
@@ -131,15 +147,18 @@ module Zeitwerk
131
147
  @inflector = Inflector.new
132
148
  @logger = self.class.default_logger
133
149
 
134
- @root_dirs = {}
135
- @preloads = []
136
- @ignored_glob_patterns = Set.new
137
- @ignored_paths = Set.new
138
- @autoloads = {}
139
- @autoloaded_dirs = []
140
- @to_unload = {}
141
- @lazy_subdirs = {}
142
- @eager_load_exclusions = Set.new
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 = {}
143
162
 
144
163
  # TODO: find a better name for these mutexes.
145
164
  @mutex = Mutex.new
@@ -154,7 +173,8 @@ module Zeitwerk
154
173
 
155
174
  # Sets a tag for the loader, useful for logging.
156
175
  #
157
- # @return [void]
176
+ # @param tag [#to_s]
177
+ # @sig (#to_s) -> void
158
178
  def tag=(tag)
159
179
  @tag = tag.to_s
160
180
  end
@@ -162,7 +182,7 @@ module Zeitwerk
162
182
  # Absolute paths of the root directories. This is a read-only collection,
163
183
  # please push here via `push_dir`.
164
184
  #
165
- # @return [<String>]
185
+ # @sig () -> Array[String]
166
186
  def dirs
167
187
  root_dirs.keys.freeze
168
188
  end
@@ -173,14 +193,18 @@ module Zeitwerk
173
193
  # the same process already manages that directory or one of its ascendants
174
194
  # or descendants.
175
195
  #
176
- # @param path [<String, Pathname>]
177
196
  # @raise [Zeitwerk::Error]
178
- # @return [void]
179
- def push_dir(path)
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
+
180
204
  abspath = File.expand_path(path)
181
205
  if dir?(abspath)
182
206
  raise_if_conflicting_directory(abspath)
183
- root_dirs[abspath] = true
207
+ root_dirs[abspath] = namespace
184
208
  else
185
209
  raise Error, "the root directory #{abspath} does not exist"
186
210
  end
@@ -190,7 +214,7 @@ module Zeitwerk
190
214
  # There is no way to undo this, either you want to reload or you don't.
191
215
  #
192
216
  # @raise [Zeitwerk::Error]
193
- # @return [void]
217
+ # @sig () -> void
194
218
  def enable_reloading
195
219
  mutex.synchronize do
196
220
  break if @reloading_enabled
@@ -203,15 +227,14 @@ module Zeitwerk
203
227
  end
204
228
  end
205
229
 
206
- # @return [Boolean]
230
+ # @sig () -> bool
207
231
  def reloading_enabled?
208
232
  @reloading_enabled
209
233
  end
210
234
 
211
235
  # Files or directories to be preloaded instead of lazy loaded.
212
236
  #
213
- # @param paths [<String, Pathname, <String, Pathname>>]
214
- # @return [void]
237
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
215
238
  def preload(*paths)
216
239
  mutex.synchronize do
217
240
  expand_paths(paths).each do |abspath|
@@ -223,8 +246,7 @@ module Zeitwerk
223
246
 
224
247
  # Configure files, directories, or glob patterns to be totally ignored.
225
248
  #
226
- # @param paths [<String, Pathname, <String, Pathname>>]
227
- # @return [void]
249
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
228
250
  def ignore(*glob_patterns)
229
251
  glob_patterns = expand_paths(glob_patterns)
230
252
  mutex.synchronize do
@@ -233,14 +255,45 @@ module Zeitwerk
233
255
  end
234
256
  end
235
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
285
+ end
286
+
236
287
  # Sets autoloads in the root namespace and preloads files, if any.
237
288
  #
238
- # @return [void]
289
+ # @sig () -> void
239
290
  def setup
240
291
  mutex.synchronize do
241
292
  break if @setup
242
293
 
243
- actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
294
+ actual_root_dirs.each do |root_dir, namespace|
295
+ set_autoloads_in_dir(root_dir, namespace)
296
+ end
244
297
  do_preload
245
298
 
246
299
  @setup = true
@@ -255,7 +308,7 @@ module Zeitwerk
255
308
  # unload them.
256
309
  #
257
310
  # @private
258
- # @return [void]
311
+ # @sig () -> void
259
312
  def unload
260
313
  mutex.synchronize do
261
314
  # We are going to keep track of the files that were required by our
@@ -306,7 +359,8 @@ module Zeitwerk
306
359
  Registry.on_unload(self)
307
360
  ExplicitNamespace.unregister(self)
308
361
 
309
- @setup = false
362
+ @setup = false
363
+ @eager_loaded = false
310
364
  end
311
365
  end
312
366
 
@@ -317,11 +371,12 @@ module Zeitwerk
317
371
  # client code in the README of the project.
318
372
  #
319
373
  # @raise [Zeitwerk::Error]
320
- # @return [void]
374
+ # @sig () -> void
321
375
  def reload
322
376
  if reloading_enabled?
323
377
  unload
324
378
  recompute_ignored_paths
379
+ recompute_collapse_dirs
325
380
  setup
326
381
  else
327
382
  raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
@@ -333,13 +388,16 @@ module Zeitwerk
333
388
  # are not eager loaded. You can opt-out specifically in specific files and
334
389
  # directories with `do_not_eager_load`.
335
390
  #
336
- # @return [void]
391
+ # @sig () -> void
337
392
  def eager_load
338
393
  mutex.synchronize do
339
394
  break if @eager_loaded
340
395
 
341
- queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
342
- queue.map! { |dir| [Object, dir] }
396
+ queue = []
397
+ actual_root_dirs.each do |root_dir, namespace|
398
+ queue << [namespace, root_dir] unless eager_load_exclusions.member?(root_dir)
399
+ end
400
+
343
401
  while to_eager_load = queue.shift
344
402
  namespace, dir = to_eager_load
345
403
 
@@ -351,8 +409,12 @@ module Zeitwerk
351
409
  cref[0].const_get(cref[1], false)
352
410
  end
353
411
  elsif dir?(abspath) && !root_dirs.key?(abspath)
354
- cname = inflector.camelize(basename, abspath)
355
- queue << [namespace.const_get(cname, false), abspath]
412
+ if collapse_dirs.member?(abspath)
413
+ queue << [namespace, abspath]
414
+ else
415
+ cname = inflector.camelize(basename, abspath)
416
+ queue << [namespace.const_get(cname, false), abspath]
417
+ end
356
418
  end
357
419
  end
358
420
  end
@@ -369,8 +431,7 @@ module Zeitwerk
369
431
  # Let eager load ignore the given files or directories. The constants
370
432
  # defined in those files are still autoloadable.
371
433
  #
372
- # @param paths [<String, Pathname, <String, Pathname>>]
373
- # @return [void]
434
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
374
435
  def do_not_eager_load(*paths)
375
436
  mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
376
437
  end
@@ -378,8 +439,7 @@ module Zeitwerk
378
439
  # Says if the given constant path would be unloaded on reload. This
379
440
  # predicate returns `false` if reloading is disabled.
380
441
  #
381
- # @param cpath [String]
382
- # @return [Boolean]
442
+ # @sig (String) -> bool
383
443
  def unloadable_cpath?(cpath)
384
444
  to_unload.key?(cpath)
385
445
  end
@@ -387,21 +447,20 @@ module Zeitwerk
387
447
  # Returns an array with the constant paths that would be unloaded on reload.
388
448
  # This predicate returns an empty array if reloading is disabled.
389
449
  #
390
- # @return [<String>]
450
+ # @sig () -> Array[String]
391
451
  def unloadable_cpaths
392
452
  to_unload.keys.freeze
393
453
  end
394
454
 
395
455
  # Logs to `$stdout`, handy shortcut for debugging.
396
456
  #
397
- # @return [void]
457
+ # @sig () -> void
398
458
  def log!
399
459
  @logger = ->(msg) { puts msg }
400
460
  end
401
461
 
402
462
  # @private
403
- # @param dir [String]
404
- # @return [Boolean]
463
+ # @sig (String) -> bool
405
464
  def manages?(dir)
406
465
  dir = dir + "/"
407
466
  ignored_paths.each do |ignored_path|
@@ -418,11 +477,11 @@ module Zeitwerk
418
477
  # --- Class methods ---------------------------------------------------------------------------
419
478
 
420
479
  class << self
421
- # @return [#call, #debug, nil]
480
+ # @sig #call | #debug | nil
422
481
  attr_accessor :default_logger
423
482
 
424
483
  # @private
425
- # @return [Mutex]
484
+ # @sig Mutex
426
485
  attr_accessor :mutex
427
486
 
428
487
  # This is a shortcut for
@@ -430,13 +489,13 @@ module Zeitwerk
430
489
  # require "zeitwerk"
431
490
  # loader = Zeitwerk::Loader.new
432
491
  # loader.tag = File.basename(__FILE__, ".rb")
433
- # loader.inflector = Zeitwerk::GemInflector.new
492
+ # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
434
493
  # loader.push_dir(__dir__)
435
494
  #
436
495
  # except that this method returns the same object in subsequent calls from
437
496
  # the same file, in the unlikely case the gem wants to be able to reload.
438
497
  #
439
- # @return [Zeitwerk::Loader]
498
+ # @sig () -> Zeitwerk::Loader
440
499
  def for_gem
441
500
  called_from = caller_locations(1, 1).first.path
442
501
  Registry.loader_for_gem(called_from)
@@ -444,7 +503,7 @@ module Zeitwerk
444
503
 
445
504
  # Broadcasts `eager_load` to all loaders.
446
505
  #
447
- # @return [void]
506
+ # @sig () -> void
448
507
  def eager_load_all
449
508
  Registry.loaders.each(&:eager_load)
450
509
  end
@@ -452,7 +511,7 @@ module Zeitwerk
452
511
  # Returns an array with the absolute paths of the root directories of all
453
512
  # registered loaders. This is a read-only collection.
454
513
  #
455
- # @return [<String>]
514
+ # @sig () -> Array[String]
456
515
  def all_dirs
457
516
  Registry.loaders.flat_map(&:dirs).freeze
458
517
  end
@@ -462,21 +521,19 @@ module Zeitwerk
462
521
 
463
522
  private # -------------------------------------------------------------------------------------
464
523
 
465
- # @return [<String>]
524
+ # @sig () -> Array[String]
466
525
  def actual_root_dirs
467
- root_dirs.keys.delete_if do |root_dir|
526
+ root_dirs.reject do |root_dir, _namespace|
468
527
  !dir?(root_dir) || ignored_paths.member?(root_dir)
469
528
  end
470
529
  end
471
530
 
472
- # @param dir [String]
473
- # @param parent [Module]
474
- # @return [void]
531
+ # @sig (String, Module) -> void
475
532
  def set_autoloads_in_dir(dir, parent)
476
533
  ls(dir) do |basename, abspath|
477
534
  begin
478
535
  if ruby?(basename)
479
- basename.slice!(-3, 3)
536
+ basename[-3..-1] = ''
480
537
  cname = inflector.camelize(basename, abspath).to_sym
481
538
  autoload_file(parent, cname, abspath)
482
539
  elsif dir?(abspath)
@@ -488,7 +545,11 @@ module Zeitwerk
488
545
  # it counts only as root. The guard checks that.
489
546
  unless root_dirs.key?(abspath)
490
547
  cname = inflector.camelize(basename, abspath).to_sym
491
- autoload_subdir(parent, cname, abspath)
548
+ if collapse_dirs.member?(abspath)
549
+ set_autoloads_in_dir(abspath, parent)
550
+ else
551
+ autoload_subdir(parent, cname, abspath)
552
+ end
492
553
  end
493
554
  end
494
555
  rescue ::NameError => error
@@ -510,10 +571,7 @@ module Zeitwerk
510
571
  end
511
572
  end
512
573
 
513
- # @param parent [Module]
514
- # @param cname [Symbol]
515
- # @param subdir [String]
516
- # @return [void]
574
+ # @sig (Module, Symbol, String) -> void
517
575
  def autoload_subdir(parent, cname, subdir)
518
576
  if autoload_path = autoload_for?(parent, cname)
519
577
  cpath = cpath(parent, cname)
@@ -533,10 +591,7 @@ module Zeitwerk
533
591
  end
534
592
  end
535
593
 
536
- # @param parent [Module]
537
- # @param cname [Symbol]
538
- # @param file [String]
539
- # @return [void]
594
+ # @sig (Module, Symbol, String) -> void
540
595
  def autoload_file(parent, cname, file)
541
596
  if autoload_path = autoload_for?(parent, cname)
542
597
  # First autoload for a Ruby file wins, just ignore subsequent ones.
@@ -557,11 +612,10 @@ module Zeitwerk
557
612
  end
558
613
  end
559
614
 
560
- # @param dir [String] directory that would have autovivified a module
561
- # @param file [String] the file where the namespace is explicitly defined
562
- # @param parent [Module]
563
- # @param cname [Symbol]
564
- # @return [void]
615
+ # `dir` is the directory that would have autovivified a namespace. `file` is
616
+ # the file where we've found the namespace is explicitly defined.
617
+ #
618
+ # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
565
619
  def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
566
620
  autoloads.delete(dir)
567
621
  Registry.unregister_autoload(dir)
@@ -570,15 +624,15 @@ module Zeitwerk
570
624
  register_explicit_namespace(cpath(parent, cname))
571
625
  end
572
626
 
573
- # @param parent [Module]
574
- # @param cname [Symbol]
575
- # @param abspath [String]
576
- # @return [void]
627
+ # @sig (Module, Symbol, String) -> void
577
628
  def set_autoload(parent, cname, abspath)
578
629
  # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
579
630
  # real path to be able to delete it from $LOADED_FEATURES on unload, and to
580
631
  # be able to do a lookup later in Kernel#require for manual require calls.
581
- realpath = File.realpath(abspath)
632
+ #
633
+ # We freeze realpath because that saves allocations in Module#autoload.
634
+ # See #125.
635
+ realpath = File.realpath(abspath).freeze
582
636
  parent.autoload(cname, realpath)
583
637
  if logger
584
638
  if ruby?(realpath)
@@ -597,9 +651,7 @@ module Zeitwerk
597
651
  end
598
652
  end
599
653
 
600
- # @param parent [Module]
601
- # @param cname [Symbol]
602
- # @return [String, nil]
654
+ # @sig (Module, Symbol) -> String?
603
655
  def autoload_for?(parent, cname)
604
656
  strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
605
657
  end
@@ -620,9 +672,7 @@ module Zeitwerk
620
672
  #
621
673
  # We need a way to strictly check in parent ignoring ancestors.
622
674
  #
623
- # @param parent [Module]
624
- # @param cname [Symbol]
625
- # @return [String, nil]
675
+ # @sig (Module, Symbol) -> String?
626
676
  if method(:autoload?).arity == 1
627
677
  def strict_autoload_path(parent, cname)
628
678
  parent.autoload?(cname) if cdef?(parent, cname)
@@ -636,15 +686,14 @@ module Zeitwerk
636
686
  # This method is called this way because I prefer `preload` to be the method
637
687
  # name to configure preloads in the public interface.
638
688
  #
639
- # @return [void]
689
+ # @sig () -> void
640
690
  def do_preload
641
691
  preloads.each do |abspath|
642
692
  do_preload_abspath(abspath)
643
693
  end
644
694
  end
645
695
 
646
- # @param abspath [String]
647
- # @return [void]
696
+ # @sig (String) -> void
648
697
  def do_preload_abspath(abspath)
649
698
  if ruby?(abspath)
650
699
  do_preload_file(abspath)
@@ -653,85 +702,87 @@ module Zeitwerk
653
702
  end
654
703
  end
655
704
 
656
- # @param dir [String]
657
- # @return [void]
705
+ # @sig (String) -> void
658
706
  def do_preload_dir(dir)
659
707
  ls(dir) do |_basename, abspath|
660
708
  do_preload_abspath(abspath)
661
709
  end
662
710
  end
663
711
 
664
- # @param file [String]
665
- # @return [Boolean]
712
+ # @sig (String) -> bool
666
713
  def do_preload_file(file)
667
714
  log("preloading #{file}") if logger
668
715
  require file
669
716
  end
670
717
 
671
- # @param parent [Module]
672
- # @param cname [Symbol]
673
- # @return [String]
718
+ # @sig (Module, Symbol) -> String
674
719
  def cpath(parent, cname)
675
720
  parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
676
721
  end
677
722
 
678
- # @param dir [String]
679
- # @yieldparam path [String, String]
680
- # @return [void]
723
+ # @sig (String) { (String, String) -> void } -> void
681
724
  def ls(dir)
682
725
  Dir.foreach(dir) do |basename|
683
726
  next if basename.start_with?(".")
727
+
684
728
  abspath = File.join(dir, basename)
685
- yield basename, abspath unless ignored_paths.member?(abspath)
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
686
734
  end
687
735
  end
688
736
 
689
- # @param path [String]
690
- # @return [Boolean]
737
+ # @sig (String) -> bool
691
738
  def ruby?(path)
692
739
  path.end_with?(".rb")
693
740
  end
694
741
 
695
- # @param path [String]
696
- # @return [Boolean]
742
+ # @sig (String) -> bool
697
743
  def dir?(path)
698
744
  File.directory?(path)
699
745
  end
700
746
 
701
- # @param paths [<String, Pathname, <String, Pathname>>]
702
- # @return [<String>]
747
+ # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
703
748
  def expand_paths(paths)
704
749
  paths.flatten.map! { |path| File.expand_path(path) }
705
750
  end
706
751
 
707
- # @param glob_patterns [<String>]
708
- # @return [<String>]
752
+ # @sig (Array[String]) -> Array[String]
709
753
  def expand_glob_patterns(glob_patterns)
710
754
  # Note that Dir.glob works with regular file names just fine. That is,
711
755
  # glob patterns technically need no wildcards.
712
756
  glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
713
757
  end
714
758
 
715
- # @return [void]
759
+ # @sig () -> void
716
760
  def recompute_ignored_paths
717
761
  ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
718
762
  end
719
763
 
720
- # @param message [String]
721
- # @return [void]
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
722
770
  def log(message)
723
771
  method_name = logger.respond_to?(:debug) ? :debug : :call
724
772
  logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
725
773
  end
726
774
 
775
+ # @sig (Module, Symbol) -> bool
727
776
  def cdef?(parent, cname)
728
777
  parent.const_defined?(cname, false)
729
778
  end
730
779
 
780
+ # @sig (String) -> void
731
781
  def register_explicit_namespace(cpath)
732
782
  ExplicitNamespace.register(cpath, self)
733
783
  end
734
784
 
785
+ # @sig (String) -> void
735
786
  def raise_if_conflicting_directory(dir)
736
787
  self.class.mutex.synchronize do
737
788
  Registry.loaders.each do |loader|
@@ -746,19 +797,15 @@ module Zeitwerk
746
797
  end
747
798
  end
748
799
 
749
- # @param parent [Module]
750
- # @param cname [Symbol]
751
- # @return [void]
800
+ # @sig (Module, Symbol) -> void
752
801
  def unload_autoload(parent, cname)
753
- parent.send(:remove_const, cname)
802
+ parent.__send__(:remove_const, cname)
754
803
  log("autoload for #{cpath(parent, cname)} removed") if logger
755
804
  end
756
805
 
757
- # @param parent [Module]
758
- # @param cname [Symbol]
759
- # @return [void]
806
+ # @sig (Module, Symbol) -> void
760
807
  def unload_cref(parent, cname)
761
- parent.send(:remove_const, cname)
808
+ parent.__send__(:remove_const, cname)
762
809
  log("#{cpath(parent, cname)} unloaded") if logger
763
810
  end
764
811
  end