zeitwerk 2.4.1 → 2.5.0.beta3
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 +221 -57
- data/lib/zeitwerk/autoloads.rb +69 -0
- data/lib/zeitwerk/explicit_namespace.rb +9 -3
- data/lib/zeitwerk/kernel.rb +6 -4
- data/lib/zeitwerk/loader/callbacks.rb +25 -6
- data/lib/zeitwerk/loader/config.rb +301 -0
- data/lib/zeitwerk/loader/helpers.rb +95 -0
- data/lib/zeitwerk/loader.rb +100 -394
- 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
|
14
|
-
|
15
|
-
# @sig #camelize
|
16
|
-
attr_accessor :inflector
|
17
|
-
|
18
|
-
# @sig #call | #debug | nil
|
19
|
-
attr_accessor :logger
|
10
|
+
require_relative "loader/config"
|
20
11
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
# "/Users/fxn/blog/app/assets" => true,
|
26
|
-
# "/Users/fxn/blog/app/channels" => true,
|
27
|
-
# ...
|
28
|
-
#
|
29
|
-
# This is a private collection maintained by the loader. The public
|
30
|
-
# interface for it is `push_dir` and `dirs`.
|
31
|
-
#
|
32
|
-
# @private
|
33
|
-
# @sig Hash[String, true]
|
34
|
-
attr_reader :root_dirs
|
12
|
+
include RealModName
|
13
|
+
include Callbacks
|
14
|
+
include Helpers
|
15
|
+
include Config
|
35
16
|
|
36
|
-
#
|
17
|
+
# Keeps track of autoloads defined by the loader which have not been
|
18
|
+
# executed so far.
|
37
19
|
#
|
38
|
-
#
|
39
|
-
# @sig Array[String]
|
40
|
-
attr_reader :preloads
|
41
|
-
|
42
|
-
# Absolute paths of files, directories, or glob patterns to be totally
|
43
|
-
# ignored.
|
20
|
+
# This metadata helps us implement a few things:
|
44
21
|
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
|
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.
|
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.
|
52
25
|
#
|
53
|
-
#
|
54
|
-
# @sig Set[String]
|
55
|
-
attr_reader :ignored_paths
|
56
|
-
|
57
|
-
# Absolute paths of directories or glob patterns to be collapsed.
|
26
|
+
# 2. When unloading, remove autoloads that have not been executed.
|
58
27
|
#
|
59
|
-
#
|
60
|
-
#
|
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.
|
28
|
+
# 3. Eager load with a recursive const_get, rather than a recursive require,
|
29
|
+
# for consistency with lazy loading.
|
65
30
|
#
|
66
31
|
# @private
|
67
|
-
# @sig
|
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,12 +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
79
|
# @private
|
133
80
|
# @sig Mutex
|
134
81
|
attr_reader :mutex
|
@@ -138,131 +85,21 @@ module Zeitwerk
|
|
138
85
|
attr_reader :mutex2
|
139
86
|
|
140
87
|
def initialize
|
141
|
-
|
142
|
-
|
143
|
-
@tag = SecureRandom.hex(3)
|
144
|
-
@inflector = Inflector.new
|
145
|
-
@logger = self.class.default_logger
|
146
|
-
|
147
|
-
@root_dirs = {}
|
148
|
-
@preloads = []
|
149
|
-
@ignored_glob_patterns = Set.new
|
150
|
-
@ignored_paths = Set.new
|
151
|
-
@collapse_glob_patterns = Set.new
|
152
|
-
@collapse_dirs = Set.new
|
153
|
-
@autoloads = {}
|
154
|
-
@autoloaded_dirs = []
|
155
|
-
@to_unload = {}
|
156
|
-
@lazy_subdirs = {}
|
157
|
-
@eager_load_exclusions = Set.new
|
158
|
-
|
159
|
-
# TODO: find a better name for these mutexes.
|
160
|
-
@mutex = Mutex.new
|
161
|
-
@mutex2 = Mutex.new
|
162
|
-
@setup = false
|
163
|
-
@eager_loaded = false
|
164
|
-
|
165
|
-
@reloading_enabled = false
|
166
|
-
|
167
|
-
Registry.register_loader(self)
|
168
|
-
end
|
169
|
-
|
170
|
-
# Sets a tag for the loader, useful for logging.
|
171
|
-
#
|
172
|
-
# @param tag [#to_s]
|
173
|
-
# @sig (#to_s) -> void
|
174
|
-
def tag=(tag)
|
175
|
-
@tag = tag.to_s
|
176
|
-
end
|
177
|
-
|
178
|
-
# Absolute paths of the root directories. This is a read-only collection,
|
179
|
-
# please push here via `push_dir`.
|
180
|
-
#
|
181
|
-
# @sig () -> Array[String]
|
182
|
-
def dirs
|
183
|
-
root_dirs.keys.freeze
|
184
|
-
end
|
185
|
-
|
186
|
-
# Pushes `path` to the list of root directories.
|
187
|
-
#
|
188
|
-
# Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
|
189
|
-
# the same process already manages that directory or one of its ascendants
|
190
|
-
# or descendants.
|
191
|
-
#
|
192
|
-
# @raise [Zeitwerk::Error]
|
193
|
-
# @sig (String | Pathname, Module) -> void
|
194
|
-
def push_dir(path, namespace: Object)
|
195
|
-
# Note that Class < Module.
|
196
|
-
unless namespace.is_a?(Module)
|
197
|
-
raise Error, "#{namespace.inspect} is not a class or module object, should be"
|
198
|
-
end
|
199
|
-
|
200
|
-
abspath = File.expand_path(path)
|
201
|
-
if dir?(abspath)
|
202
|
-
raise_if_conflicting_directory(abspath)
|
203
|
-
root_dirs[abspath] = namespace
|
204
|
-
else
|
205
|
-
raise Error, "the root directory #{abspath} does not exist"
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
# You need to call this method before setup in order to be able to reload.
|
210
|
-
# There is no way to undo this, either you want to reload or you don't.
|
211
|
-
#
|
212
|
-
# @raise [Zeitwerk::Error]
|
213
|
-
# @sig () -> void
|
214
|
-
def enable_reloading
|
215
|
-
mutex.synchronize do
|
216
|
-
break if @reloading_enabled
|
217
|
-
|
218
|
-
if @setup
|
219
|
-
raise Error, "cannot enable reloading after setup"
|
220
|
-
else
|
221
|
-
@reloading_enabled = true
|
222
|
-
end
|
223
|
-
end
|
224
|
-
end
|
88
|
+
super
|
225
89
|
|
226
|
-
|
227
|
-
|
228
|
-
@
|
229
|
-
|
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
|
230
98
|
|
231
|
-
|
232
|
-
#
|
233
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
234
|
-
def preload(*paths)
|
235
|
-
mutex.synchronize do
|
236
|
-
expand_paths(paths).each do |abspath|
|
237
|
-
preloads << abspath
|
238
|
-
do_preload_abspath(abspath) if @setup
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
|
-
# Configure files, directories, or glob patterns to be totally ignored.
|
244
|
-
#
|
245
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
246
|
-
def ignore(*glob_patterns)
|
247
|
-
glob_patterns = expand_paths(glob_patterns)
|
248
|
-
mutex.synchronize do
|
249
|
-
ignored_glob_patterns.merge(glob_patterns)
|
250
|
-
ignored_paths.merge(expand_glob_patterns(glob_patterns))
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
# Configure directories or glob patterns to be collapsed.
|
255
|
-
#
|
256
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
257
|
-
def collapse(*glob_patterns)
|
258
|
-
glob_patterns = expand_paths(glob_patterns)
|
259
|
-
mutex.synchronize do
|
260
|
-
collapse_glob_patterns.merge(glob_patterns)
|
261
|
-
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
262
|
-
end
|
99
|
+
Registry.register_loader(self)
|
263
100
|
end
|
264
101
|
|
265
|
-
# Sets autoloads in the root namespace
|
102
|
+
# Sets autoloads in the root namespace.
|
266
103
|
#
|
267
104
|
# @sig () -> void
|
268
105
|
def setup
|
@@ -272,7 +109,6 @@ module Zeitwerk
|
|
272
109
|
actual_root_dirs.each do |root_dir, namespace|
|
273
110
|
set_autoloads_in_dir(root_dir, namespace)
|
274
111
|
end
|
275
|
-
do_preload
|
276
112
|
|
277
113
|
@setup = true
|
278
114
|
end
|
@@ -285,7 +121,10 @@ module Zeitwerk
|
|
285
121
|
# else, they are eligible for garbage collection, which would effectively
|
286
122
|
# unload them.
|
287
123
|
#
|
288
|
-
#
|
124
|
+
# This method is public but undocumented. Main interface is `reload`, which
|
125
|
+
# means `unload` + `setup`. This one is avaiable to be used together with
|
126
|
+
# `unregister`, which is undocumented too.
|
127
|
+
#
|
289
128
|
# @sig () -> void
|
290
129
|
def unload
|
291
130
|
mutex.synchronize do
|
@@ -297,21 +136,26 @@ module Zeitwerk
|
|
297
136
|
# is enough.
|
298
137
|
unloaded_files = Set.new
|
299
138
|
|
300
|
-
autoloads.each do |
|
139
|
+
autoloads.each do |(parent, cname), abspath|
|
301
140
|
if parent.autoload?(cname)
|
302
141
|
unload_autoload(parent, cname)
|
303
142
|
else
|
304
143
|
# Could happen if loaded with require_relative. That is unsupported,
|
305
144
|
# and the constant path would escape unloadable_cpath? This is just
|
306
145
|
# defensive code to clean things up as much as we are able to.
|
307
|
-
unload_cref(parent, cname)
|
308
|
-
unloaded_files.add(
|
146
|
+
unload_cref(parent, cname) if cdef?(parent, cname)
|
147
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
309
148
|
end
|
310
149
|
end
|
311
150
|
|
312
|
-
to_unload.
|
313
|
-
|
314
|
-
|
151
|
+
to_unload.each do |cpath, (abspath, (parent, cname))|
|
152
|
+
unless on_unload_callbacks.empty?
|
153
|
+
value = parent.const_get(cname)
|
154
|
+
run_on_unload_callbacks(cpath, value, abspath)
|
155
|
+
end
|
156
|
+
|
157
|
+
unload_cref(parent, cname) if cdef?(parent, cname)
|
158
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
315
159
|
end
|
316
160
|
|
317
161
|
unless unloaded_files.empty?
|
@@ -335,7 +179,7 @@ module Zeitwerk
|
|
335
179
|
lazy_subdirs.clear
|
336
180
|
|
337
181
|
Registry.on_unload(self)
|
338
|
-
ExplicitNamespace.
|
182
|
+
ExplicitNamespace.unregister_loader(self)
|
339
183
|
|
340
184
|
@setup = false
|
341
185
|
@eager_loaded = false
|
@@ -371,27 +215,29 @@ module Zeitwerk
|
|
371
215
|
mutex.synchronize do
|
372
216
|
break if @eager_loaded
|
373
217
|
|
218
|
+
log("eager load start") if logger
|
219
|
+
|
374
220
|
queue = []
|
375
221
|
actual_root_dirs.each do |root_dir, namespace|
|
376
|
-
queue << [namespace, root_dir] unless
|
222
|
+
queue << [namespace, root_dir] unless excluded_from_eager_load?(root_dir)
|
377
223
|
end
|
378
224
|
|
379
225
|
while to_eager_load = queue.shift
|
380
226
|
namespace, dir = to_eager_load
|
381
227
|
|
382
228
|
ls(dir) do |basename, abspath|
|
383
|
-
next if
|
229
|
+
next if excluded_from_eager_load?(abspath)
|
384
230
|
|
385
231
|
if ruby?(abspath)
|
386
|
-
if cref = autoloads
|
387
|
-
|
232
|
+
if cref = autoloads.cref_for(abspath)
|
233
|
+
cget(*cref)
|
388
234
|
end
|
389
235
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
390
|
-
if
|
236
|
+
if collapse?(abspath)
|
391
237
|
queue << [namespace, abspath]
|
392
238
|
else
|
393
239
|
cname = inflector.camelize(basename, abspath)
|
394
|
-
queue << [namespace
|
240
|
+
queue << [cget(namespace, cname), abspath]
|
395
241
|
end
|
396
242
|
end
|
397
243
|
end
|
@@ -403,15 +249,9 @@ module Zeitwerk
|
|
403
249
|
autoloaded_dirs.clear
|
404
250
|
|
405
251
|
@eager_loaded = true
|
406
|
-
end
|
407
|
-
end
|
408
252
|
|
409
|
-
|
410
|
-
|
411
|
-
#
|
412
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
413
|
-
def do_not_eager_load(*paths)
|
414
|
-
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
253
|
+
log("eager load end") if logger
|
254
|
+
end
|
415
255
|
end
|
416
256
|
|
417
257
|
# Says if the given constant path would be unloaded on reload. This
|
@@ -430,26 +270,13 @@ module Zeitwerk
|
|
430
270
|
to_unload.keys.freeze
|
431
271
|
end
|
432
272
|
|
433
|
-
#
|
273
|
+
# This is a dangerous method.
|
434
274
|
#
|
275
|
+
# @experimental
|
435
276
|
# @sig () -> void
|
436
|
-
def
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
# @private
|
441
|
-
# @sig (String) -> bool
|
442
|
-
def manages?(dir)
|
443
|
-
dir = dir + "/"
|
444
|
-
ignored_paths.each do |ignored_path|
|
445
|
-
return false if dir.start_with?(ignored_path + "/")
|
446
|
-
end
|
447
|
-
|
448
|
-
root_dirs.each_key do |root_dir|
|
449
|
-
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
450
|
-
end
|
451
|
-
|
452
|
-
false
|
277
|
+
def unregister
|
278
|
+
Registry.unregister_loader(self)
|
279
|
+
ExplicitNamespace.unregister_loader(self)
|
453
280
|
end
|
454
281
|
|
455
282
|
# --- Class methods ---------------------------------------------------------------------------
|
@@ -499,19 +326,12 @@ module Zeitwerk
|
|
499
326
|
|
500
327
|
private # -------------------------------------------------------------------------------------
|
501
328
|
|
502
|
-
# @sig () -> Array[String]
|
503
|
-
def actual_root_dirs
|
504
|
-
root_dirs.reject do |root_dir, _namespace|
|
505
|
-
!dir?(root_dir) || ignored_paths.member?(root_dir)
|
506
|
-
end
|
507
|
-
end
|
508
|
-
|
509
329
|
# @sig (String, Module) -> void
|
510
330
|
def set_autoloads_in_dir(dir, parent)
|
511
331
|
ls(dir) do |basename, abspath|
|
512
332
|
begin
|
513
333
|
if ruby?(basename)
|
514
|
-
basename
|
334
|
+
basename.delete_suffix!(".rb")
|
515
335
|
cname = inflector.camelize(basename, abspath).to_sym
|
516
336
|
autoload_file(parent, cname, abspath)
|
517
337
|
elsif dir?(abspath)
|
@@ -521,9 +341,9 @@ module Zeitwerk
|
|
521
341
|
# To resolve the ambiguity file name -> constant path this introduces,
|
522
342
|
# the `app/models/concerns` directory is totally ignored as a namespace,
|
523
343
|
# it counts only as root. The guard checks that.
|
524
|
-
unless
|
344
|
+
unless root_dir?(abspath)
|
525
345
|
cname = inflector.camelize(basename, abspath).to_sym
|
526
|
-
if
|
346
|
+
if collapse?(abspath)
|
527
347
|
set_autoloads_in_dir(abspath, parent)
|
528
348
|
else
|
529
349
|
autoload_subdir(parent, cname, abspath)
|
@@ -551,27 +371,28 @@ module Zeitwerk
|
|
551
371
|
|
552
372
|
# @sig (Module, Symbol, String) -> void
|
553
373
|
def autoload_subdir(parent, cname, subdir)
|
554
|
-
if autoload_path =
|
374
|
+
if autoload_path = autoloads.abspath_for(parent, cname)
|
555
375
|
cpath = cpath(parent, cname)
|
556
376
|
register_explicit_namespace(cpath) if ruby?(autoload_path)
|
557
377
|
# We do not need to issue another autoload, the existing one is enough
|
558
378
|
# no matter if it is for a file or a directory. Just remember the
|
559
379
|
# subdirectory has to be visited if the namespace is used.
|
560
|
-
|
380
|
+
lazy_subdirs[cpath] << subdir
|
561
381
|
elsif !cdef?(parent, cname)
|
562
382
|
# First time we find this namespace, set an autoload for it.
|
563
|
-
|
383
|
+
lazy_subdirs[cpath(parent, cname)] << subdir
|
564
384
|
set_autoload(parent, cname, subdir)
|
565
385
|
else
|
566
386
|
# For whatever reason the constant that corresponds to this namespace has
|
567
387
|
# already been defined, we have to recurse.
|
568
|
-
|
388
|
+
log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
|
389
|
+
set_autoloads_in_dir(subdir, cget(parent, cname))
|
569
390
|
end
|
570
391
|
end
|
571
392
|
|
572
393
|
# @sig (Module, Symbol, String) -> void
|
573
394
|
def autoload_file(parent, cname, file)
|
574
|
-
if autoload_path =
|
395
|
+
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
575
396
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
576
397
|
if ruby?(autoload_path)
|
577
398
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
@@ -598,163 +419,32 @@ module Zeitwerk
|
|
598
419
|
autoloads.delete(dir)
|
599
420
|
Registry.unregister_autoload(dir)
|
600
421
|
|
422
|
+
log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
423
|
+
|
601
424
|
set_autoload(parent, cname, file)
|
602
425
|
register_explicit_namespace(cpath(parent, cname))
|
603
426
|
end
|
604
427
|
|
605
428
|
# @sig (Module, Symbol, String) -> void
|
606
429
|
def set_autoload(parent, cname, abspath)
|
607
|
-
|
608
|
-
|
609
|
-
# be able to do a lookup later in Kernel#require for manual require calls.
|
610
|
-
#
|
611
|
-
# We freeze realpath because that saves allocations in Module#autoload.
|
612
|
-
# See #125.
|
613
|
-
realpath = File.realpath(abspath).freeze
|
614
|
-
parent.autoload(cname, realpath)
|
430
|
+
autoloads.define(parent, cname, abspath)
|
431
|
+
|
615
432
|
if logger
|
616
|
-
if ruby?(
|
617
|
-
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{
|
433
|
+
if ruby?(abspath)
|
434
|
+
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
|
618
435
|
else
|
619
|
-
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{
|
436
|
+
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
|
620
437
|
end
|
621
438
|
end
|
622
439
|
|
623
|
-
|
624
|
-
Registry.register_autoload(self, realpath)
|
440
|
+
Registry.register_autoload(self, abspath)
|
625
441
|
|
626
442
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
627
443
|
unless parent.autoload?(cname)
|
628
|
-
Registry.register_inception(cpath(parent, cname),
|
629
|
-
end
|
630
|
-
end
|
631
|
-
|
632
|
-
# @sig (Module, Symbol) -> String?
|
633
|
-
def autoload_for?(parent, cname)
|
634
|
-
strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
635
|
-
end
|
636
|
-
|
637
|
-
# The autoload? predicate takes into account the ancestor chain of the
|
638
|
-
# receiver, like const_defined? and other methods in the constants API do.
|
639
|
-
#
|
640
|
-
# For example, given
|
641
|
-
#
|
642
|
-
# class A
|
643
|
-
# autoload :X, "x.rb"
|
644
|
-
# end
|
645
|
-
#
|
646
|
-
# class B < A
|
647
|
-
# end
|
648
|
-
#
|
649
|
-
# B.autoload?(:X) returns "x.rb".
|
650
|
-
#
|
651
|
-
# We need a way to strictly check in parent ignoring ancestors.
|
652
|
-
#
|
653
|
-
# @sig (Module, Symbol) -> String?
|
654
|
-
if method(:autoload?).arity == 1
|
655
|
-
def strict_autoload_path(parent, cname)
|
656
|
-
parent.autoload?(cname) if cdef?(parent, cname)
|
657
|
-
end
|
658
|
-
else
|
659
|
-
def strict_autoload_path(parent, cname)
|
660
|
-
parent.autoload?(cname, false)
|
661
|
-
end
|
662
|
-
end
|
663
|
-
|
664
|
-
# This method is called this way because I prefer `preload` to be the method
|
665
|
-
# name to configure preloads in the public interface.
|
666
|
-
#
|
667
|
-
# @sig () -> void
|
668
|
-
def do_preload
|
669
|
-
preloads.each do |abspath|
|
670
|
-
do_preload_abspath(abspath)
|
671
|
-
end
|
672
|
-
end
|
673
|
-
|
674
|
-
# @sig (String) -> void
|
675
|
-
def do_preload_abspath(abspath)
|
676
|
-
if ruby?(abspath)
|
677
|
-
do_preload_file(abspath)
|
678
|
-
elsif dir?(abspath)
|
679
|
-
do_preload_dir(abspath)
|
444
|
+
Registry.register_inception(cpath(parent, cname), abspath, self)
|
680
445
|
end
|
681
446
|
end
|
682
447
|
|
683
|
-
# @sig (String) -> void
|
684
|
-
def do_preload_dir(dir)
|
685
|
-
ls(dir) do |_basename, abspath|
|
686
|
-
do_preload_abspath(abspath)
|
687
|
-
end
|
688
|
-
end
|
689
|
-
|
690
|
-
# @sig (String) -> bool
|
691
|
-
def do_preload_file(file)
|
692
|
-
log("preloading #{file}") if logger
|
693
|
-
require file
|
694
|
-
end
|
695
|
-
|
696
|
-
# @sig (Module, Symbol) -> String
|
697
|
-
def cpath(parent, cname)
|
698
|
-
parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
699
|
-
end
|
700
|
-
|
701
|
-
# @sig (String) { (String, String) -> void } -> void
|
702
|
-
def ls(dir)
|
703
|
-
Dir.foreach(dir) do |basename|
|
704
|
-
next if basename.start_with?(".")
|
705
|
-
|
706
|
-
abspath = File.join(dir, basename)
|
707
|
-
next if ignored_paths.member?(abspath)
|
708
|
-
|
709
|
-
# We freeze abspath because that saves allocations when passed later to
|
710
|
-
# File methods. See #125.
|
711
|
-
yield basename, abspath.freeze
|
712
|
-
end
|
713
|
-
end
|
714
|
-
|
715
|
-
# @sig (String) -> bool
|
716
|
-
def ruby?(path)
|
717
|
-
path.end_with?(".rb")
|
718
|
-
end
|
719
|
-
|
720
|
-
# @sig (String) -> bool
|
721
|
-
def dir?(path)
|
722
|
-
File.directory?(path)
|
723
|
-
end
|
724
|
-
|
725
|
-
# @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
|
726
|
-
def expand_paths(paths)
|
727
|
-
paths.flatten.map! { |path| File.expand_path(path) }
|
728
|
-
end
|
729
|
-
|
730
|
-
# @sig (Array[String]) -> Array[String]
|
731
|
-
def expand_glob_patterns(glob_patterns)
|
732
|
-
# Note that Dir.glob works with regular file names just fine. That is,
|
733
|
-
# glob patterns technically need no wildcards.
|
734
|
-
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
735
|
-
end
|
736
|
-
|
737
|
-
# @sig () -> void
|
738
|
-
def recompute_ignored_paths
|
739
|
-
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
740
|
-
end
|
741
|
-
|
742
|
-
# @sig () -> void
|
743
|
-
def recompute_collapse_dirs
|
744
|
-
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
745
|
-
end
|
746
|
-
|
747
|
-
# @sig (String) -> void
|
748
|
-
def log(message)
|
749
|
-
method_name = logger.respond_to?(:debug) ? :debug : :call
|
750
|
-
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
751
|
-
end
|
752
|
-
|
753
|
-
# @sig (Module, Symbol) -> bool
|
754
|
-
def cdef?(parent, cname)
|
755
|
-
parent.const_defined?(cname, false)
|
756
|
-
end
|
757
|
-
|
758
448
|
# @sig (String) -> void
|
759
449
|
def register_explicit_namespace(cpath)
|
760
450
|
ExplicitNamespace.register(cpath, self)
|
@@ -764,17 +454,33 @@ module Zeitwerk
|
|
764
454
|
def raise_if_conflicting_directory(dir)
|
765
455
|
self.class.mutex.synchronize do
|
766
456
|
Registry.loaders.each do |loader|
|
767
|
-
if loader
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
457
|
+
next if loader == self
|
458
|
+
next if loader.ignores?(dir)
|
459
|
+
|
460
|
+
dir = dir + "/"
|
461
|
+
loader.root_dirs.each do |root_dir, _namespace|
|
462
|
+
next if ignores?(root_dir)
|
463
|
+
|
464
|
+
root_dir = root_dir + "/"
|
465
|
+
if dir.start_with?(root_dir) || root_dir.start_with?(dir)
|
466
|
+
require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
|
467
|
+
raise Error,
|
468
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
|
469
|
+
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
470
|
+
EOS
|
471
|
+
end
|
773
472
|
end
|
774
473
|
end
|
775
474
|
end
|
776
475
|
end
|
777
476
|
|
477
|
+
# @sig (String, Object, String) -> void
|
478
|
+
def run_on_unload_callbacks(cpath, value, abspath)
|
479
|
+
# Order matters. If present, run the most specific one.
|
480
|
+
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
481
|
+
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
482
|
+
end
|
483
|
+
|
778
484
|
# @sig (Module, Symbol) -> void
|
779
485
|
def unload_autoload(parent, cname)
|
780
486
|
parent.__send__(:remove_const, cname)
|