zeitwerk 2.3.1 → 2.5.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,16 +2,14 @@
2
2
 
3
3
  module Zeitwerk
4
4
  class GemInflector < Inflector
5
- # @param root_file [String]
5
+ # @sig (String) -> void
6
6
  def initialize(root_file)
7
7
  namespace = File.basename(root_file, ".rb")
8
8
  lib_dir = File.dirname(root_file)
9
9
  @version_file = File.join(lib_dir, namespace, "version.rb")
10
10
  end
11
11
 
12
- # @param basename [String]
13
- # @param abspath [String]
14
- # @return [String]
12
+ # @sig (String, String) -> String
15
13
  def camelize(basename, abspath)
16
14
  abspath == @version_file ? "VERSION" : super
17
15
  end
@@ -11,11 +11,9 @@ module Zeitwerk
11
11
  #
12
12
  # Takes into account hard-coded mappings configured with `inflect`.
13
13
  #
14
- # @param basename [String]
15
- # @param _abspath [String]
16
- # @return [String]
14
+ # @sig (String, String) -> String
17
15
  def camelize(basename, _abspath)
18
- overrides[basename] || basename.split('_').map!(&:capitalize).join
16
+ overrides[basename] || basename.split('_').each(&:capitalize!).join
19
17
  end
20
18
 
21
19
  # Configures hard-coded inflections:
@@ -30,8 +28,7 @@ module Zeitwerk
30
28
  # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
31
29
  # inflector.camelize("users_controller", abspath) # => "UsersController"
32
30
  #
33
- # @param inflections [{String => String}]
34
- # @return [void]
31
+ # @sig (Hash[String, String]) -> void
35
32
  def inflect(inflections)
36
33
  overrides.merge!(inflections)
37
34
  end
@@ -41,7 +38,7 @@ module Zeitwerk
41
38
  # Hard-coded basename to constant name user maps that override the default
42
39
  # inflection logic.
43
40
  #
44
- # @return [{String => String}]
41
+ # @sig () -> Hash[String, String]
45
42
  def overrides
46
43
  @overrides ||= {}
47
44
  end
@@ -12,15 +12,15 @@ module Kernel
12
12
  # On the other hand, if you publish a new version of a gem that is now managed
13
13
  # by Zeitwerk, client code can reference directly your classes and modules and
14
14
  # should not require anything. But if someone has legacy require calls around,
15
- # they will work as expected, and in a compatible way.
15
+ # they will work as expected, and in a compatible way. This feature is by now
16
+ # EXPERIMENTAL and UNDOCUMENTED.
16
17
  #
17
18
  # We cannot decorate with prepend + super because Kernel has already been
18
19
  # included in Object, and changes in ancestors don't get propagated into
19
20
  # already existing ancestor chains.
20
21
  alias_method :zeitwerk_original_require, :require
21
22
 
22
- # @param path [String]
23
- # @return [Boolean]
23
+ # @sig (String) -> true | false
24
24
  def require(path)
25
25
  if loader = Zeitwerk::Registry.loader_for(path)
26
26
  if path.end_with?(".rb")
@@ -29,13 +29,14 @@ module Kernel
29
29
  end
30
30
  else
31
31
  loader.on_dir_autoloaded(path)
32
+ true
32
33
  end
33
34
  else
34
35
  zeitwerk_original_require(path).tap do |required|
35
36
  if required
36
- realpath = $LOADED_FEATURES.last
37
- if loader = Zeitwerk::Registry.loader_for(realpath)
38
- loader.on_file_autoloaded(realpath)
37
+ abspath = $LOADED_FEATURES.last
38
+ if loader = Zeitwerk::Registry.loader_for(abspath)
39
+ loader.on_file_autoloaded(abspath)
39
40
  end
40
41
  end
41
42
  end
@@ -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
20
-
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
10
+ require_relative "loader/config"
35
11
 
36
- # Absolute paths of files or directories that have to be preloaded.
37
- #
38
- # @private
39
- # @return [<String>]
40
- attr_reader :preloads
41
-
42
- # Absolute paths of files, directories, or glob patterns to be totally
43
- # ignored.
44
- #
45
- # @private
46
- # @return [Set<String>]
47
- attr_reader :ignored_glob_patterns
12
+ include RealModName
13
+ include Callbacks
14
+ include Helpers
15
+ include Config
48
16
 
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.
17
+ # Keeps track of autoloads defined by the loader which have not been
18
+ # executed so far.
52
19
  #
53
- # @private
54
- # @return [Set<String>]
55
- attr_reader :ignored_paths
56
-
57
- # Absolute paths of directories or glob patterns to be collapsed.
20
+ # This metadata helps us implement a few things:
58
21
  #
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.
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.
65
25
  #
66
- # @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.
26
+ # 2. When unloading, remove autoloads that have not been executed.
73
27
  #
74
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
75
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
76
- # ...
28
+ # 3. Eager load with a recursive const_get, rather than a recursive require,
29
+ # for consistency with lazy loading.
77
30
  #
78
31
  # @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,42 @@ 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
- # @private
129
- # @return [Set<String>]
130
- attr_reader :eager_load_exclusions
131
-
132
79
  # @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
169
-
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
177
-
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
- # @raise [Zeitwerk::Error]
194
- # @return [void]
195
- def push_dir(path)
196
- abspath = File.expand_path(path)
197
- if dir?(abspath)
198
- raise_if_conflicting_directory(abspath)
199
- root_dirs[abspath] = true
200
- else
201
- raise Error, "the root directory #{abspath} does not exist"
202
- end
203
- end
204
-
205
- # You need to call this method before setup in order to be able to reload.
206
- # There is no way to undo this, either you want to reload or you don't.
207
- #
208
- # @raise [Zeitwerk::Error]
209
- # @return [void]
210
- def enable_reloading
211
- mutex.synchronize do
212
- break if @reloading_enabled
213
-
214
- if @setup
215
- raise Error, "cannot enable reloading after setup"
216
- else
217
- @reloading_enabled = true
218
- end
219
- end
220
- end
221
-
222
- # @return [Boolean]
223
- def reloading_enabled?
224
- @reloading_enabled
225
- end
226
-
227
- # Files or directories to be preloaded instead of lazy loaded.
228
- #
229
- # @param paths [<String, Pathname, <String, Pathname>>]
230
- # @return [void]
231
- def preload(*paths)
232
- mutex.synchronize do
233
- expand_paths(paths).each do |abspath|
234
- preloads << abspath
235
- do_preload_abspath(abspath) if @setup
236
- end
237
- end
238
- end
88
+ super
239
89
 
240
- # Configure files, directories, or glob patterns to be totally ignored.
241
- #
242
- # @param paths [<String, Pathname, <String, Pathname>>]
243
- # @return [void]
244
- def ignore(*glob_patterns)
245
- glob_patterns = expand_paths(glob_patterns)
246
- mutex.synchronize do
247
- ignored_glob_patterns.merge(glob_patterns)
248
- ignored_paths.merge(expand_glob_patterns(glob_patterns))
249
- end
250
- 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
251
98
 
252
- # Configure directories or glob patterns to be collapsed.
253
- #
254
- # @param paths [<String, Pathname, <String, Pathname>>]
255
- # @return [void]
256
- def collapse(*glob_patterns)
257
- glob_patterns = expand_paths(glob_patterns)
258
- mutex.synchronize do
259
- collapse_glob_patterns.merge(glob_patterns)
260
- collapse_dirs.merge(expand_glob_patterns(glob_patterns))
261
- end
99
+ Registry.register_loader(self)
262
100
  end
263
101
 
264
- # Sets autoloads in the root namespace and preloads files, if any.
102
+ # Sets autoloads in the root namespace.
265
103
  #
266
- # @return [void]
104
+ # @sig () -> void
267
105
  def setup
268
106
  mutex.synchronize do
269
107
  break if @setup
270
108
 
271
- actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
272
- do_preload
109
+ actual_root_dirs.each do |root_dir, namespace|
110
+ set_autoloads_in_dir(root_dir, namespace)
111
+ end
273
112
 
274
113
  @setup = true
275
114
  end
@@ -283,7 +122,7 @@ module Zeitwerk
283
122
  # unload them.
284
123
  #
285
124
  # @private
286
- # @return [void]
125
+ # @sig () -> void
287
126
  def unload
288
127
  mutex.synchronize do
289
128
  # We are going to keep track of the files that were required by our
@@ -294,21 +133,26 @@ module Zeitwerk
294
133
  # is enough.
295
134
  unloaded_files = Set.new
296
135
 
297
- autoloads.each do |realpath, (parent, cname)|
136
+ autoloads.each do |(parent, cname), abspath|
298
137
  if parent.autoload?(cname)
299
138
  unload_autoload(parent, cname)
300
139
  else
301
140
  # Could happen if loaded with require_relative. That is unsupported,
302
141
  # and the constant path would escape unloadable_cpath? This is just
303
142
  # defensive code to clean things up as much as we are able to.
304
- unload_cref(parent, cname) if cdef?(parent, cname)
305
- unloaded_files.add(realpath) if ruby?(realpath)
143
+ unload_cref(parent, cname) if cdef?(parent, cname)
144
+ unloaded_files.add(abspath) if ruby?(abspath)
306
145
  end
307
146
  end
308
147
 
309
- to_unload.each_value do |(realpath, (parent, cname))|
310
- unload_cref(parent, cname) if cdef?(parent, cname)
311
- unloaded_files.add(realpath) if ruby?(realpath)
148
+ to_unload.each do |cpath, (abspath, (parent, cname))|
149
+ unless on_unload_callbacks.empty?
150
+ value = parent.const_get(cname)
151
+ run_on_unload_callbacks(cpath, value, abspath)
152
+ end
153
+
154
+ unload_cref(parent, cname) if cdef?(parent, cname)
155
+ unloaded_files.add(abspath) if ruby?(abspath)
312
156
  end
313
157
 
314
158
  unless unloaded_files.empty?
@@ -346,7 +190,7 @@ module Zeitwerk
346
190
  # client code in the README of the project.
347
191
  #
348
192
  # @raise [Zeitwerk::Error]
349
- # @return [void]
193
+ # @sig () -> void
350
194
  def reload
351
195
  if reloading_enabled?
352
196
  unload
@@ -363,29 +207,34 @@ module Zeitwerk
363
207
  # are not eager loaded. You can opt-out specifically in specific files and
364
208
  # directories with `do_not_eager_load`.
365
209
  #
366
- # @return [void]
210
+ # @sig () -> void
367
211
  def eager_load
368
212
  mutex.synchronize do
369
213
  break if @eager_loaded
370
214
 
371
- queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
372
- queue.map! { |dir| [Object, dir] }
215
+ log("eager load start") if logger
216
+
217
+ queue = []
218
+ actual_root_dirs.each do |root_dir, namespace|
219
+ queue << [namespace, root_dir] unless excluded_from_eager_load?(root_dir)
220
+ end
221
+
373
222
  while to_eager_load = queue.shift
374
223
  namespace, dir = to_eager_load
375
224
 
376
225
  ls(dir) do |basename, abspath|
377
- next if eager_load_exclusions.member?(abspath)
226
+ next if excluded_from_eager_load?(abspath)
378
227
 
379
228
  if ruby?(abspath)
380
- if cref = autoloads[File.realpath(abspath)]
381
- cref[0].const_get(cref[1], false)
229
+ if cref = autoloads.cref_for(abspath)
230
+ cget(*cref)
382
231
  end
383
232
  elsif dir?(abspath) && !root_dirs.key?(abspath)
384
- if collapse_dirs.member?(abspath)
233
+ if collapse?(abspath)
385
234
  queue << [namespace, abspath]
386
235
  else
387
236
  cname = inflector.camelize(basename, abspath)
388
- queue << [namespace.const_get(cname, false), abspath]
237
+ queue << [cget(namespace, cname), abspath]
389
238
  end
390
239
  end
391
240
  end
@@ -397,23 +246,15 @@ module Zeitwerk
397
246
  autoloaded_dirs.clear
398
247
 
399
248
  @eager_loaded = true
400
- end
401
- end
402
249
 
403
- # Let eager load ignore the given files or directories. The constants
404
- # defined in those files are still autoloadable.
405
- #
406
- # @param paths [<String, Pathname, <String, Pathname>>]
407
- # @return [void]
408
- def do_not_eager_load(*paths)
409
- mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
250
+ log("eager load end") if logger
251
+ end
410
252
  end
411
253
 
412
254
  # Says if the given constant path would be unloaded on reload. This
413
255
  # predicate returns `false` if reloading is disabled.
414
256
  #
415
- # @param cpath [String]
416
- # @return [Boolean]
257
+ # @sig (String) -> bool
417
258
  def unloadable_cpath?(cpath)
418
259
  to_unload.key?(cpath)
419
260
  end
@@ -421,42 +262,19 @@ module Zeitwerk
421
262
  # Returns an array with the constant paths that would be unloaded on reload.
422
263
  # This predicate returns an empty array if reloading is disabled.
423
264
  #
424
- # @return [<String>]
265
+ # @sig () -> Array[String]
425
266
  def unloadable_cpaths
426
267
  to_unload.keys.freeze
427
268
  end
428
269
 
429
- # Logs to `$stdout`, handy shortcut for debugging.
430
- #
431
- # @return [void]
432
- def log!
433
- @logger = ->(msg) { puts msg }
434
- end
435
-
436
- # @private
437
- # @param dir [String]
438
- # @return [Boolean]
439
- def manages?(dir)
440
- dir = dir + "/"
441
- ignored_paths.each do |ignored_path|
442
- return false if dir.start_with?(ignored_path + "/")
443
- end
444
-
445
- root_dirs.each_key do |root_dir|
446
- return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
447
- end
448
-
449
- false
450
- end
451
-
452
270
  # --- Class methods ---------------------------------------------------------------------------
453
271
 
454
272
  class << self
455
- # @return [#call, #debug, nil]
273
+ # @sig #call | #debug | nil
456
274
  attr_accessor :default_logger
457
275
 
458
276
  # @private
459
- # @return [Mutex]
277
+ # @sig Mutex
460
278
  attr_accessor :mutex
461
279
 
462
280
  # This is a shortcut for
@@ -470,7 +288,7 @@ module Zeitwerk
470
288
  # except that this method returns the same object in subsequent calls from
471
289
  # the same file, in the unlikely case the gem wants to be able to reload.
472
290
  #
473
- # @return [Zeitwerk::Loader]
291
+ # @sig () -> Zeitwerk::Loader
474
292
  def for_gem
475
293
  called_from = caller_locations(1, 1).first.path
476
294
  Registry.loader_for_gem(called_from)
@@ -478,7 +296,7 @@ module Zeitwerk
478
296
 
479
297
  # Broadcasts `eager_load` to all loaders.
480
298
  #
481
- # @return [void]
299
+ # @sig () -> void
482
300
  def eager_load_all
483
301
  Registry.loaders.each(&:eager_load)
484
302
  end
@@ -486,7 +304,7 @@ module Zeitwerk
486
304
  # Returns an array with the absolute paths of the root directories of all
487
305
  # registered loaders. This is a read-only collection.
488
306
  #
489
- # @return [<String>]
307
+ # @sig () -> Array[String]
490
308
  def all_dirs
491
309
  Registry.loaders.flat_map(&:dirs).freeze
492
310
  end
@@ -496,21 +314,12 @@ module Zeitwerk
496
314
 
497
315
  private # -------------------------------------------------------------------------------------
498
316
 
499
- # @return [<String>]
500
- def actual_root_dirs
501
- root_dirs.keys.delete_if do |root_dir|
502
- !dir?(root_dir) || ignored_paths.member?(root_dir)
503
- end
504
- end
505
-
506
- # @param dir [String]
507
- # @param parent [Module]
508
- # @return [void]
317
+ # @sig (String, Module) -> void
509
318
  def set_autoloads_in_dir(dir, parent)
510
319
  ls(dir) do |basename, abspath|
511
320
  begin
512
321
  if ruby?(basename)
513
- basename[-3..-1] = ''
322
+ basename.delete_suffix!(".rb")
514
323
  cname = inflector.camelize(basename, abspath).to_sym
515
324
  autoload_file(parent, cname, abspath)
516
325
  elsif dir?(abspath)
@@ -520,9 +329,9 @@ module Zeitwerk
520
329
  # To resolve the ambiguity file name -> constant path this introduces,
521
330
  # the `app/models/concerns` directory is totally ignored as a namespace,
522
331
  # it counts only as root. The guard checks that.
523
- unless root_dirs.key?(abspath)
332
+ unless root_dir?(abspath)
524
333
  cname = inflector.camelize(basename, abspath).to_sym
525
- if collapse_dirs.member?(abspath)
334
+ if collapse?(abspath)
526
335
  set_autoloads_in_dir(abspath, parent)
527
336
  else
528
337
  autoload_subdir(parent, cname, abspath)
@@ -548,35 +357,30 @@ module Zeitwerk
548
357
  end
549
358
  end
550
359
 
551
- # @param parent [Module]
552
- # @param cname [Symbol]
553
- # @param subdir [String]
554
- # @return [void]
360
+ # @sig (Module, Symbol, String) -> void
555
361
  def autoload_subdir(parent, cname, subdir)
556
- if autoload_path = autoload_for?(parent, cname)
362
+ if autoload_path = autoloads.abspath_for(parent, cname)
557
363
  cpath = cpath(parent, cname)
558
364
  register_explicit_namespace(cpath) if ruby?(autoload_path)
559
365
  # We do not need to issue another autoload, the existing one is enough
560
366
  # no matter if it is for a file or a directory. Just remember the
561
367
  # subdirectory has to be visited if the namespace is used.
562
- (lazy_subdirs[cpath] ||= []) << subdir
368
+ lazy_subdirs[cpath] << subdir
563
369
  elsif !cdef?(parent, cname)
564
370
  # First time we find this namespace, set an autoload for it.
565
- (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
371
+ lazy_subdirs[cpath(parent, cname)] << subdir
566
372
  set_autoload(parent, cname, subdir)
567
373
  else
568
374
  # For whatever reason the constant that corresponds to this namespace has
569
375
  # already been defined, we have to recurse.
570
- set_autoloads_in_dir(subdir, parent.const_get(cname))
376
+ log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
377
+ set_autoloads_in_dir(subdir, cget(parent, cname))
571
378
  end
572
379
  end
573
380
 
574
- # @param parent [Module]
575
- # @param cname [Symbol]
576
- # @param file [String]
577
- # @return [void]
381
+ # @sig (Module, Symbol, String) -> void
578
382
  def autoload_file(parent, cname, file)
579
- if autoload_path = autoload_for?(parent, cname)
383
+ if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
580
384
  # First autoload for a Ruby file wins, just ignore subsequent ones.
581
385
  if ruby?(autoload_path)
582
386
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
@@ -595,194 +399,46 @@ module Zeitwerk
595
399
  end
596
400
  end
597
401
 
598
- # @param dir [String] directory that would have autovivified a module
599
- # @param file [String] the file where the namespace is explicitly defined
600
- # @param parent [Module]
601
- # @param cname [Symbol]
602
- # @return [void]
402
+ # `dir` is the directory that would have autovivified a namespace. `file` is
403
+ # the file where we've found the namespace is explicitly defined.
404
+ #
405
+ # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
603
406
  def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
604
407
  autoloads.delete(dir)
605
408
  Registry.unregister_autoload(dir)
606
409
 
410
+ log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
411
+
607
412
  set_autoload(parent, cname, file)
608
413
  register_explicit_namespace(cpath(parent, cname))
609
414
  end
610
415
 
611
- # @param parent [Module]
612
- # @param cname [Symbol]
613
- # @param abspath [String]
614
- # @return [void]
416
+ # @sig (Module, Symbol, String) -> void
615
417
  def set_autoload(parent, cname, abspath)
616
- # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
617
- # real path to be able to delete it from $LOADED_FEATURES on unload, and to
618
- # be able to do a lookup later in Kernel#require for manual require calls.
619
- #
620
- # We freeze realpath because that saves allocations in Module#autoload.
621
- # See #125.
622
- realpath = File.realpath(abspath).freeze
623
- parent.autoload(cname, realpath)
418
+ autoloads.define(parent, cname, abspath)
419
+
624
420
  if logger
625
- if ruby?(realpath)
626
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
421
+ if ruby?(abspath)
422
+ log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
627
423
  else
628
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
424
+ log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
629
425
  end
630
426
  end
631
427
 
632
- autoloads[realpath] = [parent, cname]
633
- Registry.register_autoload(self, realpath)
428
+ Registry.register_autoload(self, abspath)
634
429
 
635
430
  # See why in the documentation of Zeitwerk::Registry.inceptions.
636
431
  unless parent.autoload?(cname)
637
- Registry.register_inception(cpath(parent, cname), realpath, self)
638
- end
639
- end
640
-
641
- # @param parent [Module]
642
- # @param cname [Symbol]
643
- # @return [String, nil]
644
- def autoload_for?(parent, cname)
645
- strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
646
- end
647
-
648
- # The autoload? predicate takes into account the ancestor chain of the
649
- # receiver, like const_defined? and other methods in the constants API do.
650
- #
651
- # For example, given
652
- #
653
- # class A
654
- # autoload :X, "x.rb"
655
- # end
656
- #
657
- # class B < A
658
- # end
659
- #
660
- # B.autoload?(:X) returns "x.rb".
661
- #
662
- # We need a way to strictly check in parent ignoring ancestors.
663
- #
664
- # @param parent [Module]
665
- # @param cname [Symbol]
666
- # @return [String, nil]
667
- if method(:autoload?).arity == 1
668
- def strict_autoload_path(parent, cname)
669
- parent.autoload?(cname) if cdef?(parent, cname)
670
- end
671
- else
672
- def strict_autoload_path(parent, cname)
673
- parent.autoload?(cname, false)
432
+ Registry.register_inception(cpath(parent, cname), abspath, self)
674
433
  end
675
434
  end
676
435
 
677
- # This method is called this way because I prefer `preload` to be the method
678
- # name to configure preloads in the public interface.
679
- #
680
- # @return [void]
681
- def do_preload
682
- preloads.each do |abspath|
683
- do_preload_abspath(abspath)
684
- end
685
- end
686
-
687
- # @param abspath [String]
688
- # @return [void]
689
- def do_preload_abspath(abspath)
690
- if ruby?(abspath)
691
- do_preload_file(abspath)
692
- elsif dir?(abspath)
693
- do_preload_dir(abspath)
694
- end
695
- end
696
-
697
- # @param dir [String]
698
- # @return [void]
699
- def do_preload_dir(dir)
700
- ls(dir) do |_basename, abspath|
701
- do_preload_abspath(abspath)
702
- end
703
- end
704
-
705
- # @param file [String]
706
- # @return [Boolean]
707
- def do_preload_file(file)
708
- log("preloading #{file}") if logger
709
- require file
710
- end
711
-
712
- # @param parent [Module]
713
- # @param cname [Symbol]
714
- # @return [String]
715
- def cpath(parent, cname)
716
- parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
717
- end
718
-
719
- # @param dir [String]
720
- # @yieldparam path [String, String]
721
- # @return [void]
722
- def ls(dir)
723
- Dir.foreach(dir) do |basename|
724
- next if basename.start_with?(".")
725
-
726
- abspath = File.join(dir, basename)
727
- next if ignored_paths.member?(abspath)
728
-
729
- # We freeze abspath because that saves allocations when passed later to
730
- # File methods. See #125.
731
- yield basename, abspath.freeze
732
- end
733
- end
734
-
735
- # @param path [String]
736
- # @return [Boolean]
737
- def ruby?(path)
738
- path.end_with?(".rb")
739
- end
740
-
741
- # @param path [String]
742
- # @return [Boolean]
743
- def dir?(path)
744
- File.directory?(path)
745
- end
746
-
747
- # @param paths [<String, Pathname, <String, Pathname>>]
748
- # @return [<String>]
749
- def expand_paths(paths)
750
- paths.flatten.map! { |path| File.expand_path(path) }
751
- end
752
-
753
- # @param glob_patterns [<String>]
754
- # @return [<String>]
755
- def expand_glob_patterns(glob_patterns)
756
- # Note that Dir.glob works with regular file names just fine. That is,
757
- # glob patterns technically need no wildcards.
758
- glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
759
- end
760
-
761
- # @return [void]
762
- def recompute_ignored_paths
763
- ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
764
- end
765
-
766
- # @return [void]
767
- def recompute_collapse_dirs
768
- collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
769
- end
770
-
771
- # @param message [String]
772
- # @return [void]
773
- def log(message)
774
- method_name = logger.respond_to?(:debug) ? :debug : :call
775
- logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
776
- end
777
-
778
- def cdef?(parent, cname)
779
- parent.const_defined?(cname, false)
780
- end
781
-
436
+ # @sig (String) -> void
782
437
  def register_explicit_namespace(cpath)
783
438
  ExplicitNamespace.register(cpath, self)
784
439
  end
785
440
 
441
+ # @sig (String) -> void
786
442
  def raise_if_conflicting_directory(dir)
787
443
  self.class.mutex.synchronize do
788
444
  Registry.loaders.each do |loader|
@@ -797,19 +453,22 @@ module Zeitwerk
797
453
  end
798
454
  end
799
455
 
800
- # @param parent [Module]
801
- # @param cname [Symbol]
802
- # @return [void]
456
+ # @sig (String, Object, String) -> void
457
+ def run_on_unload_callbacks(cpath, value, abspath)
458
+ # Order matters. If present, run the most specific one.
459
+ on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
460
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
461
+ end
462
+
463
+ # @sig (Module, Symbol) -> void
803
464
  def unload_autoload(parent, cname)
804
- parent.send(:remove_const, cname)
465
+ parent.__send__(:remove_const, cname)
805
466
  log("autoload for #{cpath(parent, cname)} removed") if logger
806
467
  end
807
468
 
808
- # @param parent [Module]
809
- # @param cname [Symbol]
810
- # @return [void]
469
+ # @sig (Module, Symbol) -> void
811
470
  def unload_cref(parent, cname)
812
- parent.send(:remove_const, cname)
471
+ parent.__send__(:remove_const, cname)
813
472
  log("#{cpath(parent, cname)} unloaded") if logger
814
473
  end
815
474
  end