zeitwerk 2.5.4 → 2.6.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
3
4
  require "set"
4
5
 
5
6
  module Zeitwerk
@@ -7,11 +8,18 @@ module Zeitwerk
7
8
  require_relative "loader/helpers"
8
9
  require_relative "loader/callbacks"
9
10
  require_relative "loader/config"
11
+ require_relative "loader/eager_load"
12
+
13
+ extend Internal
10
14
 
11
15
  include RealModName
12
16
  include Callbacks
13
17
  include Helpers
14
18
  include Config
19
+ include EagerLoad
20
+
21
+ MUTEX = Mutex.new
22
+ private_constant :MUTEX
15
23
 
16
24
  # Maps absolute paths for which an autoload has been set ---and not
17
25
  # executed--- to their corresponding parent class or module and constant
@@ -21,9 +29,9 @@ module Zeitwerk
21
29
  # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
22
30
  # ...
23
31
  #
24
- # @private
25
32
  # @sig Hash[String, [Module, Symbol]]
26
33
  attr_reader :autoloads
34
+ internal :autoloads
27
35
 
28
36
  # We keep track of autoloaded directories to remove them from the registry
29
37
  # at the end of eager loading.
@@ -31,9 +39,9 @@ module Zeitwerk
31
39
  # Files are removed as they are autoloaded, but directories need to wait due
32
40
  # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
33
41
  #
34
- # @private
35
42
  # @sig Array[String]
36
43
  attr_reader :autoloaded_dirs
44
+ internal :autoloaded_dirs
37
45
 
38
46
  # Stores metadata needed for unloading. Its entries look like this:
39
47
  #
@@ -47,11 +55,11 @@ module Zeitwerk
47
55
  # If reloading is enabled, this hash is filled as constants are autoloaded
48
56
  # or eager loaded. Otherwise, the collection remains empty.
49
57
  #
50
- # @private
51
58
  # @sig Hash[String, [String, [Module, Symbol]]]
52
59
  attr_reader :to_unload
60
+ internal :to_unload
53
61
 
54
- # Maps constant paths of namespaces to arrays of corresponding directories.
62
+ # Maps namespace constant paths to their respective directories.
55
63
  #
56
64
  # For example, given this mapping:
57
65
  #
@@ -61,21 +69,32 @@ module Zeitwerk
61
69
  # ...
62
70
  # ]
63
71
  #
64
- # when `Admin` gets defined we know that it plays the role of a namespace and
65
- # that its children are spread over those directories. We'll visit them to set
66
- # up the corresponding autoloads.
72
+ # when `Admin` gets defined we know that it plays the role of a namespace
73
+ # and that its children are spread over those directories. We'll visit them
74
+ # to set up the corresponding autoloads.
67
75
  #
68
- # @private
69
76
  # @sig Hash[String, Array[String]]
70
- attr_reader :lazy_subdirs
77
+ attr_reader :namespace_dirs
78
+ internal :namespace_dirs
79
+
80
+ # A shadowed file is a file managed by this loader that is ignored when
81
+ # setting autoloads because its matching constant is already taken.
82
+ #
83
+ # This private set is populated as we descend. For example, if the loader
84
+ # has only scanned the top-level, `shadowed_files` does not have shadowed
85
+ # files that may exist deep in the project tree yet.
86
+ #
87
+ # @sig Set[String]
88
+ attr_reader :shadowed_files
89
+ internal :shadowed_files
71
90
 
72
- # @private
73
91
  # @sig Mutex
74
92
  attr_reader :mutex
93
+ private :mutex
75
94
 
76
- # @private
77
- # @sig Mutex
78
- attr_reader :mutex2
95
+ # @sig Monitor
96
+ attr_reader :dirs_autoload_monitor
97
+ private :dirs_autoload_monitor
79
98
 
80
99
  def initialize
81
100
  super
@@ -83,24 +102,26 @@ module Zeitwerk
83
102
  @autoloads = {}
84
103
  @autoloaded_dirs = []
85
104
  @to_unload = {}
86
- @lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
87
- @mutex = Mutex.new
88
- @mutex2 = Mutex.new
105
+ @namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
106
+ @shadowed_files = Set.new
89
107
  @setup = false
90
108
  @eager_loaded = false
91
109
 
110
+ @mutex = Mutex.new
111
+ @dirs_autoload_monitor = Monitor.new
112
+
92
113
  Registry.register_loader(self)
93
114
  end
94
115
 
95
- # Sets autoloads in the root namespace.
116
+ # Sets autoloads in the root namespaces.
96
117
  #
97
118
  # @sig () -> void
98
119
  def setup
99
120
  mutex.synchronize do
100
121
  break if @setup
101
122
 
102
- actual_root_dirs.each do |root_dir, namespace|
103
- set_autoloads_in_dir(root_dir, namespace)
123
+ actual_roots.each do |root_dir, root_namespace|
124
+ define_autoloads_for_dir(root_dir, root_namespace)
104
125
  end
105
126
 
106
127
  on_setup_callbacks.each(&:call)
@@ -117,12 +138,14 @@ module Zeitwerk
117
138
  # unload them.
118
139
  #
119
140
  # This method is public but undocumented. Main interface is `reload`, which
120
- # means `unload` + `setup`. This one is avaiable to be used together with
141
+ # means `unload` + `setup`. This one is available to be used together with
121
142
  # `unregister`, which is undocumented too.
122
143
  #
123
144
  # @sig () -> void
124
145
  def unload
125
146
  mutex.synchronize do
147
+ raise SetupRequired unless @setup
148
+
126
149
  # We are going to keep track of the files that were required by our
127
150
  # autoloads to later remove them from $LOADED_FEATURES, thus making them
128
151
  # loadable by Kernel#require again.
@@ -144,15 +167,16 @@ module Zeitwerk
144
167
  end
145
168
 
146
169
  to_unload.each do |cpath, (abspath, (parent, cname))|
147
- # We have to check cdef? in this condition. Reason is, constants whose
148
- # file does not define them have to be kept in to_unload as explained
149
- # in the implementation of on_file_autoloaded.
150
- #
151
- # If the constant is not defined, on_unload should not be triggered
152
- # for it.
153
- if !on_unload_callbacks.empty? && cdef?(parent, cname)
154
- value = parent.const_get(cname)
155
- run_on_unload_callbacks(cpath, value, abspath)
170
+ unless on_unload_callbacks.empty?
171
+ begin
172
+ value = cget(parent, cname)
173
+ rescue ::NameError
174
+ # Perhaps the user deleted the constant by hand, or perhaps an
175
+ # autoload failed to define the expected constant but the user
176
+ # rescued the exception.
177
+ else
178
+ run_on_unload_callbacks(cpath, value, abspath)
179
+ end
156
180
  end
157
181
 
158
182
  unload_cref(parent, cname)
@@ -177,10 +201,11 @@ module Zeitwerk
177
201
  autoloads.clear
178
202
  autoloaded_dirs.clear
179
203
  to_unload.clear
180
- lazy_subdirs.clear
204
+ namespace_dirs.clear
205
+ shadowed_files.clear
181
206
 
182
207
  Registry.on_unload(self)
183
- ExplicitNamespace.unregister_loader(self)
208
+ ExplicitNamespace.__unregister_loader(self)
184
209
 
185
210
  @setup = false
186
211
  @eager_loaded = false
@@ -196,67 +221,62 @@ module Zeitwerk
196
221
  # @raise [Zeitwerk::Error]
197
222
  # @sig () -> void
198
223
  def reload
199
- if reloading_enabled?
200
- unload
201
- recompute_ignored_paths
202
- recompute_collapse_dirs
203
- setup
204
- else
205
- raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
206
- end
224
+ raise ReloadingDisabledError unless reloading_enabled?
225
+ raise SetupRequired unless @setup
226
+
227
+ unload
228
+ recompute_ignored_paths
229
+ recompute_collapse_dirs
230
+ setup
207
231
  end
208
232
 
209
- # Eager loads all files in the root directories, recursively. Files do not
210
- # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
211
- # are not eager loaded. You can opt-out specifically in specific files and
212
- # directories with `do_not_eager_load`, and that can be overridden passing
213
- # `force: true`.
214
- #
215
- # @sig (true | false) -> void
216
- def eager_load(force: false)
217
- mutex.synchronize do
218
- break if @eager_loaded
233
+ # @sig (String | Pathname) -> String?
234
+ def cpath_expected_at(path)
235
+ abspath = File.expand_path(path)
219
236
 
220
- log("eager load start") if logger
237
+ raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
221
238
 
222
- honour_exclusions = !force
239
+ return unless dir?(abspath) || ruby?(abspath)
240
+ return if ignored_path?(abspath)
223
241
 
224
- queue = []
225
- actual_root_dirs.each do |root_dir, namespace|
226
- queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
227
- end
242
+ paths = []
228
243
 
229
- while to_eager_load = queue.shift
230
- namespace, dir = to_eager_load
231
-
232
- ls(dir) do |basename, abspath|
233
- next if honour_exclusions && excluded_from_eager_load?(abspath)
234
-
235
- if ruby?(abspath)
236
- if cref = autoloads[abspath]
237
- cget(*cref)
238
- end
239
- elsif dir?(abspath) && !root_dirs.key?(abspath)
240
- if collapse?(abspath)
241
- queue << [namespace, abspath]
242
- else
243
- cname = inflector.camelize(basename, abspath)
244
- queue << [cget(namespace, cname), abspath]
245
- end
246
- end
247
- end
248
- end
244
+ if ruby?(abspath)
245
+ basename = File.basename(abspath, ".rb")
246
+ return if hidden?(basename)
249
247
 
250
- autoloaded_dirs.each do |autoloaded_dir|
251
- Registry.unregister_autoload(autoloaded_dir)
252
- end
253
- autoloaded_dirs.clear
248
+ paths << [basename, abspath]
249
+ walk_up_from = File.dirname(abspath)
250
+ else
251
+ walk_up_from = abspath
252
+ end
253
+
254
+ root_namespace = nil
255
+
256
+ walk_up(walk_up_from) do |dir|
257
+ break if root_namespace = roots[dir]
258
+ return if ignored_path?(dir)
254
259
 
255
- @eager_loaded = true
260
+ basename = File.basename(dir)
261
+ return if hidden?(basename)
262
+
263
+ paths << [basename, abspath] unless collapse?(dir)
264
+ end
265
+
266
+ return unless root_namespace
267
+
268
+ if paths.empty?
269
+ real_mod_name(root_namespace)
270
+ else
271
+ cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
256
272
 
257
- log("eager load end") if logger
273
+ if root_namespace == Object
274
+ cnames.join("::")
275
+ else
276
+ "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
258
277
  end
259
278
  end
279
+ end
260
280
 
261
281
  # Says if the given constant path would be unloaded on reload. This
262
282
  # predicate returns `false` if reloading is disabled.
@@ -280,22 +300,29 @@ module Zeitwerk
280
300
  # @sig () -> void
281
301
  def unregister
282
302
  Registry.unregister_loader(self)
283
- ExplicitNamespace.unregister_loader(self)
303
+ ExplicitNamespace.__unregister_loader(self)
304
+ end
305
+
306
+ # The return value of this predicate is only meaningful if the loader has
307
+ # scanned the file. This is the case in the spots where we use it.
308
+ #
309
+ # @sig (String) -> Boolean
310
+ internal def shadowed_file?(file)
311
+ shadowed_files.member?(file)
284
312
  end
285
313
 
286
314
  # --- Class methods ---------------------------------------------------------------------------
287
315
 
288
316
  class << self
317
+ include RealModName
318
+
289
319
  # @sig #call | #debug | nil
290
320
  attr_accessor :default_logger
291
321
 
292
- # @private
293
- # @sig Mutex
294
- attr_accessor :mutex
295
-
296
322
  # This is a shortcut for
297
323
  #
298
324
  # require "zeitwerk"
325
+ #
299
326
  # loader = Zeitwerk::Loader.new
300
327
  # loader.tag = File.basename(__FILE__, ".rb")
301
328
  # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
@@ -304,17 +331,70 @@ module Zeitwerk
304
331
  # except that this method returns the same object in subsequent calls from
305
332
  # the same file, in the unlikely case the gem wants to be able to reload.
306
333
  #
307
- # @sig () -> Zeitwerk::Loader
308
- def for_gem
334
+ # This method returns a subclass of Zeitwerk::Loader, but the exact type
335
+ # is private, client code can only rely on the interface.
336
+ #
337
+ # @sig (bool) -> Zeitwerk::GemLoader
338
+ def for_gem(warn_on_extra_files: true)
309
339
  called_from = caller_locations(1, 1).first.path
310
- Registry.loader_for_gem(called_from)
340
+ Registry.loader_for_gem(called_from, namespace: Object, warn_on_extra_files: warn_on_extra_files)
311
341
  end
312
342
 
313
- # Broadcasts `eager_load` to all loaders.
343
+ # This is a shortcut for
344
+ #
345
+ # require "zeitwerk"
346
+ #
347
+ # loader = Zeitwerk::Loader.new
348
+ # loader.tag = namespace.name + "-" + File.basename(__FILE__, ".rb")
349
+ # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
350
+ # loader.push_dir(__dir__, namespace: namespace)
351
+ #
352
+ # except that this method returns the same object in subsequent calls from
353
+ # the same file, in the unlikely case the gem wants to be able to reload.
354
+ #
355
+ # This method returns a subclass of Zeitwerk::Loader, but the exact type
356
+ # is private, client code can only rely on the interface.
357
+ #
358
+ # @sig (bool) -> Zeitwerk::GemLoader
359
+ def for_gem_extension(namespace)
360
+ unless namespace.is_a?(Module) # Note that Class < Module.
361
+ raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
362
+ end
363
+
364
+ unless real_mod_name(namespace)
365
+ raise Zeitwerk::Error, "extending anonymous namespaces is unsupported"
366
+ end
367
+
368
+ called_from = caller_locations(1, 1).first.path
369
+ Registry.loader_for_gem(called_from, namespace: namespace, warn_on_extra_files: false)
370
+ end
371
+
372
+ # Broadcasts `eager_load` to all loaders. Those that have not been setup
373
+ # are skipped.
314
374
  #
315
375
  # @sig () -> void
316
376
  def eager_load_all
317
- Registry.loaders.each(&:eager_load)
377
+ Registry.loaders.each do |loader|
378
+ begin
379
+ loader.eager_load
380
+ rescue SetupRequired
381
+ # This is fine, we eager load what can be eager loaded.
382
+ end
383
+ end
384
+ end
385
+
386
+ # Broadcasts `eager_load_namespace` to all loaders. Those that have not
387
+ # been setup are skipped.
388
+ #
389
+ # @sig (Module) -> void
390
+ def eager_load_namespace(mod)
391
+ Registry.loaders.each do |loader|
392
+ begin
393
+ loader.eager_load_namespace(mod)
394
+ rescue SetupRequired
395
+ # This is fine, we eager load what can be eager loaded.
396
+ end
397
+ end
318
398
  end
319
399
 
320
400
  # Returns an array with the absolute paths of the root directories of all
@@ -326,79 +406,58 @@ module Zeitwerk
326
406
  end
327
407
  end
328
408
 
329
- self.mutex = Mutex.new
330
-
331
- private # -------------------------------------------------------------------------------------
332
-
333
409
  # @sig (String, Module) -> void
334
- def set_autoloads_in_dir(dir, parent)
410
+ private def define_autoloads_for_dir(dir, parent)
335
411
  ls(dir) do |basename, abspath|
336
- begin
337
- if ruby?(basename)
338
- basename.delete_suffix!(".rb")
339
- cname = inflector.camelize(basename, abspath).to_sym
340
- autoload_file(parent, cname, abspath)
341
- elsif dir?(abspath)
342
- # In a Rails application, `app/models/concerns` is a subdirectory of
343
- # `app/models`, but both of them are root directories.
344
- #
345
- # To resolve the ambiguity file name -> constant path this introduces,
346
- # the `app/models/concerns` directory is totally ignored as a namespace,
347
- # it counts only as root. The guard checks that.
348
- unless root_dir?(abspath)
349
- cname = inflector.camelize(basename, abspath).to_sym
350
- if collapse?(abspath)
351
- set_autoloads_in_dir(abspath, parent)
352
- else
353
- autoload_subdir(parent, cname, abspath)
354
- end
355
- end
412
+ if ruby?(basename)
413
+ basename.delete_suffix!(".rb")
414
+ autoload_file(parent, cname_for(basename, abspath), abspath)
415
+ else
416
+ if collapse?(abspath)
417
+ define_autoloads_for_dir(abspath, parent)
418
+ else
419
+ autoload_subdir(parent, cname_for(basename, abspath), abspath)
356
420
  end
357
- rescue ::NameError => error
358
- path_type = ruby?(abspath) ? "file" : "directory"
359
-
360
- raise NameError.new(<<~MESSAGE, error.name)
361
- #{error.message} inferred by #{inflector.class} from #{path_type}
362
-
363
- #{abspath}
364
-
365
- Possible ways to address this:
366
-
367
- * Tell Zeitwerk to ignore this particular #{path_type}.
368
- * Tell Zeitwerk to ignore one of its parent directories.
369
- * Rename the #{path_type} to comply with the naming conventions.
370
- * Modify the inflector to handle this case.
371
- MESSAGE
372
421
  end
373
422
  end
374
423
  end
375
424
 
376
425
  # @sig (Module, Symbol, String) -> void
377
- def autoload_subdir(parent, cname, subdir)
426
+ private def autoload_subdir(parent, cname, subdir)
378
427
  if autoload_path = autoload_path_set_by_me_for?(parent, cname)
379
428
  cpath = cpath(parent, cname)
380
- register_explicit_namespace(cpath) if ruby?(autoload_path)
381
- # We do not need to issue another autoload, the existing one is enough
382
- # no matter if it is for a file or a directory. Just remember the
383
- # subdirectory has to be visited if the namespace is used.
384
- lazy_subdirs[cpath] << subdir
429
+ if ruby?(autoload_path)
430
+ # Scanning visited a Ruby file first, and now a directory for the same
431
+ # constant has been found. This means we are dealing with an explicit
432
+ # namespace whose definition was seen first.
433
+ #
434
+ # Registering is idempotent, and we have to keep the autoload pointing
435
+ # to the file. This may run again if more directories are found later
436
+ # on, no big deal.
437
+ register_explicit_namespace(cpath)
438
+ end
439
+ # If the existing autoload points to a file, it has to be preserved, if
440
+ # not, it is fine as it is. In either case, we do not need to override.
441
+ # Just remember the subdirectory conforms this namespace.
442
+ namespace_dirs[cpath] << subdir
385
443
  elsif !cdef?(parent, cname)
386
444
  # First time we find this namespace, set an autoload for it.
387
- lazy_subdirs[cpath(parent, cname)] << subdir
388
- set_autoload(parent, cname, subdir)
445
+ namespace_dirs[cpath(parent, cname)] << subdir
446
+ define_autoload(parent, cname, subdir)
389
447
  else
390
448
  # For whatever reason the constant that corresponds to this namespace has
391
449
  # already been defined, we have to recurse.
392
450
  log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
393
- set_autoloads_in_dir(subdir, cget(parent, cname))
451
+ define_autoloads_for_dir(subdir, cget(parent, cname))
394
452
  end
395
453
  end
396
454
 
397
455
  # @sig (Module, Symbol, String) -> void
398
- def autoload_file(parent, cname, file)
456
+ private def autoload_file(parent, cname, file)
399
457
  if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
400
458
  # First autoload for a Ruby file wins, just ignore subsequent ones.
401
459
  if ruby?(autoload_path)
460
+ shadowed_files << file
402
461
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
403
462
  else
404
463
  promote_namespace_from_implicit_to_explicit(
@@ -409,9 +468,10 @@ module Zeitwerk
409
468
  )
410
469
  end
411
470
  elsif cdef?(parent, cname)
471
+ shadowed_files << file
412
472
  log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
413
473
  else
414
- set_autoload(parent, cname, file)
474
+ define_autoload(parent, cname, file)
415
475
  end
416
476
  end
417
477
 
@@ -419,18 +479,18 @@ module Zeitwerk
419
479
  # the file where we've found the namespace is explicitly defined.
420
480
  #
421
481
  # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
422
- def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
482
+ private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
423
483
  autoloads.delete(dir)
424
484
  Registry.unregister_autoload(dir)
425
485
 
426
486
  log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
427
487
 
428
- set_autoload(parent, cname, file)
488
+ define_autoload(parent, cname, file)
429
489
  register_explicit_namespace(cpath(parent, cname))
430
490
  end
431
491
 
432
492
  # @sig (Module, Symbol, String) -> void
433
- def set_autoload(parent, cname, abspath)
493
+ private def define_autoload(parent, cname, abspath)
434
494
  parent.autoload(cname, abspath)
435
495
 
436
496
  if logger
@@ -451,7 +511,7 @@ module Zeitwerk
451
511
  end
452
512
 
453
513
  # @sig (Module, Symbol) -> String?
454
- def autoload_path_set_by_me_for?(parent, cname)
514
+ private def autoload_path_set_by_me_for?(parent, cname)
455
515
  if autoload_path = strict_autoload_path(parent, cname)
456
516
  autoload_path if autoloads.key?(autoload_path)
457
517
  else
@@ -460,28 +520,28 @@ module Zeitwerk
460
520
  end
461
521
 
462
522
  # @sig (String) -> void
463
- def register_explicit_namespace(cpath)
464
- ExplicitNamespace.register(cpath, self)
523
+ private def register_explicit_namespace(cpath)
524
+ ExplicitNamespace.__register(cpath, self)
465
525
  end
466
526
 
467
527
  # @sig (String) -> void
468
- def raise_if_conflicting_directory(dir)
469
- self.class.mutex.synchronize do
528
+ private def raise_if_conflicting_directory(dir)
529
+ MUTEX.synchronize do
530
+ dir_slash = dir + "/"
531
+
470
532
  Registry.loaders.each do |loader|
471
533
  next if loader == self
472
- next if loader.ignores?(dir)
534
+ next if loader.__ignores?(dir)
473
535
 
474
- dir = dir + "/"
475
- loader.root_dirs.each do |root_dir, _namespace|
536
+ loader.__roots.each_key do |root_dir|
476
537
  next if ignores?(root_dir)
477
538
 
478
- root_dir = root_dir + "/"
479
- if dir.start_with?(root_dir) || root_dir.start_with?(dir)
539
+ root_dir_slash = root_dir + "/"
540
+ if dir_slash.start_with?(root_dir_slash) || root_dir_slash.start_with?(dir_slash)
480
541
  require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
481
542
  raise Error,
482
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
543
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
483
544
  " which is already managed by\n\n#{loader.pretty_inspect}\n"
484
- EOS
485
545
  end
486
546
  end
487
547
  end
@@ -489,23 +549,23 @@ module Zeitwerk
489
549
  end
490
550
 
491
551
  # @sig (String, Object, String) -> void
492
- def run_on_unload_callbacks(cpath, value, abspath)
552
+ private def run_on_unload_callbacks(cpath, value, abspath)
493
553
  # Order matters. If present, run the most specific one.
494
554
  on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
495
555
  on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
496
556
  end
497
557
 
498
558
  # @sig (Module, Symbol) -> void
499
- def unload_autoload(parent, cname)
500
- parent.__send__(:remove_const, cname)
559
+ private def unload_autoload(parent, cname)
560
+ crem(parent, cname)
501
561
  log("autoload for #{cpath(parent, cname)} removed") if logger
502
562
  end
503
563
 
504
564
  # @sig (Module, Symbol) -> void
505
- def unload_cref(parent, cname)
565
+ private def unload_cref(parent, cname)
506
566
  # Let's optimistically remove_const. The way we use it, this is going to
507
567
  # succeed always if all is good.
508
- parent.__send__(:remove_const, cname)
568
+ crem(parent, cname)
509
569
  rescue ::NameError
510
570
  # There are a few edge scenarios in which this may happen. If the constant
511
571
  # is gone, that is OK, anyway.
@@ -10,12 +10,11 @@ module Zeitwerk
10
10
  # @sig Array[Zeitwerk::Loader]
11
11
  attr_reader :loaders
12
12
 
13
- # Registers loaders created with `for_gem` to make the method idempotent
14
- # in case of reload.
13
+ # Registers gem loaders to let `for_gem` be idempotent in case of reload.
15
14
  #
16
15
  # @private
17
16
  # @sig Hash[String, Zeitwerk::Loader]
18
- attr_reader :loaders_managing_gems
17
+ attr_reader :gem_loaders_by_root_file
19
18
 
20
19
  # Maps absolute paths to the loaders responsible for them.
21
20
  #
@@ -77,7 +76,7 @@ module Zeitwerk
77
76
  # @sig (Zeitwerk::Loader) -> void
78
77
  def unregister_loader(loader)
79
78
  loaders.delete(loader)
80
- loaders_managing_gems.delete_if { |_, l| l == loader }
79
+ gem_loaders_by_root_file.delete_if { |_, l| l == loader }
81
80
  autoloads.delete_if { |_, l| l == loader }
82
81
  inceptions.delete_if { |_, (_, l)| l == loader }
83
82
  end
@@ -87,14 +86,8 @@ module Zeitwerk
87
86
  #
88
87
  # @private
89
88
  # @sig (String) -> Zeitwerk::Loader
90
- def loader_for_gem(root_file)
91
- loaders_managing_gems[root_file] ||= begin
92
- Loader.new.tap do |loader|
93
- loader.tag = File.basename(root_file, ".rb")
94
- loader.inflector = GemInflector.new(root_file)
95
- loader.push_dir(File.dirname(root_file))
96
- end
97
- end
89
+ def loader_for_gem(root_file, namespace:, warn_on_extra_files:)
90
+ gem_loaders_by_root_file[root_file] ||= GemLoader.__new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
98
91
  end
99
92
 
100
93
  # @private
@@ -137,9 +130,9 @@ module Zeitwerk
137
130
  end
138
131
  end
139
132
 
140
- @loaders = []
141
- @loaders_managing_gems = {}
142
- @autoloads = {}
143
- @inceptions = {}
133
+ @loaders = []
134
+ @gem_loaders_by_root_file = {}
135
+ @autoloads = {}
136
+ @inceptions = {}
144
137
  end
145
138
  end