opal-zeitwerk 0.0.4 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,731 +1,736 @@
1
- require "set"
2
- require "securerandom"
3
-
4
- module Zeitwerk
5
- class Loader
6
- require_relative "loader/callbacks"
7
- include Callbacks
8
- include RealModName
9
-
10
- # @return [String]
11
- attr_reader :tag
12
-
13
- # @return [#camelize]
14
- attr_accessor :inflector
15
-
16
- # Absolute paths of the root directories. Stored in a hash to preserve
17
- # order, easily handle duplicates, and also be able to have a fast lookup,
18
- # needed for detecting nested paths.
19
- #
20
- # "/Users/fxn/blog/app/assets" => true,
21
- # "/Users/fxn/blog/app/channels" => true,
22
- # ...
23
- #
24
- # This is a private collection maintained by the loader. The public
25
- # interface for it is `push_dir` and `dirs`.
26
- #
27
- # @private
28
- # @return [{String => true}]
29
- attr_reader :root_dirs
30
-
31
- # Absolute paths of files or directories that have to be preloaded.
32
- #
33
- # @private
34
- # @return [<String>]
35
- attr_reader :preloads
36
-
37
- # Absolute paths of files, directories, of glob patterns to be totally
38
- # ignored.
39
- #
40
- # @private
41
- # @return [Set<String>]
42
- attr_reader :ignored_glob_patterns
43
-
44
- # The actual collection of absolute file and directory names at the time the
45
- # ignored glob patterns were expanded. Computed on setup, and recomputed on
46
- # reload.
47
- #
48
- # @private
49
- # @return [Set<String>]
50
- attr_reader :ignored_paths
51
-
52
- # Maps real absolute paths for which an autoload has been set ---and not
53
- # executed--- to their corresponding parent class or module and constant
54
- # name.
55
- #
56
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
57
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
58
- # ...
59
- #
60
- # @private
61
- # @return [{String => (Module, Symbol)}]
62
- attr_reader :autoloads
63
-
64
- # We keep track of autoloaded directories to remove them from the registry
65
- # at the end of eager loading.
66
- #
67
- # Files are removed as they are autoloaded, but directories need to wait due
68
- # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
69
- #
70
- # @private
71
- # @return [<String>]
72
- attr_reader :autoloaded_dirs
73
-
74
- # Stores metadata needed for unloading. Its entries look like this:
75
- #
76
- # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
77
- #
78
- # The cpath as key helps implementing unloadable_cpath? The real file name
79
- # is stored in order to be able to delete it from $LOADED_FEATURES, and the
80
- # pair [Module, Symbol] is used to remove_const the constant from the class
81
- # or module object.
82
- #
83
- # If reloading is enabled, this hash is filled as constants are autoloaded
84
- # or eager loaded. Otherwise, the collection remains empty.
85
- #
86
- # @private
87
- # @return [{String => (String, (Module, Symbol))}]
88
- attr_reader :to_unload
89
-
90
- # Maps constant paths of namespaces to arrays of corresponding directories.
91
- #
92
- # For example, given this mapping:
93
- #
94
- # "Admin" => [
95
- # "/Users/fxn/blog/app/controllers/admin",
96
- # "/Users/fxn/blog/app/models/admin",
97
- # ...
98
- # ]
99
- #
100
- # when `Admin` gets defined we know that it plays the role of a namespace and
101
- # that its children are spread over those directories. We'll visit them to set
102
- # up the corresponding autoloads.
103
- #
104
- # @private
105
- # @return [{String => <String>}]
106
- attr_reader :lazy_subdirs
107
-
108
- # Absolute paths of files or directories not to be eager loaded.
109
- #
110
- # @private
111
- # @return [Set<String>]
112
- attr_reader :eager_load_exclusions
113
-
114
- attr_accessor :vivify_mod_dir
115
- attr_accessor :vivify_mod_class
116
-
117
- def initialize
118
- @initialized_at = Time.now
119
-
120
- @tag = SecureRandom.hex(3)
121
- @inflector = Inflector.new
122
-
123
- @root_dirs = {}
124
- @preloads = []
125
- @ignored_glob_patterns = Set.new
126
- @ignored_paths = Set.new
127
- @autoloads = {}
128
- @autoloaded_dirs = []
129
- @to_unload = {}
130
- @lazy_subdirs = {}
131
- @eager_load_exclusions = Set.new
132
-
133
- @setup = false
134
- @eager_loaded = false
135
-
136
- @reloading_enabled = false
137
-
138
- @vivify_mod_dir = false
139
- @module_paths
140
-
141
- Registry.register_loader(self)
142
- end
143
-
144
- # Sets a tag for the loader, useful for logging.
145
- #
146
- # @return [void]
147
- def tag=(tag)
148
- @tag = tag.to_s
149
- end
150
-
151
- # Absolute paths of the root directories. This is a read-only collection,
152
- # please push here via `push_dir`.
153
- #
154
- # @return [<String>]
155
- def dirs
156
- root_dirs.keys.freeze
157
- end
158
-
159
- # Pushes `path` to the list of root directories.
160
- #
161
- # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
162
- # the same process already manages that directory or one of its ascendants
163
- # or descendants.
164
- #
165
- # @param path [<String, Pathname>]
166
- # @raise [Zeitwerk::Error]
167
- # @return [void]
168
- def push_dir(path)
169
- abspath = File.expand_path(path)
170
- if dir?(abspath)
171
- raise_if_conflicting_directory(abspath)
172
- root_dirs[abspath] = true
173
- else
174
- warn_string = "Zeitwerk: the root path #{abspath} does not exist, not added"
175
- `console.warn(warn_string)`
176
- end
177
- end
178
-
179
- # You need to call this method before setup in order to be able to reload.
180
- # There is no way to undo this, either you want to reload or you don't.
181
- #
182
- # @raise [Zeitwerk::Error]
183
- # @return [void]
184
- def enable_reloading
185
- return if @reloading_enabled
186
-
187
- if @setup
188
- raise Error, "cannot enable reloading after setup"
189
- else
190
- @reloading_enabled = true
191
- end
192
- end
193
-
194
- # @return [Boolean]
195
- def reloading_enabled?
196
- @reloading_enabled
197
- end
198
-
199
- # Files or directories to be preloaded instead of lazy loaded.
200
- #
201
- # @param paths [<String, Pathname, <String, Pathname>>]
202
- # @return [void]
203
- def preload(*paths)
204
- expand_paths(paths).each do |abspath|
205
- preloads << abspath
206
- do_preload_abspath(abspath) if @setup
207
- end
208
- end
209
-
210
- # Configure files, directories, or glob patterns to be totally ignored.
211
- #
212
- # @param paths [<String, Pathname, <String, Pathname>>]
213
- # @return [void]
214
- def ignore(*glob_patterns)
215
- glob_patterns = expand_paths(glob_patterns)
216
- ignored_glob_patterns.merge(glob_patterns)
217
- ignored_paths.merge(expand_glob_patterns(glob_patterns))
218
- end
219
-
220
- # Sets autoloads in the root namespace and preloads files, if any.
221
- #
222
- # @return [void]
223
- def setup
224
- return if @setup
225
-
226
- actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
227
- do_preload
228
-
229
- @setup = true
230
- end
231
-
232
- # Removes loaded constants and configured autoloads.
233
- #
234
- # The objects the constants stored are no longer reachable through them. In
235
- # addition, since said objects are normally not referenced from anywhere
236
- # else, they are eligible for garbage collection, which would effectively
237
- # unload them.
238
- #
239
- # @private
240
- # @return [void]
241
- def unload
242
- # We are going to keep track of the files that were required by our
243
- # autoloads to later remove them from $LOADED_FEATURES, thus making them
244
- # loadable by Kernel#require again.
245
- #
246
- # Directories are not stored in $LOADED_FEATURES, keeping track of files
247
- # is enough.
248
- unloaded_files = Set.new
249
-
250
- autoloads.each do |realpath, (parent, cname)|
251
- if parent.autoload?(cname)
252
- unload_autoload(parent, cname)
253
- else
254
- # Could happen if loaded with require_relative. That is unsupported,
255
- # and the constant path would escape unloadable_cpath? This is just
256
- # defensive code to clean things up as much as we are able to.
257
- unload_cref(parent, cname) if cdef?(parent, cname)
258
- unloaded_files.add(realpath) if ruby?(realpath)
259
- end
260
- end
261
-
262
- to_unload.each_value do |(realpath, (parent, cname))|
263
- unload_cref(parent, cname) if cdef?(parent, cname)
264
- unloaded_files.add(realpath) if ruby?(realpath)
265
- end
266
-
267
- unless unloaded_files.empty?
268
- # Bootsnap decorates Kernel#require to speed it up using a cache and
269
- # this optimization does not check if $LOADED_FEATURES has the file.
270
- #
271
- # To make it aware of changes, the gem defines singleton methods in
272
- # $LOADED_FEATURES:
273
- #
274
- # https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
275
- #
276
- # Rails applications may depend on bootsnap, so for unloading to work
277
- # in that setting it is preferable that we restrict our API choice to
278
- # one of those methods.
279
- $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
280
- end
281
-
282
- autoloads.clear
283
- autoloaded_dirs.clear
284
- to_unload.clear
285
- lazy_subdirs.clear
286
-
287
- Registry.on_unload(self)
288
- ExplicitNamespace.unregister(self)
289
-
290
- @setup = false
291
- end
292
-
293
- # Unloads all loaded code, and calls setup again so that the loader is able
294
- # to pick any changes in the file system.
295
- #
296
- # This method is not thread-safe, please see how this can be achieved by
297
- # client code in the README of the project.
298
- #
299
- # @raise [Zeitwerk::Error]
300
- # @return [void]
301
- def reload
302
- if reloading_enabled?
303
- unload
304
- recompute_ignored_paths
305
- setup
306
- else
307
- raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
308
- end
309
- end
310
-
311
- # Eager loads all files in the root directories, recursively. Files do not
312
- # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
313
- # are not eager loaded. You can opt-out specifically in specific files and
314
- # directories with `do_not_eager_load`.
315
- #
316
- # @return [void]
317
- def eager_load
318
- return if @eager_loaded
319
-
320
- queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
321
- queue.map! { |dir| [Object, dir] }
322
- while to_eager_load = queue.shift
323
- namespace, dir = to_eager_load
324
-
325
- ls(dir) do |basename, abspath|
326
- next if eager_load_exclusions.member?(abspath)
327
-
328
- if ruby?(abspath)
329
- if cref = autoloads[File.realpath(abspath)]
330
- cref[0].const_get(cref[1], false)
331
- end
332
- elsif dir?(abspath) && !root_dirs.key?(abspath)
333
- cname = inflector.camelize(basename, abspath)
334
- queue << [namespace.const_get(cname, false), abspath]
335
- end
336
- end
337
- end
338
-
339
- autoloaded_dirs.each do |autoloaded_dir|
340
- Registry.unregister_autoload(autoloaded_dir)
341
- end
342
- autoloaded_dirs.clear
343
-
344
- @eager_loaded = true
345
- end
346
-
347
- # Let eager load ignore the given files or directories. The constants
348
- # defined in those files are still autoloadable.
349
- #
350
- # @param paths [<String, Pathname, <String, Pathname>>]
351
- # @return [void]
352
- def do_not_eager_load(*paths)
353
- eager_load_exclusions.merge(expand_paths(paths))
354
- end
355
-
356
- # Says if the given constant path would be unloaded on reload. This
357
- # predicate returns `false` if reloading is disabled.
358
- #
359
- # @param cpath [String]
360
- # @return [Boolean]
361
- def unloadable_cpath?(cpath)
362
- to_unload.key?(cpath)
363
- end
364
-
365
- # Returns an array with the constant paths that would be unloaded on reload.
366
- # This predicate returns an empty array if reloading is disabled.
367
- #
368
- # @return [<String>]
369
- def unloadable_cpaths
370
- to_unload.keys.freeze
371
- end
372
-
373
- # @private
374
- # @param dir [String]
375
- # @return [Boolean]
376
- def manages?(dir)
377
- dir = dir + "/"
378
- ignored_paths.each do |ignored_path|
379
- return false if dir.start_with?(ignored_path + "/")
380
- end
381
-
382
- root_dirs.each_key do |root_dir|
383
- return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
384
- end
385
-
386
- false
387
- end
388
-
389
- # --- Class methods ---------------------------------------------------------------------------
390
-
391
- class << self
392
- # Broadcasts `eager_load` to all loaders.
393
- #
394
- # @return [void]
395
- def eager_load_all
396
- Registry.loaders.each(&:eager_load)
397
- end
398
-
399
- # Returns an array with the absolute paths of the root directories of all
400
- # registered loaders. This is a read-only collection.
401
- #
402
- # @return [<String>]
403
- def all_dirs
404
- Registry.loaders.flat_map(&:dirs).freeze
405
- end
406
- end
407
-
408
- # @param dir [String]
409
- # @param parent [Module]
410
- # @return [void]
411
- def set_autoloads_in_dir(dir, parent)
412
- ls(dir) do |basename, abspath|
413
- begin
414
- if ruby?(abspath)
415
- # basename = basename.slice(-3, 3)
416
- cname = inflector.camelize(basename, abspath).to_sym
417
- autoload_file(parent, cname, abspath)
418
- elsif dir?(abspath)
419
- # In a Rails application, `app/models/concerns` is a subdirectory of
420
- # `app/models`, but both of them are root directories.
421
- #
422
- # To resolve the ambiguity file name -> constant path this introduces,
423
- # the `app/models/concerns` directory is totally ignored as a namespace,
424
- # it counts only as root. The guard checks that.
425
- unless root_dirs.key?(abspath)
426
- cname = inflector.camelize(basename, abspath).to_sym
427
- autoload_subdir(parent, cname, abspath)
428
- end
429
- end
430
- rescue ::NameError => error
431
- path_type = ruby?(abspath) ? "file" : "directory"
432
-
433
- raise NameError, <<~MESSAGE
434
- #{error.message} inferred by #{inflector.class} from #{path_type}
435
-
436
- #{abspath}
437
-
438
- Possible ways to address this:
439
-
440
- * Tell Zeitwerk to ignore this particular #{path_type}.
441
- * Tell Zeitwerk to ignore one of its parent directories.
442
- * Rename the #{path_type} to comply with the naming conventions.
443
- * Modify the inflector to handle this case.
444
- MESSAGE
445
- end
446
- end
447
- end
448
-
449
- private # -------------------------------------------------------------------------------------
450
-
451
- # @return [<String>]
452
- def actual_root_dirs
453
- root_dirs.keys.delete_if do |root_dir|
454
- !dir?(root_dir) || ignored_paths.member?(root_dir)
455
- end
456
- end
457
-
458
- # @param parent [Module]
459
- # @param cname [Symbol]
460
- # @param subdir [String]
461
- # @return [void]
462
- def autoload_subdir(parent, cname, subdir)
463
- if autoload_path = autoload_for?(parent, cname)
464
- cpath = cpath(parent, cname)
465
- register_explicit_namespace(cpath) if ruby?(autoload_path)
466
- # We do not need to issue another autoload, the existing one is enough
467
- # no matter if it is for a file or a directory. Just remember the
468
- # subdirectory has to be visited if the namespace is used.
469
- (lazy_subdirs[cpath] ||= []) << subdir
470
- elsif !cdef?(parent, cname)
471
- # First time we find this namespace, set an autoload for it.
472
- (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
473
- set_autoload(parent, cname, subdir)
474
- else
475
- # For whatever reason the constant that corresponds to this namespace has
476
- # already been defined, we have to recurse.
477
- set_autoloads_in_dir(subdir, parent.const_get(cname))
478
- end
479
- end
480
-
481
- # @param parent [Module]
482
- # @param cname [Symbol]
483
- # @param file [String]
484
- # @return [void]
485
- def autoload_file(parent, cname, file)
486
- if autoload_path = autoload_for?(parent, cname)
487
- # First autoload for a Ruby file wins, just ignore subsequent ones.
488
- if ruby?(autoload_path)
489
- # "file #{file} is ignored because #{autoload_path} has precedence"
490
- else
491
- promote_namespace_from_implicit_to_explicit(
492
- dir: autoload_path,
493
- file: file,
494
- parent: parent,
495
- cname: cname
496
- )
497
- end
498
- elsif cdef?(parent, cname)
499
- # "file #{file} is ignored because #{cpath(parent, cname)} is already defined"
500
- else
501
- set_autoload(parent, cname, file)
502
- end
503
- end
504
-
505
- # @param dir [String] directory that would have autovivified a module
506
- # @param file [String] the file where the namespace is explictly defined
507
- # @param parent [Module]
508
- # @param cname [Symbol]
509
- # @return [void]
510
- def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
511
- autoloads.delete(dir)
512
- Registry.unregister_autoload(dir)
513
-
514
- set_autoload(parent, cname, file)
515
- register_explicit_namespace(cpath(parent, cname))
516
- end
517
-
518
- # @param parent [Module]
519
- # @param cname [Symbol]
520
- # @param abspath [String]
521
- # @return [void]
522
- def set_autoload(parent, cname, abspath)
523
- # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
524
- # real path to be able to delete it from $LOADED_FEATURES on unload, and to
525
- # be able to do a lookup later in Kernel#require for manual require calls.
526
- realpath = `Opal.modules.hasOwnProperty(abspath)` ? abspath : File.realpath(abspath)
527
- parent.autoload(cname, realpath)
528
-
529
- autoloads[realpath] = [parent, cname]
530
- Registry.register_autoload(self, realpath)
531
-
532
- # See why in the documentation of Zeitwerk::Registry.inceptions.
533
- unless parent.autoload?(cname)
534
- Registry.register_inception(cpath(parent, cname), realpath, self)
535
- end
536
- end
537
-
538
- # @param parent [Module]
539
- # @param cname [Symbol]
540
- # @return [String, nil]
541
- def autoload_for?(parent, cname)
542
- strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
543
- end
544
-
545
- # The autoload? predicate takes into account the ancestor chain of the
546
- # receiver, like const_defined? and other methods in the constants API do.
547
- #
548
- # For example, given
549
- #
550
- # class A
551
- # autoload :X, "x.rb"
552
- # end
553
- #
554
- # class B < A
555
- # end
556
- #
557
- # B.autoload?(:X) returns "x.rb".
558
- #
559
- # We need a way to strictly check in parent ignoring ancestors.
560
- #
561
- # @param parent [Module]
562
- # @param cname [Symbol]
563
- # @return [String, nil]
564
- if method(:autoload?).arity == 1
565
- def strict_autoload_path(parent, cname)
566
- parent.autoload?(cname) if cdef?(parent, cname)
567
- end
568
- else
569
- def strict_autoload_path(parent, cname)
570
- parent.autoload?(cname, false)
571
- end
572
- end
573
-
574
- # This method is called this way because I prefer `preload` to be the method
575
- # name to configure preloads in the public interface.
576
- #
577
- # @return [void]
578
- def do_preload
579
- preloads.each do |abspath|
580
- do_preload_abspath(abspath)
581
- end
582
- end
583
-
584
- # @param abspath [String]
585
- # @return [void]
586
- def do_preload_abspath(abspath)
587
- if ruby?(abspath)
588
- do_preload_file(abspath)
589
- elsif dir?(abspath)
590
- do_preload_dir(abspath)
591
- end
592
- end
593
-
594
- # @param dir [String]
595
- # @return [void]
596
- def do_preload_dir(dir)
597
- ls(dir) do |_basename, abspath|
598
- do_preload_abspath(abspath)
599
- end
600
- end
601
-
602
- # @param file [String]
603
- # @return [Boolean]
604
- def do_preload_file(file)
605
- require file
606
- end
607
-
608
- # @param parent [Module]
609
- # @param cname [Symbol]
610
- # @return [String]
611
- def cpath(parent, cname)
612
- parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
613
- end
614
-
615
- # @param dir [String]
616
- # @yieldparam path [String, String]
617
- # @return [void]
618
- def ls(dir)
619
- # `console.log("dir:", dir)`
620
- outer_ls = false
621
- # cache the Opal.modules keys array for subsequent ls calls during setup
622
- %x{
623
- if (#@module_paths === nil) {
624
- #@module_paths = Object.keys(Opal.modules);
625
- outer_ls = true;
626
- }
627
- }
628
- visited_abspaths = `{}`
629
- dir_first_char = dir[0]
630
- path_start = dir.size + 1
631
- path_parts = `[]`
632
- basename = `''`
633
- @module_paths.each do |abspath|
634
- %x{
635
- if (abspath[0] === dir_first_char) {
636
- if (!abspath.startsWith(dir)) { #{next} }
637
- path_parts = abspath.slice(path_start).split('/');
638
- basename = path_parts[0];
639
- abspath = dir + '/' + basename;
640
- if (visited_abspaths.hasOwnProperty(abspath)) { #{next} }
641
- visited_abspaths[abspath] = true;
642
- // console.log("basename:", basename, "abspath:", abspath);
643
- #{yield basename, abspath unless ignored_paths.member?(abspath)}
644
- }
645
- }
646
- end
647
- # remove cache, because Opal.modules may change after setup
648
- %x{
649
- if (outer_ls) { #@module_paths = nil }
650
- }
651
- end
652
-
653
- # @param path [String]
654
- # @return [Boolean]
655
- def ruby?(abspath)
656
- `Opal.modules.hasOwnProperty(abspath)`
657
- end
658
-
659
- # @param path [String]
660
- # @return [Boolean]
661
- def dir?(path)
662
- dir_path = path + '/'
663
- module_paths = if @module_paths # possibly set by ls
664
- @module_paths
665
- else
666
- `Object.keys(Opal.modules)`
667
- end
668
- path_first = `path[0]`
669
- module_paths.each do |m_path|
670
- %x{
671
- if (m_path[0] !== path_first) { #{ next } }
672
- if (m_path.startsWith(dir_path)) { #{return true} }
673
- }
674
- end
675
- return false
676
- end
677
-
678
- # @param paths [<String, Pathname, <String, Pathname>>]
679
- # @return [<String>]
680
- def expand_paths(paths)
681
- paths.flatten.map! { |path| File.expand_path(path) }
682
- end
683
-
684
- # @param glob_patterns [<String>]
685
- # @return [<String>]
686
- def expand_glob_patterns(glob_patterns)
687
- # Note that Dir.glob works with regular file names just fine. That is,
688
- # glob patterns technically need no wildcards.
689
- glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
690
- end
691
-
692
- # @return [void]
693
- def recompute_ignored_paths
694
- ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
695
- end
696
-
697
- def cdef?(parent, cname)
698
- parent.const_defined?(cname, false)
699
- end
700
-
701
- def register_explicit_namespace(cpath)
702
- ExplicitNamespace.register(cpath, self)
703
- end
704
-
705
- def raise_if_conflicting_directory(dir)
706
- Registry.loaders.each do |loader|
707
- if loader != self && loader.manages?(dir)
708
- require "pp"
709
- raise Error,
710
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
711
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
712
- EOS
713
- end
714
- end
715
- end
716
-
717
- # @param parent [Module]
718
- # @param cname [Symbol]
719
- # @return [void]
720
- def unload_autoload(parent, cname)
721
- parent.send(:remove_const, cname)
722
- end
723
-
724
- # @param parent [Module]
725
- # @param cname [Symbol]
726
- # @return [void]
727
- def unload_cref(parent, cname)
728
- parent.send(:remove_const, cname)
729
- end
730
- end
731
- end
1
+ require "set"
2
+ require "securerandom"
3
+
4
+ module Zeitwerk
5
+ class Loader
6
+ require_relative "loader/callbacks"
7
+ include Callbacks
8
+ include RealModName
9
+
10
+ # @return [String]
11
+ attr_reader :tag
12
+
13
+ # @return [#camelize]
14
+ attr_accessor :inflector
15
+
16
+ # Absolute paths of the root directories. Stored in a hash to preserve
17
+ # order, easily handle duplicates, and also be able to have a fast lookup,
18
+ # needed for detecting nested paths.
19
+ #
20
+ # "/Users/fxn/blog/app/assets" => true,
21
+ # "/Users/fxn/blog/app/channels" => true,
22
+ # ...
23
+ #
24
+ # This is a private collection maintained by the loader. The public
25
+ # interface for it is `push_dir` and `dirs`.
26
+ #
27
+ # @private
28
+ # @return [{String => true}]
29
+ attr_reader :root_dirs
30
+
31
+ # Absolute paths of files or directories that have to be preloaded.
32
+ #
33
+ # @private
34
+ # @return [<String>]
35
+ attr_reader :preloads
36
+
37
+ # Absolute paths of files, directories, of glob patterns to be totally
38
+ # ignored.
39
+ #
40
+ # @private
41
+ # @return [Set<String>]
42
+ attr_reader :ignored_glob_patterns
43
+
44
+ # The actual collection of absolute file and directory names at the time the
45
+ # ignored glob patterns were expanded. Computed on setup, and recomputed on
46
+ # reload.
47
+ #
48
+ # @private
49
+ # @return [Set<String>]
50
+ attr_reader :ignored_paths
51
+
52
+ # Maps real absolute paths for which an autoload has been set ---and not
53
+ # executed--- to their corresponding parent class or module and constant
54
+ # name.
55
+ #
56
+ # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
57
+ # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
58
+ # ...
59
+ #
60
+ # @private
61
+ # @return [{String => (Module, Symbol)}]
62
+ attr_reader :autoloads
63
+
64
+ # We keep track of autoloaded directories to remove them from the registry
65
+ # at the end of eager loading.
66
+ #
67
+ # Files are removed as they are autoloaded, but directories need to wait due
68
+ # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
69
+ #
70
+ # @private
71
+ # @return [<String>]
72
+ attr_reader :autoloaded_dirs
73
+
74
+ # Stores metadata needed for unloading. Its entries look like this:
75
+ #
76
+ # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
77
+ #
78
+ # The cpath as key helps implementing unloadable_cpath? The real file name
79
+ # is stored in order to be able to delete it from $LOADED_FEATURES, and the
80
+ # pair [Module, Symbol] is used to remove_const the constant from the class
81
+ # or module object.
82
+ #
83
+ # If reloading is enabled, this hash is filled as constants are autoloaded
84
+ # or eager loaded. Otherwise, the collection remains empty.
85
+ #
86
+ # @private
87
+ # @return [{String => (String, (Module, Symbol))}]
88
+ attr_reader :to_unload
89
+
90
+ # Maps constant paths of namespaces to arrays of corresponding directories.
91
+ #
92
+ # For example, given this mapping:
93
+ #
94
+ # "Admin" => [
95
+ # "/Users/fxn/blog/app/controllers/admin",
96
+ # "/Users/fxn/blog/app/models/admin",
97
+ # ...
98
+ # ]
99
+ #
100
+ # when `Admin` gets defined we know that it plays the role of a namespace and
101
+ # that its children are spread over those directories. We'll visit them to set
102
+ # up the corresponding autoloads.
103
+ #
104
+ # @private
105
+ # @return [{String => <String>}]
106
+ attr_reader :lazy_subdirs
107
+
108
+ # Absolute paths of files or directories not to be eager loaded.
109
+ #
110
+ # @private
111
+ # @return [Set<String>]
112
+ attr_reader :eager_load_exclusions
113
+
114
+ attr_accessor :vivify_mod_dir
115
+ attr_accessor :vivify_mod_class
116
+
117
+ def initialize
118
+ @initialized_at = Time.now
119
+
120
+ @tag = SecureRandom.hex(3)
121
+ @inflector = Inflector.new
122
+
123
+ @root_dirs = {}
124
+ @preloads = []
125
+ @ignored_glob_patterns = Set.new
126
+ @ignored_paths = Set.new
127
+ @autoloads = {}
128
+ @autoloaded_dirs = []
129
+ @to_unload = {}
130
+ @lazy_subdirs = {}
131
+ @eager_load_exclusions = Set.new
132
+
133
+ @setup = false
134
+ @eager_loaded = false
135
+
136
+ @reloading_enabled = false
137
+
138
+ @vivify_mod_dir = false
139
+ @module_paths
140
+
141
+ Registry.register_loader(self)
142
+ end
143
+
144
+ # Sets a tag for the loader, useful for logging.
145
+ #
146
+ # @return [void]
147
+ def tag=(tag)
148
+ @tag = tag.to_s
149
+ end
150
+
151
+ # Absolute paths of the root directories. This is a read-only collection,
152
+ # please push here via `push_dir`.
153
+ #
154
+ # @return [<String>]
155
+ def dirs
156
+ root_dirs.keys
157
+ end
158
+
159
+ # Pushes `path` to the list of root directories.
160
+ #
161
+ # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
162
+ # the same process already manages that directory or one of its ascendants
163
+ # or descendants.
164
+ #
165
+ # @param path [<String, Pathname>]
166
+ # @raise [Zeitwerk::Error]
167
+ # @return [void]
168
+ def push_dir(path)
169
+ abspath = File.expand_path(path)
170
+ if dir?(abspath)
171
+ raise_if_conflicting_directory(abspath)
172
+ root_dirs[abspath] = true
173
+ else
174
+ warn_string = "Zeitwerk: the root path #{abspath} does not exist, not added"
175
+ `console.warn(warn_string)`
176
+ end
177
+ end
178
+
179
+ # You need to call this method before setup in order to be able to reload.
180
+ # There is no way to undo this, either you want to reload or you don't.
181
+ #
182
+ # @raise [Zeitwerk::Error]
183
+ # @return [void]
184
+ def enable_reloading
185
+ return if @reloading_enabled
186
+
187
+ if @setup
188
+ raise Error, "cannot enable reloading after setup"
189
+ else
190
+ @reloading_enabled = true
191
+ end
192
+ end
193
+
194
+ # @return [Boolean]
195
+ def reloading_enabled?
196
+ @reloading_enabled
197
+ end
198
+
199
+ # Files or directories to be preloaded instead of lazy loaded.
200
+ #
201
+ # @param paths [<String, Pathname, <String, Pathname>>]
202
+ # @return [void]
203
+ def preload(*paths)
204
+ expand_paths(paths).each do |abspath|
205
+ preloads << abspath
206
+ do_preload_abspath(abspath) if @setup
207
+ end
208
+ end
209
+
210
+ # Configure files, directories, or glob patterns to be totally ignored.
211
+ #
212
+ # @param paths [<String, Pathname, <String, Pathname>>]
213
+ # @return [void]
214
+ def ignore(*glob_patterns)
215
+ glob_patterns = expand_paths(glob_patterns)
216
+ ignored_glob_patterns.merge(glob_patterns)
217
+ ignored_paths.merge(expand_glob_patterns(glob_patterns))
218
+ end
219
+
220
+ # Sets autoloads in the root namespace and preloads files, if any.
221
+ #
222
+ # @return [void]
223
+ def setup
224
+ return if @setup
225
+
226
+ actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
227
+ do_preload
228
+
229
+ @setup = true
230
+ end
231
+
232
+ # Removes loaded constants and configured autoloads.
233
+ #
234
+ # The objects the constants stored are no longer reachable through them. In
235
+ # addition, since said objects are normally not referenced from anywhere
236
+ # else, they are eligible for garbage collection, which would effectively
237
+ # unload them.
238
+ #
239
+ # @private
240
+ # @return [void]
241
+ def unload
242
+ # We are going to keep track of the files that were required by our
243
+ # autoloads to later remove them from $LOADED_FEATURES, thus making them
244
+ # loadable by Kernel#require again.
245
+ #
246
+ # Directories are not stored in $LOADED_FEATURES, keeping track of files
247
+ # is enough.
248
+ unloaded_files = Set.new
249
+
250
+ autoloads.each do |realpath, (parent, cname)|
251
+ if parent.autoload?(cname)
252
+ unload_autoload(parent, cname)
253
+ else
254
+ # Could happen if loaded with require_relative. That is unsupported,
255
+ # and the constant path would escape unloadable_cpath? This is just
256
+ # defensive code to clean things up as much as we are able to.
257
+ unload_cref(parent, cname) if cdef?(parent, cname)
258
+ unloaded_files.add(realpath) if ruby?(realpath)
259
+ end
260
+ end
261
+
262
+ to_unload.each_value do |(realpath, (parent, cname))|
263
+ unload_cref(parent, cname) if cdef?(parent, cname)
264
+ unloaded_files.add(realpath) if ruby?(realpath)
265
+ end
266
+
267
+ unless unloaded_files.empty?
268
+ # Bootsnap decorates Kernel#require to speed it up using a cache and
269
+ # this optimization does not check if $LOADED_FEATURES has the file.
270
+ #
271
+ # To make it aware of changes, the gem defines singleton methods in
272
+ # $LOADED_FEATURES:
273
+ #
274
+ # https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
275
+ #
276
+ # Rails applications may depend on bootsnap, so for unloading to work
277
+ # in that setting it is preferable that we restrict our API choice to
278
+ # one of those methods.
279
+ $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
280
+ end
281
+
282
+ autoloads.clear
283
+ autoloaded_dirs.clear
284
+ to_unload.clear
285
+ lazy_subdirs.clear
286
+
287
+ Registry.on_unload(self)
288
+ ExplicitNamespace.unregister(self)
289
+
290
+ @setup = false
291
+ @eager_loaded = false
292
+ end
293
+
294
+ # Unloads all loaded code, and calls setup again so that the loader is able
295
+ # to pick any changes in the file system.
296
+ #
297
+ # This method is not thread-safe, please see how this can be achieved by
298
+ # client code in the README of the project.
299
+ #
300
+ # @raise [Zeitwerk::Error]
301
+ # @return [void]
302
+ def reload
303
+ if reloading_enabled?
304
+ unload
305
+ recompute_ignored_paths
306
+ setup
307
+ else
308
+ raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
309
+ end
310
+ end
311
+
312
+ # Eager loads all files in the root directories, recursively. Files do not
313
+ # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
314
+ # are not eager loaded. You can opt-out specifically in specific files and
315
+ # directories with `do_not_eager_load`.
316
+ #
317
+ # @return [void]
318
+ def eager_load
319
+ return if @eager_loaded
320
+
321
+ queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
322
+ queue.map! { |dir| [Object, dir] }
323
+ while to_eager_load = queue.shift
324
+ namespace, dir = to_eager_load
325
+
326
+ ls(dir) do |basename, abspath|
327
+ next if eager_load_exclusions.member?(abspath)
328
+
329
+ if ruby?(abspath)
330
+ if cref = autoloads[File.realpath(abspath)]
331
+ cref[0].const_get(cref[1], false)
332
+ end
333
+ elsif dir?(abspath) && !root_dirs.key?(abspath)
334
+ cname = inflector.camelize(basename, abspath)
335
+ queue << [namespace.const_get(cname, false), abspath]
336
+ end
337
+ end
338
+ end
339
+
340
+ autoloaded_dirs.each do |autoloaded_dir|
341
+ Registry.unregister_autoload(autoloaded_dir)
342
+ end
343
+ autoloaded_dirs.clear
344
+
345
+ @eager_loaded = true
346
+ end
347
+
348
+ # Let eager load ignore the given files or directories. The constants
349
+ # defined in those files are still autoloadable.
350
+ #
351
+ # @param paths [<String, Pathname, <String, Pathname>>]
352
+ # @return [void]
353
+ def do_not_eager_load(*paths)
354
+ eager_load_exclusions.merge(expand_paths(paths))
355
+ end
356
+
357
+ # Says if the given constant path would be unloaded on reload. This
358
+ # predicate returns `false` if reloading is disabled.
359
+ #
360
+ # @param cpath [String]
361
+ # @return [Boolean]
362
+ def unloadable_cpath?(cpath)
363
+ to_unload.key?(cpath)
364
+ end
365
+
366
+ # Returns an array with the constant paths that would be unloaded on reload.
367
+ # This predicate returns an empty array if reloading is disabled.
368
+ #
369
+ # @return [<String>]
370
+ def unloadable_cpaths
371
+ to_unload.keys
372
+ end
373
+
374
+ # @private
375
+ # @param dir [String]
376
+ # @return [Boolean]
377
+ def manages?(dir)
378
+ dir = dir + "/"
379
+ ignored_paths.each do |ignored_path|
380
+ return false if dir.start_with?(ignored_path + "/")
381
+ end
382
+
383
+ root_dirs.each_key do |root_dir|
384
+ return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
385
+ end
386
+
387
+ false
388
+ end
389
+
390
+ # --- Class methods ---------------------------------------------------------------------------
391
+
392
+ class << self
393
+ # Broadcasts `eager_load` to all loaders.
394
+ #
395
+ # @return [void]
396
+ def eager_load_all
397
+ Registry.loaders.each(&:eager_load)
398
+ end
399
+
400
+ # Returns an array with the absolute paths of the root directories of all
401
+ # registered loaders. This is a read-only collection.
402
+ #
403
+ # @return [<String>]
404
+ def all_dirs
405
+ Registry.loaders.flat_map(&:dirs)
406
+ end
407
+ end
408
+
409
+ # @param dir [String]
410
+ # @param parent [Module]
411
+ # @return [void]
412
+ def set_autoloads_in_dir(dir, parent)
413
+ ls(dir) do |basename, abspath|
414
+ begin
415
+ if ruby?(abspath)
416
+ # basename = basename.slice(-3, 3)
417
+ cname = inflector.camelize(basename, abspath).to_sym
418
+ autoload_file(parent, cname, abspath)
419
+ elsif dir?(abspath)
420
+ # In a Rails application, `app/models/concerns` is a subdirectory of
421
+ # `app/models`, but both of them are root directories.
422
+ #
423
+ # To resolve the ambiguity file name -> constant path this introduces,
424
+ # the `app/models/concerns` directory is totally ignored as a namespace,
425
+ # it counts only as root. The guard checks that.
426
+ unless root_dirs.key?(abspath)
427
+ cname = inflector.camelize(basename, abspath).to_sym
428
+ autoload_subdir(parent, cname, abspath)
429
+ end
430
+ end
431
+ rescue ::NameError => error
432
+ path_type = ruby?(abspath) ? "file" : "directory"
433
+ message = <<~MESSAGE
434
+ #{error.message} inferred by #{inflector.class} from #{path_type}
435
+
436
+ #{abspath}
437
+
438
+ Possible ways to address this:
439
+
440
+ * Tell Zeitwerk to ignore this particular #{path_type}.
441
+ * Tell Zeitwerk to ignore one of its parent directories.
442
+ * Rename the #{path_type} to comply with the naming conventions.
443
+ * Modify the inflector to handle this case.
444
+ MESSAGE
445
+ raise NameError.new(message, error.name)
446
+ end
447
+ end
448
+ end
449
+
450
+ private # -------------------------------------------------------------------------------------
451
+
452
+ # @return [<String>]
453
+ def actual_root_dirs
454
+ root_dirs.keys.delete_if do |root_dir|
455
+ !dir?(root_dir) || ignored_paths.member?(root_dir)
456
+ end
457
+ end
458
+
459
+ # @param parent [Module]
460
+ # @param cname [Symbol]
461
+ # @param subdir [String]
462
+ # @return [void]
463
+ def autoload_subdir(parent, cname, subdir)
464
+ if autoload_path = autoload_for?(parent, cname)
465
+ cpath = cpath(parent, cname)
466
+ register_explicit_namespace(cpath) if ruby?(autoload_path)
467
+ # We do not need to issue another autoload, the existing one is enough
468
+ # no matter if it is for a file or a directory. Just remember the
469
+ # subdirectory has to be visited if the namespace is used.
470
+ (lazy_subdirs[cpath] ||= []) << subdir
471
+ elsif !cdef?(parent, cname)
472
+ # First time we find this namespace, set an autoload for it.
473
+ (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
474
+ set_autoload(parent, cname, subdir)
475
+ else
476
+ # For whatever reason the constant that corresponds to this namespace has
477
+ # already been defined, we have to recurse.
478
+ set_autoloads_in_dir(subdir, parent.const_get(cname))
479
+ end
480
+ end
481
+
482
+ # @param parent [Module]
483
+ # @param cname [Symbol]
484
+ # @param file [String]
485
+ # @return [void]
486
+ def autoload_file(parent, cname, file)
487
+ if autoload_path = autoload_for?(parent, cname)
488
+ # First autoload for a Ruby file wins, just ignore subsequent ones.
489
+ if ruby?(autoload_path)
490
+ # "file #{file} is ignored because #{autoload_path} has precedence"
491
+ else
492
+ promote_namespace_from_implicit_to_explicit(
493
+ dir: autoload_path,
494
+ file: file,
495
+ parent: parent,
496
+ cname: cname
497
+ )
498
+ end
499
+ elsif cdef?(parent, cname)
500
+ # "file #{file} is ignored because #{cpath(parent, cname)} is already defined"
501
+ else
502
+ set_autoload(parent, cname, file)
503
+ if autoload_path = autoload_for?(parent, cname)
504
+ if dir?(autoload_path)
505
+ promote_namespace_from_implicit_to_explicit(
506
+ dir: autoload_path,
507
+ file: file,
508
+ parent: parent,
509
+ cname: cname
510
+ )
511
+ (lazy_subdirs[cpath(parent, cname)] ||= []) << autoload_path
512
+ end
513
+ end
514
+ end
515
+ end
516
+
517
+ # @param dir [String] directory that would have autovivified a module
518
+ # @param file [String] the file where the namespace is explictly defined
519
+ # @param parent [Module]
520
+ # @param cname [Symbol]
521
+ # @return [void]
522
+ def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
523
+ autoloads.delete(dir)
524
+ Registry.unregister_autoload(dir)
525
+
526
+ set_autoload(parent, cname, file)
527
+ register_explicit_namespace(cpath(parent, cname))
528
+ end
529
+
530
+ # @param parent [Module]
531
+ # @param cname [Symbol]
532
+ # @param abspath [String]
533
+ # @return [void]
534
+ def set_autoload(parent, cname, abspath)
535
+ # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
536
+ # real path to be able to delete it from $LOADED_FEATURES on unload, and to
537
+ # be able to do a lookup later in Kernel#require for manual require calls.
538
+ realpath = `Opal.modules.hasOwnProperty(abspath)` ? abspath : File.realpath(abspath)
539
+ parent.autoload(cname, realpath)
540
+
541
+ autoloads[realpath] = [parent, cname]
542
+ Registry.register_autoload(self, realpath)
543
+
544
+ # See why in the documentation of Zeitwerk::Registry.inceptions.
545
+ unless parent.autoload?(cname)
546
+ Registry.register_inception(cpath(parent, cname), realpath, self)
547
+ end
548
+ end
549
+
550
+ # @param parent [Module]
551
+ # @param cname [Symbol]
552
+ # @return [String, nil]
553
+ def autoload_for?(parent, cname)
554
+ strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
555
+ end
556
+
557
+ # The autoload? predicate takes into account the ancestor chain of the
558
+ # receiver, like const_defined? and other methods in the constants API do.
559
+ #
560
+ # For example, given
561
+ #
562
+ # class A
563
+ # autoload :X, "x.rb"
564
+ # end
565
+ #
566
+ # class B < A
567
+ # end
568
+ #
569
+ # B.autoload?(:X) returns "x.rb".
570
+ #
571
+ # We need a way to strictly check in parent ignoring ancestors.
572
+ #
573
+ # @param parent [Module]
574
+ # @param cname [Symbol]
575
+ # @return [String, nil]
576
+ def strict_autoload_path(parent, cname)
577
+ parent.autoload?(cname, false)
578
+ end
579
+
580
+ # This method is called this way because I prefer `preload` to be the method
581
+ # name to configure preloads in the public interface.
582
+ #
583
+ # @return [void]
584
+ def do_preload
585
+ preloads.each do |abspath|
586
+ do_preload_abspath(abspath)
587
+ end
588
+ end
589
+
590
+ # @param abspath [String]
591
+ # @return [void]
592
+ def do_preload_abspath(abspath)
593
+ if ruby?(abspath)
594
+ do_preload_file(abspath)
595
+ elsif dir?(abspath)
596
+ do_preload_dir(abspath)
597
+ end
598
+ end
599
+
600
+ # @param dir [String]
601
+ # @return [void]
602
+ def do_preload_dir(dir)
603
+ ls(dir) do |_basename, abspath|
604
+ do_preload_abspath(abspath)
605
+ end
606
+ end
607
+
608
+ # @param file [String]
609
+ # @return [Boolean]
610
+ def do_preload_file(file)
611
+ require file
612
+ end
613
+
614
+ # @param parent [Module]
615
+ # @param cname [Symbol]
616
+ # @return [String]
617
+ def cpath(parent, cname)
618
+ parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
619
+ end
620
+
621
+ # @param dir [String]
622
+ # @yieldparam path [String, String]
623
+ # @return [void]
624
+ def ls(dir)
625
+ # `console.log("dir:", dir)`
626
+ outer_ls = false
627
+ # cache the Opal.modules keys array for subsequent ls calls during setup
628
+ %x{
629
+ if (#@module_paths === nil) {
630
+ #@module_paths = Object.keys(Opal.modules);
631
+ outer_ls = true;
632
+ }
633
+ }
634
+ visited_abspaths = `{}`
635
+ dir_first_char = dir[0]
636
+ path_start = dir.size + 1
637
+ path_parts = `[]`
638
+ basename = ''
639
+ @module_paths.each do |abspath|
640
+ %x{
641
+ if (abspath[0] === dir_first_char) {
642
+ if (!abspath.startsWith(dir)) { #{next} }
643
+ path_parts = abspath.slice(path_start).split('/');
644
+ basename = path_parts[0];
645
+ abspath = dir + '/' + basename;
646
+ if (visited_abspaths.hasOwnProperty(abspath)) { #{next} }
647
+ visited_abspaths[abspath] = true;
648
+ // console.log("basename:", basename, "abspath:", abspath);
649
+ #{yield basename, abspath unless ignored_paths.member?(abspath)}
650
+ }
651
+ }
652
+ end
653
+ # remove cache, because Opal.modules may change after setup
654
+ %x{
655
+ if (outer_ls) { #@module_paths = nil }
656
+ }
657
+ end
658
+
659
+ # @param path [String]
660
+ # @return [Boolean]
661
+ def ruby?(abspath)
662
+ `Opal.modules.hasOwnProperty(abspath)`
663
+ end
664
+
665
+ # @param path [String]
666
+ # @return [Boolean]
667
+ def dir?(path)
668
+ dir_path = path + '/'
669
+ module_paths = if @module_paths # possibly set by ls
670
+ @module_paths
671
+ else
672
+ `Object.keys(Opal.modules)`
673
+ end
674
+ path_first = `path[0]`
675
+ module_paths.each do |m_path|
676
+ %x{
677
+ if (m_path[0] !== path_first) { #{ next } }
678
+ if (m_path.startsWith(dir_path)) { #{return true} }
679
+ }
680
+ end
681
+ false
682
+ end
683
+
684
+ # @param paths [<String, Pathname, <String, Pathname>>]
685
+ # @return [<String>]
686
+ def expand_paths(paths)
687
+ paths.flatten.map! { |path| File.expand_path(path) }
688
+ end
689
+
690
+ # @param glob_patterns [<String>]
691
+ # @return [<String>]
692
+ def expand_glob_patterns(glob_patterns)
693
+ # Note that Dir.glob works with regular file names just fine. That is,
694
+ # glob patterns technically need no wildcards.
695
+ glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
696
+ end
697
+
698
+ # @return [void]
699
+ def recompute_ignored_paths
700
+ ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
701
+ end
702
+
703
+ def cdef?(parent, cname)
704
+ parent.const_defined?(cname, false)
705
+ end
706
+
707
+ def register_explicit_namespace(cpath)
708
+ ExplicitNamespace.register(cpath, self)
709
+ end
710
+
711
+ def raise_if_conflicting_directory(dir)
712
+ Registry.loaders.each do |loader|
713
+ if loader != self && loader.manages?(dir)
714
+ raise Error,
715
+ "loader\n\n#{self}\n\nwants to manage directory #{dir}," \
716
+ " which is already managed by\n\n#{loader}\n"
717
+ EOS
718
+ end
719
+ end
720
+ end
721
+
722
+ # @param parent [Module]
723
+ # @param cname [Symbol]
724
+ # @return [void]
725
+ def unload_autoload(parent, cname)
726
+ parent.send(:remove_const, cname)
727
+ end
728
+
729
+ # @param parent [Module]
730
+ # @param cname [Symbol]
731
+ # @return [void]
732
+ def unload_cref(parent, cname)
733
+ parent.send(:remove_const, cname)
734
+ end
735
+ end
736
+ end