zeitwerk 2.4.0 → 2.5.0

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.
@@ -5,78 +5,31 @@ require "securerandom"
5
5
 
6
6
  module Zeitwerk
7
7
  class Loader
8
+ require_relative "loader/helpers"
8
9
  require_relative "loader/callbacks"
9
- include Callbacks
10
- include RealModName
11
-
12
- # @return [String]
13
- attr_reader :tag
14
-
15
- # @return [#camelize]
16
- attr_accessor :inflector
17
-
18
- # @return [#call, #debug, nil]
19
- attr_accessor :logger
10
+ require_relative "loader/config"
20
11
 
21
- # Absolute paths of the root directories. Stored in a hash to preserve
22
- # order, easily handle duplicates, and also be able to have a fast lookup,
23
- # needed for detecting nested paths.
24
- #
25
- # "/Users/fxn/blog/app/assets" => true,
26
- # "/Users/fxn/blog/app/channels" => true,
27
- # ...
28
- #
29
- # This is a private collection maintained by the loader. The public
30
- # interface for it is `push_dir` and `dirs`.
31
- #
32
- # @private
33
- # @return [{String => true}]
34
- attr_reader :root_dirs
12
+ include RealModName
13
+ include Callbacks
14
+ include Helpers
15
+ include Config
35
16
 
36
- # Absolute paths of files or directories that have to be preloaded.
17
+ # Keeps track of autoloads defined by the loader which have not been
18
+ # executed so far.
37
19
  #
38
- # @private
39
- # @return [<String>]
40
- attr_reader :preloads
41
-
42
- # Absolute paths of files, directories, or glob patterns to be totally
43
- # ignored.
20
+ # This metadata helps us implement a few things:
44
21
  #
45
- # @private
46
- # @return [Set<String>]
47
- attr_reader :ignored_glob_patterns
48
-
49
- # The actual collection of absolute file and directory names at the time the
50
- # ignored glob patterns were expanded. Computed on setup, and recomputed on
51
- # reload.
22
+ # 1. When autoloads are triggered, ensure they define the expected constant
23
+ # and invoke user callbacks. If reloading is enabled, remember cref and
24
+ # abspath for later unloading logic.
52
25
  #
53
- # @private
54
- # @return [Set<String>]
55
- attr_reader :ignored_paths
56
-
57
- # Absolute paths of directories or glob patterns to be collapsed.
26
+ # 2. When unloading, remove autoloads that have not been executed.
58
27
  #
59
- # @private
60
- # @return [Set<String>]
61
- attr_reader :collapse_glob_patterns
62
-
63
- # The actual collection of absolute directory names at the time the collapse
64
- # glob patterns were expanded. Computed on setup, and recomputed on reload.
28
+ # 3. Eager load with a recursive const_get, rather than a recursive require,
29
+ # for consistency with lazy loading.
65
30
  #
66
31
  # @private
67
- # @return [Set<String>]
68
- attr_reader :collapse_dirs
69
-
70
- # Maps real absolute paths for which an autoload has been set ---and not
71
- # executed--- to their corresponding parent class or module and constant
72
- # name.
73
- #
74
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
75
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
76
- # ...
77
- #
78
- # @private
79
- # @return [{String => (Module, Symbol)}]
32
+ # @sig Zeitwerk::Autoloads
80
33
  attr_reader :autoloads
81
34
 
82
35
  # We keep track of autoloaded directories to remove them from the registry
@@ -86,15 +39,15 @@ module Zeitwerk
86
39
  # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
87
40
  #
88
41
  # @private
89
- # @return [<String>]
42
+ # @sig Array[String]
90
43
  attr_reader :autoloaded_dirs
91
44
 
92
45
  # Stores metadata needed for unloading. Its entries look like this:
93
46
  #
94
47
  # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
95
48
  #
96
- # The cpath as key helps implementing unloadable_cpath? The real file name
97
- # is stored in order to be able to delete it from $LOADED_FEATURES, and the
49
+ # The cpath as key helps implementing unloadable_cpath? The file name is
50
+ # stored in order to be able to delete it from $LOADED_FEATURES, and the
98
51
  # pair [Module, Symbol] is used to remove_const the constant from the class
99
52
  # or module object.
100
53
  #
@@ -102,7 +55,7 @@ module Zeitwerk
102
55
  # or eager loaded. Otherwise, the collection remains empty.
103
56
  #
104
57
  # @private
105
- # @return [{String => (String, (Module, Symbol))}]
58
+ # @sig Hash[String, [String, [Module, Symbol]]]
106
59
  attr_reader :to_unload
107
60
 
108
61
  # Maps constant paths of namespaces to arrays of corresponding directories.
@@ -120,156 +73,35 @@ module Zeitwerk
120
73
  # up the corresponding autoloads.
121
74
  #
122
75
  # @private
123
- # @return [{String => <String>}]
76
+ # @sig Hash[String, Array[String]]
124
77
  attr_reader :lazy_subdirs
125
78
 
126
- # Absolute paths of files or directories not to be eager loaded.
127
- #
128
79
  # @private
129
- # @return [Set<String>]
130
- attr_reader :eager_load_exclusions
131
-
132
- # @private
133
- # @return [Mutex]
80
+ # @sig Mutex
134
81
  attr_reader :mutex
135
82
 
136
83
  # @private
137
- # @return [Mutex]
84
+ # @sig Mutex
138
85
  attr_reader :mutex2
139
86
 
140
87
  def initialize
141
- @initialized_at = Time.now
142
-
143
- @tag = SecureRandom.hex(3)
144
- @inflector = Inflector.new
145
- @logger = self.class.default_logger
146
-
147
- @root_dirs = {}
148
- @preloads = []
149
- @ignored_glob_patterns = Set.new
150
- @ignored_paths = Set.new
151
- @collapse_glob_patterns = Set.new
152
- @collapse_dirs = Set.new
153
- @autoloads = {}
154
- @autoloaded_dirs = []
155
- @to_unload = {}
156
- @lazy_subdirs = {}
157
- @eager_load_exclusions = Set.new
158
-
159
- # TODO: find a better name for these mutexes.
160
- @mutex = Mutex.new
161
- @mutex2 = Mutex.new
162
- @setup = false
163
- @eager_loaded = false
164
-
165
- @reloading_enabled = false
166
-
167
- Registry.register_loader(self)
168
- end
88
+ super
169
89
 
170
- # Sets a tag for the loader, useful for logging.
171
- #
172
- # @param tag [#to_s]
173
- # @return [void]
174
- def tag=(tag)
175
- @tag = tag.to_s
176
- end
90
+ @autoloads = Autoloads.new
91
+ @autoloaded_dirs = []
92
+ @to_unload = {}
93
+ @lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
94
+ @mutex = Mutex.new
95
+ @mutex2 = Mutex.new
96
+ @setup = false
97
+ @eager_loaded = false
177
98
 
178
- # Absolute paths of the root directories. This is a read-only collection,
179
- # please push here via `push_dir`.
180
- #
181
- # @return [<String>]
182
- def dirs
183
- root_dirs.keys.freeze
184
- end
185
-
186
- # Pushes `path` to the list of root directories.
187
- #
188
- # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
189
- # the same process already manages that directory or one of its ascendants
190
- # or descendants.
191
- #
192
- # @param path [<String, Pathname>]
193
- # @param namespace [Class, Module]
194
- # @raise [Zeitwerk::Error]
195
- # @return [void]
196
- def push_dir(path, namespace: Object)
197
- # Note that Class < Module.
198
- unless namespace.is_a?(Module)
199
- raise Error, "#{namespace.inspect} is not a class or module object, should be"
200
- end
201
-
202
- abspath = File.expand_path(path)
203
- if dir?(abspath)
204
- raise_if_conflicting_directory(abspath)
205
- root_dirs[abspath] = namespace
206
- else
207
- raise Error, "the root directory #{abspath} does not exist"
208
- end
209
- end
210
-
211
- # You need to call this method before setup in order to be able to reload.
212
- # There is no way to undo this, either you want to reload or you don't.
213
- #
214
- # @raise [Zeitwerk::Error]
215
- # @return [void]
216
- def enable_reloading
217
- mutex.synchronize do
218
- break if @reloading_enabled
219
-
220
- if @setup
221
- raise Error, "cannot enable reloading after setup"
222
- else
223
- @reloading_enabled = true
224
- end
225
- end
226
- end
227
-
228
- # @return [Boolean]
229
- def reloading_enabled?
230
- @reloading_enabled
231
- end
232
-
233
- # Files or directories to be preloaded instead of lazy loaded.
234
- #
235
- # @param paths [<String, Pathname, <String, Pathname>>]
236
- # @return [void]
237
- def preload(*paths)
238
- mutex.synchronize do
239
- expand_paths(paths).each do |abspath|
240
- preloads << abspath
241
- do_preload_abspath(abspath) if @setup
242
- end
243
- end
244
- end
245
-
246
- # Configure files, directories, or glob patterns to be totally ignored.
247
- #
248
- # @param paths [<String, Pathname, <String, Pathname>>]
249
- # @return [void]
250
- def ignore(*glob_patterns)
251
- glob_patterns = expand_paths(glob_patterns)
252
- mutex.synchronize do
253
- ignored_glob_patterns.merge(glob_patterns)
254
- ignored_paths.merge(expand_glob_patterns(glob_patterns))
255
- end
256
- end
257
-
258
- # Configure directories or glob patterns to be collapsed.
259
- #
260
- # @param paths [<String, Pathname, <String, Pathname>>]
261
- # @return [void]
262
- def collapse(*glob_patterns)
263
- glob_patterns = expand_paths(glob_patterns)
264
- mutex.synchronize do
265
- collapse_glob_patterns.merge(glob_patterns)
266
- collapse_dirs.merge(expand_glob_patterns(glob_patterns))
267
- end
99
+ Registry.register_loader(self)
268
100
  end
269
101
 
270
- # Sets autoloads in the root namespace and preloads files, if any.
102
+ # Sets autoloads in the root namespace.
271
103
  #
272
- # @return [void]
104
+ # @sig () -> void
273
105
  def setup
274
106
  mutex.synchronize do
275
107
  break if @setup
@@ -277,7 +109,8 @@ module Zeitwerk
277
109
  actual_root_dirs.each do |root_dir, namespace|
278
110
  set_autoloads_in_dir(root_dir, namespace)
279
111
  end
280
- do_preload
112
+
113
+ on_setup_callbacks.each(&:call)
281
114
 
282
115
  @setup = true
283
116
  end
@@ -290,8 +123,11 @@ module Zeitwerk
290
123
  # else, they are eligible for garbage collection, which would effectively
291
124
  # unload them.
292
125
  #
293
- # @private
294
- # @return [void]
126
+ # This method is public but undocumented. Main interface is `reload`, which
127
+ # means `unload` + `setup`. This one is avaiable to be used together with
128
+ # `unregister`, which is undocumented too.
129
+ #
130
+ # @sig () -> void
295
131
  def unload
296
132
  mutex.synchronize do
297
133
  # We are going to keep track of the files that were required by our
@@ -302,21 +138,26 @@ module Zeitwerk
302
138
  # is enough.
303
139
  unloaded_files = Set.new
304
140
 
305
- autoloads.each do |realpath, (parent, cname)|
141
+ autoloads.each do |(parent, cname), abspath|
306
142
  if parent.autoload?(cname)
307
143
  unload_autoload(parent, cname)
308
144
  else
309
145
  # Could happen if loaded with require_relative. That is unsupported,
310
146
  # and the constant path would escape unloadable_cpath? This is just
311
147
  # defensive code to clean things up as much as we are able to.
312
- unload_cref(parent, cname) if cdef?(parent, cname)
313
- unloaded_files.add(realpath) if ruby?(realpath)
148
+ unload_cref(parent, cname)
149
+ unloaded_files.add(abspath) if ruby?(abspath)
314
150
  end
315
151
  end
316
152
 
317
- to_unload.each_value do |(realpath, (parent, cname))|
318
- unload_cref(parent, cname) if cdef?(parent, cname)
319
- unloaded_files.add(realpath) if ruby?(realpath)
153
+ to_unload.each do |cpath, (abspath, (parent, cname))|
154
+ unless on_unload_callbacks.empty?
155
+ value = parent.const_get(cname)
156
+ run_on_unload_callbacks(cpath, value, abspath)
157
+ end
158
+
159
+ unload_cref(parent, cname)
160
+ unloaded_files.add(abspath) if ruby?(abspath)
320
161
  end
321
162
 
322
163
  unless unloaded_files.empty?
@@ -340,7 +181,7 @@ module Zeitwerk
340
181
  lazy_subdirs.clear
341
182
 
342
183
  Registry.on_unload(self)
343
- ExplicitNamespace.unregister(self)
184
+ ExplicitNamespace.unregister_loader(self)
344
185
 
345
186
  @setup = false
346
187
  @eager_loaded = false
@@ -354,7 +195,7 @@ module Zeitwerk
354
195
  # client code in the README of the project.
355
196
  #
356
197
  # @raise [Zeitwerk::Error]
357
- # @return [void]
198
+ # @sig () -> void
358
199
  def reload
359
200
  if reloading_enabled?
360
201
  unload
@@ -369,34 +210,39 @@ module Zeitwerk
369
210
  # Eager loads all files in the root directories, recursively. Files do not
370
211
  # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
371
212
  # are not eager loaded. You can opt-out specifically in specific files and
372
- # directories with `do_not_eager_load`.
213
+ # directories with `do_not_eager_load`, and that can be overridden passing
214
+ # `force: true`.
373
215
  #
374
- # @return [void]
375
- def eager_load
216
+ # @sig (true | false) -> void
217
+ def eager_load(force: false)
376
218
  mutex.synchronize do
377
219
  break if @eager_loaded
378
220
 
221
+ log("eager load start") if logger
222
+
223
+ honour_exclusions = !force
224
+
379
225
  queue = []
380
226
  actual_root_dirs.each do |root_dir, namespace|
381
- queue << [namespace, root_dir] unless eager_load_exclusions.member?(root_dir)
227
+ queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
382
228
  end
383
229
 
384
230
  while to_eager_load = queue.shift
385
231
  namespace, dir = to_eager_load
386
232
 
387
233
  ls(dir) do |basename, abspath|
388
- next if eager_load_exclusions.member?(abspath)
234
+ next if honour_exclusions && excluded_from_eager_load?(abspath)
389
235
 
390
236
  if ruby?(abspath)
391
- if cref = autoloads[File.realpath(abspath)]
392
- cref[0].const_get(cref[1], false)
237
+ if cref = autoloads.cref_for(abspath)
238
+ cget(*cref)
393
239
  end
394
240
  elsif dir?(abspath) && !root_dirs.key?(abspath)
395
- if collapse_dirs.member?(abspath)
241
+ if collapse?(abspath)
396
242
  queue << [namespace, abspath]
397
243
  else
398
244
  cname = inflector.camelize(basename, abspath)
399
- queue << [namespace.const_get(cname, false), abspath]
245
+ queue << [cget(namespace, cname), abspath]
400
246
  end
401
247
  end
402
248
  end
@@ -408,23 +254,15 @@ module Zeitwerk
408
254
  autoloaded_dirs.clear
409
255
 
410
256
  @eager_loaded = true
411
- end
412
- end
413
257
 
414
- # Let eager load ignore the given files or directories. The constants
415
- # defined in those files are still autoloadable.
416
- #
417
- # @param paths [<String, Pathname, <String, Pathname>>]
418
- # @return [void]
419
- def do_not_eager_load(*paths)
420
- mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
258
+ log("eager load end") if logger
259
+ end
421
260
  end
422
261
 
423
262
  # Says if the given constant path would be unloaded on reload. This
424
263
  # predicate returns `false` if reloading is disabled.
425
264
  #
426
- # @param cpath [String]
427
- # @return [Boolean]
265
+ # @sig (String) -> bool
428
266
  def unloadable_cpath?(cpath)
429
267
  to_unload.key?(cpath)
430
268
  end
@@ -432,42 +270,28 @@ module Zeitwerk
432
270
  # Returns an array with the constant paths that would be unloaded on reload.
433
271
  # This predicate returns an empty array if reloading is disabled.
434
272
  #
435
- # @return [<String>]
273
+ # @sig () -> Array[String]
436
274
  def unloadable_cpaths
437
275
  to_unload.keys.freeze
438
276
  end
439
277
 
440
- # Logs to `$stdout`, handy shortcut for debugging.
278
+ # This is a dangerous method.
441
279
  #
442
- # @return [void]
443
- def log!
444
- @logger = ->(msg) { puts msg }
445
- end
446
-
447
- # @private
448
- # @param dir [String]
449
- # @return [Boolean]
450
- def manages?(dir)
451
- dir = dir + "/"
452
- ignored_paths.each do |ignored_path|
453
- return false if dir.start_with?(ignored_path + "/")
454
- end
455
-
456
- root_dirs.each_key do |root_dir|
457
- return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
458
- end
459
-
460
- false
280
+ # @experimental
281
+ # @sig () -> void
282
+ def unregister
283
+ Registry.unregister_loader(self)
284
+ ExplicitNamespace.unregister_loader(self)
461
285
  end
462
286
 
463
287
  # --- Class methods ---------------------------------------------------------------------------
464
288
 
465
289
  class << self
466
- # @return [#call, #debug, nil]
290
+ # @sig #call | #debug | nil
467
291
  attr_accessor :default_logger
468
292
 
469
293
  # @private
470
- # @return [Mutex]
294
+ # @sig Mutex
471
295
  attr_accessor :mutex
472
296
 
473
297
  # This is a shortcut for
@@ -481,7 +305,7 @@ module Zeitwerk
481
305
  # except that this method returns the same object in subsequent calls from
482
306
  # the same file, in the unlikely case the gem wants to be able to reload.
483
307
  #
484
- # @return [Zeitwerk::Loader]
308
+ # @sig () -> Zeitwerk::Loader
485
309
  def for_gem
486
310
  called_from = caller_locations(1, 1).first.path
487
311
  Registry.loader_for_gem(called_from)
@@ -489,7 +313,7 @@ module Zeitwerk
489
313
 
490
314
  # Broadcasts `eager_load` to all loaders.
491
315
  #
492
- # @return [void]
316
+ # @sig () -> void
493
317
  def eager_load_all
494
318
  Registry.loaders.each(&:eager_load)
495
319
  end
@@ -497,7 +321,7 @@ module Zeitwerk
497
321
  # Returns an array with the absolute paths of the root directories of all
498
322
  # registered loaders. This is a read-only collection.
499
323
  #
500
- # @return [<String>]
324
+ # @sig () -> Array[String]
501
325
  def all_dirs
502
326
  Registry.loaders.flat_map(&:dirs).freeze
503
327
  end
@@ -507,21 +331,12 @@ module Zeitwerk
507
331
 
508
332
  private # -------------------------------------------------------------------------------------
509
333
 
510
- # @return [<String>]
511
- def actual_root_dirs
512
- root_dirs.reject do |root_dir, _namespace|
513
- !dir?(root_dir) || ignored_paths.member?(root_dir)
514
- end
515
- end
516
-
517
- # @param dir [String]
518
- # @param parent [Module]
519
- # @return [void]
334
+ # @sig (String, Module) -> void
520
335
  def set_autoloads_in_dir(dir, parent)
521
336
  ls(dir) do |basename, abspath|
522
337
  begin
523
338
  if ruby?(basename)
524
- basename[-3..-1] = ''
339
+ basename.delete_suffix!(".rb")
525
340
  cname = inflector.camelize(basename, abspath).to_sym
526
341
  autoload_file(parent, cname, abspath)
527
342
  elsif dir?(abspath)
@@ -531,9 +346,9 @@ module Zeitwerk
531
346
  # To resolve the ambiguity file name -> constant path this introduces,
532
347
  # the `app/models/concerns` directory is totally ignored as a namespace,
533
348
  # it counts only as root. The guard checks that.
534
- unless root_dirs.key?(abspath)
349
+ unless root_dir?(abspath)
535
350
  cname = inflector.camelize(basename, abspath).to_sym
536
- if collapse_dirs.member?(abspath)
351
+ if collapse?(abspath)
537
352
  set_autoloads_in_dir(abspath, parent)
538
353
  else
539
354
  autoload_subdir(parent, cname, abspath)
@@ -559,35 +374,30 @@ module Zeitwerk
559
374
  end
560
375
  end
561
376
 
562
- # @param parent [Module]
563
- # @param cname [Symbol]
564
- # @param subdir [String]
565
- # @return [void]
377
+ # @sig (Module, Symbol, String) -> void
566
378
  def autoload_subdir(parent, cname, subdir)
567
- if autoload_path = autoload_for?(parent, cname)
379
+ if autoload_path = autoloads.abspath_for(parent, cname)
568
380
  cpath = cpath(parent, cname)
569
381
  register_explicit_namespace(cpath) if ruby?(autoload_path)
570
382
  # We do not need to issue another autoload, the existing one is enough
571
383
  # no matter if it is for a file or a directory. Just remember the
572
384
  # subdirectory has to be visited if the namespace is used.
573
- (lazy_subdirs[cpath] ||= []) << subdir
385
+ lazy_subdirs[cpath] << subdir
574
386
  elsif !cdef?(parent, cname)
575
387
  # First time we find this namespace, set an autoload for it.
576
- (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
388
+ lazy_subdirs[cpath(parent, cname)] << subdir
577
389
  set_autoload(parent, cname, subdir)
578
390
  else
579
391
  # For whatever reason the constant that corresponds to this namespace has
580
392
  # already been defined, we have to recurse.
581
- set_autoloads_in_dir(subdir, parent.const_get(cname))
393
+ log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
394
+ set_autoloads_in_dir(subdir, cget(parent, cname))
582
395
  end
583
396
  end
584
397
 
585
- # @param parent [Module]
586
- # @param cname [Symbol]
587
- # @param file [String]
588
- # @return [void]
398
+ # @sig (Module, Symbol, String) -> void
589
399
  def autoload_file(parent, cname, file)
590
- if autoload_path = autoload_for?(parent, cname)
400
+ if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
591
401
  # First autoload for a Ruby file wins, just ignore subsequent ones.
592
402
  if ruby?(autoload_path)
593
403
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
@@ -606,221 +416,91 @@ module Zeitwerk
606
416
  end
607
417
  end
608
418
 
609
- # @param dir [String] directory that would have autovivified a module
610
- # @param file [String] the file where the namespace is explicitly defined
611
- # @param parent [Module]
612
- # @param cname [Symbol]
613
- # @return [void]
419
+ # `dir` is the directory that would have autovivified a namespace. `file` is
420
+ # the file where we've found the namespace is explicitly defined.
421
+ #
422
+ # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
614
423
  def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
615
424
  autoloads.delete(dir)
616
425
  Registry.unregister_autoload(dir)
617
426
 
427
+ log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
428
+
618
429
  set_autoload(parent, cname, file)
619
430
  register_explicit_namespace(cpath(parent, cname))
620
431
  end
621
432
 
622
- # @param parent [Module]
623
- # @param cname [Symbol]
624
- # @param abspath [String]
625
- # @return [void]
433
+ # @sig (Module, Symbol, String) -> void
626
434
  def set_autoload(parent, cname, abspath)
627
- # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
628
- # real path to be able to delete it from $LOADED_FEATURES on unload, and to
629
- # be able to do a lookup later in Kernel#require for manual require calls.
630
- #
631
- # We freeze realpath because that saves allocations in Module#autoload.
632
- # See #125.
633
- realpath = File.realpath(abspath).freeze
634
- parent.autoload(cname, realpath)
435
+ autoloads.define(parent, cname, abspath)
436
+
635
437
  if logger
636
- if ruby?(realpath)
637
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
438
+ if ruby?(abspath)
439
+ log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
638
440
  else
639
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
441
+ log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
640
442
  end
641
443
  end
642
444
 
643
- autoloads[realpath] = [parent, cname]
644
- Registry.register_autoload(self, realpath)
445
+ Registry.register_autoload(self, abspath)
645
446
 
646
447
  # See why in the documentation of Zeitwerk::Registry.inceptions.
647
448
  unless parent.autoload?(cname)
648
- Registry.register_inception(cpath(parent, cname), realpath, self)
649
- end
650
- end
651
-
652
- # @param parent [Module]
653
- # @param cname [Symbol]
654
- # @return [String, nil]
655
- def autoload_for?(parent, cname)
656
- strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
657
- end
658
-
659
- # The autoload? predicate takes into account the ancestor chain of the
660
- # receiver, like const_defined? and other methods in the constants API do.
661
- #
662
- # For example, given
663
- #
664
- # class A
665
- # autoload :X, "x.rb"
666
- # end
667
- #
668
- # class B < A
669
- # end
670
- #
671
- # B.autoload?(:X) returns "x.rb".
672
- #
673
- # We need a way to strictly check in parent ignoring ancestors.
674
- #
675
- # @param parent [Module]
676
- # @param cname [Symbol]
677
- # @return [String, nil]
678
- if method(:autoload?).arity == 1
679
- def strict_autoload_path(parent, cname)
680
- parent.autoload?(cname) if cdef?(parent, cname)
681
- end
682
- else
683
- def strict_autoload_path(parent, cname)
684
- parent.autoload?(cname, false)
449
+ Registry.register_inception(cpath(parent, cname), abspath, self)
685
450
  end
686
451
  end
687
452
 
688
- # This method is called this way because I prefer `preload` to be the method
689
- # name to configure preloads in the public interface.
690
- #
691
- # @return [void]
692
- def do_preload
693
- preloads.each do |abspath|
694
- do_preload_abspath(abspath)
695
- end
696
- end
697
-
698
- # @param abspath [String]
699
- # @return [void]
700
- def do_preload_abspath(abspath)
701
- if ruby?(abspath)
702
- do_preload_file(abspath)
703
- elsif dir?(abspath)
704
- do_preload_dir(abspath)
705
- end
706
- end
707
-
708
- # @param dir [String]
709
- # @return [void]
710
- def do_preload_dir(dir)
711
- ls(dir) do |_basename, abspath|
712
- do_preload_abspath(abspath)
713
- end
714
- end
715
-
716
- # @param file [String]
717
- # @return [Boolean]
718
- def do_preload_file(file)
719
- log("preloading #{file}") if logger
720
- require file
721
- end
722
-
723
- # @param parent [Module]
724
- # @param cname [Symbol]
725
- # @return [String]
726
- def cpath(parent, cname)
727
- parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
728
- end
729
-
730
- # @param dir [String]
731
- # @yieldparam path [String, String]
732
- # @return [void]
733
- def ls(dir)
734
- Dir.foreach(dir) do |basename|
735
- next if basename.start_with?(".")
736
-
737
- abspath = File.join(dir, basename)
738
- next if ignored_paths.member?(abspath)
739
-
740
- # We freeze abspath because that saves allocations when passed later to
741
- # File methods. See #125.
742
- yield basename, abspath.freeze
743
- end
744
- end
745
-
746
- # @param path [String]
747
- # @return [Boolean]
748
- def ruby?(path)
749
- path.end_with?(".rb")
750
- end
751
-
752
- # @param path [String]
753
- # @return [Boolean]
754
- def dir?(path)
755
- File.directory?(path)
756
- end
757
-
758
- # @param paths [<String, Pathname, <String, Pathname>>]
759
- # @return [<String>]
760
- def expand_paths(paths)
761
- paths.flatten.map! { |path| File.expand_path(path) }
762
- end
763
-
764
- # @param glob_patterns [<String>]
765
- # @return [<String>]
766
- def expand_glob_patterns(glob_patterns)
767
- # Note that Dir.glob works with regular file names just fine. That is,
768
- # glob patterns technically need no wildcards.
769
- glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
770
- end
771
-
772
- # @return [void]
773
- def recompute_ignored_paths
774
- ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
775
- end
776
-
777
- # @return [void]
778
- def recompute_collapse_dirs
779
- collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
780
- end
781
-
782
- # @param message [String]
783
- # @return [void]
784
- def log(message)
785
- method_name = logger.respond_to?(:debug) ? :debug : :call
786
- logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
787
- end
788
-
789
- def cdef?(parent, cname)
790
- parent.const_defined?(cname, false)
791
- end
792
-
453
+ # @sig (String) -> void
793
454
  def register_explicit_namespace(cpath)
794
455
  ExplicitNamespace.register(cpath, self)
795
456
  end
796
457
 
458
+ # @sig (String) -> void
797
459
  def raise_if_conflicting_directory(dir)
798
460
  self.class.mutex.synchronize do
799
461
  Registry.loaders.each do |loader|
800
- if loader != self && loader.manages?(dir)
801
- require "pp"
802
- raise Error,
803
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
804
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
805
- EOS
462
+ next if loader == self
463
+ next if loader.ignores?(dir)
464
+
465
+ dir = dir + "/"
466
+ loader.root_dirs.each do |root_dir, _namespace|
467
+ next if ignores?(root_dir)
468
+
469
+ root_dir = root_dir + "/"
470
+ if dir.start_with?(root_dir) || root_dir.start_with?(dir)
471
+ require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
472
+ raise Error,
473
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
474
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
475
+ EOS
476
+ end
806
477
  end
807
478
  end
808
479
  end
809
480
  end
810
481
 
811
- # @param parent [Module]
812
- # @param cname [Symbol]
813
- # @return [void]
482
+ # @sig (String, Object, String) -> void
483
+ def run_on_unload_callbacks(cpath, value, abspath)
484
+ # Order matters. If present, run the most specific one.
485
+ on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
486
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
487
+ end
488
+
489
+ # @sig (Module, Symbol) -> void
814
490
  def unload_autoload(parent, cname)
815
- parent.send(:remove_const, cname)
491
+ parent.__send__(:remove_const, cname)
816
492
  log("autoload for #{cpath(parent, cname)} removed") if logger
817
493
  end
818
494
 
819
- # @param parent [Module]
820
- # @param cname [Symbol]
821
- # @return [void]
495
+ # @sig (Module, Symbol) -> void
822
496
  def unload_cref(parent, cname)
823
- parent.send(:remove_const, cname)
497
+ # Let's optimistically remove_const. The way we use it, this is going to
498
+ # succeed always if all is good.
499
+ parent.__send__(:remove_const, cname)
500
+ rescue ::NameError
501
+ # There are a few edge scenarios in which this may happen. If the constant
502
+ # is gone, that is OK, anyway.
503
+ else
824
504
  log("#{cpath(parent, cname)} unloaded") if logger
825
505
  end
826
506
  end