zeitwerk 2.2.2 → 2.4.2

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