zeitwerk 2.3.1 → 2.5.0.beta
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 +213 -47
- data/lib/zeitwerk.rb +2 -0
- data/lib/zeitwerk/autoloads.rb +69 -0
- data/lib/zeitwerk/explicit_namespace.rb +17 -10
- data/lib/zeitwerk/gem_inflector.rb +2 -4
- data/lib/zeitwerk/inflector.rb +4 -7
- data/lib/zeitwerk/kernel.rb +7 -6
- data/lib/zeitwerk/loader.rb +112 -453
- data/lib/zeitwerk/loader/callbacks.rb +28 -12
- data/lib/zeitwerk/loader/config.rb +308 -0
- data/lib/zeitwerk/loader/helpers.rb +95 -0
- data/lib/zeitwerk/real_mod_name.rb +1 -2
- data/lib/zeitwerk/registry.rb +19 -30
- data/lib/zeitwerk/version.rb +1 -1
- metadata +12 -9
@@ -2,16 +2,14 @@
|
|
2
2
|
|
3
3
|
module Zeitwerk
|
4
4
|
class GemInflector < Inflector
|
5
|
-
# @
|
5
|
+
# @sig (String) -> void
|
6
6
|
def initialize(root_file)
|
7
7
|
namespace = File.basename(root_file, ".rb")
|
8
8
|
lib_dir = File.dirname(root_file)
|
9
9
|
@version_file = File.join(lib_dir, namespace, "version.rb")
|
10
10
|
end
|
11
11
|
|
12
|
-
# @
|
13
|
-
# @param abspath [String]
|
14
|
-
# @return [String]
|
12
|
+
# @sig (String, String) -> String
|
15
13
|
def camelize(basename, abspath)
|
16
14
|
abspath == @version_file ? "VERSION" : super
|
17
15
|
end
|
data/lib/zeitwerk/inflector.rb
CHANGED
@@ -11,11 +11,9 @@ module Zeitwerk
|
|
11
11
|
#
|
12
12
|
# Takes into account hard-coded mappings configured with `inflect`.
|
13
13
|
#
|
14
|
-
# @
|
15
|
-
# @param _abspath [String]
|
16
|
-
# @return [String]
|
14
|
+
# @sig (String, String) -> String
|
17
15
|
def camelize(basename, _abspath)
|
18
|
-
overrides[basename] || basename.split('_').
|
16
|
+
overrides[basename] || basename.split('_').each(&:capitalize!).join
|
19
17
|
end
|
20
18
|
|
21
19
|
# Configures hard-coded inflections:
|
@@ -30,8 +28,7 @@ module Zeitwerk
|
|
30
28
|
# inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
|
31
29
|
# inflector.camelize("users_controller", abspath) # => "UsersController"
|
32
30
|
#
|
33
|
-
# @
|
34
|
-
# @return [void]
|
31
|
+
# @sig (Hash[String, String]) -> void
|
35
32
|
def inflect(inflections)
|
36
33
|
overrides.merge!(inflections)
|
37
34
|
end
|
@@ -41,7 +38,7 @@ module Zeitwerk
|
|
41
38
|
# Hard-coded basename to constant name user maps that override the default
|
42
39
|
# inflection logic.
|
43
40
|
#
|
44
|
-
# @
|
41
|
+
# @sig () -> Hash[String, String]
|
45
42
|
def overrides
|
46
43
|
@overrides ||= {}
|
47
44
|
end
|
data/lib/zeitwerk/kernel.rb
CHANGED
@@ -12,15 +12,15 @@ module Kernel
|
|
12
12
|
# On the other hand, if you publish a new version of a gem that is now managed
|
13
13
|
# by Zeitwerk, client code can reference directly your classes and modules and
|
14
14
|
# should not require anything. But if someone has legacy require calls around,
|
15
|
-
# they will work as expected, and in a compatible way.
|
15
|
+
# they will work as expected, and in a compatible way. This feature is by now
|
16
|
+
# EXPERIMENTAL and UNDOCUMENTED.
|
16
17
|
#
|
17
18
|
# We cannot decorate with prepend + super because Kernel has already been
|
18
19
|
# included in Object, and changes in ancestors don't get propagated into
|
19
20
|
# already existing ancestor chains.
|
20
21
|
alias_method :zeitwerk_original_require, :require
|
21
22
|
|
22
|
-
# @
|
23
|
-
# @return [Boolean]
|
23
|
+
# @sig (String) -> true | false
|
24
24
|
def require(path)
|
25
25
|
if loader = Zeitwerk::Registry.loader_for(path)
|
26
26
|
if path.end_with?(".rb")
|
@@ -29,13 +29,14 @@ module Kernel
|
|
29
29
|
end
|
30
30
|
else
|
31
31
|
loader.on_dir_autoloaded(path)
|
32
|
+
true
|
32
33
|
end
|
33
34
|
else
|
34
35
|
zeitwerk_original_require(path).tap do |required|
|
35
36
|
if required
|
36
|
-
|
37
|
-
if loader = Zeitwerk::Registry.loader_for(
|
38
|
-
loader.on_file_autoloaded(
|
37
|
+
abspath = $LOADED_FEATURES.last
|
38
|
+
if loader = Zeitwerk::Registry.loader_for(abspath)
|
39
|
+
loader.on_file_autoloaded(abspath)
|
39
40
|
end
|
40
41
|
end
|
41
42
|
end
|
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
|
20
|
-
|
21
|
-
# Absolute paths of the root directories. Stored in a hash to preserve
|
22
|
-
# order, easily handle duplicates, and also be able to have a fast lookup,
|
23
|
-
# needed for detecting nested paths.
|
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
|
10
|
+
require_relative "loader/config"
|
35
11
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
attr_reader :preloads
|
41
|
-
|
42
|
-
# Absolute paths of files, directories, or glob patterns to be totally
|
43
|
-
# ignored.
|
44
|
-
#
|
45
|
-
# @private
|
46
|
-
# @return [Set<String>]
|
47
|
-
attr_reader :ignored_glob_patterns
|
12
|
+
include RealModName
|
13
|
+
include Callbacks
|
14
|
+
include Helpers
|
15
|
+
include Config
|
48
16
|
|
49
|
-
#
|
50
|
-
#
|
51
|
-
# reload.
|
17
|
+
# Keeps track of autoloads defined by the loader which have not been
|
18
|
+
# executed so far.
|
52
19
|
#
|
53
|
-
#
|
54
|
-
# @return [Set<String>]
|
55
|
-
attr_reader :ignored_paths
|
56
|
-
|
57
|
-
# Absolute paths of directories or glob patterns to be collapsed.
|
20
|
+
# This metadata helps us implement a few things:
|
58
21
|
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
|
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.
|
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.
|
65
25
|
#
|
66
|
-
#
|
67
|
-
# @return [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.
|
26
|
+
# 2. When unloading, remove autoloads that have not been executed.
|
73
27
|
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
# ...
|
28
|
+
# 3. Eager load with a recursive const_get, rather than a recursive require,
|
29
|
+
# for consistency with lazy loading.
|
77
30
|
#
|
78
31
|
# @private
|
79
|
-
# @
|
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,42 @@ 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
|
-
# @private
|
129
|
-
# @return [Set<String>]
|
130
|
-
attr_reader :eager_load_exclusions
|
131
|
-
|
132
79
|
# @private
|
133
|
-
# @
|
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
|
169
|
-
|
170
|
-
# Sets a tag for the loader, useful for logging.
|
171
|
-
#
|
172
|
-
# @param tag [#to_s]
|
173
|
-
# @return [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
|
-
# @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
|
-
# @raise [Zeitwerk::Error]
|
194
|
-
# @return [void]
|
195
|
-
def push_dir(path)
|
196
|
-
abspath = File.expand_path(path)
|
197
|
-
if dir?(abspath)
|
198
|
-
raise_if_conflicting_directory(abspath)
|
199
|
-
root_dirs[abspath] = true
|
200
|
-
else
|
201
|
-
raise Error, "the root directory #{abspath} does not exist"
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
# You need to call this method before setup in order to be able to reload.
|
206
|
-
# There is no way to undo this, either you want to reload or you don't.
|
207
|
-
#
|
208
|
-
# @raise [Zeitwerk::Error]
|
209
|
-
# @return [void]
|
210
|
-
def enable_reloading
|
211
|
-
mutex.synchronize do
|
212
|
-
break if @reloading_enabled
|
213
|
-
|
214
|
-
if @setup
|
215
|
-
raise Error, "cannot enable reloading after setup"
|
216
|
-
else
|
217
|
-
@reloading_enabled = true
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
# @return [Boolean]
|
223
|
-
def reloading_enabled?
|
224
|
-
@reloading_enabled
|
225
|
-
end
|
226
|
-
|
227
|
-
# Files or directories to be preloaded instead of lazy loaded.
|
228
|
-
#
|
229
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
230
|
-
# @return [void]
|
231
|
-
def preload(*paths)
|
232
|
-
mutex.synchronize do
|
233
|
-
expand_paths(paths).each do |abspath|
|
234
|
-
preloads << abspath
|
235
|
-
do_preload_abspath(abspath) if @setup
|
236
|
-
end
|
237
|
-
end
|
238
|
-
end
|
88
|
+
super
|
239
89
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
ignored_paths.merge(expand_glob_patterns(glob_patterns))
|
249
|
-
end
|
250
|
-
end
|
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
|
251
98
|
|
252
|
-
|
253
|
-
#
|
254
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
255
|
-
# @return [void]
|
256
|
-
def collapse(*glob_patterns)
|
257
|
-
glob_patterns = expand_paths(glob_patterns)
|
258
|
-
mutex.synchronize do
|
259
|
-
collapse_glob_patterns.merge(glob_patterns)
|
260
|
-
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
261
|
-
end
|
99
|
+
Registry.register_loader(self)
|
262
100
|
end
|
263
101
|
|
264
|
-
# Sets autoloads in the root namespace
|
102
|
+
# Sets autoloads in the root namespace.
|
265
103
|
#
|
266
|
-
# @
|
104
|
+
# @sig () -> void
|
267
105
|
def setup
|
268
106
|
mutex.synchronize do
|
269
107
|
break if @setup
|
270
108
|
|
271
|
-
actual_root_dirs.each
|
272
|
-
|
109
|
+
actual_root_dirs.each do |root_dir, namespace|
|
110
|
+
set_autoloads_in_dir(root_dir, namespace)
|
111
|
+
end
|
273
112
|
|
274
113
|
@setup = true
|
275
114
|
end
|
@@ -283,7 +122,7 @@ module Zeitwerk
|
|
283
122
|
# unload them.
|
284
123
|
#
|
285
124
|
# @private
|
286
|
-
# @
|
125
|
+
# @sig () -> void
|
287
126
|
def unload
|
288
127
|
mutex.synchronize do
|
289
128
|
# We are going to keep track of the files that were required by our
|
@@ -294,21 +133,26 @@ module Zeitwerk
|
|
294
133
|
# is enough.
|
295
134
|
unloaded_files = Set.new
|
296
135
|
|
297
|
-
autoloads.each do |
|
136
|
+
autoloads.each do |(parent, cname), abspath|
|
298
137
|
if parent.autoload?(cname)
|
299
138
|
unload_autoload(parent, cname)
|
300
139
|
else
|
301
140
|
# Could happen if loaded with require_relative. That is unsupported,
|
302
141
|
# and the constant path would escape unloadable_cpath? This is just
|
303
142
|
# defensive code to clean things up as much as we are able to.
|
304
|
-
unload_cref(parent, cname)
|
305
|
-
unloaded_files.add(
|
143
|
+
unload_cref(parent, cname) if cdef?(parent, cname)
|
144
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
306
145
|
end
|
307
146
|
end
|
308
147
|
|
309
|
-
to_unload.
|
310
|
-
|
311
|
-
|
148
|
+
to_unload.each do |cpath, (abspath, (parent, cname))|
|
149
|
+
unless on_unload_callbacks.empty?
|
150
|
+
value = parent.const_get(cname)
|
151
|
+
run_on_unload_callbacks(cpath, value, abspath)
|
152
|
+
end
|
153
|
+
|
154
|
+
unload_cref(parent, cname) if cdef?(parent, cname)
|
155
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
312
156
|
end
|
313
157
|
|
314
158
|
unless unloaded_files.empty?
|
@@ -346,7 +190,7 @@ module Zeitwerk
|
|
346
190
|
# client code in the README of the project.
|
347
191
|
#
|
348
192
|
# @raise [Zeitwerk::Error]
|
349
|
-
# @
|
193
|
+
# @sig () -> void
|
350
194
|
def reload
|
351
195
|
if reloading_enabled?
|
352
196
|
unload
|
@@ -363,29 +207,34 @@ module Zeitwerk
|
|
363
207
|
# are not eager loaded. You can opt-out specifically in specific files and
|
364
208
|
# directories with `do_not_eager_load`.
|
365
209
|
#
|
366
|
-
# @
|
210
|
+
# @sig () -> void
|
367
211
|
def eager_load
|
368
212
|
mutex.synchronize do
|
369
213
|
break if @eager_loaded
|
370
214
|
|
371
|
-
|
372
|
-
|
215
|
+
log("eager load start") if logger
|
216
|
+
|
217
|
+
queue = []
|
218
|
+
actual_root_dirs.each do |root_dir, namespace|
|
219
|
+
queue << [namespace, root_dir] unless excluded_from_eager_load?(root_dir)
|
220
|
+
end
|
221
|
+
|
373
222
|
while to_eager_load = queue.shift
|
374
223
|
namespace, dir = to_eager_load
|
375
224
|
|
376
225
|
ls(dir) do |basename, abspath|
|
377
|
-
next if
|
226
|
+
next if excluded_from_eager_load?(abspath)
|
378
227
|
|
379
228
|
if ruby?(abspath)
|
380
|
-
if cref = autoloads
|
381
|
-
|
229
|
+
if cref = autoloads.cref_for(abspath)
|
230
|
+
cget(*cref)
|
382
231
|
end
|
383
232
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
384
|
-
if
|
233
|
+
if collapse?(abspath)
|
385
234
|
queue << [namespace, abspath]
|
386
235
|
else
|
387
236
|
cname = inflector.camelize(basename, abspath)
|
388
|
-
queue << [namespace
|
237
|
+
queue << [cget(namespace, cname), abspath]
|
389
238
|
end
|
390
239
|
end
|
391
240
|
end
|
@@ -397,23 +246,15 @@ module Zeitwerk
|
|
397
246
|
autoloaded_dirs.clear
|
398
247
|
|
399
248
|
@eager_loaded = true
|
400
|
-
end
|
401
|
-
end
|
402
249
|
|
403
|
-
|
404
|
-
|
405
|
-
#
|
406
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
407
|
-
# @return [void]
|
408
|
-
def do_not_eager_load(*paths)
|
409
|
-
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
250
|
+
log("eager load end") if logger
|
251
|
+
end
|
410
252
|
end
|
411
253
|
|
412
254
|
# Says if the given constant path would be unloaded on reload. This
|
413
255
|
# predicate returns `false` if reloading is disabled.
|
414
256
|
#
|
415
|
-
# @
|
416
|
-
# @return [Boolean]
|
257
|
+
# @sig (String) -> bool
|
417
258
|
def unloadable_cpath?(cpath)
|
418
259
|
to_unload.key?(cpath)
|
419
260
|
end
|
@@ -421,42 +262,19 @@ module Zeitwerk
|
|
421
262
|
# Returns an array with the constant paths that would be unloaded on reload.
|
422
263
|
# This predicate returns an empty array if reloading is disabled.
|
423
264
|
#
|
424
|
-
# @
|
265
|
+
# @sig () -> Array[String]
|
425
266
|
def unloadable_cpaths
|
426
267
|
to_unload.keys.freeze
|
427
268
|
end
|
428
269
|
|
429
|
-
# Logs to `$stdout`, handy shortcut for debugging.
|
430
|
-
#
|
431
|
-
# @return [void]
|
432
|
-
def log!
|
433
|
-
@logger = ->(msg) { puts msg }
|
434
|
-
end
|
435
|
-
|
436
|
-
# @private
|
437
|
-
# @param dir [String]
|
438
|
-
# @return [Boolean]
|
439
|
-
def manages?(dir)
|
440
|
-
dir = dir + "/"
|
441
|
-
ignored_paths.each do |ignored_path|
|
442
|
-
return false if dir.start_with?(ignored_path + "/")
|
443
|
-
end
|
444
|
-
|
445
|
-
root_dirs.each_key do |root_dir|
|
446
|
-
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
447
|
-
end
|
448
|
-
|
449
|
-
false
|
450
|
-
end
|
451
|
-
|
452
270
|
# --- Class methods ---------------------------------------------------------------------------
|
453
271
|
|
454
272
|
class << self
|
455
|
-
# @
|
273
|
+
# @sig #call | #debug | nil
|
456
274
|
attr_accessor :default_logger
|
457
275
|
|
458
276
|
# @private
|
459
|
-
# @
|
277
|
+
# @sig Mutex
|
460
278
|
attr_accessor :mutex
|
461
279
|
|
462
280
|
# This is a shortcut for
|
@@ -470,7 +288,7 @@ module Zeitwerk
|
|
470
288
|
# except that this method returns the same object in subsequent calls from
|
471
289
|
# the same file, in the unlikely case the gem wants to be able to reload.
|
472
290
|
#
|
473
|
-
# @
|
291
|
+
# @sig () -> Zeitwerk::Loader
|
474
292
|
def for_gem
|
475
293
|
called_from = caller_locations(1, 1).first.path
|
476
294
|
Registry.loader_for_gem(called_from)
|
@@ -478,7 +296,7 @@ module Zeitwerk
|
|
478
296
|
|
479
297
|
# Broadcasts `eager_load` to all loaders.
|
480
298
|
#
|
481
|
-
# @
|
299
|
+
# @sig () -> void
|
482
300
|
def eager_load_all
|
483
301
|
Registry.loaders.each(&:eager_load)
|
484
302
|
end
|
@@ -486,7 +304,7 @@ module Zeitwerk
|
|
486
304
|
# Returns an array with the absolute paths of the root directories of all
|
487
305
|
# registered loaders. This is a read-only collection.
|
488
306
|
#
|
489
|
-
# @
|
307
|
+
# @sig () -> Array[String]
|
490
308
|
def all_dirs
|
491
309
|
Registry.loaders.flat_map(&:dirs).freeze
|
492
310
|
end
|
@@ -496,21 +314,12 @@ module Zeitwerk
|
|
496
314
|
|
497
315
|
private # -------------------------------------------------------------------------------------
|
498
316
|
|
499
|
-
# @
|
500
|
-
def actual_root_dirs
|
501
|
-
root_dirs.keys.delete_if do |root_dir|
|
502
|
-
!dir?(root_dir) || ignored_paths.member?(root_dir)
|
503
|
-
end
|
504
|
-
end
|
505
|
-
|
506
|
-
# @param dir [String]
|
507
|
-
# @param parent [Module]
|
508
|
-
# @return [void]
|
317
|
+
# @sig (String, Module) -> void
|
509
318
|
def set_autoloads_in_dir(dir, parent)
|
510
319
|
ls(dir) do |basename, abspath|
|
511
320
|
begin
|
512
321
|
if ruby?(basename)
|
513
|
-
basename
|
322
|
+
basename.delete_suffix!(".rb")
|
514
323
|
cname = inflector.camelize(basename, abspath).to_sym
|
515
324
|
autoload_file(parent, cname, abspath)
|
516
325
|
elsif dir?(abspath)
|
@@ -520,9 +329,9 @@ module Zeitwerk
|
|
520
329
|
# To resolve the ambiguity file name -> constant path this introduces,
|
521
330
|
# the `app/models/concerns` directory is totally ignored as a namespace,
|
522
331
|
# it counts only as root. The guard checks that.
|
523
|
-
unless
|
332
|
+
unless root_dir?(abspath)
|
524
333
|
cname = inflector.camelize(basename, abspath).to_sym
|
525
|
-
if
|
334
|
+
if collapse?(abspath)
|
526
335
|
set_autoloads_in_dir(abspath, parent)
|
527
336
|
else
|
528
337
|
autoload_subdir(parent, cname, abspath)
|
@@ -548,35 +357,30 @@ module Zeitwerk
|
|
548
357
|
end
|
549
358
|
end
|
550
359
|
|
551
|
-
# @
|
552
|
-
# @param cname [Symbol]
|
553
|
-
# @param subdir [String]
|
554
|
-
# @return [void]
|
360
|
+
# @sig (Module, Symbol, String) -> void
|
555
361
|
def autoload_subdir(parent, cname, subdir)
|
556
|
-
if autoload_path =
|
362
|
+
if autoload_path = autoloads.abspath_for(parent, cname)
|
557
363
|
cpath = cpath(parent, cname)
|
558
364
|
register_explicit_namespace(cpath) if ruby?(autoload_path)
|
559
365
|
# We do not need to issue another autoload, the existing one is enough
|
560
366
|
# no matter if it is for a file or a directory. Just remember the
|
561
367
|
# subdirectory has to be visited if the namespace is used.
|
562
|
-
|
368
|
+
lazy_subdirs[cpath] << subdir
|
563
369
|
elsif !cdef?(parent, cname)
|
564
370
|
# First time we find this namespace, set an autoload for it.
|
565
|
-
|
371
|
+
lazy_subdirs[cpath(parent, cname)] << subdir
|
566
372
|
set_autoload(parent, cname, subdir)
|
567
373
|
else
|
568
374
|
# For whatever reason the constant that corresponds to this namespace has
|
569
375
|
# already been defined, we have to recurse.
|
570
|
-
|
376
|
+
log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
|
377
|
+
set_autoloads_in_dir(subdir, cget(parent, cname))
|
571
378
|
end
|
572
379
|
end
|
573
380
|
|
574
|
-
# @
|
575
|
-
# @param cname [Symbol]
|
576
|
-
# @param file [String]
|
577
|
-
# @return [void]
|
381
|
+
# @sig (Module, Symbol, String) -> void
|
578
382
|
def autoload_file(parent, cname, file)
|
579
|
-
if autoload_path =
|
383
|
+
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
580
384
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
581
385
|
if ruby?(autoload_path)
|
582
386
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
@@ -595,194 +399,46 @@ module Zeitwerk
|
|
595
399
|
end
|
596
400
|
end
|
597
401
|
|
598
|
-
#
|
599
|
-
#
|
600
|
-
#
|
601
|
-
# @
|
602
|
-
# @return [void]
|
402
|
+
# `dir` is the directory that would have autovivified a namespace. `file` is
|
403
|
+
# the file where we've found the namespace is explicitly defined.
|
404
|
+
#
|
405
|
+
# @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
|
603
406
|
def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
|
604
407
|
autoloads.delete(dir)
|
605
408
|
Registry.unregister_autoload(dir)
|
606
409
|
|
410
|
+
log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
411
|
+
|
607
412
|
set_autoload(parent, cname, file)
|
608
413
|
register_explicit_namespace(cpath(parent, cname))
|
609
414
|
end
|
610
415
|
|
611
|
-
# @
|
612
|
-
# @param cname [Symbol]
|
613
|
-
# @param abspath [String]
|
614
|
-
# @return [void]
|
416
|
+
# @sig (Module, Symbol, String) -> void
|
615
417
|
def set_autoload(parent, cname, abspath)
|
616
|
-
|
617
|
-
|
618
|
-
# be able to do a lookup later in Kernel#require for manual require calls.
|
619
|
-
#
|
620
|
-
# We freeze realpath because that saves allocations in Module#autoload.
|
621
|
-
# See #125.
|
622
|
-
realpath = File.realpath(abspath).freeze
|
623
|
-
parent.autoload(cname, realpath)
|
418
|
+
autoloads.define(parent, cname, abspath)
|
419
|
+
|
624
420
|
if logger
|
625
|
-
if ruby?(
|
626
|
-
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{
|
421
|
+
if ruby?(abspath)
|
422
|
+
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
|
627
423
|
else
|
628
|
-
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{
|
424
|
+
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
|
629
425
|
end
|
630
426
|
end
|
631
427
|
|
632
|
-
|
633
|
-
Registry.register_autoload(self, realpath)
|
428
|
+
Registry.register_autoload(self, abspath)
|
634
429
|
|
635
430
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
636
431
|
unless parent.autoload?(cname)
|
637
|
-
Registry.register_inception(cpath(parent, cname),
|
638
|
-
end
|
639
|
-
end
|
640
|
-
|
641
|
-
# @param parent [Module]
|
642
|
-
# @param cname [Symbol]
|
643
|
-
# @return [String, nil]
|
644
|
-
def autoload_for?(parent, cname)
|
645
|
-
strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
646
|
-
end
|
647
|
-
|
648
|
-
# The autoload? predicate takes into account the ancestor chain of the
|
649
|
-
# receiver, like const_defined? and other methods in the constants API do.
|
650
|
-
#
|
651
|
-
# For example, given
|
652
|
-
#
|
653
|
-
# class A
|
654
|
-
# autoload :X, "x.rb"
|
655
|
-
# end
|
656
|
-
#
|
657
|
-
# class B < A
|
658
|
-
# end
|
659
|
-
#
|
660
|
-
# B.autoload?(:X) returns "x.rb".
|
661
|
-
#
|
662
|
-
# We need a way to strictly check in parent ignoring ancestors.
|
663
|
-
#
|
664
|
-
# @param parent [Module]
|
665
|
-
# @param cname [Symbol]
|
666
|
-
# @return [String, nil]
|
667
|
-
if method(:autoload?).arity == 1
|
668
|
-
def strict_autoload_path(parent, cname)
|
669
|
-
parent.autoload?(cname) if cdef?(parent, cname)
|
670
|
-
end
|
671
|
-
else
|
672
|
-
def strict_autoload_path(parent, cname)
|
673
|
-
parent.autoload?(cname, false)
|
432
|
+
Registry.register_inception(cpath(parent, cname), abspath, self)
|
674
433
|
end
|
675
434
|
end
|
676
435
|
|
677
|
-
#
|
678
|
-
# name to configure preloads in the public interface.
|
679
|
-
#
|
680
|
-
# @return [void]
|
681
|
-
def do_preload
|
682
|
-
preloads.each do |abspath|
|
683
|
-
do_preload_abspath(abspath)
|
684
|
-
end
|
685
|
-
end
|
686
|
-
|
687
|
-
# @param abspath [String]
|
688
|
-
# @return [void]
|
689
|
-
def do_preload_abspath(abspath)
|
690
|
-
if ruby?(abspath)
|
691
|
-
do_preload_file(abspath)
|
692
|
-
elsif dir?(abspath)
|
693
|
-
do_preload_dir(abspath)
|
694
|
-
end
|
695
|
-
end
|
696
|
-
|
697
|
-
# @param dir [String]
|
698
|
-
# @return [void]
|
699
|
-
def do_preload_dir(dir)
|
700
|
-
ls(dir) do |_basename, abspath|
|
701
|
-
do_preload_abspath(abspath)
|
702
|
-
end
|
703
|
-
end
|
704
|
-
|
705
|
-
# @param file [String]
|
706
|
-
# @return [Boolean]
|
707
|
-
def do_preload_file(file)
|
708
|
-
log("preloading #{file}") if logger
|
709
|
-
require file
|
710
|
-
end
|
711
|
-
|
712
|
-
# @param parent [Module]
|
713
|
-
# @param cname [Symbol]
|
714
|
-
# @return [String]
|
715
|
-
def cpath(parent, cname)
|
716
|
-
parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
717
|
-
end
|
718
|
-
|
719
|
-
# @param dir [String]
|
720
|
-
# @yieldparam path [String, String]
|
721
|
-
# @return [void]
|
722
|
-
def ls(dir)
|
723
|
-
Dir.foreach(dir) do |basename|
|
724
|
-
next if basename.start_with?(".")
|
725
|
-
|
726
|
-
abspath = File.join(dir, basename)
|
727
|
-
next if ignored_paths.member?(abspath)
|
728
|
-
|
729
|
-
# We freeze abspath because that saves allocations when passed later to
|
730
|
-
# File methods. See #125.
|
731
|
-
yield basename, abspath.freeze
|
732
|
-
end
|
733
|
-
end
|
734
|
-
|
735
|
-
# @param path [String]
|
736
|
-
# @return [Boolean]
|
737
|
-
def ruby?(path)
|
738
|
-
path.end_with?(".rb")
|
739
|
-
end
|
740
|
-
|
741
|
-
# @param path [String]
|
742
|
-
# @return [Boolean]
|
743
|
-
def dir?(path)
|
744
|
-
File.directory?(path)
|
745
|
-
end
|
746
|
-
|
747
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
748
|
-
# @return [<String>]
|
749
|
-
def expand_paths(paths)
|
750
|
-
paths.flatten.map! { |path| File.expand_path(path) }
|
751
|
-
end
|
752
|
-
|
753
|
-
# @param glob_patterns [<String>]
|
754
|
-
# @return [<String>]
|
755
|
-
def expand_glob_patterns(glob_patterns)
|
756
|
-
# Note that Dir.glob works with regular file names just fine. That is,
|
757
|
-
# glob patterns technically need no wildcards.
|
758
|
-
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
759
|
-
end
|
760
|
-
|
761
|
-
# @return [void]
|
762
|
-
def recompute_ignored_paths
|
763
|
-
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
764
|
-
end
|
765
|
-
|
766
|
-
# @return [void]
|
767
|
-
def recompute_collapse_dirs
|
768
|
-
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
769
|
-
end
|
770
|
-
|
771
|
-
# @param message [String]
|
772
|
-
# @return [void]
|
773
|
-
def log(message)
|
774
|
-
method_name = logger.respond_to?(:debug) ? :debug : :call
|
775
|
-
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
776
|
-
end
|
777
|
-
|
778
|
-
def cdef?(parent, cname)
|
779
|
-
parent.const_defined?(cname, false)
|
780
|
-
end
|
781
|
-
|
436
|
+
# @sig (String) -> void
|
782
437
|
def register_explicit_namespace(cpath)
|
783
438
|
ExplicitNamespace.register(cpath, self)
|
784
439
|
end
|
785
440
|
|
441
|
+
# @sig (String) -> void
|
786
442
|
def raise_if_conflicting_directory(dir)
|
787
443
|
self.class.mutex.synchronize do
|
788
444
|
Registry.loaders.each do |loader|
|
@@ -797,19 +453,22 @@ module Zeitwerk
|
|
797
453
|
end
|
798
454
|
end
|
799
455
|
|
800
|
-
# @
|
801
|
-
|
802
|
-
|
456
|
+
# @sig (String, Object, String) -> void
|
457
|
+
def run_on_unload_callbacks(cpath, value, abspath)
|
458
|
+
# Order matters. If present, run the most specific one.
|
459
|
+
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
460
|
+
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
461
|
+
end
|
462
|
+
|
463
|
+
# @sig (Module, Symbol) -> void
|
803
464
|
def unload_autoload(parent, cname)
|
804
|
-
parent.
|
465
|
+
parent.__send__(:remove_const, cname)
|
805
466
|
log("autoload for #{cpath(parent, cname)} removed") if logger
|
806
467
|
end
|
807
468
|
|
808
|
-
# @
|
809
|
-
# @param cname [Symbol]
|
810
|
-
# @return [void]
|
469
|
+
# @sig (Module, Symbol) -> void
|
811
470
|
def unload_cref(parent, cname)
|
812
|
-
parent.
|
471
|
+
parent.__send__(:remove_const, cname)
|
813
472
|
log("#{cpath(parent, cname)} unloaded") if logger
|
814
473
|
end
|
815
474
|
end
|