zeitwerk 2.4.0 → 2.5.0
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 +325 -60
- data/lib/zeitwerk/autoloads.rb +71 -0
- data/lib/zeitwerk/error.rb +2 -0
- data/lib/zeitwerk/explicit_namespace.rb +20 -11
- data/lib/zeitwerk/gem_inflector.rb +2 -4
- data/lib/zeitwerk/inflector.rb +3 -6
- data/lib/zeitwerk/kernel.rb +8 -7
- data/lib/zeitwerk/loader/callbacks.rb +35 -14
- data/lib/zeitwerk/loader/config.rb +321 -0
- data/lib/zeitwerk/loader/helpers.rb +97 -0
- data/lib/zeitwerk/loader.rb +144 -464
- data/lib/zeitwerk/real_mod_name.rb +3 -2
- data/lib/zeitwerk/registry.rb +28 -30
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -0
- metadata +10 -7
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
|
-
# @return [String]
|
|
13
|
-
attr_reader :tag
|
|
14
|
-
|
|
15
|
-
# @return [#camelize]
|
|
16
|
-
attr_accessor :inflector
|
|
17
|
-
|
|
18
|
-
# @return [#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
|
-
# @return [{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
|
-
# @return [<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
|
-
# @return [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
|
-
# @
|
|
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
|
-
# @return [{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
|
|
@@ -86,15 +39,15 @@ module Zeitwerk
|
|
|
86
39
|
# to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
|
|
87
40
|
#
|
|
88
41
|
# @private
|
|
89
|
-
# @
|
|
42
|
+
# @sig Array[String]
|
|
90
43
|
attr_reader :autoloaded_dirs
|
|
91
44
|
|
|
92
45
|
# Stores metadata needed for unloading. Its entries look like this:
|
|
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
|
#
|
|
@@ -102,7 +55,7 @@ module Zeitwerk
|
|
|
102
55
|
# or eager loaded. Otherwise, the collection remains empty.
|
|
103
56
|
#
|
|
104
57
|
# @private
|
|
105
|
-
# @
|
|
58
|
+
# @sig Hash[String, [String, [Module, Symbol]]]
|
|
106
59
|
attr_reader :to_unload
|
|
107
60
|
|
|
108
61
|
# Maps constant paths of namespaces to arrays of corresponding directories.
|
|
@@ -120,156 +73,35 @@ module Zeitwerk
|
|
|
120
73
|
# up the corresponding autoloads.
|
|
121
74
|
#
|
|
122
75
|
# @private
|
|
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
79
|
# @private
|
|
129
|
-
# @
|
|
130
|
-
attr_reader :eager_load_exclusions
|
|
131
|
-
|
|
132
|
-
# @private
|
|
133
|
-
# @return [Mutex]
|
|
80
|
+
# @sig Mutex
|
|
134
81
|
attr_reader :mutex
|
|
135
82
|
|
|
136
83
|
# @private
|
|
137
|
-
# @
|
|
84
|
+
# @sig Mutex
|
|
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
|
|
88
|
+
super
|
|
169
89
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
@
|
|
176
|
-
|
|
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
|
|
177
98
|
|
|
178
|
-
|
|
179
|
-
# please push here via `push_dir`.
|
|
180
|
-
#
|
|
181
|
-
# @return [<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
|
-
# @param path [<String, Pathname>]
|
|
193
|
-
# @param namespace [Class, Module]
|
|
194
|
-
# @raise [Zeitwerk::Error]
|
|
195
|
-
# @return [void]
|
|
196
|
-
def push_dir(path, namespace: Object)
|
|
197
|
-
# Note that Class < Module.
|
|
198
|
-
unless namespace.is_a?(Module)
|
|
199
|
-
raise Error, "#{namespace.inspect} is not a class or module object, should be"
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
abspath = File.expand_path(path)
|
|
203
|
-
if dir?(abspath)
|
|
204
|
-
raise_if_conflicting_directory(abspath)
|
|
205
|
-
root_dirs[abspath] = namespace
|
|
206
|
-
else
|
|
207
|
-
raise Error, "the root directory #{abspath} does not exist"
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# You need to call this method before setup in order to be able to reload.
|
|
212
|
-
# There is no way to undo this, either you want to reload or you don't.
|
|
213
|
-
#
|
|
214
|
-
# @raise [Zeitwerk::Error]
|
|
215
|
-
# @return [void]
|
|
216
|
-
def enable_reloading
|
|
217
|
-
mutex.synchronize do
|
|
218
|
-
break if @reloading_enabled
|
|
219
|
-
|
|
220
|
-
if @setup
|
|
221
|
-
raise Error, "cannot enable reloading after setup"
|
|
222
|
-
else
|
|
223
|
-
@reloading_enabled = true
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# @return [Boolean]
|
|
229
|
-
def reloading_enabled?
|
|
230
|
-
@reloading_enabled
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# Files or directories to be preloaded instead of lazy loaded.
|
|
234
|
-
#
|
|
235
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
|
236
|
-
# @return [void]
|
|
237
|
-
def preload(*paths)
|
|
238
|
-
mutex.synchronize do
|
|
239
|
-
expand_paths(paths).each do |abspath|
|
|
240
|
-
preloads << abspath
|
|
241
|
-
do_preload_abspath(abspath) if @setup
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Configure files, directories, or glob patterns to be totally ignored.
|
|
247
|
-
#
|
|
248
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
|
249
|
-
# @return [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
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
|
261
|
-
# @return [void]
|
|
262
|
-
def collapse(*glob_patterns)
|
|
263
|
-
glob_patterns = expand_paths(glob_patterns)
|
|
264
|
-
mutex.synchronize do
|
|
265
|
-
collapse_glob_patterns.merge(glob_patterns)
|
|
266
|
-
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
|
267
|
-
end
|
|
99
|
+
Registry.register_loader(self)
|
|
268
100
|
end
|
|
269
101
|
|
|
270
|
-
# Sets autoloads in the root namespace
|
|
102
|
+
# Sets autoloads in the root namespace.
|
|
271
103
|
#
|
|
272
|
-
# @
|
|
104
|
+
# @sig () -> void
|
|
273
105
|
def setup
|
|
274
106
|
mutex.synchronize do
|
|
275
107
|
break if @setup
|
|
@@ -277,7 +109,8 @@ module Zeitwerk
|
|
|
277
109
|
actual_root_dirs.each do |root_dir, namespace|
|
|
278
110
|
set_autoloads_in_dir(root_dir, namespace)
|
|
279
111
|
end
|
|
280
|
-
|
|
112
|
+
|
|
113
|
+
on_setup_callbacks.each(&:call)
|
|
281
114
|
|
|
282
115
|
@setup = true
|
|
283
116
|
end
|
|
@@ -290,8 +123,11 @@ module Zeitwerk
|
|
|
290
123
|
# else, they are eligible for garbage collection, which would effectively
|
|
291
124
|
# unload them.
|
|
292
125
|
#
|
|
293
|
-
#
|
|
294
|
-
#
|
|
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
|
+
#
|
|
130
|
+
# @sig () -> void
|
|
295
131
|
def unload
|
|
296
132
|
mutex.synchronize do
|
|
297
133
|
# We are going to keep track of the files that were required by our
|
|
@@ -302,21 +138,26 @@ module Zeitwerk
|
|
|
302
138
|
# is enough.
|
|
303
139
|
unloaded_files = Set.new
|
|
304
140
|
|
|
305
|
-
autoloads.each do |
|
|
141
|
+
autoloads.each do |(parent, cname), abspath|
|
|
306
142
|
if parent.autoload?(cname)
|
|
307
143
|
unload_autoload(parent, cname)
|
|
308
144
|
else
|
|
309
145
|
# Could happen if loaded with require_relative. That is unsupported,
|
|
310
146
|
# and the constant path would escape unloadable_cpath? This is just
|
|
311
147
|
# defensive code to clean things up as much as we are able to.
|
|
312
|
-
unload_cref(parent, cname)
|
|
313
|
-
unloaded_files.add(
|
|
148
|
+
unload_cref(parent, cname)
|
|
149
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
|
314
150
|
end
|
|
315
151
|
end
|
|
316
152
|
|
|
317
|
-
to_unload.
|
|
318
|
-
|
|
319
|
-
|
|
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)
|
|
160
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
|
320
161
|
end
|
|
321
162
|
|
|
322
163
|
unless unloaded_files.empty?
|
|
@@ -340,7 +181,7 @@ module Zeitwerk
|
|
|
340
181
|
lazy_subdirs.clear
|
|
341
182
|
|
|
342
183
|
Registry.on_unload(self)
|
|
343
|
-
ExplicitNamespace.
|
|
184
|
+
ExplicitNamespace.unregister_loader(self)
|
|
344
185
|
|
|
345
186
|
@setup = false
|
|
346
187
|
@eager_loaded = false
|
|
@@ -354,7 +195,7 @@ module Zeitwerk
|
|
|
354
195
|
# client code in the README of the project.
|
|
355
196
|
#
|
|
356
197
|
# @raise [Zeitwerk::Error]
|
|
357
|
-
# @
|
|
198
|
+
# @sig () -> void
|
|
358
199
|
def reload
|
|
359
200
|
if reloading_enabled?
|
|
360
201
|
unload
|
|
@@ -369,34 +210,39 @@ module Zeitwerk
|
|
|
369
210
|
# Eager loads all files in the root directories, recursively. Files do not
|
|
370
211
|
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
|
|
371
212
|
# are not eager loaded. You can opt-out specifically in specific files and
|
|
372
|
-
# directories with `do_not_eager_load
|
|
213
|
+
# directories with `do_not_eager_load`, and that can be overridden passing
|
|
214
|
+
# `force: true`.
|
|
373
215
|
#
|
|
374
|
-
# @
|
|
375
|
-
def eager_load
|
|
216
|
+
# @sig (true | false) -> void
|
|
217
|
+
def eager_load(force: false)
|
|
376
218
|
mutex.synchronize do
|
|
377
219
|
break if @eager_loaded
|
|
378
220
|
|
|
221
|
+
log("eager load start") if logger
|
|
222
|
+
|
|
223
|
+
honour_exclusions = !force
|
|
224
|
+
|
|
379
225
|
queue = []
|
|
380
226
|
actual_root_dirs.each do |root_dir, namespace|
|
|
381
|
-
queue << [namespace, root_dir] unless
|
|
227
|
+
queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
|
|
382
228
|
end
|
|
383
229
|
|
|
384
230
|
while to_eager_load = queue.shift
|
|
385
231
|
namespace, dir = to_eager_load
|
|
386
232
|
|
|
387
233
|
ls(dir) do |basename, abspath|
|
|
388
|
-
next if
|
|
234
|
+
next if honour_exclusions && excluded_from_eager_load?(abspath)
|
|
389
235
|
|
|
390
236
|
if ruby?(abspath)
|
|
391
|
-
if cref = autoloads
|
|
392
|
-
|
|
237
|
+
if cref = autoloads.cref_for(abspath)
|
|
238
|
+
cget(*cref)
|
|
393
239
|
end
|
|
394
240
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
|
395
|
-
if
|
|
241
|
+
if collapse?(abspath)
|
|
396
242
|
queue << [namespace, abspath]
|
|
397
243
|
else
|
|
398
244
|
cname = inflector.camelize(basename, abspath)
|
|
399
|
-
queue << [namespace
|
|
245
|
+
queue << [cget(namespace, cname), abspath]
|
|
400
246
|
end
|
|
401
247
|
end
|
|
402
248
|
end
|
|
@@ -408,23 +254,15 @@ module Zeitwerk
|
|
|
408
254
|
autoloaded_dirs.clear
|
|
409
255
|
|
|
410
256
|
@eager_loaded = true
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
257
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
#
|
|
417
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
|
418
|
-
# @return [void]
|
|
419
|
-
def do_not_eager_load(*paths)
|
|
420
|
-
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
|
258
|
+
log("eager load end") if logger
|
|
259
|
+
end
|
|
421
260
|
end
|
|
422
261
|
|
|
423
262
|
# Says if the given constant path would be unloaded on reload. This
|
|
424
263
|
# predicate returns `false` if reloading is disabled.
|
|
425
264
|
#
|
|
426
|
-
# @
|
|
427
|
-
# @return [Boolean]
|
|
265
|
+
# @sig (String) -> bool
|
|
428
266
|
def unloadable_cpath?(cpath)
|
|
429
267
|
to_unload.key?(cpath)
|
|
430
268
|
end
|
|
@@ -432,42 +270,28 @@ module Zeitwerk
|
|
|
432
270
|
# Returns an array with the constant paths that would be unloaded on reload.
|
|
433
271
|
# This predicate returns an empty array if reloading is disabled.
|
|
434
272
|
#
|
|
435
|
-
# @
|
|
273
|
+
# @sig () -> Array[String]
|
|
436
274
|
def unloadable_cpaths
|
|
437
275
|
to_unload.keys.freeze
|
|
438
276
|
end
|
|
439
277
|
|
|
440
|
-
#
|
|
278
|
+
# This is a dangerous method.
|
|
441
279
|
#
|
|
442
|
-
# @
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
# @private
|
|
448
|
-
# @param dir [String]
|
|
449
|
-
# @return [Boolean]
|
|
450
|
-
def manages?(dir)
|
|
451
|
-
dir = dir + "/"
|
|
452
|
-
ignored_paths.each do |ignored_path|
|
|
453
|
-
return false if dir.start_with?(ignored_path + "/")
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
root_dirs.each_key do |root_dir|
|
|
457
|
-
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
false
|
|
280
|
+
# @experimental
|
|
281
|
+
# @sig () -> void
|
|
282
|
+
def unregister
|
|
283
|
+
Registry.unregister_loader(self)
|
|
284
|
+
ExplicitNamespace.unregister_loader(self)
|
|
461
285
|
end
|
|
462
286
|
|
|
463
287
|
# --- Class methods ---------------------------------------------------------------------------
|
|
464
288
|
|
|
465
289
|
class << self
|
|
466
|
-
# @
|
|
290
|
+
# @sig #call | #debug | nil
|
|
467
291
|
attr_accessor :default_logger
|
|
468
292
|
|
|
469
293
|
# @private
|
|
470
|
-
# @
|
|
294
|
+
# @sig Mutex
|
|
471
295
|
attr_accessor :mutex
|
|
472
296
|
|
|
473
297
|
# This is a shortcut for
|
|
@@ -481,7 +305,7 @@ module Zeitwerk
|
|
|
481
305
|
# except that this method returns the same object in subsequent calls from
|
|
482
306
|
# the same file, in the unlikely case the gem wants to be able to reload.
|
|
483
307
|
#
|
|
484
|
-
# @
|
|
308
|
+
# @sig () -> Zeitwerk::Loader
|
|
485
309
|
def for_gem
|
|
486
310
|
called_from = caller_locations(1, 1).first.path
|
|
487
311
|
Registry.loader_for_gem(called_from)
|
|
@@ -489,7 +313,7 @@ module Zeitwerk
|
|
|
489
313
|
|
|
490
314
|
# Broadcasts `eager_load` to all loaders.
|
|
491
315
|
#
|
|
492
|
-
# @
|
|
316
|
+
# @sig () -> void
|
|
493
317
|
def eager_load_all
|
|
494
318
|
Registry.loaders.each(&:eager_load)
|
|
495
319
|
end
|
|
@@ -497,7 +321,7 @@ module Zeitwerk
|
|
|
497
321
|
# Returns an array with the absolute paths of the root directories of all
|
|
498
322
|
# registered loaders. This is a read-only collection.
|
|
499
323
|
#
|
|
500
|
-
# @
|
|
324
|
+
# @sig () -> Array[String]
|
|
501
325
|
def all_dirs
|
|
502
326
|
Registry.loaders.flat_map(&:dirs).freeze
|
|
503
327
|
end
|
|
@@ -507,21 +331,12 @@ module Zeitwerk
|
|
|
507
331
|
|
|
508
332
|
private # -------------------------------------------------------------------------------------
|
|
509
333
|
|
|
510
|
-
# @
|
|
511
|
-
def actual_root_dirs
|
|
512
|
-
root_dirs.reject do |root_dir, _namespace|
|
|
513
|
-
!dir?(root_dir) || ignored_paths.member?(root_dir)
|
|
514
|
-
end
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
# @param dir [String]
|
|
518
|
-
# @param parent [Module]
|
|
519
|
-
# @return [void]
|
|
334
|
+
# @sig (String, Module) -> void
|
|
520
335
|
def set_autoloads_in_dir(dir, parent)
|
|
521
336
|
ls(dir) do |basename, abspath|
|
|
522
337
|
begin
|
|
523
338
|
if ruby?(basename)
|
|
524
|
-
basename
|
|
339
|
+
basename.delete_suffix!(".rb")
|
|
525
340
|
cname = inflector.camelize(basename, abspath).to_sym
|
|
526
341
|
autoload_file(parent, cname, abspath)
|
|
527
342
|
elsif dir?(abspath)
|
|
@@ -531,9 +346,9 @@ module Zeitwerk
|
|
|
531
346
|
# To resolve the ambiguity file name -> constant path this introduces,
|
|
532
347
|
# the `app/models/concerns` directory is totally ignored as a namespace,
|
|
533
348
|
# it counts only as root. The guard checks that.
|
|
534
|
-
unless
|
|
349
|
+
unless root_dir?(abspath)
|
|
535
350
|
cname = inflector.camelize(basename, abspath).to_sym
|
|
536
|
-
if
|
|
351
|
+
if collapse?(abspath)
|
|
537
352
|
set_autoloads_in_dir(abspath, parent)
|
|
538
353
|
else
|
|
539
354
|
autoload_subdir(parent, cname, abspath)
|
|
@@ -559,35 +374,30 @@ module Zeitwerk
|
|
|
559
374
|
end
|
|
560
375
|
end
|
|
561
376
|
|
|
562
|
-
# @
|
|
563
|
-
# @param cname [Symbol]
|
|
564
|
-
# @param subdir [String]
|
|
565
|
-
# @return [void]
|
|
377
|
+
# @sig (Module, Symbol, String) -> void
|
|
566
378
|
def autoload_subdir(parent, cname, subdir)
|
|
567
|
-
if autoload_path =
|
|
379
|
+
if autoload_path = autoloads.abspath_for(parent, cname)
|
|
568
380
|
cpath = cpath(parent, cname)
|
|
569
381
|
register_explicit_namespace(cpath) if ruby?(autoload_path)
|
|
570
382
|
# We do not need to issue another autoload, the existing one is enough
|
|
571
383
|
# no matter if it is for a file or a directory. Just remember the
|
|
572
384
|
# subdirectory has to be visited if the namespace is used.
|
|
573
|
-
|
|
385
|
+
lazy_subdirs[cpath] << subdir
|
|
574
386
|
elsif !cdef?(parent, cname)
|
|
575
387
|
# First time we find this namespace, set an autoload for it.
|
|
576
|
-
|
|
388
|
+
lazy_subdirs[cpath(parent, cname)] << subdir
|
|
577
389
|
set_autoload(parent, cname, subdir)
|
|
578
390
|
else
|
|
579
391
|
# For whatever reason the constant that corresponds to this namespace has
|
|
580
392
|
# already been defined, we have to recurse.
|
|
581
|
-
|
|
393
|
+
log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
|
|
394
|
+
set_autoloads_in_dir(subdir, cget(parent, cname))
|
|
582
395
|
end
|
|
583
396
|
end
|
|
584
397
|
|
|
585
|
-
# @
|
|
586
|
-
# @param cname [Symbol]
|
|
587
|
-
# @param file [String]
|
|
588
|
-
# @return [void]
|
|
398
|
+
# @sig (Module, Symbol, String) -> void
|
|
589
399
|
def autoload_file(parent, cname, file)
|
|
590
|
-
if autoload_path =
|
|
400
|
+
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
|
591
401
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
|
592
402
|
if ruby?(autoload_path)
|
|
593
403
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
|
@@ -606,221 +416,91 @@ module Zeitwerk
|
|
|
606
416
|
end
|
|
607
417
|
end
|
|
608
418
|
|
|
609
|
-
#
|
|
610
|
-
#
|
|
611
|
-
#
|
|
612
|
-
# @
|
|
613
|
-
# @return [void]
|
|
419
|
+
# `dir` is the directory that would have autovivified a namespace. `file` is
|
|
420
|
+
# the file where we've found the namespace is explicitly defined.
|
|
421
|
+
#
|
|
422
|
+
# @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
|
|
614
423
|
def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
|
|
615
424
|
autoloads.delete(dir)
|
|
616
425
|
Registry.unregister_autoload(dir)
|
|
617
426
|
|
|
427
|
+
log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
|
428
|
+
|
|
618
429
|
set_autoload(parent, cname, file)
|
|
619
430
|
register_explicit_namespace(cpath(parent, cname))
|
|
620
431
|
end
|
|
621
432
|
|
|
622
|
-
# @
|
|
623
|
-
# @param cname [Symbol]
|
|
624
|
-
# @param abspath [String]
|
|
625
|
-
# @return [void]
|
|
433
|
+
# @sig (Module, Symbol, String) -> void
|
|
626
434
|
def set_autoload(parent, cname, abspath)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
# be able to do a lookup later in Kernel#require for manual require calls.
|
|
630
|
-
#
|
|
631
|
-
# We freeze realpath because that saves allocations in Module#autoload.
|
|
632
|
-
# See #125.
|
|
633
|
-
realpath = File.realpath(abspath).freeze
|
|
634
|
-
parent.autoload(cname, realpath)
|
|
435
|
+
autoloads.define(parent, cname, abspath)
|
|
436
|
+
|
|
635
437
|
if logger
|
|
636
|
-
if ruby?(
|
|
637
|
-
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{
|
|
438
|
+
if ruby?(abspath)
|
|
439
|
+
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
|
|
638
440
|
else
|
|
639
|
-
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{
|
|
441
|
+
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
|
|
640
442
|
end
|
|
641
443
|
end
|
|
642
444
|
|
|
643
|
-
|
|
644
|
-
Registry.register_autoload(self, realpath)
|
|
445
|
+
Registry.register_autoload(self, abspath)
|
|
645
446
|
|
|
646
447
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
|
647
448
|
unless parent.autoload?(cname)
|
|
648
|
-
Registry.register_inception(cpath(parent, cname),
|
|
649
|
-
end
|
|
650
|
-
end
|
|
651
|
-
|
|
652
|
-
# @param parent [Module]
|
|
653
|
-
# @param cname [Symbol]
|
|
654
|
-
# @return [String, nil]
|
|
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
|
-
# @param parent [Module]
|
|
676
|
-
# @param cname [Symbol]
|
|
677
|
-
# @return [String, nil]
|
|
678
|
-
if method(:autoload?).arity == 1
|
|
679
|
-
def strict_autoload_path(parent, cname)
|
|
680
|
-
parent.autoload?(cname) if cdef?(parent, cname)
|
|
681
|
-
end
|
|
682
|
-
else
|
|
683
|
-
def strict_autoload_path(parent, cname)
|
|
684
|
-
parent.autoload?(cname, false)
|
|
449
|
+
Registry.register_inception(cpath(parent, cname), abspath, self)
|
|
685
450
|
end
|
|
686
451
|
end
|
|
687
452
|
|
|
688
|
-
#
|
|
689
|
-
# name to configure preloads in the public interface.
|
|
690
|
-
#
|
|
691
|
-
# @return [void]
|
|
692
|
-
def do_preload
|
|
693
|
-
preloads.each do |abspath|
|
|
694
|
-
do_preload_abspath(abspath)
|
|
695
|
-
end
|
|
696
|
-
end
|
|
697
|
-
|
|
698
|
-
# @param abspath [String]
|
|
699
|
-
# @return [void]
|
|
700
|
-
def do_preload_abspath(abspath)
|
|
701
|
-
if ruby?(abspath)
|
|
702
|
-
do_preload_file(abspath)
|
|
703
|
-
elsif dir?(abspath)
|
|
704
|
-
do_preload_dir(abspath)
|
|
705
|
-
end
|
|
706
|
-
end
|
|
707
|
-
|
|
708
|
-
# @param dir [String]
|
|
709
|
-
# @return [void]
|
|
710
|
-
def do_preload_dir(dir)
|
|
711
|
-
ls(dir) do |_basename, abspath|
|
|
712
|
-
do_preload_abspath(abspath)
|
|
713
|
-
end
|
|
714
|
-
end
|
|
715
|
-
|
|
716
|
-
# @param file [String]
|
|
717
|
-
# @return [Boolean]
|
|
718
|
-
def do_preload_file(file)
|
|
719
|
-
log("preloading #{file}") if logger
|
|
720
|
-
require file
|
|
721
|
-
end
|
|
722
|
-
|
|
723
|
-
# @param parent [Module]
|
|
724
|
-
# @param cname [Symbol]
|
|
725
|
-
# @return [String]
|
|
726
|
-
def cpath(parent, cname)
|
|
727
|
-
parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
|
728
|
-
end
|
|
729
|
-
|
|
730
|
-
# @param dir [String]
|
|
731
|
-
# @yieldparam path [String, String]
|
|
732
|
-
# @return [void]
|
|
733
|
-
def ls(dir)
|
|
734
|
-
Dir.foreach(dir) do |basename|
|
|
735
|
-
next if basename.start_with?(".")
|
|
736
|
-
|
|
737
|
-
abspath = File.join(dir, basename)
|
|
738
|
-
next if ignored_paths.member?(abspath)
|
|
739
|
-
|
|
740
|
-
# We freeze abspath because that saves allocations when passed later to
|
|
741
|
-
# File methods. See #125.
|
|
742
|
-
yield basename, abspath.freeze
|
|
743
|
-
end
|
|
744
|
-
end
|
|
745
|
-
|
|
746
|
-
# @param path [String]
|
|
747
|
-
# @return [Boolean]
|
|
748
|
-
def ruby?(path)
|
|
749
|
-
path.end_with?(".rb")
|
|
750
|
-
end
|
|
751
|
-
|
|
752
|
-
# @param path [String]
|
|
753
|
-
# @return [Boolean]
|
|
754
|
-
def dir?(path)
|
|
755
|
-
File.directory?(path)
|
|
756
|
-
end
|
|
757
|
-
|
|
758
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
|
759
|
-
# @return [<String>]
|
|
760
|
-
def expand_paths(paths)
|
|
761
|
-
paths.flatten.map! { |path| File.expand_path(path) }
|
|
762
|
-
end
|
|
763
|
-
|
|
764
|
-
# @param glob_patterns [<String>]
|
|
765
|
-
# @return [<String>]
|
|
766
|
-
def expand_glob_patterns(glob_patterns)
|
|
767
|
-
# Note that Dir.glob works with regular file names just fine. That is,
|
|
768
|
-
# glob patterns technically need no wildcards.
|
|
769
|
-
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
|
770
|
-
end
|
|
771
|
-
|
|
772
|
-
# @return [void]
|
|
773
|
-
def recompute_ignored_paths
|
|
774
|
-
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
|
775
|
-
end
|
|
776
|
-
|
|
777
|
-
# @return [void]
|
|
778
|
-
def recompute_collapse_dirs
|
|
779
|
-
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
|
780
|
-
end
|
|
781
|
-
|
|
782
|
-
# @param message [String]
|
|
783
|
-
# @return [void]
|
|
784
|
-
def log(message)
|
|
785
|
-
method_name = logger.respond_to?(:debug) ? :debug : :call
|
|
786
|
-
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
|
787
|
-
end
|
|
788
|
-
|
|
789
|
-
def cdef?(parent, cname)
|
|
790
|
-
parent.const_defined?(cname, false)
|
|
791
|
-
end
|
|
792
|
-
|
|
453
|
+
# @sig (String) -> void
|
|
793
454
|
def register_explicit_namespace(cpath)
|
|
794
455
|
ExplicitNamespace.register(cpath, self)
|
|
795
456
|
end
|
|
796
457
|
|
|
458
|
+
# @sig (String) -> void
|
|
797
459
|
def raise_if_conflicting_directory(dir)
|
|
798
460
|
self.class.mutex.synchronize do
|
|
799
461
|
Registry.loaders.each do |loader|
|
|
800
|
-
if loader
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
462
|
+
next if loader == self
|
|
463
|
+
next if loader.ignores?(dir)
|
|
464
|
+
|
|
465
|
+
dir = dir + "/"
|
|
466
|
+
loader.root_dirs.each do |root_dir, _namespace|
|
|
467
|
+
next if ignores?(root_dir)
|
|
468
|
+
|
|
469
|
+
root_dir = root_dir + "/"
|
|
470
|
+
if dir.start_with?(root_dir) || root_dir.start_with?(dir)
|
|
471
|
+
require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
|
|
472
|
+
raise Error,
|
|
473
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
|
|
474
|
+
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
|
475
|
+
EOS
|
|
476
|
+
end
|
|
806
477
|
end
|
|
807
478
|
end
|
|
808
479
|
end
|
|
809
480
|
end
|
|
810
481
|
|
|
811
|
-
# @
|
|
812
|
-
|
|
813
|
-
|
|
482
|
+
# @sig (String, Object, String) -> void
|
|
483
|
+
def run_on_unload_callbacks(cpath, value, abspath)
|
|
484
|
+
# Order matters. If present, run the most specific one.
|
|
485
|
+
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
|
486
|
+
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# @sig (Module, Symbol) -> void
|
|
814
490
|
def unload_autoload(parent, cname)
|
|
815
|
-
parent.
|
|
491
|
+
parent.__send__(:remove_const, cname)
|
|
816
492
|
log("autoload for #{cpath(parent, cname)} removed") if logger
|
|
817
493
|
end
|
|
818
494
|
|
|
819
|
-
# @
|
|
820
|
-
# @param cname [Symbol]
|
|
821
|
-
# @return [void]
|
|
495
|
+
# @sig (Module, Symbol) -> void
|
|
822
496
|
def unload_cref(parent, cname)
|
|
823
|
-
|
|
497
|
+
# Let's optimistically remove_const. The way we use it, this is going to
|
|
498
|
+
# succeed always if all is good.
|
|
499
|
+
parent.__send__(:remove_const, cname)
|
|
500
|
+
rescue ::NameError
|
|
501
|
+
# There are a few edge scenarios in which this may happen. If the constant
|
|
502
|
+
# is gone, that is OK, anyway.
|
|
503
|
+
else
|
|
824
504
|
log("#{cpath(parent, cname)} unloaded") if logger
|
|
825
505
|
end
|
|
826
506
|
end
|