opal-zeitwerk 0.0.4 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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