zeitwerk 2.5.4 → 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 +394 -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 +6 -3
- 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 +222 -162
- data/lib/zeitwerk/registry.rb +9 -16
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +2 -0
- metadata +6 -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,11 +8,18 @@ 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
25
|
# executed--- to their corresponding parent class or module and constant
|
@@ -21,9 +29,9 @@ module Zeitwerk
|
|
21
29
|
# "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
|
22
30
|
# ...
|
23
31
|
#
|
24
|
-
# @private
|
25
32
|
# @sig Hash[String, [Module, Symbol]]
|
26
33
|
attr_reader :autoloads
|
34
|
+
internal :autoloads
|
27
35
|
|
28
36
|
# We keep track of autoloaded directories to remove them from the registry
|
29
37
|
# at the end of eager loading.
|
@@ -31,9 +39,9 @@ module Zeitwerk
|
|
31
39
|
# Files are removed as they are autoloaded, but directories need to wait due
|
32
40
|
# to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
|
33
41
|
#
|
34
|
-
# @private
|
35
42
|
# @sig Array[String]
|
36
43
|
attr_reader :autoloaded_dirs
|
44
|
+
internal :autoloaded_dirs
|
37
45
|
|
38
46
|
# Stores metadata needed for unloading. Its entries look like this:
|
39
47
|
#
|
@@ -47,11 +55,11 @@ module Zeitwerk
|
|
47
55
|
# If reloading is enabled, this hash is filled as constants are autoloaded
|
48
56
|
# or eager loaded. Otherwise, the collection remains empty.
|
49
57
|
#
|
50
|
-
# @private
|
51
58
|
# @sig Hash[String, [String, [Module, Symbol]]]
|
52
59
|
attr_reader :to_unload
|
60
|
+
internal :to_unload
|
53
61
|
|
54
|
-
# Maps constant paths
|
62
|
+
# Maps namespace constant paths to their respective directories.
|
55
63
|
#
|
56
64
|
# For example, given this mapping:
|
57
65
|
#
|
@@ -61,21 +69,32 @@ module Zeitwerk
|
|
61
69
|
# ...
|
62
70
|
# ]
|
63
71
|
#
|
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.
|
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.
|
67
75
|
#
|
68
|
-
# @private
|
69
76
|
# @sig Hash[String, Array[String]]
|
70
|
-
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
|
71
90
|
|
72
|
-
# @private
|
73
91
|
# @sig Mutex
|
74
92
|
attr_reader :mutex
|
93
|
+
private :mutex
|
75
94
|
|
76
|
-
# @
|
77
|
-
|
78
|
-
|
95
|
+
# @sig Monitor
|
96
|
+
attr_reader :dirs_autoload_monitor
|
97
|
+
private :dirs_autoload_monitor
|
79
98
|
|
80
99
|
def initialize
|
81
100
|
super
|
@@ -83,24 +102,26 @@ module Zeitwerk
|
|
83
102
|
@autoloads = {}
|
84
103
|
@autoloaded_dirs = []
|
85
104
|
@to_unload = {}
|
86
|
-
@
|
87
|
-
@
|
88
|
-
@mutex2 = Mutex.new
|
105
|
+
@namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
|
106
|
+
@shadowed_files = Set.new
|
89
107
|
@setup = false
|
90
108
|
@eager_loaded = false
|
91
109
|
|
110
|
+
@mutex = Mutex.new
|
111
|
+
@dirs_autoload_monitor = Monitor.new
|
112
|
+
|
92
113
|
Registry.register_loader(self)
|
93
114
|
end
|
94
115
|
|
95
|
-
# Sets autoloads in the root
|
116
|
+
# Sets autoloads in the root namespaces.
|
96
117
|
#
|
97
118
|
# @sig () -> void
|
98
119
|
def setup
|
99
120
|
mutex.synchronize do
|
100
121
|
break if @setup
|
101
122
|
|
102
|
-
|
103
|
-
|
123
|
+
actual_roots.each do |root_dir, root_namespace|
|
124
|
+
define_autoloads_for_dir(root_dir, root_namespace)
|
104
125
|
end
|
105
126
|
|
106
127
|
on_setup_callbacks.each(&:call)
|
@@ -117,12 +138,14 @@ module Zeitwerk
|
|
117
138
|
# unload them.
|
118
139
|
#
|
119
140
|
# This method is public but undocumented. Main interface is `reload`, which
|
120
|
-
# means `unload` + `setup`. This one is
|
141
|
+
# means `unload` + `setup`. This one is available to be used together with
|
121
142
|
# `unregister`, which is undocumented too.
|
122
143
|
#
|
123
144
|
# @sig () -> void
|
124
145
|
def unload
|
125
146
|
mutex.synchronize do
|
147
|
+
raise SetupRequired unless @setup
|
148
|
+
|
126
149
|
# We are going to keep track of the files that were required by our
|
127
150
|
# autoloads to later remove them from $LOADED_FEATURES, thus making them
|
128
151
|
# loadable by Kernel#require again.
|
@@ -144,15 +167,16 @@ module Zeitwerk
|
|
144
167
|
end
|
145
168
|
|
146
169
|
to_unload.each do |cpath, (abspath, (parent, cname))|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
170
|
+
unless on_unload_callbacks.empty?
|
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
|
156
180
|
end
|
157
181
|
|
158
182
|
unload_cref(parent, cname)
|
@@ -177,10 +201,11 @@ module Zeitwerk
|
|
177
201
|
autoloads.clear
|
178
202
|
autoloaded_dirs.clear
|
179
203
|
to_unload.clear
|
180
|
-
|
204
|
+
namespace_dirs.clear
|
205
|
+
shadowed_files.clear
|
181
206
|
|
182
207
|
Registry.on_unload(self)
|
183
|
-
ExplicitNamespace.
|
208
|
+
ExplicitNamespace.__unregister_loader(self)
|
184
209
|
|
185
210
|
@setup = false
|
186
211
|
@eager_loaded = false
|
@@ -196,67 +221,62 @@ module Zeitwerk
|
|
196
221
|
# @raise [Zeitwerk::Error]
|
197
222
|
# @sig () -> void
|
198
223
|
def reload
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
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
|
207
231
|
end
|
208
232
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
# directories with `do_not_eager_load`, and that can be overridden passing
|
213
|
-
# `force: true`.
|
214
|
-
#
|
215
|
-
# @sig (true | false) -> void
|
216
|
-
def eager_load(force: false)
|
217
|
-
mutex.synchronize do
|
218
|
-
break if @eager_loaded
|
233
|
+
# @sig (String | Pathname) -> String?
|
234
|
+
def cpath_expected_at(path)
|
235
|
+
abspath = File.expand_path(path)
|
219
236
|
|
220
|
-
|
237
|
+
raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
221
238
|
|
222
|
-
|
239
|
+
return unless dir?(abspath) || ruby?(abspath)
|
240
|
+
return if ignored_path?(abspath)
|
223
241
|
|
224
|
-
|
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
|
242
|
+
paths = []
|
228
243
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
ls(dir) do |basename, abspath|
|
233
|
-
next if honour_exclusions && excluded_from_eager_load?(abspath)
|
234
|
-
|
235
|
-
if ruby?(abspath)
|
236
|
-
if cref = autoloads[abspath]
|
237
|
-
cget(*cref)
|
238
|
-
end
|
239
|
-
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
240
|
-
if collapse?(abspath)
|
241
|
-
queue << [namespace, abspath]
|
242
|
-
else
|
243
|
-
cname = inflector.camelize(basename, abspath)
|
244
|
-
queue << [cget(namespace, cname), abspath]
|
245
|
-
end
|
246
|
-
end
|
247
|
-
end
|
248
|
-
end
|
244
|
+
if ruby?(abspath)
|
245
|
+
basename = File.basename(abspath, ".rb")
|
246
|
+
return if hidden?(basename)
|
249
247
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
248
|
+
paths << [basename, abspath]
|
249
|
+
walk_up_from = File.dirname(abspath)
|
250
|
+
else
|
251
|
+
walk_up_from = abspath
|
252
|
+
end
|
253
|
+
|
254
|
+
root_namespace = nil
|
255
|
+
|
256
|
+
walk_up(walk_up_from) do |dir|
|
257
|
+
break if root_namespace = roots[dir]
|
258
|
+
return if ignored_path?(dir)
|
254
259
|
|
255
|
-
|
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) }
|
256
272
|
|
257
|
-
|
273
|
+
if root_namespace == Object
|
274
|
+
cnames.join("::")
|
275
|
+
else
|
276
|
+
"#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
|
258
277
|
end
|
259
278
|
end
|
279
|
+
end
|
260
280
|
|
261
281
|
# Says if the given constant path would be unloaded on reload. This
|
262
282
|
# predicate returns `false` if reloading is disabled.
|
@@ -280,22 +300,29 @@ module Zeitwerk
|
|
280
300
|
# @sig () -> void
|
281
301
|
def unregister
|
282
302
|
Registry.unregister_loader(self)
|
283
|
-
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)
|
284
312
|
end
|
285
313
|
|
286
314
|
# --- Class methods ---------------------------------------------------------------------------
|
287
315
|
|
288
316
|
class << self
|
317
|
+
include RealModName
|
318
|
+
|
289
319
|
# @sig #call | #debug | nil
|
290
320
|
attr_accessor :default_logger
|
291
321
|
|
292
|
-
# @private
|
293
|
-
# @sig Mutex
|
294
|
-
attr_accessor :mutex
|
295
|
-
|
296
322
|
# This is a shortcut for
|
297
323
|
#
|
298
324
|
# require "zeitwerk"
|
325
|
+
#
|
299
326
|
# loader = Zeitwerk::Loader.new
|
300
327
|
# loader.tag = File.basename(__FILE__, ".rb")
|
301
328
|
# loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
@@ -304,17 +331,70 @@ module Zeitwerk
|
|
304
331
|
# except that this method returns the same object in subsequent calls from
|
305
332
|
# the same file, in the unlikely case the gem wants to be able to reload.
|
306
333
|
#
|
307
|
-
#
|
308
|
-
|
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)
|
309
339
|
called_from = caller_locations(1, 1).first.path
|
310
|
-
Registry.loader_for_gem(called_from)
|
340
|
+
Registry.loader_for_gem(called_from, namespace: Object, warn_on_extra_files: warn_on_extra_files)
|
311
341
|
end
|
312
342
|
|
313
|
-
#
|
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.
|
314
374
|
#
|
315
375
|
# @sig () -> void
|
316
376
|
def eager_load_all
|
317
|
-
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
|
318
398
|
end
|
319
399
|
|
320
400
|
# Returns an array with the absolute paths of the root directories of all
|
@@ -326,79 +406,58 @@ module Zeitwerk
|
|
326
406
|
end
|
327
407
|
end
|
328
408
|
|
329
|
-
self.mutex = Mutex.new
|
330
|
-
|
331
|
-
private # -------------------------------------------------------------------------------------
|
332
|
-
|
333
409
|
# @sig (String, Module) -> void
|
334
|
-
def
|
410
|
+
private def define_autoloads_for_dir(dir, parent)
|
335
411
|
ls(dir) do |basename, abspath|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
#
|
345
|
-
# To resolve the ambiguity file name -> constant path this introduces,
|
346
|
-
# the `app/models/concerns` directory is totally ignored as a namespace,
|
347
|
-
# it counts only as root. The guard checks that.
|
348
|
-
unless root_dir?(abspath)
|
349
|
-
cname = inflector.camelize(basename, abspath).to_sym
|
350
|
-
if collapse?(abspath)
|
351
|
-
set_autoloads_in_dir(abspath, parent)
|
352
|
-
else
|
353
|
-
autoload_subdir(parent, cname, abspath)
|
354
|
-
end
|
355
|
-
end
|
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)
|
356
420
|
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
421
|
end
|
373
422
|
end
|
374
423
|
end
|
375
424
|
|
376
425
|
# @sig (Module, Symbol, String) -> void
|
377
|
-
def autoload_subdir(parent, cname, subdir)
|
426
|
+
private def autoload_subdir(parent, cname, subdir)
|
378
427
|
if autoload_path = autoload_path_set_by_me_for?(parent, cname)
|
379
428
|
cpath = cpath(parent, cname)
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
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
|
385
443
|
elsif !cdef?(parent, cname)
|
386
444
|
# First time we find this namespace, set an autoload for it.
|
387
|
-
|
388
|
-
|
445
|
+
namespace_dirs[cpath(parent, cname)] << subdir
|
446
|
+
define_autoload(parent, cname, subdir)
|
389
447
|
else
|
390
448
|
# For whatever reason the constant that corresponds to this namespace has
|
391
449
|
# already been defined, we have to recurse.
|
392
450
|
log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
|
393
|
-
|
451
|
+
define_autoloads_for_dir(subdir, cget(parent, cname))
|
394
452
|
end
|
395
453
|
end
|
396
454
|
|
397
455
|
# @sig (Module, Symbol, String) -> void
|
398
|
-
def autoload_file(parent, cname, file)
|
456
|
+
private def autoload_file(parent, cname, file)
|
399
457
|
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
400
458
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
401
459
|
if ruby?(autoload_path)
|
460
|
+
shadowed_files << file
|
402
461
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
403
462
|
else
|
404
463
|
promote_namespace_from_implicit_to_explicit(
|
@@ -409,9 +468,10 @@ module Zeitwerk
|
|
409
468
|
)
|
410
469
|
end
|
411
470
|
elsif cdef?(parent, cname)
|
471
|
+
shadowed_files << file
|
412
472
|
log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
|
413
473
|
else
|
414
|
-
|
474
|
+
define_autoload(parent, cname, file)
|
415
475
|
end
|
416
476
|
end
|
417
477
|
|
@@ -419,18 +479,18 @@ module Zeitwerk
|
|
419
479
|
# the file where we've found the namespace is explicitly defined.
|
420
480
|
#
|
421
481
|
# @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
|
422
|
-
def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
|
482
|
+
private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
|
423
483
|
autoloads.delete(dir)
|
424
484
|
Registry.unregister_autoload(dir)
|
425
485
|
|
426
486
|
log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
427
487
|
|
428
|
-
|
488
|
+
define_autoload(parent, cname, file)
|
429
489
|
register_explicit_namespace(cpath(parent, cname))
|
430
490
|
end
|
431
491
|
|
432
492
|
# @sig (Module, Symbol, String) -> void
|
433
|
-
def
|
493
|
+
private def define_autoload(parent, cname, abspath)
|
434
494
|
parent.autoload(cname, abspath)
|
435
495
|
|
436
496
|
if logger
|
@@ -451,7 +511,7 @@ module Zeitwerk
|
|
451
511
|
end
|
452
512
|
|
453
513
|
# @sig (Module, Symbol) -> String?
|
454
|
-
def autoload_path_set_by_me_for?(parent, cname)
|
514
|
+
private def autoload_path_set_by_me_for?(parent, cname)
|
455
515
|
if autoload_path = strict_autoload_path(parent, cname)
|
456
516
|
autoload_path if autoloads.key?(autoload_path)
|
457
517
|
else
|
@@ -460,28 +520,28 @@ module Zeitwerk
|
|
460
520
|
end
|
461
521
|
|
462
522
|
# @sig (String) -> void
|
463
|
-
def register_explicit_namespace(cpath)
|
464
|
-
ExplicitNamespace.
|
523
|
+
private def register_explicit_namespace(cpath)
|
524
|
+
ExplicitNamespace.__register(cpath, self)
|
465
525
|
end
|
466
526
|
|
467
527
|
# @sig (String) -> void
|
468
|
-
def raise_if_conflicting_directory(dir)
|
469
|
-
|
528
|
+
private def raise_if_conflicting_directory(dir)
|
529
|
+
MUTEX.synchronize do
|
530
|
+
dir_slash = dir + "/"
|
531
|
+
|
470
532
|
Registry.loaders.each do |loader|
|
471
533
|
next if loader == self
|
472
|
-
next if loader.
|
534
|
+
next if loader.__ignores?(dir)
|
473
535
|
|
474
|
-
|
475
|
-
loader.root_dirs.each do |root_dir, _namespace|
|
536
|
+
loader.__roots.each_key do |root_dir|
|
476
537
|
next if ignores?(root_dir)
|
477
538
|
|
478
|
-
|
479
|
-
if
|
539
|
+
root_dir_slash = root_dir + "/"
|
540
|
+
if dir_slash.start_with?(root_dir_slash) || root_dir_slash.start_with?(dir_slash)
|
480
541
|
require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
|
481
542
|
raise Error,
|
482
|
-
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir
|
543
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
483
544
|
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
484
|
-
EOS
|
485
545
|
end
|
486
546
|
end
|
487
547
|
end
|
@@ -489,23 +549,23 @@ module Zeitwerk
|
|
489
549
|
end
|
490
550
|
|
491
551
|
# @sig (String, Object, String) -> void
|
492
|
-
def run_on_unload_callbacks(cpath, value, abspath)
|
552
|
+
private def run_on_unload_callbacks(cpath, value, abspath)
|
493
553
|
# Order matters. If present, run the most specific one.
|
494
554
|
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
495
555
|
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
496
556
|
end
|
497
557
|
|
498
558
|
# @sig (Module, Symbol) -> void
|
499
|
-
def unload_autoload(parent, cname)
|
500
|
-
parent
|
559
|
+
private def unload_autoload(parent, cname)
|
560
|
+
crem(parent, cname)
|
501
561
|
log("autoload for #{cpath(parent, cname)} removed") if logger
|
502
562
|
end
|
503
563
|
|
504
564
|
# @sig (Module, Symbol) -> void
|
505
|
-
def unload_cref(parent, cname)
|
565
|
+
private def unload_cref(parent, cname)
|
506
566
|
# Let's optimistically remove_const. The way we use it, this is going to
|
507
567
|
# succeed always if all is good.
|
508
|
-
parent
|
568
|
+
crem(parent, cname)
|
509
569
|
rescue ::NameError
|
510
570
|
# There are a few edge scenarios in which this may happen. If the constant
|
511
571
|
# is gone, that is OK, anyway.
|
data/lib/zeitwerk/registry.rb
CHANGED
@@ -10,12 +10,11 @@ module Zeitwerk
|
|
10
10
|
# @sig Array[Zeitwerk::Loader]
|
11
11
|
attr_reader :loaders
|
12
12
|
|
13
|
-
# Registers loaders
|
14
|
-
# in case of reload.
|
13
|
+
# Registers gem loaders to let `for_gem` be idempotent in case of reload.
|
15
14
|
#
|
16
15
|
# @private
|
17
16
|
# @sig Hash[String, Zeitwerk::Loader]
|
18
|
-
attr_reader :
|
17
|
+
attr_reader :gem_loaders_by_root_file
|
19
18
|
|
20
19
|
# Maps absolute paths to the loaders responsible for them.
|
21
20
|
#
|
@@ -77,7 +76,7 @@ module Zeitwerk
|
|
77
76
|
# @sig (Zeitwerk::Loader) -> void
|
78
77
|
def unregister_loader(loader)
|
79
78
|
loaders.delete(loader)
|
80
|
-
|
79
|
+
gem_loaders_by_root_file.delete_if { |_, l| l == loader }
|
81
80
|
autoloads.delete_if { |_, l| l == loader }
|
82
81
|
inceptions.delete_if { |_, (_, l)| l == loader }
|
83
82
|
end
|
@@ -87,14 +86,8 @@ module Zeitwerk
|
|
87
86
|
#
|
88
87
|
# @private
|
89
88
|
# @sig (String) -> Zeitwerk::Loader
|
90
|
-
def loader_for_gem(root_file)
|
91
|
-
|
92
|
-
Loader.new.tap do |loader|
|
93
|
-
loader.tag = File.basename(root_file, ".rb")
|
94
|
-
loader.inflector = GemInflector.new(root_file)
|
95
|
-
loader.push_dir(File.dirname(root_file))
|
96
|
-
end
|
97
|
-
end
|
89
|
+
def loader_for_gem(root_file, namespace:, warn_on_extra_files:)
|
90
|
+
gem_loaders_by_root_file[root_file] ||= GemLoader.__new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
|
98
91
|
end
|
99
92
|
|
100
93
|
# @private
|
@@ -137,9 +130,9 @@ module Zeitwerk
|
|
137
130
|
end
|
138
131
|
end
|
139
132
|
|
140
|
-
@loaders
|
141
|
-
@
|
142
|
-
@autoloads
|
143
|
-
@inceptions
|
133
|
+
@loaders = []
|
134
|
+
@gem_loaders_by_root_file = {}
|
135
|
+
@autoloads = {}
|
136
|
+
@inceptions = {}
|
144
137
|
end
|
145
138
|
end
|