zeitwerk 2.5.4 → 2.6.18

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