opal-zeitwerk 0.1.0 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,737 +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
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
- return 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
- require "pp"
715
- raise Error,
716
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
717
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
718
- EOS
719
- end
720
- end
721
- end
722
-
723
- # @param parent [Module]
724
- # @param cname [Symbol]
725
- # @return [void]
726
- def unload_autoload(parent, cname)
727
- parent.send(:remove_const, cname)
728
- end
729
-
730
- # @param parent [Module]
731
- # @param cname [Symbol]
732
- # @return [void]
733
- def unload_cref(parent, cname)
734
- parent.send(:remove_const, cname)
735
- end
736
- end
737
- 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