zeitwerk 2.2.1 → 2.4.1

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.
@@ -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,21 @@ 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
 
119
132
  # @private
120
- # @return [Mutex]
133
+ # @sig Mutex
121
134
  attr_reader :mutex
122
135
 
123
136
  # @private
124
- # @return [Mutex]
137
+ # @sig Mutex
125
138
  attr_reader :mutex2
126
139
 
127
140
  def initialize
@@ -131,15 +144,17 @@ module Zeitwerk
131
144
  @inflector = Inflector.new
132
145
  @logger = self.class.default_logger
133
146
 
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
147
+ @root_dirs = {}
148
+ @preloads = []
149
+ @ignored_glob_patterns = Set.new
150
+ @ignored_paths = Set.new
151
+ @collapse_glob_patterns = Set.new
152
+ @collapse_dirs = Set.new
153
+ @autoloads = {}
154
+ @autoloaded_dirs = []
155
+ @to_unload = {}
156
+ @lazy_subdirs = {}
157
+ @eager_load_exclusions = Set.new
143
158
 
144
159
  # TODO: find a better name for these mutexes.
145
160
  @mutex = Mutex.new
@@ -154,7 +169,8 @@ module Zeitwerk
154
169
 
155
170
  # Sets a tag for the loader, useful for logging.
156
171
  #
157
- # @return [void]
172
+ # @param tag [#to_s]
173
+ # @sig (#to_s) -> void
158
174
  def tag=(tag)
159
175
  @tag = tag.to_s
160
176
  end
@@ -162,7 +178,7 @@ module Zeitwerk
162
178
  # Absolute paths of the root directories. This is a read-only collection,
163
179
  # please push here via `push_dir`.
164
180
  #
165
- # @return [<String>]
181
+ # @sig () -> Array[String]
166
182
  def dirs
167
183
  root_dirs.keys.freeze
168
184
  end
@@ -173,14 +189,18 @@ module Zeitwerk
173
189
  # the same process already manages that directory or one of its ascendants
174
190
  # or descendants.
175
191
  #
176
- # @param path [<String, Pathname>]
177
192
  # @raise [Zeitwerk::Error]
178
- # @return [void]
179
- def push_dir(path)
193
+ # @sig (String | Pathname, Module) -> void
194
+ def push_dir(path, namespace: Object)
195
+ # Note that Class < Module.
196
+ unless namespace.is_a?(Module)
197
+ raise Error, "#{namespace.inspect} is not a class or module object, should be"
198
+ end
199
+
180
200
  abspath = File.expand_path(path)
181
201
  if dir?(abspath)
182
202
  raise_if_conflicting_directory(abspath)
183
- root_dirs[abspath] = true
203
+ root_dirs[abspath] = namespace
184
204
  else
185
205
  raise Error, "the root directory #{abspath} does not exist"
186
206
  end
@@ -190,7 +210,7 @@ module Zeitwerk
190
210
  # There is no way to undo this, either you want to reload or you don't.
191
211
  #
192
212
  # @raise [Zeitwerk::Error]
193
- # @return [void]
213
+ # @sig () -> void
194
214
  def enable_reloading
195
215
  mutex.synchronize do
196
216
  break if @reloading_enabled
@@ -203,15 +223,14 @@ module Zeitwerk
203
223
  end
204
224
  end
205
225
 
206
- # @return [Boolean]
226
+ # @sig () -> bool
207
227
  def reloading_enabled?
208
228
  @reloading_enabled
209
229
  end
210
230
 
211
231
  # Files or directories to be preloaded instead of lazy loaded.
212
232
  #
213
- # @param paths [<String, Pathname, <String, Pathname>>]
214
- # @return [void]
233
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
215
234
  def preload(*paths)
216
235
  mutex.synchronize do
217
236
  expand_paths(paths).each do |abspath|
@@ -223,8 +242,7 @@ module Zeitwerk
223
242
 
224
243
  # Configure files, directories, or glob patterns to be totally ignored.
225
244
  #
226
- # @param paths [<String, Pathname, <String, Pathname>>]
227
- # @return [void]
245
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
228
246
  def ignore(*glob_patterns)
229
247
  glob_patterns = expand_paths(glob_patterns)
230
248
  mutex.synchronize do
@@ -233,14 +251,27 @@ module Zeitwerk
233
251
  end
234
252
  end
235
253
 
254
+ # Configure directories or glob patterns to be collapsed.
255
+ #
256
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
257
+ def collapse(*glob_patterns)
258
+ glob_patterns = expand_paths(glob_patterns)
259
+ mutex.synchronize do
260
+ collapse_glob_patterns.merge(glob_patterns)
261
+ collapse_dirs.merge(expand_glob_patterns(glob_patterns))
262
+ end
263
+ end
264
+
236
265
  # Sets autoloads in the root namespace and preloads files, if any.
237
266
  #
238
- # @return [void]
267
+ # @sig () -> void
239
268
  def setup
240
269
  mutex.synchronize do
241
270
  break if @setup
242
271
 
243
- actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
272
+ actual_root_dirs.each do |root_dir, namespace|
273
+ set_autoloads_in_dir(root_dir, namespace)
274
+ end
244
275
  do_preload
245
276
 
246
277
  @setup = true
@@ -255,7 +286,7 @@ module Zeitwerk
255
286
  # unload them.
256
287
  #
257
288
  # @private
258
- # @return [void]
289
+ # @sig () -> void
259
290
  def unload
260
291
  mutex.synchronize do
261
292
  # We are going to keep track of the files that were required by our
@@ -306,7 +337,8 @@ module Zeitwerk
306
337
  Registry.on_unload(self)
307
338
  ExplicitNamespace.unregister(self)
308
339
 
309
- @setup = false
340
+ @setup = false
341
+ @eager_loaded = false
310
342
  end
311
343
  end
312
344
 
@@ -317,11 +349,12 @@ module Zeitwerk
317
349
  # client code in the README of the project.
318
350
  #
319
351
  # @raise [Zeitwerk::Error]
320
- # @return [void]
352
+ # @sig () -> void
321
353
  def reload
322
354
  if reloading_enabled?
323
355
  unload
324
356
  recompute_ignored_paths
357
+ recompute_collapse_dirs
325
358
  setup
326
359
  else
327
360
  raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
@@ -333,13 +366,16 @@ module Zeitwerk
333
366
  # are not eager loaded. You can opt-out specifically in specific files and
334
367
  # directories with `do_not_eager_load`.
335
368
  #
336
- # @return [void]
369
+ # @sig () -> void
337
370
  def eager_load
338
371
  mutex.synchronize do
339
372
  break if @eager_loaded
340
373
 
341
- queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
342
- queue.map! { |dir| [Object, dir] }
374
+ queue = []
375
+ actual_root_dirs.each do |root_dir, namespace|
376
+ queue << [namespace, root_dir] unless eager_load_exclusions.member?(root_dir)
377
+ end
378
+
343
379
  while to_eager_load = queue.shift
344
380
  namespace, dir = to_eager_load
345
381
 
@@ -351,8 +387,12 @@ module Zeitwerk
351
387
  cref[0].const_get(cref[1], false)
352
388
  end
353
389
  elsif dir?(abspath) && !root_dirs.key?(abspath)
354
- cname = inflector.camelize(basename, abspath)
355
- queue << [namespace.const_get(cname, false), abspath]
390
+ if collapse_dirs.member?(abspath)
391
+ queue << [namespace, abspath]
392
+ else
393
+ cname = inflector.camelize(basename, abspath)
394
+ queue << [namespace.const_get(cname, false), abspath]
395
+ end
356
396
  end
357
397
  end
358
398
  end
@@ -369,8 +409,7 @@ module Zeitwerk
369
409
  # Let eager load ignore the given files or directories. The constants
370
410
  # defined in those files are still autoloadable.
371
411
  #
372
- # @param paths [<String, Pathname, <String, Pathname>>]
373
- # @return [void]
412
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
374
413
  def do_not_eager_load(*paths)
375
414
  mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
376
415
  end
@@ -378,8 +417,7 @@ module Zeitwerk
378
417
  # Says if the given constant path would be unloaded on reload. This
379
418
  # predicate returns `false` if reloading is disabled.
380
419
  #
381
- # @param cpath [String]
382
- # @return [Boolean]
420
+ # @sig (String) -> bool
383
421
  def unloadable_cpath?(cpath)
384
422
  to_unload.key?(cpath)
385
423
  end
@@ -387,21 +425,20 @@ module Zeitwerk
387
425
  # Returns an array with the constant paths that would be unloaded on reload.
388
426
  # This predicate returns an empty array if reloading is disabled.
389
427
  #
390
- # @return [<String>]
428
+ # @sig () -> Array[String]
391
429
  def unloadable_cpaths
392
430
  to_unload.keys.freeze
393
431
  end
394
432
 
395
433
  # Logs to `$stdout`, handy shortcut for debugging.
396
434
  #
397
- # @return [void]
435
+ # @sig () -> void
398
436
  def log!
399
437
  @logger = ->(msg) { puts msg }
400
438
  end
401
439
 
402
440
  # @private
403
- # @param dir [String]
404
- # @return [Boolean]
441
+ # @sig (String) -> bool
405
442
  def manages?(dir)
406
443
  dir = dir + "/"
407
444
  ignored_paths.each do |ignored_path|
@@ -418,11 +455,11 @@ module Zeitwerk
418
455
  # --- Class methods ---------------------------------------------------------------------------
419
456
 
420
457
  class << self
421
- # @return [#call, #debug, nil]
458
+ # @sig #call | #debug | nil
422
459
  attr_accessor :default_logger
423
460
 
424
461
  # @private
425
- # @return [Mutex]
462
+ # @sig Mutex
426
463
  attr_accessor :mutex
427
464
 
428
465
  # This is a shortcut for
@@ -430,13 +467,13 @@ module Zeitwerk
430
467
  # require "zeitwerk"
431
468
  # loader = Zeitwerk::Loader.new
432
469
  # loader.tag = File.basename(__FILE__, ".rb")
433
- # loader.inflector = Zeitwerk::GemInflector.new
470
+ # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
434
471
  # loader.push_dir(__dir__)
435
472
  #
436
473
  # except that this method returns the same object in subsequent calls from
437
474
  # the same file, in the unlikely case the gem wants to be able to reload.
438
475
  #
439
- # @return [Zeitwerk::Loader]
476
+ # @sig () -> Zeitwerk::Loader
440
477
  def for_gem
441
478
  called_from = caller_locations(1, 1).first.path
442
479
  Registry.loader_for_gem(called_from)
@@ -444,7 +481,7 @@ module Zeitwerk
444
481
 
445
482
  # Broadcasts `eager_load` to all loaders.
446
483
  #
447
- # @return [void]
484
+ # @sig () -> void
448
485
  def eager_load_all
449
486
  Registry.loaders.each(&:eager_load)
450
487
  end
@@ -452,7 +489,7 @@ module Zeitwerk
452
489
  # Returns an array with the absolute paths of the root directories of all
453
490
  # registered loaders. This is a read-only collection.
454
491
  #
455
- # @return [<String>]
492
+ # @sig () -> Array[String]
456
493
  def all_dirs
457
494
  Registry.loaders.flat_map(&:dirs).freeze
458
495
  end
@@ -462,21 +499,19 @@ module Zeitwerk
462
499
 
463
500
  private # -------------------------------------------------------------------------------------
464
501
 
465
- # @return [<String>]
502
+ # @sig () -> Array[String]
466
503
  def actual_root_dirs
467
- root_dirs.keys.delete_if do |root_dir|
504
+ root_dirs.reject do |root_dir, _namespace|
468
505
  !dir?(root_dir) || ignored_paths.member?(root_dir)
469
506
  end
470
507
  end
471
508
 
472
- # @param dir [String]
473
- # @param parent [Module]
474
- # @return [void]
509
+ # @sig (String, Module) -> void
475
510
  def set_autoloads_in_dir(dir, parent)
476
511
  ls(dir) do |basename, abspath|
477
512
  begin
478
513
  if ruby?(basename)
479
- basename.slice!(-3, 3)
514
+ basename[-3..-1] = ''
480
515
  cname = inflector.camelize(basename, abspath).to_sym
481
516
  autoload_file(parent, cname, abspath)
482
517
  elsif dir?(abspath)
@@ -488,13 +523,17 @@ module Zeitwerk
488
523
  # it counts only as root. The guard checks that.
489
524
  unless root_dirs.key?(abspath)
490
525
  cname = inflector.camelize(basename, abspath).to_sym
491
- autoload_subdir(parent, cname, abspath)
526
+ if collapse_dirs.member?(abspath)
527
+ set_autoloads_in_dir(abspath, parent)
528
+ else
529
+ autoload_subdir(parent, cname, abspath)
530
+ end
492
531
  end
493
532
  end
494
533
  rescue ::NameError => error
495
534
  path_type = ruby?(abspath) ? "file" : "directory"
496
535
 
497
- raise NameError, <<~MESSAGE
536
+ raise NameError.new(<<~MESSAGE, error.name)
498
537
  #{error.message} inferred by #{inflector.class} from #{path_type}
499
538
 
500
539
  #{abspath}
@@ -510,10 +549,7 @@ module Zeitwerk
510
549
  end
511
550
  end
512
551
 
513
- # @param parent [Module]
514
- # @param cname [Symbol]
515
- # @param subdir [String]
516
- # @return [void]
552
+ # @sig (Module, Symbol, String) -> void
517
553
  def autoload_subdir(parent, cname, subdir)
518
554
  if autoload_path = autoload_for?(parent, cname)
519
555
  cpath = cpath(parent, cname)
@@ -533,10 +569,7 @@ module Zeitwerk
533
569
  end
534
570
  end
535
571
 
536
- # @param parent [Module]
537
- # @param cname [Symbol]
538
- # @param file [String]
539
- # @return [void]
572
+ # @sig (Module, Symbol, String) -> void
540
573
  def autoload_file(parent, cname, file)
541
574
  if autoload_path = autoload_for?(parent, cname)
542
575
  # First autoload for a Ruby file wins, just ignore subsequent ones.
@@ -557,11 +590,10 @@ module Zeitwerk
557
590
  end
558
591
  end
559
592
 
560
- # @param dir [String] directory that would have autovivified a module
561
- # @param file [String] the file where the namespace is explictly defined
562
- # @param parent [Module]
563
- # @param cname [Symbol]
564
- # @return [void]
593
+ # `dir` is the directory that would have autovivified a namespace. `file` is
594
+ # the file where we've found the namespace is explicitly defined.
595
+ #
596
+ # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
565
597
  def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
566
598
  autoloads.delete(dir)
567
599
  Registry.unregister_autoload(dir)
@@ -570,15 +602,15 @@ module Zeitwerk
570
602
  register_explicit_namespace(cpath(parent, cname))
571
603
  end
572
604
 
573
- # @param parent [Module]
574
- # @param cname [Symbol]
575
- # @param abspath [String]
576
- # @return [void]
605
+ # @sig (Module, Symbol, String) -> void
577
606
  def set_autoload(parent, cname, abspath)
578
607
  # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
579
608
  # real path to be able to delete it from $LOADED_FEATURES on unload, and to
580
609
  # be able to do a lookup later in Kernel#require for manual require calls.
581
- realpath = File.realpath(abspath)
610
+ #
611
+ # We freeze realpath because that saves allocations in Module#autoload.
612
+ # See #125.
613
+ realpath = File.realpath(abspath).freeze
582
614
  parent.autoload(cname, realpath)
583
615
  if logger
584
616
  if ruby?(realpath)
@@ -597,9 +629,7 @@ module Zeitwerk
597
629
  end
598
630
  end
599
631
 
600
- # @param parent [Module]
601
- # @param cname [Symbol]
602
- # @return [String, nil]
632
+ # @sig (Module, Symbol) -> String?
603
633
  def autoload_for?(parent, cname)
604
634
  strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
605
635
  end
@@ -620,9 +650,7 @@ module Zeitwerk
620
650
  #
621
651
  # We need a way to strictly check in parent ignoring ancestors.
622
652
  #
623
- # @param parent [Module]
624
- # @param cname [Symbol]
625
- # @return [String, nil]
653
+ # @sig (Module, Symbol) -> String?
626
654
  if method(:autoload?).arity == 1
627
655
  def strict_autoload_path(parent, cname)
628
656
  parent.autoload?(cname) if cdef?(parent, cname)
@@ -636,15 +664,14 @@ module Zeitwerk
636
664
  # This method is called this way because I prefer `preload` to be the method
637
665
  # name to configure preloads in the public interface.
638
666
  #
639
- # @return [void]
667
+ # @sig () -> void
640
668
  def do_preload
641
669
  preloads.each do |abspath|
642
670
  do_preload_abspath(abspath)
643
671
  end
644
672
  end
645
673
 
646
- # @param abspath [String]
647
- # @return [void]
674
+ # @sig (String) -> void
648
675
  def do_preload_abspath(abspath)
649
676
  if ruby?(abspath)
650
677
  do_preload_file(abspath)
@@ -653,85 +680,87 @@ module Zeitwerk
653
680
  end
654
681
  end
655
682
 
656
- # @param dir [String]
657
- # @return [void]
683
+ # @sig (String) -> void
658
684
  def do_preload_dir(dir)
659
685
  ls(dir) do |_basename, abspath|
660
686
  do_preload_abspath(abspath)
661
687
  end
662
688
  end
663
689
 
664
- # @param file [String]
665
- # @return [Boolean]
690
+ # @sig (String) -> bool
666
691
  def do_preload_file(file)
667
692
  log("preloading #{file}") if logger
668
693
  require file
669
694
  end
670
695
 
671
- # @param parent [Module]
672
- # @param cname [Symbol]
673
- # @return [String]
696
+ # @sig (Module, Symbol) -> String
674
697
  def cpath(parent, cname)
675
698
  parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
676
699
  end
677
700
 
678
- # @param dir [String]
679
- # @yieldparam path [String, String]
680
- # @return [void]
701
+ # @sig (String) { (String, String) -> void } -> void
681
702
  def ls(dir)
682
703
  Dir.foreach(dir) do |basename|
683
704
  next if basename.start_with?(".")
705
+
684
706
  abspath = File.join(dir, basename)
685
- yield basename, abspath unless ignored_paths.member?(abspath)
707
+ next if ignored_paths.member?(abspath)
708
+
709
+ # We freeze abspath because that saves allocations when passed later to
710
+ # File methods. See #125.
711
+ yield basename, abspath.freeze
686
712
  end
687
713
  end
688
714
 
689
- # @param path [String]
690
- # @return [Boolean]
715
+ # @sig (String) -> bool
691
716
  def ruby?(path)
692
717
  path.end_with?(".rb")
693
718
  end
694
719
 
695
- # @param path [String]
696
- # @return [Boolean]
720
+ # @sig (String) -> bool
697
721
  def dir?(path)
698
722
  File.directory?(path)
699
723
  end
700
724
 
701
- # @param paths [<String, Pathname, <String, Pathname>>]
702
- # @return [<String>]
725
+ # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
703
726
  def expand_paths(paths)
704
727
  paths.flatten.map! { |path| File.expand_path(path) }
705
728
  end
706
729
 
707
- # @param glob_patterns [<String>]
708
- # @return [<String>]
730
+ # @sig (Array[String]) -> Array[String]
709
731
  def expand_glob_patterns(glob_patterns)
710
732
  # Note that Dir.glob works with regular file names just fine. That is,
711
733
  # glob patterns technically need no wildcards.
712
734
  glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
713
735
  end
714
736
 
715
- # @return [void]
737
+ # @sig () -> void
716
738
  def recompute_ignored_paths
717
739
  ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
718
740
  end
719
741
 
720
- # @param message [String]
721
- # @return [void]
742
+ # @sig () -> void
743
+ def recompute_collapse_dirs
744
+ collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
745
+ end
746
+
747
+ # @sig (String) -> void
722
748
  def log(message)
723
749
  method_name = logger.respond_to?(:debug) ? :debug : :call
724
750
  logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
725
751
  end
726
752
 
753
+ # @sig (Module, Symbol) -> bool
727
754
  def cdef?(parent, cname)
728
755
  parent.const_defined?(cname, false)
729
756
  end
730
757
 
758
+ # @sig (String) -> void
731
759
  def register_explicit_namespace(cpath)
732
760
  ExplicitNamespace.register(cpath, self)
733
761
  end
734
762
 
763
+ # @sig (String) -> void
735
764
  def raise_if_conflicting_directory(dir)
736
765
  self.class.mutex.synchronize do
737
766
  Registry.loaders.each do |loader|
@@ -746,19 +775,15 @@ module Zeitwerk
746
775
  end
747
776
  end
748
777
 
749
- # @param parent [Module]
750
- # @param cname [Symbol]
751
- # @return [void]
778
+ # @sig (Module, Symbol) -> void
752
779
  def unload_autoload(parent, cname)
753
- parent.send(:remove_const, cname)
780
+ parent.__send__(:remove_const, cname)
754
781
  log("autoload for #{cpath(parent, cname)} removed") if logger
755
782
  end
756
783
 
757
- # @param parent [Module]
758
- # @param cname [Symbol]
759
- # @return [void]
784
+ # @sig (Module, Symbol) -> void
760
785
  def unload_cref(parent, cname)
761
- parent.send(:remove_const, cname)
786
+ parent.__send__(:remove_const, cname)
762
787
  log("#{cpath(parent, cname)} unloaded") if logger
763
788
  end
764
789
  end