zeitwerk 2.3.1 → 2.5.0.beta

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.
@@ -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