zeitwerk 2.6.13 → 2.7.2
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.
- checksums.yaml +4 -4
- data/README.md +58 -45
- data/lib/zeitwerk/core_ext/module.rb +20 -0
- data/lib/zeitwerk/cref/map.rb +124 -0
- data/lib/zeitwerk/cref.rb +71 -0
- data/lib/zeitwerk/gem_loader.rb +1 -2
- data/lib/zeitwerk/internal.rb +1 -0
- data/lib/zeitwerk/loader/callbacks.rb +23 -25
- data/lib/zeitwerk/loader/config.rb +8 -8
- data/lib/zeitwerk/loader/eager_load.rb +13 -15
- data/lib/zeitwerk/loader/helpers.rb +23 -64
- data/lib/zeitwerk/loader.rb +196 -131
- data/lib/zeitwerk/null_inflector.rb +1 -0
- data/lib/zeitwerk/real_mod_name.rb +2 -8
- data/lib/zeitwerk/registry/explicit_namespaces.rb +64 -0
- data/lib/zeitwerk/registry/inceptions.rb +31 -0
- data/lib/zeitwerk/registry.rb +3 -56
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +4 -2
- metadata +10 -9
- data/lib/zeitwerk/explicit_namespace.rb +0 -93
- /data/lib/zeitwerk/{kernel.rb → core_ext/kernel.rb} +0 -0
@@ -30,27 +30,43 @@ module Zeitwerk::Loader::Helpers
|
|
30
30
|
|
31
31
|
if dir?(abspath)
|
32
32
|
next if roots.key?(abspath)
|
33
|
-
|
33
|
+
|
34
|
+
if !has_at_least_one_ruby_file?(abspath)
|
35
|
+
log("directory #{abspath} is ignored because it has no Ruby files") if logger
|
36
|
+
next
|
37
|
+
end
|
38
|
+
|
39
|
+
ftype = :directory
|
34
40
|
else
|
35
41
|
next unless ruby?(abspath)
|
42
|
+
ftype = :file
|
36
43
|
end
|
37
44
|
|
38
45
|
# We freeze abspath because that saves allocations when passed later to
|
39
46
|
# File methods. See #125.
|
40
|
-
yield basename, abspath.freeze
|
47
|
+
yield basename, abspath.freeze, ftype
|
41
48
|
end
|
42
49
|
end
|
43
50
|
|
51
|
+
# Looks for a Ruby file using breadth-first search. This type of search is
|
52
|
+
# important to list as less directories as possible and return fast in the
|
53
|
+
# common case in which there are Ruby files.
|
54
|
+
#
|
44
55
|
# @sig (String) -> bool
|
45
56
|
private def has_at_least_one_ruby_file?(dir)
|
46
57
|
to_visit = [dir]
|
47
58
|
|
48
|
-
while dir = to_visit.shift
|
49
|
-
|
59
|
+
while (dir = to_visit.shift)
|
60
|
+
Dir.each_child(dir) do |basename|
|
61
|
+
next if hidden?(basename)
|
62
|
+
|
63
|
+
abspath = File.join(dir, basename)
|
64
|
+
next if ignored_path?(abspath)
|
65
|
+
|
50
66
|
if dir?(abspath)
|
51
|
-
to_visit << abspath
|
67
|
+
to_visit << abspath unless roots.key?(abspath)
|
52
68
|
else
|
53
|
-
return true
|
69
|
+
return true if ruby?(abspath)
|
54
70
|
end
|
55
71
|
end
|
56
72
|
end
|
@@ -82,64 +98,7 @@ module Zeitwerk::Loader::Helpers
|
|
82
98
|
end
|
83
99
|
end
|
84
100
|
|
85
|
-
# ---
|
86
|
-
|
87
|
-
# The autoload? predicate takes into account the ancestor chain of the
|
88
|
-
# receiver, like const_defined? and other methods in the constants API do.
|
89
|
-
#
|
90
|
-
# For example, given
|
91
|
-
#
|
92
|
-
# class A
|
93
|
-
# autoload :X, "x.rb"
|
94
|
-
# end
|
95
|
-
#
|
96
|
-
# class B < A
|
97
|
-
# end
|
98
|
-
#
|
99
|
-
# B.autoload?(:X) returns "x.rb".
|
100
|
-
#
|
101
|
-
# We need a way to strictly check in parent ignoring ancestors.
|
102
|
-
#
|
103
|
-
# @sig (Module, Symbol) -> String?
|
104
|
-
if method(:autoload?).arity == 1
|
105
|
-
private def strict_autoload_path(parent, cname)
|
106
|
-
parent.autoload?(cname) if cdef?(parent, cname)
|
107
|
-
end
|
108
|
-
else
|
109
|
-
private def strict_autoload_path(parent, cname)
|
110
|
-
parent.autoload?(cname, false)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
# @sig (Module, Symbol) -> String
|
115
|
-
if Symbol.method_defined?(:name)
|
116
|
-
# Symbol#name was introduced in Ruby 3.0. It returns always the same
|
117
|
-
# frozen object, so we may save a few string allocations.
|
118
|
-
private def cpath(parent, cname)
|
119
|
-
Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
|
120
|
-
end
|
121
|
-
else
|
122
|
-
private def cpath(parent, cname)
|
123
|
-
Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
# @sig (Module, Symbol) -> bool
|
128
|
-
private def cdef?(parent, cname)
|
129
|
-
parent.const_defined?(cname, false)
|
130
|
-
end
|
131
|
-
|
132
|
-
# @raise [NameError]
|
133
|
-
# @sig (Module, Symbol) -> Object
|
134
|
-
private def cget(parent, cname)
|
135
|
-
parent.const_get(cname, false)
|
136
|
-
end
|
137
|
-
|
138
|
-
# @raise [NameError]
|
139
|
-
# @sig (Module, Symbol) -> Object
|
140
|
-
private def crem(parent, cname)
|
141
|
-
parent.__send__(:remove_const, cname)
|
142
|
-
end
|
101
|
+
# --- Inflection --------------------------------------------------------------------------------
|
143
102
|
|
144
103
|
CNAME_VALIDATOR = Module.new
|
145
104
|
private_constant :CNAME_VALIDATOR
|
data/lib/zeitwerk/loader.rb
CHANGED
@@ -22,17 +22,40 @@ module Zeitwerk
|
|
22
22
|
private_constant :MUTEX
|
23
23
|
|
24
24
|
# Maps absolute paths for which an autoload has been set ---and not
|
25
|
-
# executed--- to their corresponding
|
26
|
-
# name.
|
25
|
+
# executed--- to their corresponding Zeitwerk::Cref object.
|
27
26
|
#
|
28
|
-
# "/Users/fxn/blog/app/models/user.rb" =>
|
29
|
-
# "/Users/fxn/blog/app/models/hotel/pricing.rb" =>
|
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, ...>,
|
30
29
|
# ...
|
31
30
|
#
|
32
|
-
# @sig Hash[String,
|
31
|
+
# @sig Hash[String, Zeitwerk::Cref]
|
33
32
|
attr_reader :autoloads
|
34
33
|
internal :autoloads
|
35
34
|
|
35
|
+
# When the path passed to Module#autoload is in the stack of features being
|
36
|
+
# loaded at the moment, Ruby passes. For example, Module#autoload? returns
|
37
|
+
# `nil` even if the autoload has not been attempted. See
|
38
|
+
#
|
39
|
+
# https://bugs.ruby-lang.org/issues/21035
|
40
|
+
#
|
41
|
+
# We call these "inceptions".
|
42
|
+
#
|
43
|
+
# A common case is the entry point of gems managed by Zeitwerk. Their main
|
44
|
+
# file is normally required and, while doing so, the loader sets an autoload
|
45
|
+
# on the gem namespace. That autoload hits this edge case.
|
46
|
+
#
|
47
|
+
# There is some logic that neeeds to know if an autoload for a given
|
48
|
+
# constant already exists. We check Module#autoload? first, and fallback to
|
49
|
+
# the inceptions just in case.
|
50
|
+
#
|
51
|
+
# This map keeps track of pairs (cref, autoload_path) found by the loader.
|
52
|
+
# The module Zeitwerk::Registry::Inceptions, on the other hand, acts as a
|
53
|
+
# global registry for them.
|
54
|
+
#
|
55
|
+
# @sig Zeitwerk::Cref::Map[String]
|
56
|
+
attr_reader :inceptions
|
57
|
+
internal :inceptions
|
58
|
+
|
36
59
|
# We keep track of autoloaded directories to remove them from the registry
|
37
60
|
# at the end of eager loading.
|
38
61
|
#
|
@@ -43,46 +66,31 @@ module Zeitwerk
|
|
43
66
|
attr_reader :autoloaded_dirs
|
44
67
|
internal :autoloaded_dirs
|
45
68
|
|
46
|
-
#
|
47
|
-
#
|
48
|
-
# "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
|
49
|
-
#
|
50
|
-
# The cpath as key helps implementing unloadable_cpath? The file name is
|
51
|
-
# stored in order to be able to delete it from $LOADED_FEATURES, and the
|
52
|
-
# pair [Module, Symbol] is used to remove_const the constant from the class
|
53
|
-
# or module object.
|
69
|
+
# If reloading is enabled, this collection maps autoload paths to their
|
70
|
+
# autoloaded crefs.
|
54
71
|
#
|
55
|
-
#
|
56
|
-
#
|
72
|
+
# On unload, the autoload paths are passed to callbacks, files deleted from
|
73
|
+
# $LOADED_FEATURES, and the crefs are deleted.
|
57
74
|
#
|
58
|
-
# @sig Hash[String,
|
75
|
+
# @sig Hash[String, Zeitwerk::Cref]
|
59
76
|
attr_reader :to_unload
|
60
77
|
internal :to_unload
|
61
78
|
|
62
|
-
# Maps namespace
|
79
|
+
# Maps namespace crefs to the directories that conform the namespace.
|
63
80
|
#
|
64
|
-
#
|
81
|
+
# When these crefs get defined we know their children are spread over those
|
82
|
+
# directories. We'll visit them to set up the corresponding autoloads.
|
65
83
|
#
|
66
|
-
#
|
67
|
-
# "/Users/fxn/blog/app/controllers/admin",
|
68
|
-
# "/Users/fxn/blog/app/models/admin",
|
69
|
-
# ...
|
70
|
-
# ]
|
71
|
-
#
|
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.
|
75
|
-
#
|
76
|
-
# @sig Hash[String, Array[String]]
|
84
|
+
# @sig Zeitwerk::Cref::Map[String]
|
77
85
|
attr_reader :namespace_dirs
|
78
86
|
internal :namespace_dirs
|
79
87
|
|
80
88
|
# A shadowed file is a file managed by this loader that is ignored when
|
81
89
|
# setting autoloads because its matching constant is already taken.
|
82
90
|
#
|
83
|
-
# This private set is populated as we descend. For example, if the
|
84
|
-
# has only scanned the top-level, `shadowed_files` does not have
|
85
|
-
# files that may exist deep in the project tree
|
91
|
+
# This private set is populated lazily, as we descend. For example, if the
|
92
|
+
# loader has only scanned the top-level, `shadowed_files` does not have the
|
93
|
+
# shadowed files that may exist deep in the project tree.
|
86
94
|
#
|
87
95
|
# @sig Set[String]
|
88
96
|
attr_reader :shadowed_files
|
@@ -100,9 +108,10 @@ module Zeitwerk
|
|
100
108
|
super
|
101
109
|
|
102
110
|
@autoloads = {}
|
111
|
+
@inceptions = Zeitwerk::Cref::Map.new
|
103
112
|
@autoloaded_dirs = []
|
104
113
|
@to_unload = {}
|
105
|
-
@namespace_dirs =
|
114
|
+
@namespace_dirs = Zeitwerk::Cref::Map.new
|
106
115
|
@shadowed_files = Set.new
|
107
116
|
@setup = false
|
108
117
|
@eager_loaded = false
|
@@ -154,32 +163,32 @@ module Zeitwerk
|
|
154
163
|
# is enough.
|
155
164
|
unloaded_files = Set.new
|
156
165
|
|
157
|
-
autoloads.each do |abspath,
|
158
|
-
if
|
159
|
-
unload_autoload(
|
166
|
+
autoloads.each do |abspath, cref|
|
167
|
+
if cref.autoload?
|
168
|
+
unload_autoload(cref)
|
160
169
|
else
|
161
170
|
# Could happen if loaded with require_relative. That is unsupported,
|
162
171
|
# and the constant path would escape unloadable_cpath? This is just
|
163
172
|
# defensive code to clean things up as much as we are able to.
|
164
|
-
unload_cref(
|
173
|
+
unload_cref(cref)
|
165
174
|
unloaded_files.add(abspath) if ruby?(abspath)
|
166
175
|
end
|
167
176
|
end
|
168
177
|
|
169
|
-
to_unload.each do |
|
178
|
+
to_unload.each do |abspath, cref|
|
170
179
|
unless on_unload_callbacks.empty?
|
171
180
|
begin
|
172
|
-
value =
|
181
|
+
value = cref.get
|
173
182
|
rescue ::NameError
|
174
183
|
# Perhaps the user deleted the constant by hand, or perhaps an
|
175
184
|
# autoload failed to define the expected constant but the user
|
176
185
|
# rescued the exception.
|
177
186
|
else
|
178
|
-
run_on_unload_callbacks(
|
187
|
+
run_on_unload_callbacks(cref, value, abspath)
|
179
188
|
end
|
180
189
|
end
|
181
190
|
|
182
|
-
unload_cref(
|
191
|
+
unload_cref(cref)
|
183
192
|
unloaded_files.add(abspath) if ruby?(abspath)
|
184
193
|
end
|
185
194
|
|
@@ -204,8 +213,10 @@ module Zeitwerk
|
|
204
213
|
namespace_dirs.clear
|
205
214
|
shadowed_files.clear
|
206
215
|
|
216
|
+
unregister_inceptions
|
217
|
+
unregister_explicit_namespaces
|
218
|
+
|
207
219
|
Registry.on_unload(self)
|
208
|
-
ExplicitNamespace.__unregister_loader(self)
|
209
220
|
|
210
221
|
@setup = false
|
211
222
|
@eager_loaded = false
|
@@ -230,68 +241,107 @@ module Zeitwerk
|
|
230
241
|
setup
|
231
242
|
end
|
232
243
|
|
233
|
-
|
234
|
-
|
235
|
-
|
244
|
+
# Returns a hash that maps the absolute paths of the managed files and
|
245
|
+
# directories to their respective expected constant paths.
|
246
|
+
#
|
247
|
+
# @sig () -> Hash[String, String]
|
248
|
+
def all_expected_cpaths
|
249
|
+
result = {}
|
236
250
|
|
237
|
-
|
251
|
+
actual_roots.each do |root_dir, root_namespace|
|
252
|
+
queue = [[root_dir, real_mod_name(root_namespace)]]
|
238
253
|
|
239
|
-
|
240
|
-
|
254
|
+
while (dir, cpath = queue.shift)
|
255
|
+
result[dir] = cpath
|
241
256
|
|
242
|
-
|
257
|
+
prefix = cpath == "Object" ? "" : cpath + "::"
|
243
258
|
|
244
|
-
|
245
|
-
|
246
|
-
|
259
|
+
ls(dir) do |basename, abspath, ftype|
|
260
|
+
if ftype == :file
|
261
|
+
basename.delete_suffix!(".rb")
|
262
|
+
result[abspath] = prefix + inflector.camelize(basename, abspath)
|
263
|
+
else
|
264
|
+
if collapse?(abspath)
|
265
|
+
queue << [abspath, cpath]
|
266
|
+
else
|
267
|
+
queue << [abspath, prefix + inflector.camelize(basename, abspath)]
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
247
273
|
|
248
|
-
|
249
|
-
walk_up_from = File.dirname(abspath)
|
250
|
-
else
|
251
|
-
walk_up_from = abspath
|
274
|
+
result
|
252
275
|
end
|
253
276
|
|
254
|
-
|
277
|
+
# @sig (String | Pathname) -> String?
|
278
|
+
def cpath_expected_at(path)
|
279
|
+
abspath = File.expand_path(path)
|
255
280
|
|
256
|
-
|
257
|
-
break if root_namespace = roots[dir]
|
258
|
-
return if ignored_path?(dir)
|
281
|
+
raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
259
282
|
|
260
|
-
|
261
|
-
return if
|
283
|
+
return unless dir?(abspath) || ruby?(abspath)
|
284
|
+
return if ignored_path?(abspath)
|
262
285
|
|
263
|
-
paths
|
264
|
-
end
|
286
|
+
paths = []
|
265
287
|
|
266
|
-
|
288
|
+
if ruby?(abspath)
|
289
|
+
basename = File.basename(abspath, ".rb")
|
290
|
+
return if hidden?(basename)
|
267
291
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
292
|
+
paths << [basename, abspath]
|
293
|
+
walk_up_from = File.dirname(abspath)
|
294
|
+
else
|
295
|
+
walk_up_from = abspath
|
296
|
+
end
|
272
297
|
|
273
|
-
|
274
|
-
|
298
|
+
root_namespace = nil
|
299
|
+
|
300
|
+
walk_up(walk_up_from) do |dir|
|
301
|
+
break if root_namespace = roots[dir]
|
302
|
+
return if ignored_path?(dir)
|
303
|
+
|
304
|
+
basename = File.basename(dir)
|
305
|
+
return if hidden?(basename)
|
306
|
+
|
307
|
+
paths << [basename, abspath] unless collapse?(dir)
|
308
|
+
end
|
309
|
+
|
310
|
+
return unless root_namespace
|
311
|
+
|
312
|
+
if paths.empty?
|
313
|
+
real_mod_name(root_namespace)
|
275
314
|
else
|
276
|
-
|
315
|
+
cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
|
316
|
+
|
317
|
+
if root_namespace == Object
|
318
|
+
cnames.join("::")
|
319
|
+
else
|
320
|
+
"#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
|
321
|
+
end
|
277
322
|
end
|
278
323
|
end
|
279
|
-
end
|
280
324
|
|
281
325
|
# Says if the given constant path would be unloaded on reload. This
|
282
326
|
# predicate returns `false` if reloading is disabled.
|
283
327
|
#
|
328
|
+
# This is an undocumented method that I wrote to help transition from the
|
329
|
+
# classic autoloader in Rails. Its usage was removed from Rails in 7.0.
|
330
|
+
#
|
284
331
|
# @sig (String) -> bool
|
285
332
|
def unloadable_cpath?(cpath)
|
286
|
-
|
333
|
+
unloadable_cpaths.include?(cpath)
|
287
334
|
end
|
288
335
|
|
289
336
|
# Returns an array with the constant paths that would be unloaded on reload.
|
290
337
|
# This predicate returns an empty array if reloading is disabled.
|
291
338
|
#
|
339
|
+
# This is an undocumented method that I wrote to help transition from the
|
340
|
+
# classic autoloader in Rails. Its usage was removed from Rails in 7.0.
|
341
|
+
#
|
292
342
|
# @sig () -> Array[String]
|
293
343
|
def unloadable_cpaths
|
294
|
-
to_unload.
|
344
|
+
to_unload.values.map(&:path)
|
295
345
|
end
|
296
346
|
|
297
347
|
# This is a dangerous method.
|
@@ -299,8 +349,9 @@ module Zeitwerk
|
|
299
349
|
# @experimental
|
300
350
|
# @sig () -> void
|
301
351
|
def unregister
|
352
|
+
unregister_inceptions
|
353
|
+
unregister_explicit_namespaces
|
302
354
|
Registry.unregister_loader(self)
|
303
|
-
ExplicitNamespace.__unregister_loader(self)
|
304
355
|
end
|
305
356
|
|
306
357
|
# The return value of this predicate is only meaningful if the loader has
|
@@ -408,24 +459,25 @@ module Zeitwerk
|
|
408
459
|
|
409
460
|
# @sig (String, Module) -> void
|
410
461
|
private def define_autoloads_for_dir(dir, parent)
|
411
|
-
ls(dir) do |basename, abspath|
|
412
|
-
if
|
462
|
+
ls(dir) do |basename, abspath, ftype|
|
463
|
+
if ftype == :file
|
413
464
|
basename.delete_suffix!(".rb")
|
414
|
-
|
465
|
+
cref = Cref.new(parent, cname_for(basename, abspath))
|
466
|
+
autoload_file(cref, abspath)
|
415
467
|
else
|
416
468
|
if collapse?(abspath)
|
417
469
|
define_autoloads_for_dir(abspath, parent)
|
418
470
|
else
|
419
|
-
|
471
|
+
cref = Cref.new(parent, cname_for(basename, abspath))
|
472
|
+
autoload_subdir(cref, abspath)
|
420
473
|
end
|
421
474
|
end
|
422
475
|
end
|
423
476
|
end
|
424
477
|
|
425
478
|
# @sig (Module, Symbol, String) -> void
|
426
|
-
private def autoload_subdir(
|
427
|
-
if autoload_path = autoload_path_set_by_me_for?(
|
428
|
-
cpath = cpath(parent, cname)
|
479
|
+
private def autoload_subdir(cref, subdir)
|
480
|
+
if autoload_path = autoload_path_set_by_me_for?(cref)
|
429
481
|
if ruby?(autoload_path)
|
430
482
|
# Scanning visited a Ruby file first, and now a directory for the same
|
431
483
|
# constant has been found. This means we are dealing with an explicit
|
@@ -434,94 +486,107 @@ module Zeitwerk
|
|
434
486
|
# Registering is idempotent, and we have to keep the autoload pointing
|
435
487
|
# to the file. This may run again if more directories are found later
|
436
488
|
# on, no big deal.
|
437
|
-
register_explicit_namespace(
|
489
|
+
register_explicit_namespace(cref)
|
438
490
|
end
|
439
491
|
# If the existing autoload points to a file, it has to be preserved, if
|
440
492
|
# not, it is fine as it is. In either case, we do not need to override.
|
441
493
|
# Just remember the subdirectory conforms this namespace.
|
442
|
-
namespace_dirs[
|
443
|
-
elsif !
|
494
|
+
namespace_dirs.get_or_set(cref) { [] } << subdir
|
495
|
+
elsif !cref.defined?
|
444
496
|
# First time we find this namespace, set an autoload for it.
|
445
|
-
namespace_dirs
|
446
|
-
define_autoload(
|
497
|
+
namespace_dirs.get_or_set(cref) { [] } << subdir
|
498
|
+
define_autoload(cref, subdir)
|
447
499
|
else
|
448
500
|
# For whatever reason the constant that corresponds to this namespace has
|
449
501
|
# already been defined, we have to recurse.
|
450
|
-
log("the namespace #{
|
451
|
-
define_autoloads_for_dir(subdir,
|
502
|
+
log("the namespace #{cref} already exists, descending into #{subdir}") if logger
|
503
|
+
define_autoloads_for_dir(subdir, cref.get)
|
452
504
|
end
|
453
505
|
end
|
454
506
|
|
455
507
|
# @sig (Module, Symbol, String) -> void
|
456
|
-
private def autoload_file(
|
457
|
-
if autoload_path =
|
508
|
+
private def autoload_file(cref, file)
|
509
|
+
if autoload_path = cref.autoload? || Registry::Inceptions.registered?(cref)
|
458
510
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
459
511
|
if ruby?(autoload_path)
|
460
512
|
shadowed_files << file
|
461
513
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
462
514
|
else
|
463
|
-
promote_namespace_from_implicit_to_explicit(
|
464
|
-
dir: autoload_path,
|
465
|
-
file: file,
|
466
|
-
parent: parent,
|
467
|
-
cname: cname
|
468
|
-
)
|
515
|
+
promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
|
469
516
|
end
|
470
|
-
elsif
|
517
|
+
elsif cref.defined?
|
471
518
|
shadowed_files << file
|
472
|
-
log("file #{file} is ignored because #{
|
519
|
+
log("file #{file} is ignored because #{cref} is already defined") if logger
|
473
520
|
else
|
474
|
-
define_autoload(
|
521
|
+
define_autoload(cref, file)
|
475
522
|
end
|
476
523
|
end
|
477
524
|
|
478
525
|
# `dir` is the directory that would have autovivified a namespace. `file` is
|
479
526
|
# the file where we've found the namespace is explicitly defined.
|
480
527
|
#
|
481
|
-
# @sig (dir: String, file: String,
|
482
|
-
private def promote_namespace_from_implicit_to_explicit(dir:, file:,
|
528
|
+
# @sig (dir: String, file: String, cref: Zeitwerk::Cref) -> void
|
529
|
+
private def promote_namespace_from_implicit_to_explicit(dir:, file:, cref:)
|
483
530
|
autoloads.delete(dir)
|
484
531
|
Registry.unregister_autoload(dir)
|
485
532
|
|
486
|
-
log("earlier autoload for #{
|
533
|
+
log("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
487
534
|
|
488
|
-
|
489
|
-
|
535
|
+
# Order matters: When Module#const_added is triggered by the autoload, we
|
536
|
+
# don't want the namespace to be registered yet.
|
537
|
+
define_autoload(cref, file)
|
538
|
+
register_explicit_namespace(cref)
|
490
539
|
end
|
491
540
|
|
492
541
|
# @sig (Module, Symbol, String) -> void
|
493
|
-
private def define_autoload(
|
494
|
-
|
542
|
+
private def define_autoload(cref, abspath)
|
543
|
+
cref.autoload(abspath)
|
495
544
|
|
496
545
|
if logger
|
497
546
|
if ruby?(abspath)
|
498
|
-
log("autoload set for #{
|
547
|
+
log("autoload set for #{cref}, to be loaded from #{abspath}")
|
499
548
|
else
|
500
|
-
log("autoload set for #{
|
549
|
+
log("autoload set for #{cref}, to be autovivified from #{abspath}")
|
501
550
|
end
|
502
551
|
end
|
503
552
|
|
504
|
-
autoloads[abspath] =
|
553
|
+
autoloads[abspath] = cref
|
505
554
|
Registry.register_autoload(self, abspath)
|
506
555
|
|
507
|
-
|
508
|
-
unless parent.autoload?(cname)
|
509
|
-
Registry.register_inception(cpath(parent, cname), abspath, self)
|
510
|
-
end
|
556
|
+
register_inception(cref, abspath) unless cref.autoload?
|
511
557
|
end
|
512
558
|
|
513
559
|
# @sig (Module, Symbol) -> String?
|
514
|
-
private def autoload_path_set_by_me_for?(
|
515
|
-
if autoload_path =
|
560
|
+
private def autoload_path_set_by_me_for?(cref)
|
561
|
+
if autoload_path = cref.autoload?
|
516
562
|
autoload_path if autoloads.key?(autoload_path)
|
517
563
|
else
|
518
|
-
|
564
|
+
inceptions[cref]
|
519
565
|
end
|
520
566
|
end
|
521
567
|
|
522
|
-
# @sig (
|
523
|
-
private def register_explicit_namespace(
|
524
|
-
|
568
|
+
# @sig (Zeitwerk::Cref) -> void
|
569
|
+
private def register_explicit_namespace(cref)
|
570
|
+
Registry::ExplicitNamespaces.__register(cref, self)
|
571
|
+
end
|
572
|
+
|
573
|
+
# @sig () -> void
|
574
|
+
private def unregister_explicit_namespaces
|
575
|
+
Registry::ExplicitNamespaces.__unregister_loader(self)
|
576
|
+
end
|
577
|
+
|
578
|
+
# @sig (Zeitwerk::Cref, String) -> void
|
579
|
+
private def register_inception(cref, abspath)
|
580
|
+
inceptions[cref] = abspath
|
581
|
+
Registry::Inceptions.register(cref, abspath)
|
582
|
+
end
|
583
|
+
|
584
|
+
# @sig () -> void
|
585
|
+
private def unregister_inceptions
|
586
|
+
inceptions.each_key do |cref|
|
587
|
+
Registry::Inceptions.unregister(cref)
|
588
|
+
end
|
589
|
+
inceptions.clear
|
525
590
|
end
|
526
591
|
|
527
592
|
# @sig (String) -> void
|
@@ -548,29 +613,29 @@ module Zeitwerk
|
|
548
613
|
end
|
549
614
|
end
|
550
615
|
|
551
|
-
# @sig (String,
|
552
|
-
private def run_on_unload_callbacks(
|
616
|
+
# @sig (String, top, String) -> void
|
617
|
+
private def run_on_unload_callbacks(cref, value, abspath)
|
553
618
|
# Order matters. If present, run the most specific one.
|
554
|
-
on_unload_callbacks[
|
555
|
-
on_unload_callbacks[:ANY]&.each { |c| c.call(
|
619
|
+
on_unload_callbacks[cref.path]&.each { |c| c.call(value, abspath) }
|
620
|
+
on_unload_callbacks[:ANY]&.each { |c| c.call(cref.path, value, abspath) }
|
556
621
|
end
|
557
622
|
|
558
623
|
# @sig (Module, Symbol) -> void
|
559
|
-
private def unload_autoload(
|
560
|
-
|
561
|
-
log("autoload for #{
|
624
|
+
private def unload_autoload(cref)
|
625
|
+
cref.remove
|
626
|
+
log("autoload for #{cref} removed") if logger
|
562
627
|
end
|
563
628
|
|
564
629
|
# @sig (Module, Symbol) -> void
|
565
|
-
private def unload_cref(
|
630
|
+
private def unload_cref(cref)
|
566
631
|
# Let's optimistically remove_const. The way we use it, this is going to
|
567
632
|
# succeed always if all is good.
|
568
|
-
|
633
|
+
cref.remove
|
569
634
|
rescue ::NameError
|
570
635
|
# There are a few edge scenarios in which this may happen. If the constant
|
571
636
|
# is gone, that is OK, anyway.
|
572
637
|
else
|
573
|
-
log("#{
|
638
|
+
log("#{cref} unloaded") if logger
|
574
639
|
end
|
575
640
|
end
|
576
641
|
end
|
@@ -10,13 +10,7 @@ module Zeitwerk::RealModName
|
|
10
10
|
# The name method can be overridden, hence the indirection in this method.
|
11
11
|
#
|
12
12
|
# @sig (Module) -> String?
|
13
|
-
|
14
|
-
|
15
|
-
UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
|
16
|
-
end
|
17
|
-
else
|
18
|
-
def real_mod_name(mod)
|
19
|
-
UNBOUND_METHOD_MODULE_NAME.bind(mod).call
|
20
|
-
end
|
13
|
+
def real_mod_name(mod)
|
14
|
+
UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
|
21
15
|
end
|
22
16
|
end
|