zeitwerk 2.4.2 → 2.5.0.beta4
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 +215 -68
- data/lib/zeitwerk/autoloads.rb +71 -0
- data/lib/zeitwerk/error.rb +2 -0
- data/lib/zeitwerk/explicit_namespace.rb +11 -3
- data/lib/zeitwerk/kernel.rb +6 -5
- data/lib/zeitwerk/loader/callbacks.rb +11 -8
- data/lib/zeitwerk/loader/config.rb +321 -0
- data/lib/zeitwerk/loader/helpers.rb +97 -0
- data/lib/zeitwerk/loader.rb +102 -416
- data/lib/zeitwerk/real_mod_name.rb +2 -0
- data/lib/zeitwerk/registry.rb +16 -7
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -0
- metadata +9 -6
data/lib/zeitwerk/loader.rb
CHANGED
@@ -5,78 +5,31 @@ require "securerandom"
|
|
5
5
|
|
6
6
|
module Zeitwerk
|
7
7
|
class Loader
|
8
|
+
require_relative "loader/helpers"
|
8
9
|
require_relative "loader/callbacks"
|
9
|
-
|
10
|
-
include RealModName
|
11
|
-
|
12
|
-
# @sig String
|
13
|
-
attr_reader :tag
|
10
|
+
require_relative "loader/config"
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
attr_accessor :logger
|
12
|
+
include RealModName
|
13
|
+
include Callbacks
|
14
|
+
include Helpers
|
15
|
+
include Config
|
20
16
|
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# needed for detecting nested paths.
|
17
|
+
# Keeps track of autoloads defined by the loader which have not been
|
18
|
+
# executed so far.
|
24
19
|
#
|
25
|
-
#
|
26
|
-
# "/Users/fxn/blog/app/channels" => true,
|
27
|
-
# ...
|
20
|
+
# This metadata helps us implement a few things:
|
28
21
|
#
|
29
|
-
#
|
30
|
-
#
|
22
|
+
# 1. When autoloads are triggered, ensure they define the expected constant
|
23
|
+
# and invoke user callbacks. If reloading is enabled, remember cref and
|
24
|
+
# abspath for later unloading logic.
|
31
25
|
#
|
32
|
-
#
|
33
|
-
# @sig Hash[String, true]
|
34
|
-
attr_reader :root_dirs
|
35
|
-
|
36
|
-
# Absolute paths of files or directories that have to be preloaded.
|
26
|
+
# 2. When unloading, remove autoloads that have not been executed.
|
37
27
|
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
attr_reader :preloads
|
41
|
-
|
42
|
-
# Absolute paths of files, directories, or glob patterns to be totally
|
43
|
-
# ignored.
|
28
|
+
# 3. Eager load with a recursive const_get, rather than a recursive require,
|
29
|
+
# for consistency with lazy loading.
|
44
30
|
#
|
45
31
|
# @private
|
46
|
-
# @sig
|
47
|
-
attr_reader :ignored_glob_patterns
|
48
|
-
|
49
|
-
# The actual collection of absolute file and directory names at the time the
|
50
|
-
# ignored glob patterns were expanded. Computed on setup, and recomputed on
|
51
|
-
# reload.
|
52
|
-
#
|
53
|
-
# @private
|
54
|
-
# @sig Set[String]
|
55
|
-
attr_reader :ignored_paths
|
56
|
-
|
57
|
-
# Absolute paths of directories or glob patterns to be collapsed.
|
58
|
-
#
|
59
|
-
# @private
|
60
|
-
# @sig Set[String]
|
61
|
-
attr_reader :collapse_glob_patterns
|
62
|
-
|
63
|
-
# The actual collection of absolute directory names at the time the collapse
|
64
|
-
# glob patterns were expanded. Computed on setup, and recomputed on reload.
|
65
|
-
#
|
66
|
-
# @private
|
67
|
-
# @sig Set[String]
|
68
|
-
attr_reader :collapse_dirs
|
69
|
-
|
70
|
-
# Maps real absolute paths for which an autoload has been set ---and not
|
71
|
-
# executed--- to their corresponding parent class or module and constant
|
72
|
-
# name.
|
73
|
-
#
|
74
|
-
# "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
|
75
|
-
# "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
|
76
|
-
# ...
|
77
|
-
#
|
78
|
-
# @private
|
79
|
-
# @sig Hash[String, [Module, Symbol]]
|
32
|
+
# @sig Zeitwerk::Autoloads
|
80
33
|
attr_reader :autoloads
|
81
34
|
|
82
35
|
# We keep track of autoloaded directories to remove them from the registry
|
@@ -93,8 +46,8 @@ module Zeitwerk
|
|
93
46
|
#
|
94
47
|
# "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
|
95
48
|
#
|
96
|
-
# The cpath as key helps implementing unloadable_cpath? The
|
97
|
-
#
|
49
|
+
# The cpath as key helps implementing unloadable_cpath? The file name is
|
50
|
+
# stored in order to be able to delete it from $LOADED_FEATURES, and the
|
98
51
|
# pair [Module, Symbol] is used to remove_const the constant from the class
|
99
52
|
# or module object.
|
100
53
|
#
|
@@ -123,15 +76,6 @@ module Zeitwerk
|
|
123
76
|
# @sig Hash[String, Array[String]]
|
124
77
|
attr_reader :lazy_subdirs
|
125
78
|
|
126
|
-
# Absolute paths of files or directories not to be eager loaded.
|
127
|
-
#
|
128
|
-
# @private
|
129
|
-
# @sig Set[String]
|
130
|
-
attr_reader :eager_load_exclusions
|
131
|
-
|
132
|
-
# User-oriented callbacks to be fired when a constant is loaded.
|
133
|
-
attr_reader :on_load_callbacks
|
134
|
-
|
135
79
|
# @private
|
136
80
|
# @sig Mutex
|
137
81
|
attr_reader :mutex
|
@@ -141,150 +85,21 @@ module Zeitwerk
|
|
141
85
|
attr_reader :mutex2
|
142
86
|
|
143
87
|
def initialize
|
144
|
-
|
145
|
-
|
146
|
-
@tag = SecureRandom.hex(3)
|
147
|
-
@inflector = Inflector.new
|
148
|
-
@logger = self.class.default_logger
|
149
|
-
|
150
|
-
@root_dirs = {}
|
151
|
-
@preloads = []
|
152
|
-
@ignored_glob_patterns = Set.new
|
153
|
-
@ignored_paths = Set.new
|
154
|
-
@collapse_glob_patterns = Set.new
|
155
|
-
@collapse_dirs = Set.new
|
156
|
-
@autoloads = {}
|
157
|
-
@autoloaded_dirs = []
|
158
|
-
@to_unload = {}
|
159
|
-
@lazy_subdirs = {}
|
160
|
-
@eager_load_exclusions = Set.new
|
161
|
-
@on_load_callbacks = {}
|
162
|
-
|
163
|
-
# TODO: find a better name for these mutexes.
|
164
|
-
@mutex = Mutex.new
|
165
|
-
@mutex2 = Mutex.new
|
166
|
-
@setup = false
|
167
|
-
@eager_loaded = false
|
168
|
-
|
169
|
-
@reloading_enabled = false
|
170
|
-
|
171
|
-
Registry.register_loader(self)
|
172
|
-
end
|
173
|
-
|
174
|
-
# Sets a tag for the loader, useful for logging.
|
175
|
-
#
|
176
|
-
# @param tag [#to_s]
|
177
|
-
# @sig (#to_s) -> void
|
178
|
-
def tag=(tag)
|
179
|
-
@tag = tag.to_s
|
180
|
-
end
|
181
|
-
|
182
|
-
# Absolute paths of the root directories. This is a read-only collection,
|
183
|
-
# please push here via `push_dir`.
|
184
|
-
#
|
185
|
-
# @sig () -> Array[String]
|
186
|
-
def dirs
|
187
|
-
root_dirs.keys.freeze
|
188
|
-
end
|
189
|
-
|
190
|
-
# Pushes `path` to the list of root directories.
|
191
|
-
#
|
192
|
-
# Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
|
193
|
-
# the same process already manages that directory or one of its ascendants
|
194
|
-
# or descendants.
|
195
|
-
#
|
196
|
-
# @raise [Zeitwerk::Error]
|
197
|
-
# @sig (String | Pathname, Module) -> void
|
198
|
-
def push_dir(path, namespace: Object)
|
199
|
-
# Note that Class < Module.
|
200
|
-
unless namespace.is_a?(Module)
|
201
|
-
raise Error, "#{namespace.inspect} is not a class or module object, should be"
|
202
|
-
end
|
88
|
+
super
|
203
89
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
90
|
+
@autoloads = Autoloads.new
|
91
|
+
@autoloaded_dirs = []
|
92
|
+
@to_unload = {}
|
93
|
+
@lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
|
94
|
+
@mutex = Mutex.new
|
95
|
+
@mutex2 = Mutex.new
|
96
|
+
@setup = false
|
97
|
+
@eager_loaded = false
|
212
98
|
|
213
|
-
|
214
|
-
# There is no way to undo this, either you want to reload or you don't.
|
215
|
-
#
|
216
|
-
# @raise [Zeitwerk::Error]
|
217
|
-
# @sig () -> void
|
218
|
-
def enable_reloading
|
219
|
-
mutex.synchronize do
|
220
|
-
break if @reloading_enabled
|
221
|
-
|
222
|
-
if @setup
|
223
|
-
raise Error, "cannot enable reloading after setup"
|
224
|
-
else
|
225
|
-
@reloading_enabled = true
|
226
|
-
end
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
# @sig () -> bool
|
231
|
-
def reloading_enabled?
|
232
|
-
@reloading_enabled
|
233
|
-
end
|
234
|
-
|
235
|
-
# Files or directories to be preloaded instead of lazy loaded.
|
236
|
-
#
|
237
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
238
|
-
def preload(*paths)
|
239
|
-
mutex.synchronize do
|
240
|
-
expand_paths(paths).each do |abspath|
|
241
|
-
preloads << abspath
|
242
|
-
do_preload_abspath(abspath) if @setup
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
# Configure files, directories, or glob patterns to be totally ignored.
|
248
|
-
#
|
249
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
250
|
-
def ignore(*glob_patterns)
|
251
|
-
glob_patterns = expand_paths(glob_patterns)
|
252
|
-
mutex.synchronize do
|
253
|
-
ignored_glob_patterns.merge(glob_patterns)
|
254
|
-
ignored_paths.merge(expand_glob_patterns(glob_patterns))
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
# Configure directories or glob patterns to be collapsed.
|
259
|
-
#
|
260
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
261
|
-
def collapse(*glob_patterns)
|
262
|
-
glob_patterns = expand_paths(glob_patterns)
|
263
|
-
mutex.synchronize do
|
264
|
-
collapse_glob_patterns.merge(glob_patterns)
|
265
|
-
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
# Configure a block to be invoked once a certain constant path is loaded.
|
270
|
-
# Supports multiple callbacks, and if there are many, they are executed in
|
271
|
-
# the order in which they were defined.
|
272
|
-
#
|
273
|
-
# loader.on_load("SomeApiClient") do
|
274
|
-
# SomeApiClient.endpoint = "https://api.dev"
|
275
|
-
# end
|
276
|
-
#
|
277
|
-
# @raise [TypeError]
|
278
|
-
# @sig (String) { () -> void } -> void
|
279
|
-
def on_load(cpath, &block)
|
280
|
-
raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String)
|
281
|
-
|
282
|
-
mutex.synchronize do
|
283
|
-
(on_load_callbacks[cpath] ||= []) << block
|
284
|
-
end
|
99
|
+
Registry.register_loader(self)
|
285
100
|
end
|
286
101
|
|
287
|
-
# Sets autoloads in the root namespace
|
102
|
+
# Sets autoloads in the root namespace.
|
288
103
|
#
|
289
104
|
# @sig () -> void
|
290
105
|
def setup
|
@@ -294,7 +109,8 @@ module Zeitwerk
|
|
294
109
|
actual_root_dirs.each do |root_dir, namespace|
|
295
110
|
set_autoloads_in_dir(root_dir, namespace)
|
296
111
|
end
|
297
|
-
|
112
|
+
|
113
|
+
on_setup_callbacks.each(&:call)
|
298
114
|
|
299
115
|
@setup = true
|
300
116
|
end
|
@@ -307,7 +123,10 @@ module Zeitwerk
|
|
307
123
|
# else, they are eligible for garbage collection, which would effectively
|
308
124
|
# unload them.
|
309
125
|
#
|
310
|
-
#
|
126
|
+
# This method is public but undocumented. Main interface is `reload`, which
|
127
|
+
# means `unload` + `setup`. This one is avaiable to be used together with
|
128
|
+
# `unregister`, which is undocumented too.
|
129
|
+
#
|
311
130
|
# @sig () -> void
|
312
131
|
def unload
|
313
132
|
mutex.synchronize do
|
@@ -319,21 +138,26 @@ module Zeitwerk
|
|
319
138
|
# is enough.
|
320
139
|
unloaded_files = Set.new
|
321
140
|
|
322
|
-
autoloads.each do |
|
141
|
+
autoloads.each do |(parent, cname), abspath|
|
323
142
|
if parent.autoload?(cname)
|
324
143
|
unload_autoload(parent, cname)
|
325
144
|
else
|
326
145
|
# Could happen if loaded with require_relative. That is unsupported,
|
327
146
|
# and the constant path would escape unloadable_cpath? This is just
|
328
147
|
# defensive code to clean things up as much as we are able to.
|
329
|
-
unload_cref(parent, cname)
|
330
|
-
unloaded_files.add(
|
148
|
+
unload_cref(parent, cname) if cdef?(parent, cname)
|
149
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
331
150
|
end
|
332
151
|
end
|
333
152
|
|
334
|
-
to_unload.
|
335
|
-
|
336
|
-
|
153
|
+
to_unload.each do |cpath, (abspath, (parent, cname))|
|
154
|
+
unless on_unload_callbacks.empty?
|
155
|
+
value = parent.const_get(cname)
|
156
|
+
run_on_unload_callbacks(cpath, value, abspath)
|
157
|
+
end
|
158
|
+
|
159
|
+
unload_cref(parent, cname) if cdef?(parent, cname)
|
160
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
337
161
|
end
|
338
162
|
|
339
163
|
unless unloaded_files.empty?
|
@@ -357,7 +181,7 @@ module Zeitwerk
|
|
357
181
|
lazy_subdirs.clear
|
358
182
|
|
359
183
|
Registry.on_unload(self)
|
360
|
-
ExplicitNamespace.
|
184
|
+
ExplicitNamespace.unregister_loader(self)
|
361
185
|
|
362
186
|
@setup = false
|
363
187
|
@eager_loaded = false
|
@@ -393,27 +217,29 @@ module Zeitwerk
|
|
393
217
|
mutex.synchronize do
|
394
218
|
break if @eager_loaded
|
395
219
|
|
220
|
+
log("eager load start") if logger
|
221
|
+
|
396
222
|
queue = []
|
397
223
|
actual_root_dirs.each do |root_dir, namespace|
|
398
|
-
queue << [namespace, root_dir] unless
|
224
|
+
queue << [namespace, root_dir] unless excluded_from_eager_load?(root_dir)
|
399
225
|
end
|
400
226
|
|
401
227
|
while to_eager_load = queue.shift
|
402
228
|
namespace, dir = to_eager_load
|
403
229
|
|
404
230
|
ls(dir) do |basename, abspath|
|
405
|
-
next if
|
231
|
+
next if excluded_from_eager_load?(abspath)
|
406
232
|
|
407
233
|
if ruby?(abspath)
|
408
|
-
if cref = autoloads
|
409
|
-
|
234
|
+
if cref = autoloads.cref_for(abspath)
|
235
|
+
cget(*cref)
|
410
236
|
end
|
411
237
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
412
|
-
if
|
238
|
+
if collapse?(abspath)
|
413
239
|
queue << [namespace, abspath]
|
414
240
|
else
|
415
241
|
cname = inflector.camelize(basename, abspath)
|
416
|
-
queue << [namespace
|
242
|
+
queue << [cget(namespace, cname), abspath]
|
417
243
|
end
|
418
244
|
end
|
419
245
|
end
|
@@ -425,15 +251,9 @@ module Zeitwerk
|
|
425
251
|
autoloaded_dirs.clear
|
426
252
|
|
427
253
|
@eager_loaded = true
|
428
|
-
end
|
429
|
-
end
|
430
254
|
|
431
|
-
|
432
|
-
|
433
|
-
#
|
434
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
435
|
-
def do_not_eager_load(*paths)
|
436
|
-
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
255
|
+
log("eager load end") if logger
|
256
|
+
end
|
437
257
|
end
|
438
258
|
|
439
259
|
# Says if the given constant path would be unloaded on reload. This
|
@@ -452,26 +272,13 @@ module Zeitwerk
|
|
452
272
|
to_unload.keys.freeze
|
453
273
|
end
|
454
274
|
|
455
|
-
#
|
275
|
+
# This is a dangerous method.
|
456
276
|
#
|
277
|
+
# @experimental
|
457
278
|
# @sig () -> void
|
458
|
-
def
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
# @private
|
463
|
-
# @sig (String) -> bool
|
464
|
-
def manages?(dir)
|
465
|
-
dir = dir + "/"
|
466
|
-
ignored_paths.each do |ignored_path|
|
467
|
-
return false if dir.start_with?(ignored_path + "/")
|
468
|
-
end
|
469
|
-
|
470
|
-
root_dirs.each_key do |root_dir|
|
471
|
-
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
472
|
-
end
|
473
|
-
|
474
|
-
false
|
279
|
+
def unregister
|
280
|
+
Registry.unregister_loader(self)
|
281
|
+
ExplicitNamespace.unregister_loader(self)
|
475
282
|
end
|
476
283
|
|
477
284
|
# --- Class methods ---------------------------------------------------------------------------
|
@@ -521,19 +328,12 @@ module Zeitwerk
|
|
521
328
|
|
522
329
|
private # -------------------------------------------------------------------------------------
|
523
330
|
|
524
|
-
# @sig () -> Array[String]
|
525
|
-
def actual_root_dirs
|
526
|
-
root_dirs.reject do |root_dir, _namespace|
|
527
|
-
!dir?(root_dir) || ignored_paths.member?(root_dir)
|
528
|
-
end
|
529
|
-
end
|
530
|
-
|
531
331
|
# @sig (String, Module) -> void
|
532
332
|
def set_autoloads_in_dir(dir, parent)
|
533
333
|
ls(dir) do |basename, abspath|
|
534
334
|
begin
|
535
335
|
if ruby?(basename)
|
536
|
-
basename
|
336
|
+
basename.delete_suffix!(".rb")
|
537
337
|
cname = inflector.camelize(basename, abspath).to_sym
|
538
338
|
autoload_file(parent, cname, abspath)
|
539
339
|
elsif dir?(abspath)
|
@@ -543,9 +343,9 @@ module Zeitwerk
|
|
543
343
|
# To resolve the ambiguity file name -> constant path this introduces,
|
544
344
|
# the `app/models/concerns` directory is totally ignored as a namespace,
|
545
345
|
# it counts only as root. The guard checks that.
|
546
|
-
unless
|
346
|
+
unless root_dir?(abspath)
|
547
347
|
cname = inflector.camelize(basename, abspath).to_sym
|
548
|
-
if
|
348
|
+
if collapse?(abspath)
|
549
349
|
set_autoloads_in_dir(abspath, parent)
|
550
350
|
else
|
551
351
|
autoload_subdir(parent, cname, abspath)
|
@@ -573,27 +373,28 @@ module Zeitwerk
|
|
573
373
|
|
574
374
|
# @sig (Module, Symbol, String) -> void
|
575
375
|
def autoload_subdir(parent, cname, subdir)
|
576
|
-
if autoload_path =
|
376
|
+
if autoload_path = autoloads.abspath_for(parent, cname)
|
577
377
|
cpath = cpath(parent, cname)
|
578
378
|
register_explicit_namespace(cpath) if ruby?(autoload_path)
|
579
379
|
# We do not need to issue another autoload, the existing one is enough
|
580
380
|
# no matter if it is for a file or a directory. Just remember the
|
581
381
|
# subdirectory has to be visited if the namespace is used.
|
582
|
-
|
382
|
+
lazy_subdirs[cpath] << subdir
|
583
383
|
elsif !cdef?(parent, cname)
|
584
384
|
# First time we find this namespace, set an autoload for it.
|
585
|
-
|
385
|
+
lazy_subdirs[cpath(parent, cname)] << subdir
|
586
386
|
set_autoload(parent, cname, subdir)
|
587
387
|
else
|
588
388
|
# For whatever reason the constant that corresponds to this namespace has
|
589
389
|
# already been defined, we have to recurse.
|
590
|
-
|
390
|
+
log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
|
391
|
+
set_autoloads_in_dir(subdir, cget(parent, cname))
|
591
392
|
end
|
592
393
|
end
|
593
394
|
|
594
395
|
# @sig (Module, Symbol, String) -> void
|
595
396
|
def autoload_file(parent, cname, file)
|
596
|
-
if autoload_path =
|
397
|
+
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
597
398
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
598
399
|
if ruby?(autoload_path)
|
599
400
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
@@ -620,163 +421,32 @@ module Zeitwerk
|
|
620
421
|
autoloads.delete(dir)
|
621
422
|
Registry.unregister_autoload(dir)
|
622
423
|
|
424
|
+
log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
425
|
+
|
623
426
|
set_autoload(parent, cname, file)
|
624
427
|
register_explicit_namespace(cpath(parent, cname))
|
625
428
|
end
|
626
429
|
|
627
430
|
# @sig (Module, Symbol, String) -> void
|
628
431
|
def set_autoload(parent, cname, abspath)
|
629
|
-
|
630
|
-
|
631
|
-
# be able to do a lookup later in Kernel#require for manual require calls.
|
632
|
-
#
|
633
|
-
# We freeze realpath because that saves allocations in Module#autoload.
|
634
|
-
# See #125.
|
635
|
-
realpath = File.realpath(abspath).freeze
|
636
|
-
parent.autoload(cname, realpath)
|
432
|
+
autoloads.define(parent, cname, abspath)
|
433
|
+
|
637
434
|
if logger
|
638
|
-
if ruby?(
|
639
|
-
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{
|
435
|
+
if ruby?(abspath)
|
436
|
+
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
|
640
437
|
else
|
641
|
-
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{
|
438
|
+
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
|
642
439
|
end
|
643
440
|
end
|
644
441
|
|
645
|
-
|
646
|
-
Registry.register_autoload(self, realpath)
|
442
|
+
Registry.register_autoload(self, abspath)
|
647
443
|
|
648
444
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
649
445
|
unless parent.autoload?(cname)
|
650
|
-
Registry.register_inception(cpath(parent, cname),
|
651
|
-
end
|
652
|
-
end
|
653
|
-
|
654
|
-
# @sig (Module, Symbol) -> String?
|
655
|
-
def autoload_for?(parent, cname)
|
656
|
-
strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
657
|
-
end
|
658
|
-
|
659
|
-
# The autoload? predicate takes into account the ancestor chain of the
|
660
|
-
# receiver, like const_defined? and other methods in the constants API do.
|
661
|
-
#
|
662
|
-
# For example, given
|
663
|
-
#
|
664
|
-
# class A
|
665
|
-
# autoload :X, "x.rb"
|
666
|
-
# end
|
667
|
-
#
|
668
|
-
# class B < A
|
669
|
-
# end
|
670
|
-
#
|
671
|
-
# B.autoload?(:X) returns "x.rb".
|
672
|
-
#
|
673
|
-
# We need a way to strictly check in parent ignoring ancestors.
|
674
|
-
#
|
675
|
-
# @sig (Module, Symbol) -> String?
|
676
|
-
if method(:autoload?).arity == 1
|
677
|
-
def strict_autoload_path(parent, cname)
|
678
|
-
parent.autoload?(cname) if cdef?(parent, cname)
|
679
|
-
end
|
680
|
-
else
|
681
|
-
def strict_autoload_path(parent, cname)
|
682
|
-
parent.autoload?(cname, false)
|
683
|
-
end
|
684
|
-
end
|
685
|
-
|
686
|
-
# This method is called this way because I prefer `preload` to be the method
|
687
|
-
# name to configure preloads in the public interface.
|
688
|
-
#
|
689
|
-
# @sig () -> void
|
690
|
-
def do_preload
|
691
|
-
preloads.each do |abspath|
|
692
|
-
do_preload_abspath(abspath)
|
693
|
-
end
|
694
|
-
end
|
695
|
-
|
696
|
-
# @sig (String) -> void
|
697
|
-
def do_preload_abspath(abspath)
|
698
|
-
if ruby?(abspath)
|
699
|
-
do_preload_file(abspath)
|
700
|
-
elsif dir?(abspath)
|
701
|
-
do_preload_dir(abspath)
|
446
|
+
Registry.register_inception(cpath(parent, cname), abspath, self)
|
702
447
|
end
|
703
448
|
end
|
704
449
|
|
705
|
-
# @sig (String) -> void
|
706
|
-
def do_preload_dir(dir)
|
707
|
-
ls(dir) do |_basename, abspath|
|
708
|
-
do_preload_abspath(abspath)
|
709
|
-
end
|
710
|
-
end
|
711
|
-
|
712
|
-
# @sig (String) -> bool
|
713
|
-
def do_preload_file(file)
|
714
|
-
log("preloading #{file}") if logger
|
715
|
-
require file
|
716
|
-
end
|
717
|
-
|
718
|
-
# @sig (Module, Symbol) -> String
|
719
|
-
def cpath(parent, cname)
|
720
|
-
parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
721
|
-
end
|
722
|
-
|
723
|
-
# @sig (String) { (String, String) -> void } -> void
|
724
|
-
def ls(dir)
|
725
|
-
Dir.foreach(dir) do |basename|
|
726
|
-
next if basename.start_with?(".")
|
727
|
-
|
728
|
-
abspath = File.join(dir, basename)
|
729
|
-
next if ignored_paths.member?(abspath)
|
730
|
-
|
731
|
-
# We freeze abspath because that saves allocations when passed later to
|
732
|
-
# File methods. See #125.
|
733
|
-
yield basename, abspath.freeze
|
734
|
-
end
|
735
|
-
end
|
736
|
-
|
737
|
-
# @sig (String) -> bool
|
738
|
-
def ruby?(path)
|
739
|
-
path.end_with?(".rb")
|
740
|
-
end
|
741
|
-
|
742
|
-
# @sig (String) -> bool
|
743
|
-
def dir?(path)
|
744
|
-
File.directory?(path)
|
745
|
-
end
|
746
|
-
|
747
|
-
# @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
|
748
|
-
def expand_paths(paths)
|
749
|
-
paths.flatten.map! { |path| File.expand_path(path) }
|
750
|
-
end
|
751
|
-
|
752
|
-
# @sig (Array[String]) -> Array[String]
|
753
|
-
def expand_glob_patterns(glob_patterns)
|
754
|
-
# Note that Dir.glob works with regular file names just fine. That is,
|
755
|
-
# glob patterns technically need no wildcards.
|
756
|
-
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
757
|
-
end
|
758
|
-
|
759
|
-
# @sig () -> void
|
760
|
-
def recompute_ignored_paths
|
761
|
-
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
762
|
-
end
|
763
|
-
|
764
|
-
# @sig () -> void
|
765
|
-
def recompute_collapse_dirs
|
766
|
-
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
767
|
-
end
|
768
|
-
|
769
|
-
# @sig (String) -> void
|
770
|
-
def log(message)
|
771
|
-
method_name = logger.respond_to?(:debug) ? :debug : :call
|
772
|
-
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
773
|
-
end
|
774
|
-
|
775
|
-
# @sig (Module, Symbol) -> bool
|
776
|
-
def cdef?(parent, cname)
|
777
|
-
parent.const_defined?(cname, false)
|
778
|
-
end
|
779
|
-
|
780
450
|
# @sig (String) -> void
|
781
451
|
def register_explicit_namespace(cpath)
|
782
452
|
ExplicitNamespace.register(cpath, self)
|
@@ -786,17 +456,33 @@ module Zeitwerk
|
|
786
456
|
def raise_if_conflicting_directory(dir)
|
787
457
|
self.class.mutex.synchronize do
|
788
458
|
Registry.loaders.each do |loader|
|
789
|
-
if loader
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
459
|
+
next if loader == self
|
460
|
+
next if loader.ignores?(dir)
|
461
|
+
|
462
|
+
dir = dir + "/"
|
463
|
+
loader.root_dirs.each do |root_dir, _namespace|
|
464
|
+
next if ignores?(root_dir)
|
465
|
+
|
466
|
+
root_dir = root_dir + "/"
|
467
|
+
if dir.start_with?(root_dir) || root_dir.start_with?(dir)
|
468
|
+
require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
|
469
|
+
raise Error,
|
470
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
|
471
|
+
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
472
|
+
EOS
|
473
|
+
end
|
795
474
|
end
|
796
475
|
end
|
797
476
|
end
|
798
477
|
end
|
799
478
|
|
479
|
+
# @sig (String, Object, String) -> void
|
480
|
+
def run_on_unload_callbacks(cpath, value, abspath)
|
481
|
+
# Order matters. If present, run the most specific one.
|
482
|
+
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
483
|
+
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
484
|
+
end
|
485
|
+
|
800
486
|
# @sig (Module, Symbol) -> void
|
801
487
|
def unload_autoload(parent, cname)
|
802
488
|
parent.__send__(:remove_const, cname)
|