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