zeitwerk 2.6.8 → 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.
- checksums.yaml +4 -4
- data/README.md +170 -24
- data/lib/zeitwerk/cref.rb +99 -0
- data/lib/zeitwerk/gem_loader.rb +1 -2
- data/lib/zeitwerk/kernel.rb +3 -7
- data/lib/zeitwerk/loader/callbacks.rb +29 -22
- data/lib/zeitwerk/loader/eager_load.rb +21 -17
- data/lib/zeitwerk/loader/helpers.rb +61 -56
- data/lib/zeitwerk/loader.rb +153 -93
- data/lib/zeitwerk/null_inflector.rb +5 -0
- data/lib/zeitwerk/registry.rb +5 -2
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +2 -0
- metadata +5 -3
@@ -45,8 +45,10 @@ module Zeitwerk::Loader::EagerLoad
|
|
45
45
|
|
46
46
|
break if root_namespace = roots[dir]
|
47
47
|
|
48
|
+
basename = File.basename(dir)
|
49
|
+
return if hidden?(basename)
|
50
|
+
|
48
51
|
unless collapse?(dir)
|
49
|
-
basename = File.basename(dir)
|
50
52
|
cnames << inflector.camelize(basename, dir).to_sym
|
51
53
|
end
|
52
54
|
end
|
@@ -59,8 +61,8 @@ module Zeitwerk::Loader::EagerLoad
|
|
59
61
|
cnames.reverse_each do |cname|
|
60
62
|
# Can happen if there are no Ruby files. This is not an error condition,
|
61
63
|
# the directory is actually managed. Could have Ruby files later.
|
62
|
-
return unless
|
63
|
-
namespace =
|
64
|
+
return unless namespace.const_defined?(cname, false)
|
65
|
+
namespace = namespace.const_get(cname, false)
|
64
66
|
end
|
65
67
|
|
66
68
|
# A shortcircuiting test depends on the invocation of this method. Please
|
@@ -119,6 +121,8 @@ module Zeitwerk::Loader::EagerLoad
|
|
119
121
|
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
|
120
122
|
|
121
123
|
basename = File.basename(abspath, ".rb")
|
124
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
|
125
|
+
|
122
126
|
base_cname = inflector.camelize(basename, abspath).to_sym
|
123
127
|
|
124
128
|
root_namespace = nil
|
@@ -129,8 +133,10 @@ module Zeitwerk::Loader::EagerLoad
|
|
129
133
|
|
130
134
|
break if root_namespace = roots[dir]
|
131
135
|
|
136
|
+
basename = File.basename(dir)
|
137
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
|
138
|
+
|
132
139
|
unless collapse?(dir)
|
133
|
-
basename = File.basename(dir)
|
134
140
|
cnames << inflector.camelize(basename, dir).to_sym
|
135
141
|
end
|
136
142
|
end
|
@@ -139,12 +145,12 @@ module Zeitwerk::Loader::EagerLoad
|
|
139
145
|
|
140
146
|
namespace = root_namespace
|
141
147
|
cnames.reverse_each do |cname|
|
142
|
-
namespace =
|
148
|
+
namespace = namespace.const_get(cname, false)
|
143
149
|
end
|
144
150
|
|
145
151
|
raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
|
146
152
|
|
147
|
-
|
153
|
+
namespace.const_get(base_cname, false)
|
148
154
|
end
|
149
155
|
|
150
156
|
# The caller is responsible for making sure `namespace` is the namespace that
|
@@ -158,22 +164,20 @@ module Zeitwerk::Loader::EagerLoad
|
|
158
164
|
log("eager load directory #{dir} start") if logger
|
159
165
|
|
160
166
|
queue = [[dir, namespace]]
|
161
|
-
while
|
162
|
-
|
163
|
-
|
164
|
-
ls(dir) do |basename, abspath|
|
167
|
+
while (current_dir, namespace = queue.shift)
|
168
|
+
ls(current_dir) do |basename, abspath, ftype|
|
165
169
|
next if honour_exclusions && eager_load_exclusions.member?(abspath)
|
166
170
|
|
167
|
-
if
|
168
|
-
if (cref = autoloads[abspath])
|
169
|
-
|
171
|
+
if ftype == :file
|
172
|
+
if (cref = autoloads[abspath])
|
173
|
+
cref.get
|
170
174
|
end
|
171
175
|
else
|
172
176
|
if collapse?(abspath)
|
173
177
|
queue << [abspath, namespace]
|
174
178
|
else
|
175
179
|
cname = inflector.camelize(basename, abspath).to_sym
|
176
|
-
queue << [abspath,
|
180
|
+
queue << [abspath, namespace.const_get(cname, false)]
|
177
181
|
end
|
178
182
|
end
|
179
183
|
end
|
@@ -203,9 +207,9 @@ module Zeitwerk::Loader::EagerLoad
|
|
203
207
|
next_dirs = []
|
204
208
|
|
205
209
|
suffix.split("::").each do |segment|
|
206
|
-
while dir = dirs.shift
|
207
|
-
ls(dir) do |basename, abspath|
|
208
|
-
next unless
|
210
|
+
while (dir = dirs.shift)
|
211
|
+
ls(dir) do |basename, abspath, ftype|
|
212
|
+
next unless ftype == :directory
|
209
213
|
|
210
214
|
if collapse?(abspath)
|
211
215
|
dirs << abspath
|
@@ -30,27 +30,45 @@ 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
|
+
children = Dir.children(dir)
|
61
|
+
|
62
|
+
children.each do |basename|
|
63
|
+
next if hidden?(basename)
|
64
|
+
|
65
|
+
abspath = File.join(dir, basename)
|
66
|
+
next if ignored_path?(abspath)
|
67
|
+
|
50
68
|
if dir?(abspath)
|
51
|
-
to_visit << abspath
|
69
|
+
to_visit << abspath unless roots.key?(abspath)
|
52
70
|
else
|
53
|
-
return true
|
71
|
+
return true if ruby?(abspath)
|
54
72
|
end
|
55
73
|
end
|
56
74
|
end
|
@@ -82,62 +100,49 @@ module Zeitwerk::Loader::Helpers
|
|
82
100
|
end
|
83
101
|
end
|
84
102
|
|
85
|
-
# ---
|
103
|
+
# --- Inflection --------------------------------------------------------------------------------
|
86
104
|
|
87
|
-
|
88
|
-
|
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
|
105
|
+
CNAME_VALIDATOR = Module.new
|
106
|
+
private_constant :CNAME_VALIDATOR
|
113
107
|
|
114
|
-
# @
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
108
|
+
# @raise [Zeitwerk::NameError]
|
109
|
+
# @sig (String, String) -> Symbol
|
110
|
+
private def cname_for(basename, abspath)
|
111
|
+
cname = inflector.camelize(basename, abspath)
|
112
|
+
|
113
|
+
unless cname.is_a?(String)
|
114
|
+
raise TypeError, "#{inflector.class}#camelize must return a String, received #{cname.inspect}"
|
120
115
|
end
|
121
|
-
|
122
|
-
|
123
|
-
|
116
|
+
|
117
|
+
if cname.include?("::")
|
118
|
+
raise Zeitwerk::NameError.new(<<~MESSAGE, cname)
|
119
|
+
wrong constant name #{cname} inferred by #{inflector.class} from
|
120
|
+
|
121
|
+
#{abspath}
|
122
|
+
|
123
|
+
#{inflector.class}#camelize should return a simple constant name without "::"
|
124
|
+
MESSAGE
|
124
125
|
end
|
125
|
-
end
|
126
126
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
127
|
+
begin
|
128
|
+
CNAME_VALIDATOR.const_defined?(cname, false)
|
129
|
+
rescue ::NameError => error
|
130
|
+
path_type = ruby?(abspath) ? "file" : "directory"
|
131
131
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
132
|
+
raise Zeitwerk::NameError.new(<<~MESSAGE, error.name)
|
133
|
+
#{error.message} inferred by #{inflector.class} from #{path_type}
|
134
|
+
|
135
|
+
#{abspath}
|
136
|
+
|
137
|
+
Possible ways to address this:
|
138
|
+
|
139
|
+
* Tell Zeitwerk to ignore this particular #{path_type}.
|
140
|
+
* Tell Zeitwerk to ignore one of its parent directories.
|
141
|
+
* Rename the #{path_type} to comply with the naming conventions.
|
142
|
+
* Modify the inflector to handle this case.
|
143
|
+
MESSAGE
|
144
|
+
end
|
137
145
|
|
138
|
-
|
139
|
-
# @sig (Module, Symbol) -> Object
|
140
|
-
private def crem(parent, cname)
|
141
|
-
parent.__send__(:remove_const, cname)
|
146
|
+
cname.to_sym
|
142
147
|
end
|
143
148
|
end
|
data/lib/zeitwerk/loader.rb
CHANGED
@@ -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
|
@@ -21,14 +22,13 @@ module Zeitwerk
|
|
21
22
|
private_constant :MUTEX
|
22
23
|
|
23
24
|
# Maps absolute paths for which an autoload has been set ---and not
|
24
|
-
# executed--- to their corresponding
|
25
|
-
# name.
|
25
|
+
# executed--- to their corresponding Zeitwerk::Cref object.
|
26
26
|
#
|
27
|
-
# "/Users/fxn/blog/app/models/user.rb" =>
|
28
|
-
# "/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, ...>,
|
29
29
|
# ...
|
30
30
|
#
|
31
|
-
# @sig Hash[String,
|
31
|
+
# @sig Hash[String, Zeitwerk::Cref]
|
32
32
|
attr_reader :autoloads
|
33
33
|
internal :autoloads
|
34
34
|
|
@@ -44,17 +44,19 @@ module Zeitwerk
|
|
44
44
|
|
45
45
|
# Stores metadata needed for unloading. Its entries look like this:
|
46
46
|
#
|
47
|
-
# "Admin::Role" => [
|
47
|
+
# "Admin::Role" => [
|
48
|
+
# ".../admin/role.rb",
|
49
|
+
# #<Zeitwerk::Cref:... @mod=Admin, @cname=:Role, ...>
|
50
|
+
# ]
|
48
51
|
#
|
49
52
|
# The cpath as key helps implementing unloadable_cpath? The file name is
|
50
53
|
# stored in order to be able to delete it from $LOADED_FEATURES, and the
|
51
|
-
#
|
52
|
-
# or module object.
|
54
|
+
# cref is used to remove the constant from the parent class or module.
|
53
55
|
#
|
54
56
|
# If reloading is enabled, this hash is filled as constants are autoloaded
|
55
57
|
# or eager loaded. Otherwise, the collection remains empty.
|
56
58
|
#
|
57
|
-
# @sig Hash[String, [String,
|
59
|
+
# @sig Hash[String, [String, Zeitwerk::Cref]]
|
58
60
|
attr_reader :to_unload
|
59
61
|
internal :to_unload
|
60
62
|
|
@@ -91,9 +93,9 @@ module Zeitwerk
|
|
91
93
|
attr_reader :mutex
|
92
94
|
private :mutex
|
93
95
|
|
94
|
-
# @sig
|
95
|
-
attr_reader :
|
96
|
-
private :
|
96
|
+
# @sig Monitor
|
97
|
+
attr_reader :dirs_autoload_monitor
|
98
|
+
private :dirs_autoload_monitor
|
97
99
|
|
98
100
|
def initialize
|
99
101
|
super
|
@@ -103,11 +105,12 @@ module Zeitwerk
|
|
103
105
|
@to_unload = {}
|
104
106
|
@namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
|
105
107
|
@shadowed_files = Set.new
|
106
|
-
@mutex = Mutex.new
|
107
|
-
@mutex2 = Mutex.new
|
108
108
|
@setup = false
|
109
109
|
@eager_loaded = false
|
110
110
|
|
111
|
+
@mutex = Mutex.new
|
112
|
+
@dirs_autoload_monitor = Monitor.new
|
113
|
+
|
111
114
|
Registry.register_loader(self)
|
112
115
|
end
|
113
116
|
|
@@ -119,7 +122,7 @@ module Zeitwerk
|
|
119
122
|
break if @setup
|
120
123
|
|
121
124
|
actual_roots.each do |root_dir, root_namespace|
|
122
|
-
|
125
|
+
define_autoloads_for_dir(root_dir, root_namespace)
|
123
126
|
end
|
124
127
|
|
125
128
|
on_setup_callbacks.each(&:call)
|
@@ -152,22 +155,22 @@ module Zeitwerk
|
|
152
155
|
# is enough.
|
153
156
|
unloaded_files = Set.new
|
154
157
|
|
155
|
-
autoloads.each do |abspath,
|
156
|
-
if
|
157
|
-
unload_autoload(
|
158
|
+
autoloads.each do |abspath, cref|
|
159
|
+
if cref.autoload?
|
160
|
+
unload_autoload(cref)
|
158
161
|
else
|
159
162
|
# Could happen if loaded with require_relative. That is unsupported,
|
160
163
|
# and the constant path would escape unloadable_cpath? This is just
|
161
164
|
# defensive code to clean things up as much as we are able to.
|
162
|
-
unload_cref(
|
165
|
+
unload_cref(cref)
|
163
166
|
unloaded_files.add(abspath) if ruby?(abspath)
|
164
167
|
end
|
165
168
|
end
|
166
169
|
|
167
|
-
to_unload.each do |cpath, (abspath,
|
170
|
+
to_unload.each do |cpath, (abspath, cref)|
|
168
171
|
unless on_unload_callbacks.empty?
|
169
172
|
begin
|
170
|
-
value =
|
173
|
+
value = cref.get
|
171
174
|
rescue ::NameError
|
172
175
|
# Perhaps the user deleted the constant by hand, or perhaps an
|
173
176
|
# autoload failed to define the expected constant but the user
|
@@ -177,7 +180,7 @@ module Zeitwerk
|
|
177
180
|
end
|
178
181
|
end
|
179
182
|
|
180
|
-
unload_cref(
|
183
|
+
unload_cref(cref)
|
181
184
|
unloaded_files.add(abspath) if ruby?(abspath)
|
182
185
|
end
|
183
186
|
|
@@ -228,6 +231,87 @@ module Zeitwerk
|
|
228
231
|
setup
|
229
232
|
end
|
230
233
|
|
234
|
+
# Returns a hash that maps the absolute paths of the managed files and
|
235
|
+
# directories to their respective expected constant paths.
|
236
|
+
#
|
237
|
+
# @sig () -> Hash[String, String]
|
238
|
+
def all_expected_cpaths
|
239
|
+
result = {}
|
240
|
+
|
241
|
+
actual_roots.each do |root_dir, root_namespace|
|
242
|
+
queue = [[root_dir, real_mod_name(root_namespace)]]
|
243
|
+
|
244
|
+
while (dir, cpath = queue.shift)
|
245
|
+
result[dir] = cpath
|
246
|
+
|
247
|
+
prefix = cpath == "Object" ? "" : cpath + "::"
|
248
|
+
|
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
|
254
|
+
if collapse?(abspath)
|
255
|
+
queue << [abspath, cpath]
|
256
|
+
else
|
257
|
+
queue << [abspath, prefix + inflector.camelize(basename, abspath)]
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
result
|
265
|
+
end
|
266
|
+
|
267
|
+
# @sig (String | Pathname) -> String?
|
268
|
+
def cpath_expected_at(path)
|
269
|
+
abspath = File.expand_path(path)
|
270
|
+
|
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
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
231
315
|
# Says if the given constant path would be unloaded on reload. This
|
232
316
|
# predicate returns `false` if reloading is disabled.
|
233
317
|
#
|
@@ -357,44 +441,26 @@ module Zeitwerk
|
|
357
441
|
end
|
358
442
|
|
359
443
|
# @sig (String, Module) -> void
|
360
|
-
private def
|
361
|
-
ls(dir) do |basename, abspath|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
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)
|
367
453
|
else
|
368
|
-
|
369
|
-
|
370
|
-
else
|
371
|
-
cname = inflector.camelize(basename, abspath).to_sym
|
372
|
-
autoload_subdir(parent, cname, abspath)
|
373
|
-
end
|
454
|
+
cref = Cref.new(parent, cname_for(basename, abspath))
|
455
|
+
autoload_subdir(cref, abspath)
|
374
456
|
end
|
375
|
-
rescue ::NameError => error
|
376
|
-
path_type = ruby?(abspath) ? "file" : "directory"
|
377
|
-
|
378
|
-
raise NameError.new(<<~MESSAGE, error.name)
|
379
|
-
#{error.message} inferred by #{inflector.class} from #{path_type}
|
380
|
-
|
381
|
-
#{abspath}
|
382
|
-
|
383
|
-
Possible ways to address this:
|
384
|
-
|
385
|
-
* Tell Zeitwerk to ignore this particular #{path_type}.
|
386
|
-
* Tell Zeitwerk to ignore one of its parent directories.
|
387
|
-
* Rename the #{path_type} to comply with the naming conventions.
|
388
|
-
* Modify the inflector to handle this case.
|
389
|
-
MESSAGE
|
390
457
|
end
|
391
458
|
end
|
392
459
|
end
|
393
460
|
|
394
461
|
# @sig (Module, Symbol, String) -> void
|
395
|
-
private def autoload_subdir(
|
396
|
-
if autoload_path = autoload_path_set_by_me_for?(
|
397
|
-
cpath = cpath(parent, cname)
|
462
|
+
private def autoload_subdir(cref, subdir)
|
463
|
+
if autoload_path = autoload_path_set_by_me_for?(cref)
|
398
464
|
if ruby?(autoload_path)
|
399
465
|
# Scanning visited a Ruby file first, and now a directory for the same
|
400
466
|
# constant has been found. This means we are dealing with an explicit
|
@@ -403,88 +469,83 @@ module Zeitwerk
|
|
403
469
|
# Registering is idempotent, and we have to keep the autoload pointing
|
404
470
|
# to the file. This may run again if more directories are found later
|
405
471
|
# on, no big deal.
|
406
|
-
register_explicit_namespace(
|
472
|
+
register_explicit_namespace(cref.path)
|
407
473
|
end
|
408
474
|
# If the existing autoload points to a file, it has to be preserved, if
|
409
475
|
# not, it is fine as it is. In either case, we do not need to override.
|
410
476
|
# Just remember the subdirectory conforms this namespace.
|
411
|
-
namespace_dirs[
|
412
|
-
elsif !
|
477
|
+
namespace_dirs[cref.path] << subdir
|
478
|
+
elsif !cref.defined?
|
413
479
|
# First time we find this namespace, set an autoload for it.
|
414
|
-
namespace_dirs[
|
415
|
-
|
480
|
+
namespace_dirs[cref.path] << subdir
|
481
|
+
define_autoload(cref, subdir)
|
416
482
|
else
|
417
483
|
# For whatever reason the constant that corresponds to this namespace has
|
418
484
|
# already been defined, we have to recurse.
|
419
|
-
log("the namespace #{
|
420
|
-
|
485
|
+
log("the namespace #{cref.path} already exists, descending into #{subdir}") if logger
|
486
|
+
define_autoloads_for_dir(subdir, cref.get)
|
421
487
|
end
|
422
488
|
end
|
423
489
|
|
424
490
|
# @sig (Module, Symbol, String) -> void
|
425
|
-
private def autoload_file(
|
426
|
-
if autoload_path =
|
491
|
+
private def autoload_file(cref, file)
|
492
|
+
if autoload_path = cref.autoload? || Registry.inception?(cref.path)
|
427
493
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
428
494
|
if ruby?(autoload_path)
|
429
495
|
shadowed_files << file
|
430
496
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
431
497
|
else
|
432
|
-
promote_namespace_from_implicit_to_explicit(
|
433
|
-
dir: autoload_path,
|
434
|
-
file: file,
|
435
|
-
parent: parent,
|
436
|
-
cname: cname
|
437
|
-
)
|
498
|
+
promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
|
438
499
|
end
|
439
|
-
elsif
|
500
|
+
elsif cref.defined?
|
440
501
|
shadowed_files << file
|
441
|
-
log("file #{file} is ignored because #{
|
502
|
+
log("file #{file} is ignored because #{cref.path} is already defined") if logger
|
442
503
|
else
|
443
|
-
|
504
|
+
define_autoload(cref, file)
|
444
505
|
end
|
445
506
|
end
|
446
507
|
|
447
508
|
# `dir` is the directory that would have autovivified a namespace. `file` is
|
448
509
|
# the file where we've found the namespace is explicitly defined.
|
449
510
|
#
|
450
|
-
# @sig (dir: String, file: String,
|
451
|
-
private def promote_namespace_from_implicit_to_explicit(dir:, file:,
|
511
|
+
# @sig (dir: String, file: String, cref: Zeitwerk::Cref) -> void
|
512
|
+
private def promote_namespace_from_implicit_to_explicit(dir:, file:, cref:)
|
452
513
|
autoloads.delete(dir)
|
453
514
|
Registry.unregister_autoload(dir)
|
454
515
|
|
455
|
-
log("earlier autoload for #{
|
516
|
+
log("earlier autoload for #{cref.path} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
456
517
|
|
457
|
-
|
458
|
-
register_explicit_namespace(
|
518
|
+
define_autoload(cref, file)
|
519
|
+
register_explicit_namespace(cref.path)
|
459
520
|
end
|
460
521
|
|
461
522
|
# @sig (Module, Symbol, String) -> void
|
462
|
-
private def
|
463
|
-
|
523
|
+
private def define_autoload(cref, abspath)
|
524
|
+
cref.autoload(abspath)
|
464
525
|
|
465
526
|
if logger
|
466
527
|
if ruby?(abspath)
|
467
|
-
log("autoload set for #{
|
528
|
+
log("autoload set for #{cref.path}, to be loaded from #{abspath}")
|
468
529
|
else
|
469
|
-
log("autoload set for #{
|
530
|
+
log("autoload set for #{cref.path}, to be autovivified from #{abspath}")
|
470
531
|
end
|
471
532
|
end
|
472
533
|
|
473
|
-
autoloads[abspath] =
|
534
|
+
autoloads[abspath] = cref
|
474
535
|
Registry.register_autoload(self, abspath)
|
475
536
|
|
476
537
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
477
|
-
unless
|
478
|
-
Registry.register_inception(
|
538
|
+
unless cref.autoload?
|
539
|
+
Registry.register_inception(cref.path, abspath, self)
|
479
540
|
end
|
480
541
|
end
|
481
542
|
|
482
543
|
# @sig (Module, Symbol) -> String?
|
483
|
-
private def autoload_path_set_by_me_for?(
|
484
|
-
if autoload_path =
|
544
|
+
private def autoload_path_set_by_me_for?(cref)
|
545
|
+
if autoload_path = cref.autoload?
|
485
546
|
autoload_path if autoloads.key?(autoload_path)
|
486
547
|
else
|
487
|
-
Registry.inception?(
|
548
|
+
Registry.inception?(cref.path, self)
|
488
549
|
end
|
489
550
|
end
|
490
551
|
|
@@ -511,7 +572,6 @@ module Zeitwerk
|
|
511
572
|
raise Error,
|
512
573
|
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
513
574
|
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
514
|
-
EOS
|
515
575
|
end
|
516
576
|
end
|
517
577
|
end
|
@@ -526,21 +586,21 @@ module Zeitwerk
|
|
526
586
|
end
|
527
587
|
|
528
588
|
# @sig (Module, Symbol) -> void
|
529
|
-
private def unload_autoload(
|
530
|
-
|
531
|
-
log("autoload for #{
|
589
|
+
private def unload_autoload(cref)
|
590
|
+
cref.remove
|
591
|
+
log("autoload for #{cref.path} removed") if logger
|
532
592
|
end
|
533
593
|
|
534
594
|
# @sig (Module, Symbol) -> void
|
535
|
-
private def unload_cref(
|
595
|
+
private def unload_cref(cref)
|
536
596
|
# Let's optimistically remove_const. The way we use it, this is going to
|
537
597
|
# succeed always if all is good.
|
538
|
-
|
598
|
+
cref.remove
|
539
599
|
rescue ::NameError
|
540
600
|
# There are a few edge scenarios in which this may happen. If the constant
|
541
601
|
# is gone, that is OK, anyway.
|
542
602
|
else
|
543
|
-
log("#{
|
603
|
+
log("#{cref.path} unloaded") if logger
|
544
604
|
end
|
545
605
|
end
|
546
606
|
end
|