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.
- checksums.yaml +4 -4
- data/README.md +473 -50
- data/lib/zeitwerk/cref.rb +99 -0
- data/lib/zeitwerk/error.rb +9 -0
- data/lib/zeitwerk/explicit_namespace.rb +14 -10
- data/lib/zeitwerk/gem_inflector.rb +2 -2
- data/lib/zeitwerk/gem_loader.rb +68 -0
- data/lib/zeitwerk/internal.rb +12 -0
- data/lib/zeitwerk/kernel.rb +6 -7
- data/lib/zeitwerk/loader/callbacks.rb +34 -27
- data/lib/zeitwerk/loader/config.rb +95 -52
- data/lib/zeitwerk/loader/eager_load.rb +232 -0
- data/lib/zeitwerk/loader/helpers.rb +106 -55
- data/lib/zeitwerk/loader.rb +287 -197
- data/lib/zeitwerk/null_inflector.rb +5 -0
- data/lib/zeitwerk/registry.rb +14 -18
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +4 -0
- metadata +8 -3
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
|
@@ -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
|
18
|
-
# name.
|
25
|
+
# executed--- to their corresponding Zeitwerk::Cref object.
|
19
26
|
#
|
20
|
-
# "/Users/fxn/blog/app/models/user.rb" =>
|
21
|
-
# "/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, ...>,
|
22
29
|
# ...
|
23
30
|
#
|
24
|
-
# @
|
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" => [
|
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
|
-
#
|
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
|
-
# @
|
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
|
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
|
65
|
-
# that its children are spread over those directories. We'll visit them
|
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 :
|
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
|
-
# @
|
77
|
-
|
78
|
-
|
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
|
-
@
|
87
|
-
@
|
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
|
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
|
-
|
103
|
-
|
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
|
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,
|
135
|
-
if
|
136
|
-
unload_autoload(
|
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(
|
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,
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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(
|
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
|
-
|
205
|
+
namespace_dirs.clear
|
206
|
+
shadowed_files.clear
|
181
207
|
|
182
208
|
Registry.on_unload(self)
|
183
|
-
ExplicitNamespace.
|
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
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
#
|
210
|
-
#
|
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 (
|
216
|
-
def
|
217
|
-
|
218
|
-
break if @eager_loaded
|
237
|
+
# @sig () -> Hash[String, String]
|
238
|
+
def all_expected_cpaths
|
239
|
+
result = {}
|
219
240
|
|
220
|
-
|
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
|
230
|
-
|
244
|
+
while (dir, cpath = queue.shift)
|
245
|
+
result[dir] = cpath
|
231
246
|
|
232
|
-
|
233
|
-
next if honour_exclusions && excluded_from_eager_load?(abspath)
|
247
|
+
prefix = cpath == "Object" ? "" : cpath + "::"
|
234
248
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
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 << [
|
255
|
+
queue << [abspath, cpath]
|
242
256
|
else
|
243
|
-
|
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
|
-
|
251
|
-
|
252
|
-
end
|
253
|
-
autoloaded_dirs.clear
|
264
|
+
result
|
265
|
+
end
|
254
266
|
|
255
|
-
|
267
|
+
# @sig (String | Pathname) -> String?
|
268
|
+
def cpath_expected_at(path)
|
269
|
+
abspath = File.expand_path(path)
|
256
270
|
|
257
|
-
|
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.
|
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
|
-
#
|
308
|
-
|
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
|
-
#
|
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
|
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
|
335
|
-
ls(dir) do |basename, abspath|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
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(
|
378
|
-
if autoload_path = autoload_path_set_by_me_for?(
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
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
|
-
|
388
|
-
|
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 #{
|
393
|
-
|
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(
|
399
|
-
if autoload_path =
|
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
|
412
|
-
|
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
|
-
|
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,
|
422
|
-
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:)
|
423
513
|
autoloads.delete(dir)
|
424
514
|
Registry.unregister_autoload(dir)
|
425
515
|
|
426
|
-
log("earlier autoload for #{
|
516
|
+
log("earlier autoload for #{cref.path} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
427
517
|
|
428
|
-
|
429
|
-
register_explicit_namespace(
|
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
|
434
|
-
|
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 #{
|
528
|
+
log("autoload set for #{cref.path}, to be loaded from #{abspath}")
|
439
529
|
else
|
440
|
-
log("autoload set for #{
|
530
|
+
log("autoload set for #{cref.path}, to be autovivified from #{abspath}")
|
441
531
|
end
|
442
532
|
end
|
443
533
|
|
444
|
-
autoloads[abspath] =
|
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
|
449
|
-
Registry.register_inception(
|
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?(
|
455
|
-
if autoload_path =
|
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?(
|
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.
|
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
|
-
|
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.
|
564
|
+
next if loader.__ignores?(dir)
|
473
565
|
|
474
|
-
|
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
|
-
|
479
|
-
if
|
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
|
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(
|
500
|
-
|
501
|
-
log("autoload for #{
|
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(
|
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
|
-
|
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("#{
|
603
|
+
log("#{cref.path} unloaded") if logger
|
514
604
|
end
|
515
605
|
end
|
516
606
|
end
|