zeitwerk 2.4.0 → 2.5.0.beta2
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 +209 -57
- data/lib/zeitwerk.rb +13 -0
- data/lib/zeitwerk/autoloads.rb +69 -0
- data/lib/zeitwerk/explicit_namespace.rb +18 -11
- data/lib/zeitwerk/gem_inflector.rb +2 -4
- data/lib/zeitwerk/inflector.rb +3 -6
- data/lib/zeitwerk/kernel.rb +7 -6
- data/lib/zeitwerk/loader.rb +116 -456
- 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 +28 -30
- data/lib/zeitwerk/version.rb +1 -1
- metadata +12 -9
@@ -14,24 +14,22 @@ module Zeitwerk
|
|
14
14
|
# the file system, to the loader responsible for them.
|
15
15
|
#
|
16
16
|
# @private
|
17
|
-
# @
|
17
|
+
# @sig Hash[String, Zeitwerk::Loader]
|
18
18
|
attr_reader :cpaths
|
19
19
|
|
20
20
|
# @private
|
21
|
-
# @
|
21
|
+
# @sig Mutex
|
22
22
|
attr_reader :mutex
|
23
23
|
|
24
24
|
# @private
|
25
|
-
# @
|
25
|
+
# @sig TracePoint
|
26
26
|
attr_reader :tracer
|
27
27
|
|
28
28
|
# Asserts `cpath` corresponds to an explicit namespace for which `loader`
|
29
29
|
# is responsible.
|
30
30
|
#
|
31
31
|
# @private
|
32
|
-
# @
|
33
|
-
# @param loader [Zeitwerk::Loader]
|
34
|
-
# @return [void]
|
32
|
+
# @sig (String, Zeitwerk::Loader) -> void
|
35
33
|
def register(cpath, loader)
|
36
34
|
mutex.synchronize do
|
37
35
|
cpaths[cpath] = loader
|
@@ -42,27 +40,36 @@ module Zeitwerk
|
|
42
40
|
end
|
43
41
|
|
44
42
|
# @private
|
45
|
-
# @
|
46
|
-
|
47
|
-
def unregister(loader)
|
43
|
+
# @sig (Zeitwerk::Loader) -> void
|
44
|
+
def unregister_loader(loader)
|
48
45
|
cpaths.delete_if { |_cpath, l| l == loader }
|
49
46
|
disable_tracer_if_unneeded
|
50
47
|
end
|
51
48
|
|
49
|
+
private
|
50
|
+
|
51
|
+
# @sig () -> void
|
52
52
|
def disable_tracer_if_unneeded
|
53
53
|
mutex.synchronize do
|
54
54
|
tracer.disable if cpaths.empty?
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
+
# @sig (TracePoint) -> void
|
58
59
|
def tracepoint_class_callback(event)
|
59
60
|
# If the class is a singleton class, we won't do anything with it so we
|
60
61
|
# can bail out immediately. This is several orders of magnitude faster
|
61
62
|
# than accessing its name.
|
62
63
|
return if event.self.singleton_class?
|
63
64
|
|
64
|
-
#
|
65
|
-
#
|
65
|
+
# It might be tempting to return if name.nil?, to avoid the computation
|
66
|
+
# of a hash code and delete call. But Ruby does not trigger the :class
|
67
|
+
# event on Class.new or Module.new, so that would incur in an extra call
|
68
|
+
# for nothing.
|
69
|
+
#
|
70
|
+
# On the other hand, if we were called, cpaths is not empty. Otherwise
|
71
|
+
# the tracer is disabled. So we do need to go ahead with the hash code
|
72
|
+
# computation and delete call.
|
66
73
|
if loader = cpaths.delete(real_mod_name(event.self))
|
67
74
|
loader.on_namespace_loaded(event.self)
|
68
75
|
disable_tracer_if_unneeded
|
@@ -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,9 +11,7 @@ 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
16
|
overrides[basename] || basename.split('_').each(&:capitalize!).join
|
19
17
|
end
|
@@ -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,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
|
-
# @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
|
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,6 @@ 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
|
-
do_preload
|
281
112
|
|
282
113
|
@setup = true
|
283
114
|
end
|
@@ -290,8 +121,11 @@ module Zeitwerk
|
|
290
121
|
# else, they are eligible for garbage collection, which would effectively
|
291
122
|
# unload them.
|
292
123
|
#
|
293
|
-
#
|
294
|
-
#
|
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
|
+
#
|
128
|
+
# @sig () -> void
|
295
129
|
def unload
|
296
130
|
mutex.synchronize do
|
297
131
|
# We are going to keep track of the files that were required by our
|
@@ -302,21 +136,26 @@ module Zeitwerk
|
|
302
136
|
# is enough.
|
303
137
|
unloaded_files = Set.new
|
304
138
|
|
305
|
-
autoloads.each do |
|
139
|
+
autoloads.each do |(parent, cname), abspath|
|
306
140
|
if parent.autoload?(cname)
|
307
141
|
unload_autoload(parent, cname)
|
308
142
|
else
|
309
143
|
# Could happen if loaded with require_relative. That is unsupported,
|
310
144
|
# and the constant path would escape unloadable_cpath? This is just
|
311
145
|
# defensive code to clean things up as much as we are able to.
|
312
|
-
unload_cref(parent, cname)
|
313
|
-
unloaded_files.add(
|
146
|
+
unload_cref(parent, cname) if cdef?(parent, cname)
|
147
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
314
148
|
end
|
315
149
|
end
|
316
150
|
|
317
|
-
to_unload.
|
318
|
-
|
319
|
-
|
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)
|
320
159
|
end
|
321
160
|
|
322
161
|
unless unloaded_files.empty?
|
@@ -340,7 +179,7 @@ module Zeitwerk
|
|
340
179
|
lazy_subdirs.clear
|
341
180
|
|
342
181
|
Registry.on_unload(self)
|
343
|
-
ExplicitNamespace.
|
182
|
+
ExplicitNamespace.unregister_loader(self)
|
344
183
|
|
345
184
|
@setup = false
|
346
185
|
@eager_loaded = false
|
@@ -354,7 +193,7 @@ module Zeitwerk
|
|
354
193
|
# client code in the README of the project.
|
355
194
|
#
|
356
195
|
# @raise [Zeitwerk::Error]
|
357
|
-
# @
|
196
|
+
# @sig () -> void
|
358
197
|
def reload
|
359
198
|
if reloading_enabled?
|
360
199
|
unload
|
@@ -371,32 +210,34 @@ module Zeitwerk
|
|
371
210
|
# are not eager loaded. You can opt-out specifically in specific files and
|
372
211
|
# directories with `do_not_eager_load`.
|
373
212
|
#
|
374
|
-
# @
|
213
|
+
# @sig () -> void
|
375
214
|
def eager_load
|
376
215
|
mutex.synchronize do
|
377
216
|
break if @eager_loaded
|
378
217
|
|
218
|
+
log("eager load start") if logger
|
219
|
+
|
379
220
|
queue = []
|
380
221
|
actual_root_dirs.each do |root_dir, namespace|
|
381
|
-
queue << [namespace, root_dir] unless
|
222
|
+
queue << [namespace, root_dir] unless excluded_from_eager_load?(root_dir)
|
382
223
|
end
|
383
224
|
|
384
225
|
while to_eager_load = queue.shift
|
385
226
|
namespace, dir = to_eager_load
|
386
227
|
|
387
228
|
ls(dir) do |basename, abspath|
|
388
|
-
next if
|
229
|
+
next if excluded_from_eager_load?(abspath)
|
389
230
|
|
390
231
|
if ruby?(abspath)
|
391
|
-
if cref = autoloads
|
392
|
-
|
232
|
+
if cref = autoloads.cref_for(abspath)
|
233
|
+
cget(*cref)
|
393
234
|
end
|
394
235
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
395
|
-
if
|
236
|
+
if collapse?(abspath)
|
396
237
|
queue << [namespace, abspath]
|
397
238
|
else
|
398
239
|
cname = inflector.camelize(basename, abspath)
|
399
|
-
queue << [namespace
|
240
|
+
queue << [cget(namespace, cname), abspath]
|
400
241
|
end
|
401
242
|
end
|
402
243
|
end
|
@@ -408,23 +249,15 @@ module Zeitwerk
|
|
408
249
|
autoloaded_dirs.clear
|
409
250
|
|
410
251
|
@eager_loaded = true
|
411
|
-
end
|
412
|
-
end
|
413
252
|
|
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)) }
|
253
|
+
log("eager load end") if logger
|
254
|
+
end
|
421
255
|
end
|
422
256
|
|
423
257
|
# Says if the given constant path would be unloaded on reload. This
|
424
258
|
# predicate returns `false` if reloading is disabled.
|
425
259
|
#
|
426
|
-
# @
|
427
|
-
# @return [Boolean]
|
260
|
+
# @sig (String) -> bool
|
428
261
|
def unloadable_cpath?(cpath)
|
429
262
|
to_unload.key?(cpath)
|
430
263
|
end
|
@@ -432,42 +265,28 @@ module Zeitwerk
|
|
432
265
|
# Returns an array with the constant paths that would be unloaded on reload.
|
433
266
|
# This predicate returns an empty array if reloading is disabled.
|
434
267
|
#
|
435
|
-
# @
|
268
|
+
# @sig () -> Array[String]
|
436
269
|
def unloadable_cpaths
|
437
270
|
to_unload.keys.freeze
|
438
271
|
end
|
439
272
|
|
440
|
-
#
|
273
|
+
# This is a dangerous method.
|
441
274
|
#
|
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
|
275
|
+
# @experimental
|
276
|
+
# @sig () -> void
|
277
|
+
def unregister
|
278
|
+
Registry.unregister_loader(self)
|
279
|
+
ExplicitNamespace.unregister_loader(self)
|
461
280
|
end
|
462
281
|
|
463
282
|
# --- Class methods ---------------------------------------------------------------------------
|
464
283
|
|
465
284
|
class << self
|
466
|
-
# @
|
285
|
+
# @sig #call | #debug | nil
|
467
286
|
attr_accessor :default_logger
|
468
287
|
|
469
288
|
# @private
|
470
|
-
# @
|
289
|
+
# @sig Mutex
|
471
290
|
attr_accessor :mutex
|
472
291
|
|
473
292
|
# This is a shortcut for
|
@@ -481,7 +300,7 @@ module Zeitwerk
|
|
481
300
|
# except that this method returns the same object in subsequent calls from
|
482
301
|
# the same file, in the unlikely case the gem wants to be able to reload.
|
483
302
|
#
|
484
|
-
# @
|
303
|
+
# @sig () -> Zeitwerk::Loader
|
485
304
|
def for_gem
|
486
305
|
called_from = caller_locations(1, 1).first.path
|
487
306
|
Registry.loader_for_gem(called_from)
|
@@ -489,7 +308,7 @@ module Zeitwerk
|
|
489
308
|
|
490
309
|
# Broadcasts `eager_load` to all loaders.
|
491
310
|
#
|
492
|
-
# @
|
311
|
+
# @sig () -> void
|
493
312
|
def eager_load_all
|
494
313
|
Registry.loaders.each(&:eager_load)
|
495
314
|
end
|
@@ -497,7 +316,7 @@ module Zeitwerk
|
|
497
316
|
# Returns an array with the absolute paths of the root directories of all
|
498
317
|
# registered loaders. This is a read-only collection.
|
499
318
|
#
|
500
|
-
# @
|
319
|
+
# @sig () -> Array[String]
|
501
320
|
def all_dirs
|
502
321
|
Registry.loaders.flat_map(&:dirs).freeze
|
503
322
|
end
|
@@ -507,21 +326,12 @@ module Zeitwerk
|
|
507
326
|
|
508
327
|
private # -------------------------------------------------------------------------------------
|
509
328
|
|
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]
|
329
|
+
# @sig (String, Module) -> void
|
520
330
|
def set_autoloads_in_dir(dir, parent)
|
521
331
|
ls(dir) do |basename, abspath|
|
522
332
|
begin
|
523
333
|
if ruby?(basename)
|
524
|
-
basename
|
334
|
+
basename.delete_suffix!(".rb")
|
525
335
|
cname = inflector.camelize(basename, abspath).to_sym
|
526
336
|
autoload_file(parent, cname, abspath)
|
527
337
|
elsif dir?(abspath)
|
@@ -531,9 +341,9 @@ module Zeitwerk
|
|
531
341
|
# To resolve the ambiguity file name -> constant path this introduces,
|
532
342
|
# the `app/models/concerns` directory is totally ignored as a namespace,
|
533
343
|
# it counts only as root. The guard checks that.
|
534
|
-
unless
|
344
|
+
unless root_dir?(abspath)
|
535
345
|
cname = inflector.camelize(basename, abspath).to_sym
|
536
|
-
if
|
346
|
+
if collapse?(abspath)
|
537
347
|
set_autoloads_in_dir(abspath, parent)
|
538
348
|
else
|
539
349
|
autoload_subdir(parent, cname, abspath)
|
@@ -559,35 +369,30 @@ module Zeitwerk
|
|
559
369
|
end
|
560
370
|
end
|
561
371
|
|
562
|
-
# @
|
563
|
-
# @param cname [Symbol]
|
564
|
-
# @param subdir [String]
|
565
|
-
# @return [void]
|
372
|
+
# @sig (Module, Symbol, String) -> void
|
566
373
|
def autoload_subdir(parent, cname, subdir)
|
567
|
-
if autoload_path =
|
374
|
+
if autoload_path = autoloads.abspath_for(parent, cname)
|
568
375
|
cpath = cpath(parent, cname)
|
569
376
|
register_explicit_namespace(cpath) if ruby?(autoload_path)
|
570
377
|
# We do not need to issue another autoload, the existing one is enough
|
571
378
|
# no matter if it is for a file or a directory. Just remember the
|
572
379
|
# subdirectory has to be visited if the namespace is used.
|
573
|
-
|
380
|
+
lazy_subdirs[cpath] << subdir
|
574
381
|
elsif !cdef?(parent, cname)
|
575
382
|
# First time we find this namespace, set an autoload for it.
|
576
|
-
|
383
|
+
lazy_subdirs[cpath(parent, cname)] << subdir
|
577
384
|
set_autoload(parent, cname, subdir)
|
578
385
|
else
|
579
386
|
# For whatever reason the constant that corresponds to this namespace has
|
580
387
|
# already been defined, we have to recurse.
|
581
|
-
|
388
|
+
log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
|
389
|
+
set_autoloads_in_dir(subdir, cget(parent, cname))
|
582
390
|
end
|
583
391
|
end
|
584
392
|
|
585
|
-
# @
|
586
|
-
# @param cname [Symbol]
|
587
|
-
# @param file [String]
|
588
|
-
# @return [void]
|
393
|
+
# @sig (Module, Symbol, String) -> void
|
589
394
|
def autoload_file(parent, cname, file)
|
590
|
-
if autoload_path =
|
395
|
+
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
591
396
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
592
397
|
if ruby?(autoload_path)
|
593
398
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
@@ -606,194 +411,46 @@ module Zeitwerk
|
|
606
411
|
end
|
607
412
|
end
|
608
413
|
|
609
|
-
#
|
610
|
-
#
|
611
|
-
#
|
612
|
-
# @
|
613
|
-
# @return [void]
|
414
|
+
# `dir` is the directory that would have autovivified a namespace. `file` is
|
415
|
+
# the file where we've found the namespace is explicitly defined.
|
416
|
+
#
|
417
|
+
# @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
|
614
418
|
def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
|
615
419
|
autoloads.delete(dir)
|
616
420
|
Registry.unregister_autoload(dir)
|
617
421
|
|
422
|
+
log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
423
|
+
|
618
424
|
set_autoload(parent, cname, file)
|
619
425
|
register_explicit_namespace(cpath(parent, cname))
|
620
426
|
end
|
621
427
|
|
622
|
-
# @
|
623
|
-
# @param cname [Symbol]
|
624
|
-
# @param abspath [String]
|
625
|
-
# @return [void]
|
428
|
+
# @sig (Module, Symbol, String) -> void
|
626
429
|
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)
|
430
|
+
autoloads.define(parent, cname, abspath)
|
431
|
+
|
635
432
|
if logger
|
636
|
-
if ruby?(
|
637
|
-
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}")
|
638
435
|
else
|
639
|
-
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{
|
436
|
+
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
|
640
437
|
end
|
641
438
|
end
|
642
439
|
|
643
|
-
|
644
|
-
Registry.register_autoload(self, realpath)
|
440
|
+
Registry.register_autoload(self, abspath)
|
645
441
|
|
646
442
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
647
443
|
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)
|
685
|
-
end
|
686
|
-
end
|
687
|
-
|
688
|
-
# This method is called this way because I prefer `preload` to be the method
|
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
|
444
|
+
Registry.register_inception(cpath(parent, cname), abspath, self)
|
743
445
|
end
|
744
446
|
end
|
745
447
|
|
746
|
-
# @
|
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
|
-
|
448
|
+
# @sig (String) -> void
|
793
449
|
def register_explicit_namespace(cpath)
|
794
450
|
ExplicitNamespace.register(cpath, self)
|
795
451
|
end
|
796
452
|
|
453
|
+
# @sig (String) -> void
|
797
454
|
def raise_if_conflicting_directory(dir)
|
798
455
|
self.class.mutex.synchronize do
|
799
456
|
Registry.loaders.each do |loader|
|
@@ -808,19 +465,22 @@ module Zeitwerk
|
|
808
465
|
end
|
809
466
|
end
|
810
467
|
|
811
|
-
# @
|
812
|
-
|
813
|
-
|
468
|
+
# @sig (String, Object, String) -> void
|
469
|
+
def run_on_unload_callbacks(cpath, value, abspath)
|
470
|
+
# Order matters. If present, run the most specific one.
|
471
|
+
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
472
|
+
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
473
|
+
end
|
474
|
+
|
475
|
+
# @sig (Module, Symbol) -> void
|
814
476
|
def unload_autoload(parent, cname)
|
815
|
-
parent.
|
477
|
+
parent.__send__(:remove_const, cname)
|
816
478
|
log("autoload for #{cpath(parent, cname)} removed") if logger
|
817
479
|
end
|
818
480
|
|
819
|
-
# @
|
820
|
-
# @param cname [Symbol]
|
821
|
-
# @return [void]
|
481
|
+
# @sig (Module, Symbol) -> void
|
822
482
|
def unload_cref(parent, cname)
|
823
|
-
parent.
|
483
|
+
parent.__send__(:remove_const, cname)
|
824
484
|
log("#{cpath(parent, cname)} unloaded") if logger
|
825
485
|
end
|
826
486
|
end
|