zeitwerk 2.6.18 → 2.8.1
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 +152 -115
- data/lib/zeitwerk/{kernel.rb → core_ext/kernel.rb} +4 -4
- data/lib/zeitwerk/core_ext/module.rb +20 -0
- data/lib/zeitwerk/cref/map.rb +159 -0
- data/lib/zeitwerk/cref.rb +29 -52
- data/lib/zeitwerk/error.rb +14 -1
- data/lib/zeitwerk/gem_inflector.rb +5 -5
- data/lib/zeitwerk/gem_loader.rb +10 -10
- data/lib/zeitwerk/inflector.rb +11 -11
- data/lib/zeitwerk/internal.rb +1 -0
- data/lib/zeitwerk/loader/callbacks.rb +21 -24
- data/lib/zeitwerk/loader/config.rb +109 -70
- data/lib/zeitwerk/loader/eager_load.rb +50 -56
- data/lib/zeitwerk/loader/file_system.rb +212 -0
- data/lib/zeitwerk/loader/helpers.rb +4 -106
- data/lib/zeitwerk/loader.rb +314 -239
- data/lib/zeitwerk/null_inflector.rb +1 -0
- data/lib/zeitwerk/real_mod_name.rb +9 -12
- data/lib/zeitwerk/registry/autoloads.rb +38 -0
- data/lib/zeitwerk/registry/explicit_namespaces.rb +61 -0
- data/lib/zeitwerk/registry/inceptions.rb +31 -0
- data/lib/zeitwerk/registry/loaders.rb +33 -0
- data/lib/zeitwerk/registry.rb +41 -93
- data/lib/zeitwerk/version.rb +2 -1
- data/lib/zeitwerk.rb +15 -14
- metadata +13 -10
- data/lib/zeitwerk/explicit_namespace.rb +0 -93
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'securerandom'
|
|
5
5
|
|
|
6
6
|
module Zeitwerk::Loader::Config
|
|
7
7
|
extend Zeitwerk::Internal
|
|
8
8
|
include Zeitwerk::RealModName
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
#: camelize(String, String) -> String
|
|
11
11
|
attr_accessor :inflector
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
#: call(String) -> void | debug(String) -> void | nil
|
|
14
14
|
attr_accessor :logger
|
|
15
15
|
|
|
16
16
|
# Absolute paths of the root directories, mapped to their respective root namespaces:
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
18
|
+
# '/Users/fxn/blog/app/channels' => Object,
|
|
19
|
+
# '/Users/fxn/blog/app/adapters' => ActiveJob::QueueAdapters,
|
|
20
20
|
# ...
|
|
21
21
|
#
|
|
22
22
|
# Stored in a hash to preserve order, easily handle duplicates, and have a
|
|
@@ -25,14 +25,19 @@ module Zeitwerk::Loader::Config
|
|
|
25
25
|
# This is a private collection maintained by the loader. The public
|
|
26
26
|
# interface for it is `push_dir` and `dirs`.
|
|
27
27
|
#
|
|
28
|
-
|
|
28
|
+
#: Hash[String, Module]
|
|
29
29
|
attr_reader :roots
|
|
30
30
|
internal :roots
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
32
|
+
# Basename of files that define namespaces. For example, if `nsfile` is
|
|
33
|
+
# 'ns.rb', then `foo/ns.rb` defines the `Foo` namespace.
|
|
34
34
|
#
|
|
35
|
-
|
|
35
|
+
#: String?
|
|
36
|
+
attr_reader :nsfile
|
|
37
|
+
|
|
38
|
+
# Absolute paths of files, directories, or glob patterns to be ignored.
|
|
39
|
+
#
|
|
40
|
+
#: Set[String]
|
|
36
41
|
attr_reader :ignored_glob_patterns
|
|
37
42
|
private :ignored_glob_patterns
|
|
38
43
|
|
|
@@ -40,59 +45,70 @@ module Zeitwerk::Loader::Config
|
|
|
40
45
|
# ignored glob patterns were expanded. Computed on setup, and recomputed on
|
|
41
46
|
# reload.
|
|
42
47
|
#
|
|
43
|
-
|
|
48
|
+
#: Set[String]
|
|
44
49
|
attr_reader :ignored_paths
|
|
45
50
|
private :ignored_paths
|
|
46
51
|
|
|
47
52
|
# Absolute paths of directories or glob patterns to be collapsed.
|
|
48
53
|
#
|
|
49
|
-
|
|
54
|
+
#: Set[String]
|
|
50
55
|
attr_reader :collapse_glob_patterns
|
|
51
56
|
private :collapse_glob_patterns
|
|
52
57
|
|
|
53
58
|
# The actual collection of absolute directory names at the time the collapse
|
|
54
|
-
# glob patterns were expanded. Computed on setup
|
|
59
|
+
# glob patterns were expanded. Computed on setup and recomputed on reload.
|
|
55
60
|
#
|
|
56
|
-
|
|
61
|
+
#: Set[String]
|
|
57
62
|
attr_reader :collapse_dirs
|
|
58
63
|
private :collapse_dirs
|
|
59
64
|
|
|
65
|
+
# Absolute paths of directories that are parents of collapsed directories.
|
|
66
|
+
# This is a cache to optimize some tree walks. Computed on setup and
|
|
67
|
+
# recomputed on reload.
|
|
68
|
+
#
|
|
69
|
+
#: Set[String]
|
|
70
|
+
attr_reader :collapse_parents
|
|
71
|
+
private :collapse_parents
|
|
72
|
+
|
|
60
73
|
# Absolute paths of files or directories not to be eager loaded.
|
|
61
74
|
#
|
|
62
|
-
|
|
75
|
+
#: Set[String]
|
|
63
76
|
attr_reader :eager_load_exclusions
|
|
64
77
|
private :eager_load_exclusions
|
|
65
78
|
|
|
66
79
|
# User-oriented callbacks to be fired on setup and on reload.
|
|
67
80
|
#
|
|
68
|
-
|
|
81
|
+
#: Array[{ () -> void }]
|
|
69
82
|
attr_reader :on_setup_callbacks
|
|
70
83
|
private :on_setup_callbacks
|
|
71
84
|
|
|
72
85
|
# User-oriented callbacks to be fired when a constant is loaded.
|
|
73
86
|
#
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
#: Hash[String, Array[{ (top, String) -> void }]]
|
|
88
|
+
#| Hash[Symbol, Array[{ (String, top, String) -> void }]]
|
|
76
89
|
attr_reader :on_load_callbacks
|
|
77
90
|
private :on_load_callbacks
|
|
78
91
|
|
|
79
92
|
# User-oriented callbacks to be fired before constants are removed.
|
|
80
93
|
#
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
#: Hash[String, Array[{ (top, String) -> void }]]
|
|
95
|
+
#| Hash[Symbol, Array[{ (String, top, String) -> void }]]
|
|
83
96
|
attr_reader :on_unload_callbacks
|
|
84
97
|
private :on_unload_callbacks
|
|
85
98
|
|
|
99
|
+
#: () -> void
|
|
86
100
|
def initialize
|
|
87
101
|
@inflector = Zeitwerk::Inflector.new
|
|
88
102
|
@logger = self.class.default_logger
|
|
89
103
|
@tag = SecureRandom.hex(3)
|
|
90
104
|
@initialized_at = Time.now
|
|
91
105
|
@roots = {}
|
|
106
|
+
@nsfile = nil
|
|
92
107
|
@ignored_glob_patterns = Set.new
|
|
93
108
|
@ignored_paths = Set.new
|
|
94
109
|
@collapse_glob_patterns = Set.new
|
|
95
110
|
@collapse_dirs = Set.new
|
|
111
|
+
@collapse_parents = Set.new
|
|
96
112
|
@eager_load_exclusions = Set.new
|
|
97
113
|
@reloading_enabled = false
|
|
98
114
|
@on_setup_callbacks = []
|
|
@@ -106,20 +122,19 @@ module Zeitwerk::Loader::Config
|
|
|
106
122
|
# the same process already manages that directory or one of its ascendants or
|
|
107
123
|
# descendants.
|
|
108
124
|
#
|
|
109
|
-
|
|
110
|
-
# @sig (String | Pathname, Module) -> void
|
|
125
|
+
#: (String | Pathname, namespace: Module) -> void ! Zeitwerk::Error
|
|
111
126
|
def push_dir(path, namespace: Object)
|
|
112
127
|
unless namespace.is_a?(Module) # Note that Class < Module.
|
|
113
128
|
raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
|
|
114
129
|
end
|
|
115
130
|
|
|
116
131
|
unless real_mod_name(namespace)
|
|
117
|
-
raise Zeitwerk::Error,
|
|
132
|
+
raise Zeitwerk::Error, 'root namespaces cannot be anonymous'
|
|
118
133
|
end
|
|
119
134
|
|
|
120
135
|
abspath = File.expand_path(path)
|
|
121
|
-
if dir?(abspath)
|
|
122
|
-
|
|
136
|
+
if @fs.dir?(abspath)
|
|
137
|
+
raise_if_conflicting_root_dir(abspath)
|
|
123
138
|
roots[abspath] = namespace
|
|
124
139
|
else
|
|
125
140
|
raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
|
|
@@ -131,18 +146,30 @@ module Zeitwerk::Loader::Config
|
|
|
131
146
|
# Implemented as a method instead of via attr_reader for symmetry with the
|
|
132
147
|
# writer below.
|
|
133
148
|
#
|
|
134
|
-
|
|
149
|
+
#: () -> String
|
|
135
150
|
def tag
|
|
136
151
|
@tag
|
|
137
152
|
end
|
|
138
153
|
|
|
139
154
|
# Sets a tag for the loader, useful for logging.
|
|
140
155
|
#
|
|
141
|
-
|
|
156
|
+
#: (to_s() -> String) -> void
|
|
142
157
|
def tag=(tag)
|
|
143
158
|
@tag = tag.to_s
|
|
144
159
|
end
|
|
145
160
|
|
|
161
|
+
#: (String?) -> void ! TypeError, ArgumentError
|
|
162
|
+
def nsfile=(nsfile)
|
|
163
|
+
unless nsfile.nil?
|
|
164
|
+
raise TypeError, 'nsfiles must be strings' unless nsfile.is_a?(String)
|
|
165
|
+
raise ArgumentError, 'nsfiles must have .rb extension' unless @fs.rb_extension?(nsfile)
|
|
166
|
+
raise ArgumentError, 'nsfiles must be basenames, not paths' unless File.basename(nsfile) == nsfile
|
|
167
|
+
raise ArgumentError, 'nsfiles cannot be hidden' if @fs.hidden?(nsfile)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
@nsfile = nsfile
|
|
171
|
+
end
|
|
172
|
+
|
|
146
173
|
# If `namespaces` is falsey (default), returns an array with the absolute
|
|
147
174
|
# paths of the root directories as strings. If truthy, returns a hash table
|
|
148
175
|
# instead. Keys are the absolute paths of the root directories as strings,
|
|
@@ -152,7 +179,7 @@ module Zeitwerk::Loader::Config
|
|
|
152
179
|
#
|
|
153
180
|
# These are read-only collections, please add to them with `push_dir`.
|
|
154
181
|
#
|
|
155
|
-
|
|
182
|
+
#: (?namespaces: boolish, ?ignored: boolish) -> Array[String] | Hash[String, Module]
|
|
156
183
|
def dirs(namespaces: false, ignored: false)
|
|
157
184
|
if namespaces
|
|
158
185
|
if ignored || ignored_paths.empty?
|
|
@@ -172,21 +199,20 @@ module Zeitwerk::Loader::Config
|
|
|
172
199
|
# You need to call this method before setup in order to be able to reload.
|
|
173
200
|
# There is no way to undo this, either you want to reload or you don't.
|
|
174
201
|
#
|
|
175
|
-
|
|
176
|
-
# @sig () -> void
|
|
202
|
+
#: () -> void ! Zeitwerk::Error
|
|
177
203
|
def enable_reloading
|
|
178
204
|
mutex.synchronize do
|
|
179
205
|
break if @reloading_enabled
|
|
180
206
|
|
|
181
207
|
if @setup
|
|
182
|
-
raise Zeitwerk::Error,
|
|
208
|
+
raise Zeitwerk::Error, 'cannot enable reloading after setup'
|
|
183
209
|
else
|
|
184
210
|
@reloading_enabled = true
|
|
185
211
|
end
|
|
186
212
|
end
|
|
187
213
|
end
|
|
188
214
|
|
|
189
|
-
|
|
215
|
+
#: () -> bool
|
|
190
216
|
def reloading_enabled?
|
|
191
217
|
@reloading_enabled
|
|
192
218
|
end
|
|
@@ -194,14 +220,14 @@ module Zeitwerk::Loader::Config
|
|
|
194
220
|
# Let eager load ignore the given files or directories. The constants defined
|
|
195
221
|
# in those files are still autoloadable.
|
|
196
222
|
#
|
|
197
|
-
|
|
223
|
+
#: (*(String | Pathname | Array[String | Pathname])) -> void
|
|
198
224
|
def do_not_eager_load(*paths)
|
|
199
225
|
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
|
200
226
|
end
|
|
201
227
|
|
|
202
228
|
# Configure files, directories, or glob patterns to be totally ignored.
|
|
203
229
|
#
|
|
204
|
-
|
|
230
|
+
#: (*(String | Pathname | Array[String | Pathname])) -> void
|
|
205
231
|
def ignore(*glob_patterns)
|
|
206
232
|
glob_patterns = expand_paths(glob_patterns)
|
|
207
233
|
mutex.synchronize do
|
|
@@ -212,19 +238,23 @@ module Zeitwerk::Loader::Config
|
|
|
212
238
|
|
|
213
239
|
# Configure directories or glob patterns to be collapsed.
|
|
214
240
|
#
|
|
215
|
-
|
|
241
|
+
#: (*(String | Pathname | Array[String | Pathname])) -> void
|
|
216
242
|
def collapse(*glob_patterns)
|
|
217
243
|
glob_patterns = expand_paths(glob_patterns)
|
|
218
244
|
mutex.synchronize do
|
|
219
245
|
collapse_glob_patterns.merge(glob_patterns)
|
|
220
|
-
|
|
246
|
+
new_collapse_dirs = expand_glob_patterns(glob_patterns)
|
|
247
|
+
collapse_dirs.merge(new_collapse_dirs)
|
|
248
|
+
new_collapse_dirs.each do |dir|
|
|
249
|
+
collapse_parents << File.dirname(dir)
|
|
250
|
+
end
|
|
221
251
|
end
|
|
222
252
|
end
|
|
223
253
|
|
|
224
254
|
# Configure a block to be called after setup and on each reload.
|
|
225
255
|
# If setup was already done, the block runs immediately.
|
|
226
256
|
#
|
|
227
|
-
|
|
257
|
+
#: () { () -> void } -> void
|
|
228
258
|
def on_setup(&block)
|
|
229
259
|
mutex.synchronize do
|
|
230
260
|
on_setup_callbacks << block
|
|
@@ -236,8 +266,8 @@ module Zeitwerk::Loader::Config
|
|
|
236
266
|
# Supports multiple callbacks, and if there are many, they are executed in
|
|
237
267
|
# the order in which they were defined.
|
|
238
268
|
#
|
|
239
|
-
# loader.on_load(
|
|
240
|
-
# klass.endpoint =
|
|
269
|
+
# loader.on_load('SomeApiClient') do |klass, _abspath|
|
|
270
|
+
# klass.endpoint = 'https://api.dev'
|
|
241
271
|
# end
|
|
242
272
|
#
|
|
243
273
|
# Can also be configured for any constant loaded:
|
|
@@ -246,11 +276,9 @@ module Zeitwerk::Loader::Config
|
|
|
246
276
|
# # ...
|
|
247
277
|
# end
|
|
248
278
|
#
|
|
249
|
-
|
|
250
|
-
# @sig (String) { (Object, String) -> void } -> void
|
|
251
|
-
# (:ANY) { (String, Object, String) -> void } -> void
|
|
279
|
+
#: (String?) { (top, String) -> void } -> void ! TypeError
|
|
252
280
|
def on_load(cpath = :ANY, &block)
|
|
253
|
-
raise TypeError,
|
|
281
|
+
raise TypeError, 'on_load only accepts strings' unless cpath.is_a?(String) || cpath == :ANY
|
|
254
282
|
|
|
255
283
|
mutex.synchronize do
|
|
256
284
|
(on_load_callbacks[cpath] ||= []) << block
|
|
@@ -261,7 +289,7 @@ module Zeitwerk::Loader::Config
|
|
|
261
289
|
# Supports multiple callbacks, and if there are many, they are executed in the
|
|
262
290
|
# order in which they were defined.
|
|
263
291
|
#
|
|
264
|
-
# loader.on_unload(
|
|
292
|
+
# loader.on_unload('Country') do |klass, _abspath|
|
|
265
293
|
# klass.clear_cache
|
|
266
294
|
# end
|
|
267
295
|
#
|
|
@@ -271,11 +299,9 @@ module Zeitwerk::Loader::Config
|
|
|
271
299
|
# # ...
|
|
272
300
|
# end
|
|
273
301
|
#
|
|
274
|
-
|
|
275
|
-
# @sig (String) { (Object) -> void } -> void
|
|
276
|
-
# (:ANY) { (String, Object) -> void } -> void
|
|
302
|
+
#: (String?) { (top, String) -> void } -> void ! TypeError
|
|
277
303
|
def on_unload(cpath = :ANY, &block)
|
|
278
|
-
raise TypeError,
|
|
304
|
+
raise TypeError, 'on_unload only accepts strings' unless cpath.is_a?(String) || cpath == :ANY
|
|
279
305
|
|
|
280
306
|
mutex.synchronize do
|
|
281
307
|
(on_unload_callbacks[cpath] ||= []) << block
|
|
@@ -284,7 +310,7 @@ module Zeitwerk::Loader::Config
|
|
|
284
310
|
|
|
285
311
|
# Logs to `$stdout`, handy shortcut for debugging.
|
|
286
312
|
#
|
|
287
|
-
|
|
313
|
+
#: () -> void
|
|
288
314
|
def log!
|
|
289
315
|
@logger = ->(msg) { puts msg }
|
|
290
316
|
end
|
|
@@ -292,73 +318,86 @@ module Zeitwerk::Loader::Config
|
|
|
292
318
|
# Returns true if the argument has been configured to be ignored, or is a
|
|
293
319
|
# descendant of an ignored directory.
|
|
294
320
|
#
|
|
295
|
-
|
|
321
|
+
#: (String) -> bool
|
|
296
322
|
internal def ignores?(abspath)
|
|
297
323
|
# Common use case.
|
|
298
324
|
return false if ignored_paths.empty?
|
|
299
325
|
|
|
300
|
-
walk_up(abspath) do |path|
|
|
326
|
+
@fs.walk_up(abspath) do |path|
|
|
301
327
|
return true if ignored_path?(path)
|
|
302
|
-
return false if
|
|
328
|
+
return false if root_dir?(path)
|
|
303
329
|
end
|
|
304
330
|
|
|
305
331
|
false
|
|
306
332
|
end
|
|
307
333
|
|
|
308
|
-
|
|
309
|
-
|
|
334
|
+
#: (String) -> bool
|
|
335
|
+
internal def ignored_path?(abspath)
|
|
310
336
|
ignored_paths.member?(abspath)
|
|
311
337
|
end
|
|
312
338
|
|
|
313
|
-
|
|
339
|
+
#: () -> Array[String]
|
|
314
340
|
private def actual_roots
|
|
315
341
|
roots.reject do |root_dir, _root_namespace|
|
|
316
|
-
|
|
342
|
+
!@fs.dir?(root_dir) || ignored_path?(root_dir)
|
|
317
343
|
end
|
|
318
344
|
end
|
|
319
345
|
|
|
320
|
-
|
|
321
|
-
|
|
346
|
+
#: (String) -> bool
|
|
347
|
+
internal def root_dir?(dir)
|
|
322
348
|
roots.key?(dir)
|
|
323
349
|
end
|
|
324
350
|
|
|
325
|
-
|
|
351
|
+
#: (String) -> bool
|
|
352
|
+
internal def collapse?(dir)
|
|
353
|
+
collapse_dirs.member?(dir)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
#: (String) -> bool
|
|
357
|
+
internal def collapse_parent?(dir)
|
|
358
|
+
collapse_parents.member?(dir)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
#: (String) -> bool
|
|
326
362
|
private def excluded_from_eager_load?(abspath)
|
|
327
363
|
# Optimize this common use case.
|
|
328
364
|
return false if eager_load_exclusions.empty?
|
|
329
365
|
|
|
330
|
-
walk_up(abspath) do |path|
|
|
366
|
+
@fs.walk_up(abspath) do |path|
|
|
331
367
|
return true if eager_load_exclusions.member?(path)
|
|
332
|
-
return false if
|
|
368
|
+
return false if root_dir?(path)
|
|
333
369
|
end
|
|
334
370
|
|
|
335
371
|
false
|
|
336
372
|
end
|
|
337
373
|
|
|
338
|
-
|
|
339
|
-
private def collapse?(dir)
|
|
340
|
-
collapse_dirs.member?(dir)
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
# @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
|
|
374
|
+
#: (String | Pathname | Array[String | Pathname]) -> Array[String]
|
|
344
375
|
private def expand_paths(paths)
|
|
345
376
|
paths.flatten.map! { |path| File.expand_path(path) }
|
|
346
377
|
end
|
|
347
378
|
|
|
348
|
-
|
|
379
|
+
#: (Array[String]) -> Array[String]
|
|
349
380
|
private def expand_glob_patterns(glob_patterns)
|
|
350
381
|
# Note that Dir.glob works with regular file names just fine. That is,
|
|
351
382
|
# glob patterns technically need no wildcards.
|
|
352
383
|
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
|
353
384
|
end
|
|
354
385
|
|
|
355
|
-
|
|
386
|
+
#: () -> void
|
|
356
387
|
private def recompute_ignored_paths
|
|
357
388
|
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
|
358
389
|
end
|
|
359
390
|
|
|
360
|
-
|
|
391
|
+
#: () -> void
|
|
361
392
|
private def recompute_collapse_dirs
|
|
362
393
|
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
|
363
394
|
end
|
|
395
|
+
|
|
396
|
+
#: () -> void
|
|
397
|
+
private def recompute_collapse_parents
|
|
398
|
+
collapse_parents.clear
|
|
399
|
+
collapse_dirs.each do |dir|
|
|
400
|
+
collapse_parents << File.dirname(dir)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
364
403
|
end
|
|
@@ -1,56 +1,55 @@
|
|
|
1
1
|
module Zeitwerk::Loader::EagerLoad
|
|
2
2
|
# Eager loads all files in the root directories, recursively. Files do not
|
|
3
|
-
# need to be in `$LOAD_PATH`, absolute file names are used.
|
|
4
|
-
# shadowed files are not eager loaded. You can opt-out specifically in
|
|
5
|
-
# specific files and directories with `do_not_eager_load`, and that can be
|
|
6
|
-
# overridden passing `force: true`.
|
|
3
|
+
# need to be in `$LOAD_PATH`, absolute file names are used.
|
|
7
4
|
#
|
|
8
|
-
#
|
|
5
|
+
# Ignored files are not eager loaded. You can opt-out specifically in specific
|
|
6
|
+
# files and directories with `do_not_eager_load`, and that can be overridden
|
|
7
|
+
# passing `force: true`.
|
|
8
|
+
#
|
|
9
|
+
#: (?force: boolish) -> void
|
|
9
10
|
def eager_load(force: false)
|
|
10
11
|
mutex.synchronize do
|
|
11
12
|
break if @eager_loaded
|
|
12
13
|
raise Zeitwerk::SetupRequired unless @setup
|
|
13
14
|
|
|
14
|
-
log
|
|
15
|
+
log { 'eager load start' }
|
|
15
16
|
|
|
16
17
|
actual_roots.each do |root_dir, root_namespace|
|
|
17
18
|
actual_eager_load_dir(root_dir, root_namespace, force: force)
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
autoloaded_dirs.each do |autoloaded_dir|
|
|
21
|
-
Zeitwerk::Registry.
|
|
22
|
+
Zeitwerk::Registry.autoloads.unregister(autoloaded_dir)
|
|
22
23
|
end
|
|
23
24
|
autoloaded_dirs.clear
|
|
24
25
|
|
|
25
26
|
@eager_loaded = true
|
|
26
27
|
|
|
27
|
-
log
|
|
28
|
+
log { 'eager load end' }
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
#: (String | Pathname) -> void
|
|
32
33
|
def eager_load_dir(path)
|
|
33
34
|
raise Zeitwerk::SetupRequired unless @setup
|
|
34
35
|
|
|
35
36
|
abspath = File.expand_path(path)
|
|
36
37
|
|
|
37
|
-
raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
|
|
38
|
+
raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
paths = []
|
|
40
41
|
|
|
41
42
|
root_namespace = nil
|
|
42
|
-
walk_up(abspath) do |dir|
|
|
43
|
+
@fs.walk_up(abspath) do |dir|
|
|
43
44
|
return if ignored_path?(dir)
|
|
44
45
|
return if eager_load_exclusions.member?(dir)
|
|
45
46
|
|
|
46
47
|
break if root_namespace = roots[dir]
|
|
47
48
|
|
|
48
49
|
basename = File.basename(dir)
|
|
49
|
-
return if hidden?(basename)
|
|
50
|
+
return if @fs.hidden?(basename)
|
|
50
51
|
|
|
51
|
-
unless collapse?(dir)
|
|
52
|
-
cnames << inflector.camelize(basename, dir).to_sym
|
|
53
|
-
end
|
|
52
|
+
paths << [basename, dir] unless collapse?(dir)
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
|
@@ -58,7 +57,8 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
58
57
|
return if @eager_loaded
|
|
59
58
|
|
|
60
59
|
namespace = root_namespace
|
|
61
|
-
|
|
60
|
+
paths.reverse_each do |basename, dir|
|
|
61
|
+
cname = cname_for(basename, dir)
|
|
62
62
|
# Can happen if there are no Ruby files. This is not an error condition,
|
|
63
63
|
# the directory is actually managed. Could have Ruby files later.
|
|
64
64
|
return unless namespace.const_defined?(cname, false)
|
|
@@ -70,7 +70,7 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
70
70
|
actual_eager_load_dir(abspath, namespace)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
#: (Module) -> void
|
|
74
74
|
def eager_load_namespace(mod)
|
|
75
75
|
raise Zeitwerk::SetupRequired unless @setup
|
|
76
76
|
|
|
@@ -84,7 +84,7 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
84
84
|
return unless mod_name
|
|
85
85
|
|
|
86
86
|
actual_roots.each do |root_dir, root_namespace|
|
|
87
|
-
if
|
|
87
|
+
if Object.equal?(mod)
|
|
88
88
|
# A shortcircuiting test depends on the invocation of this method.
|
|
89
89
|
# Please keep them in sync if refactored.
|
|
90
90
|
actual_eager_load_dir(root_dir, root_namespace)
|
|
@@ -92,11 +92,11 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
92
92
|
eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
|
|
93
93
|
else
|
|
94
94
|
root_namespace_name = real_mod_name(root_namespace)
|
|
95
|
-
if root_namespace_name.start_with?(mod_name +
|
|
95
|
+
if root_namespace_name.start_with?(mod_name + '::')
|
|
96
96
|
actual_eager_load_dir(root_dir, root_namespace)
|
|
97
97
|
elsif mod_name == root_namespace_name
|
|
98
98
|
actual_eager_load_dir(root_dir, root_namespace)
|
|
99
|
-
elsif mod_name.start_with?(root_namespace_name +
|
|
99
|
+
elsif mod_name.start_with?(root_namespace_name + '::')
|
|
100
100
|
eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
|
|
101
101
|
else
|
|
102
102
|
# Unrelated constant hierarchies, do nothing.
|
|
@@ -112,60 +112,62 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
112
112
|
# The method is implemented as `constantize` for files, in a sense, to be able
|
|
113
113
|
# to descend orderly and make sure the file is loadable.
|
|
114
114
|
#
|
|
115
|
-
|
|
115
|
+
#: (String | Pathname) -> void
|
|
116
116
|
def load_file(path)
|
|
117
117
|
abspath = File.expand_path(path)
|
|
118
118
|
|
|
119
119
|
raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
|
120
|
-
raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if
|
|
120
|
+
raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
|
|
121
121
|
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(
|
|
125
|
-
|
|
126
|
-
base_cname = inflector.camelize(basename, abspath).to_sym
|
|
123
|
+
file_basename = File.basename(abspath)
|
|
124
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
|
|
127
125
|
|
|
128
126
|
root_namespace = nil
|
|
129
|
-
|
|
127
|
+
paths = []
|
|
130
128
|
|
|
131
|
-
walk_up(File.dirname(abspath)) do |dir|
|
|
129
|
+
@fs.walk_up(File.dirname(abspath)) do |dir|
|
|
132
130
|
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(dir)
|
|
133
131
|
|
|
134
132
|
break if root_namespace = roots[dir]
|
|
135
133
|
|
|
136
134
|
basename = File.basename(dir)
|
|
137
|
-
raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
|
|
135
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)
|
|
138
136
|
|
|
139
|
-
unless collapse?(dir)
|
|
140
|
-
cnames << inflector.camelize(basename, dir).to_sym
|
|
141
|
-
end
|
|
137
|
+
paths << [basename, dir] unless collapse?(dir)
|
|
142
138
|
end
|
|
143
139
|
|
|
144
140
|
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
|
145
141
|
|
|
146
142
|
namespace = root_namespace
|
|
147
|
-
|
|
143
|
+
paths.reverse_each do |basename, dir|
|
|
144
|
+
cname = cname_for(basename, dir)
|
|
148
145
|
namespace = namespace.const_get(cname, false)
|
|
149
146
|
end
|
|
150
147
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
if file_basename == @nsfile
|
|
149
|
+
namespace
|
|
150
|
+
elsif shadowed_file?(abspath)
|
|
151
|
+
raise Zeitwerk::Error.new("#{abspath} is shadowed")
|
|
152
|
+
else
|
|
153
|
+
cname = cname_for(file_basename.delete_suffix('.rb'), abspath)
|
|
154
|
+
namespace.const_get(cname, false)
|
|
155
|
+
end
|
|
154
156
|
end
|
|
155
157
|
|
|
156
158
|
# The caller is responsible for making sure `namespace` is the namespace that
|
|
157
159
|
# corresponds to `dir`.
|
|
158
160
|
#
|
|
159
|
-
|
|
161
|
+
#: (String, Module, ?force: boolish) -> void
|
|
160
162
|
private def actual_eager_load_dir(dir, namespace, force: false)
|
|
161
163
|
honour_exclusions = !force
|
|
162
164
|
return if honour_exclusions && excluded_from_eager_load?(dir)
|
|
163
165
|
|
|
164
|
-
log
|
|
166
|
+
log { "eager load directory #{dir} start" }
|
|
165
167
|
|
|
166
168
|
queue = [[dir, namespace]]
|
|
167
169
|
while (current_dir, namespace = queue.shift)
|
|
168
|
-
ls(current_dir) do |basename, abspath, ftype|
|
|
170
|
+
@fs.ls(current_dir) do |basename, abspath, ftype|
|
|
169
171
|
next if honour_exclusions && eager_load_exclusions.member?(abspath)
|
|
170
172
|
|
|
171
173
|
if ftype == :file
|
|
@@ -173,27 +175,23 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
173
175
|
cref.get
|
|
174
176
|
end
|
|
175
177
|
else
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
else
|
|
179
|
-
cname = inflector.camelize(basename, abspath).to_sym
|
|
180
|
-
queue << [abspath, namespace.const_get(cname, false)]
|
|
181
|
-
end
|
|
178
|
+
cname = cname_for(basename, abspath)
|
|
179
|
+
queue << [abspath, namespace.const_get(cname, false)]
|
|
182
180
|
end
|
|
183
181
|
end
|
|
184
182
|
end
|
|
185
183
|
|
|
186
|
-
log
|
|
184
|
+
log { "eager load directory #{dir} end" }
|
|
187
185
|
end
|
|
188
186
|
|
|
189
187
|
# In order to invoke this method, the caller has to ensure `child` is a
|
|
190
188
|
# strict namespace descendant of `root_namespace`.
|
|
191
189
|
#
|
|
192
|
-
|
|
190
|
+
#: (Module, String, String, Module) -> void
|
|
193
191
|
private def eager_load_child_namespace(child, child_name, root_dir, root_namespace)
|
|
194
192
|
suffix = child_name
|
|
195
193
|
unless root_namespace.equal?(Object)
|
|
196
|
-
suffix = suffix.delete_prefix(real_mod_name(root_namespace) +
|
|
194
|
+
suffix = suffix.delete_prefix(real_mod_name(root_namespace) + '::')
|
|
197
195
|
end
|
|
198
196
|
|
|
199
197
|
# These directories are at the same namespace level, there may be more if
|
|
@@ -206,14 +204,10 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
206
204
|
dirs = [root_dir]
|
|
207
205
|
next_dirs = []
|
|
208
206
|
|
|
209
|
-
suffix.split(
|
|
207
|
+
suffix.split('::').each do |segment|
|
|
210
208
|
while (dir = dirs.shift)
|
|
211
|
-
ls(dir) do |basename, abspath, ftype|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if collapse?(abspath)
|
|
215
|
-
dirs << abspath
|
|
216
|
-
elsif segment == inflector.camelize(basename, abspath)
|
|
209
|
+
@fs.ls(dir) do |basename, abspath, ftype|
|
|
210
|
+
if ftype == :directory && segment == cname_for(basename, abspath).to_s
|
|
217
211
|
next_dirs << abspath
|
|
218
212
|
end
|
|
219
213
|
end
|