zeitwerk 2.6.8 → 2.7.5
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 +193 -64
- data/lib/zeitwerk/{kernel.rb → core_ext/kernel.rb} +6 -10
- data/lib/zeitwerk/core_ext/module.rb +20 -0
- data/lib/zeitwerk/cref/map.rb +159 -0
- data/lib/zeitwerk/cref.rb +69 -0
- data/lib/zeitwerk/error.rb +2 -0
- data/lib/zeitwerk/gem_inflector.rb +2 -2
- data/lib/zeitwerk/gem_loader.rb +6 -7
- data/lib/zeitwerk/inflector.rb +3 -3
- data/lib/zeitwerk/internal.rb +1 -0
- data/lib/zeitwerk/loader/callbacks.rb +44 -40
- data/lib/zeitwerk/loader/config.rb +46 -53
- data/lib/zeitwerk/loader/eager_load.rb +49 -47
- data/lib/zeitwerk/loader/file_system.rb +165 -0
- data/lib/zeitwerk/loader/helpers.rb +28 -125
- data/lib/zeitwerk/loader.rb +287 -190
- data/lib/zeitwerk/null_inflector.rb +6 -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 +42 -91
- data/lib/zeitwerk/version.rb +2 -1
- data/lib/zeitwerk.rb +6 -3
- metadata +15 -10
- data/lib/zeitwerk/explicit_namespace.rb +0 -93
|
@@ -7,10 +7,10 @@ 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:
|
|
@@ -25,14 +25,13 @@ 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
|
-
# Absolute paths of files, directories, or glob patterns to be
|
|
33
|
-
# ignored.
|
|
32
|
+
# Absolute paths of files, directories, or glob patterns to be ignored.
|
|
34
33
|
#
|
|
35
|
-
|
|
34
|
+
#: Set[String]
|
|
36
35
|
attr_reader :ignored_glob_patterns
|
|
37
36
|
private :ignored_glob_patterns
|
|
38
37
|
|
|
@@ -40,46 +39,46 @@ module Zeitwerk::Loader::Config
|
|
|
40
39
|
# ignored glob patterns were expanded. Computed on setup, and recomputed on
|
|
41
40
|
# reload.
|
|
42
41
|
#
|
|
43
|
-
|
|
42
|
+
#: Set[String]
|
|
44
43
|
attr_reader :ignored_paths
|
|
45
44
|
private :ignored_paths
|
|
46
45
|
|
|
47
46
|
# Absolute paths of directories or glob patterns to be collapsed.
|
|
48
47
|
#
|
|
49
|
-
|
|
48
|
+
#: Set[String]
|
|
50
49
|
attr_reader :collapse_glob_patterns
|
|
51
50
|
private :collapse_glob_patterns
|
|
52
51
|
|
|
53
52
|
# The actual collection of absolute directory names at the time the collapse
|
|
54
53
|
# glob patterns were expanded. Computed on setup, and recomputed on reload.
|
|
55
54
|
#
|
|
56
|
-
|
|
55
|
+
#: Set[String]
|
|
57
56
|
attr_reader :collapse_dirs
|
|
58
57
|
private :collapse_dirs
|
|
59
58
|
|
|
60
59
|
# Absolute paths of files or directories not to be eager loaded.
|
|
61
60
|
#
|
|
62
|
-
|
|
61
|
+
#: Set[String]
|
|
63
62
|
attr_reader :eager_load_exclusions
|
|
64
63
|
private :eager_load_exclusions
|
|
65
64
|
|
|
66
65
|
# User-oriented callbacks to be fired on setup and on reload.
|
|
67
66
|
#
|
|
68
|
-
|
|
67
|
+
#: Array[{ () -> void }]
|
|
69
68
|
attr_reader :on_setup_callbacks
|
|
70
69
|
private :on_setup_callbacks
|
|
71
70
|
|
|
72
71
|
# User-oriented callbacks to be fired when a constant is loaded.
|
|
73
72
|
#
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
#: Hash[String, Array[{ (top, String) -> void }]]
|
|
74
|
+
#| Hash[Symbol, Array[{ (String, top, String) -> void }]]
|
|
76
75
|
attr_reader :on_load_callbacks
|
|
77
76
|
private :on_load_callbacks
|
|
78
77
|
|
|
79
78
|
# User-oriented callbacks to be fired before constants are removed.
|
|
80
79
|
#
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
#: Hash[String, Array[{ (top, String) -> void }]]
|
|
81
|
+
#| Hash[Symbol, Array[{ (String, top, String) -> void }]]
|
|
83
82
|
attr_reader :on_unload_callbacks
|
|
84
83
|
private :on_unload_callbacks
|
|
85
84
|
|
|
@@ -106,8 +105,7 @@ module Zeitwerk::Loader::Config
|
|
|
106
105
|
# the same process already manages that directory or one of its ascendants or
|
|
107
106
|
# descendants.
|
|
108
107
|
#
|
|
109
|
-
|
|
110
|
-
# @sig (String | Pathname, Module) -> void
|
|
108
|
+
#: (String | Pathname, namespace: Module) -> void ! Zeitwerk::Error
|
|
111
109
|
def push_dir(path, namespace: Object)
|
|
112
110
|
unless namespace.is_a?(Module) # Note that Class < Module.
|
|
113
111
|
raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
|
|
@@ -118,8 +116,8 @@ module Zeitwerk::Loader::Config
|
|
|
118
116
|
end
|
|
119
117
|
|
|
120
118
|
abspath = File.expand_path(path)
|
|
121
|
-
if dir?(abspath)
|
|
122
|
-
|
|
119
|
+
if @fs.dir?(abspath)
|
|
120
|
+
raise_if_conflicting_root_dir(abspath)
|
|
123
121
|
roots[abspath] = namespace
|
|
124
122
|
else
|
|
125
123
|
raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
|
|
@@ -131,14 +129,14 @@ module Zeitwerk::Loader::Config
|
|
|
131
129
|
# Implemented as a method instead of via attr_reader for symmetry with the
|
|
132
130
|
# writer below.
|
|
133
131
|
#
|
|
134
|
-
|
|
132
|
+
#: () -> String
|
|
135
133
|
def tag
|
|
136
134
|
@tag
|
|
137
135
|
end
|
|
138
136
|
|
|
139
137
|
# Sets a tag for the loader, useful for logging.
|
|
140
138
|
#
|
|
141
|
-
|
|
139
|
+
#: (to_s() -> String) -> void
|
|
142
140
|
def tag=(tag)
|
|
143
141
|
@tag = tag.to_s
|
|
144
142
|
end
|
|
@@ -152,7 +150,7 @@ module Zeitwerk::Loader::Config
|
|
|
152
150
|
#
|
|
153
151
|
# These are read-only collections, please add to them with `push_dir`.
|
|
154
152
|
#
|
|
155
|
-
|
|
153
|
+
#: (?namespaces: boolish, ?ignored: boolish) -> Array[String] | Hash[String, Module]
|
|
156
154
|
def dirs(namespaces: false, ignored: false)
|
|
157
155
|
if namespaces
|
|
158
156
|
if ignored || ignored_paths.empty?
|
|
@@ -172,8 +170,7 @@ module Zeitwerk::Loader::Config
|
|
|
172
170
|
# You need to call this method before setup in order to be able to reload.
|
|
173
171
|
# There is no way to undo this, either you want to reload or you don't.
|
|
174
172
|
#
|
|
175
|
-
|
|
176
|
-
# @sig () -> void
|
|
173
|
+
#: () -> void ! Zeitwerk::Error
|
|
177
174
|
def enable_reloading
|
|
178
175
|
mutex.synchronize do
|
|
179
176
|
break if @reloading_enabled
|
|
@@ -186,7 +183,7 @@ module Zeitwerk::Loader::Config
|
|
|
186
183
|
end
|
|
187
184
|
end
|
|
188
185
|
|
|
189
|
-
|
|
186
|
+
#: () -> bool
|
|
190
187
|
def reloading_enabled?
|
|
191
188
|
@reloading_enabled
|
|
192
189
|
end
|
|
@@ -194,14 +191,14 @@ module Zeitwerk::Loader::Config
|
|
|
194
191
|
# Let eager load ignore the given files or directories. The constants defined
|
|
195
192
|
# in those files are still autoloadable.
|
|
196
193
|
#
|
|
197
|
-
|
|
194
|
+
#: (*(String | Pathname | Array[String | Pathname])) -> void
|
|
198
195
|
def do_not_eager_load(*paths)
|
|
199
196
|
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
|
200
197
|
end
|
|
201
198
|
|
|
202
199
|
# Configure files, directories, or glob patterns to be totally ignored.
|
|
203
200
|
#
|
|
204
|
-
|
|
201
|
+
#: (*(String | Pathname | Array[String | Pathname])) -> void
|
|
205
202
|
def ignore(*glob_patterns)
|
|
206
203
|
glob_patterns = expand_paths(glob_patterns)
|
|
207
204
|
mutex.synchronize do
|
|
@@ -212,7 +209,7 @@ module Zeitwerk::Loader::Config
|
|
|
212
209
|
|
|
213
210
|
# Configure directories or glob patterns to be collapsed.
|
|
214
211
|
#
|
|
215
|
-
|
|
212
|
+
#: (*(String | Pathname | Array[String | Pathname])) -> void
|
|
216
213
|
def collapse(*glob_patterns)
|
|
217
214
|
glob_patterns = expand_paths(glob_patterns)
|
|
218
215
|
mutex.synchronize do
|
|
@@ -224,7 +221,7 @@ module Zeitwerk::Loader::Config
|
|
|
224
221
|
# Configure a block to be called after setup and on each reload.
|
|
225
222
|
# If setup was already done, the block runs immediately.
|
|
226
223
|
#
|
|
227
|
-
|
|
224
|
+
#: () { () -> void } -> void
|
|
228
225
|
def on_setup(&block)
|
|
229
226
|
mutex.synchronize do
|
|
230
227
|
on_setup_callbacks << block
|
|
@@ -246,9 +243,7 @@ module Zeitwerk::Loader::Config
|
|
|
246
243
|
# # ...
|
|
247
244
|
# end
|
|
248
245
|
#
|
|
249
|
-
|
|
250
|
-
# @sig (String) { (Object, String) -> void } -> void
|
|
251
|
-
# (:ANY) { (String, Object, String) -> void } -> void
|
|
246
|
+
#: (String?) { (top, String) -> void } -> void ! TypeError
|
|
252
247
|
def on_load(cpath = :ANY, &block)
|
|
253
248
|
raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
|
|
254
249
|
|
|
@@ -271,9 +266,7 @@ module Zeitwerk::Loader::Config
|
|
|
271
266
|
# # ...
|
|
272
267
|
# end
|
|
273
268
|
#
|
|
274
|
-
|
|
275
|
-
# @sig (String) { (Object) -> void } -> void
|
|
276
|
-
# (:ANY) { (String, Object) -> void } -> void
|
|
269
|
+
#: (String?) { (top, String) -> void } -> void ! TypeError
|
|
277
270
|
def on_unload(cpath = :ANY, &block)
|
|
278
271
|
raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
|
|
279
272
|
|
|
@@ -284,7 +277,7 @@ module Zeitwerk::Loader::Config
|
|
|
284
277
|
|
|
285
278
|
# Logs to `$stdout`, handy shortcut for debugging.
|
|
286
279
|
#
|
|
287
|
-
|
|
280
|
+
#: () -> void
|
|
288
281
|
def log!
|
|
289
282
|
@logger = ->(msg) { puts msg }
|
|
290
283
|
end
|
|
@@ -292,72 +285,72 @@ module Zeitwerk::Loader::Config
|
|
|
292
285
|
# Returns true if the argument has been configured to be ignored, or is a
|
|
293
286
|
# descendant of an ignored directory.
|
|
294
287
|
#
|
|
295
|
-
|
|
288
|
+
#: (String) -> bool
|
|
296
289
|
internal def ignores?(abspath)
|
|
297
290
|
# Common use case.
|
|
298
291
|
return false if ignored_paths.empty?
|
|
299
292
|
|
|
300
|
-
walk_up(abspath) do |path|
|
|
293
|
+
@fs.walk_up(abspath) do |path|
|
|
301
294
|
return true if ignored_path?(path)
|
|
302
|
-
return false if
|
|
295
|
+
return false if root_dir?(path)
|
|
303
296
|
end
|
|
304
297
|
|
|
305
298
|
false
|
|
306
299
|
end
|
|
307
300
|
|
|
308
|
-
|
|
309
|
-
|
|
301
|
+
#: (String) -> bool
|
|
302
|
+
internal def ignored_path?(abspath)
|
|
310
303
|
ignored_paths.member?(abspath)
|
|
311
304
|
end
|
|
312
305
|
|
|
313
|
-
|
|
306
|
+
#: () -> Array[String]
|
|
314
307
|
private def actual_roots
|
|
315
308
|
roots.reject do |root_dir, _root_namespace|
|
|
316
|
-
|
|
309
|
+
!@fs.dir?(root_dir) || ignored_path?(root_dir)
|
|
317
310
|
end
|
|
318
311
|
end
|
|
319
312
|
|
|
320
|
-
|
|
321
|
-
|
|
313
|
+
#: (String) -> bool
|
|
314
|
+
internal def root_dir?(dir)
|
|
322
315
|
roots.key?(dir)
|
|
323
316
|
end
|
|
324
317
|
|
|
325
|
-
|
|
318
|
+
#: (String) -> bool
|
|
326
319
|
private def excluded_from_eager_load?(abspath)
|
|
327
320
|
# Optimize this common use case.
|
|
328
321
|
return false if eager_load_exclusions.empty?
|
|
329
322
|
|
|
330
|
-
walk_up(abspath) do |path|
|
|
323
|
+
@fs.walk_up(abspath) do |path|
|
|
331
324
|
return true if eager_load_exclusions.member?(path)
|
|
332
|
-
return false if
|
|
325
|
+
return false if root_dir?(path)
|
|
333
326
|
end
|
|
334
327
|
|
|
335
328
|
false
|
|
336
329
|
end
|
|
337
330
|
|
|
338
|
-
|
|
331
|
+
#: (String) -> bool
|
|
339
332
|
private def collapse?(dir)
|
|
340
333
|
collapse_dirs.member?(dir)
|
|
341
334
|
end
|
|
342
335
|
|
|
343
|
-
|
|
336
|
+
#: (String | Pathname | Array[String | Pathname]) -> Array[String]
|
|
344
337
|
private def expand_paths(paths)
|
|
345
338
|
paths.flatten.map! { |path| File.expand_path(path) }
|
|
346
339
|
end
|
|
347
340
|
|
|
348
|
-
|
|
341
|
+
#: (Array[String]) -> Array[String]
|
|
349
342
|
private def expand_glob_patterns(glob_patterns)
|
|
350
343
|
# Note that Dir.glob works with regular file names just fine. That is,
|
|
351
344
|
# glob patterns technically need no wildcards.
|
|
352
345
|
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
|
353
346
|
end
|
|
354
347
|
|
|
355
|
-
|
|
348
|
+
#: () -> void
|
|
356
349
|
private def recompute_ignored_paths
|
|
357
350
|
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
|
358
351
|
end
|
|
359
352
|
|
|
360
|
-
|
|
353
|
+
#: () -> void
|
|
361
354
|
private def recompute_collapse_dirs
|
|
362
355
|
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
|
363
356
|
end
|
|
@@ -5,50 +5,50 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
5
5
|
# specific files and directories with `do_not_eager_load`, and that can be
|
|
6
6
|
# overridden passing `force: true`.
|
|
7
7
|
#
|
|
8
|
-
|
|
8
|
+
#: (?force: boolish) -> void
|
|
9
9
|
def eager_load(force: false)
|
|
10
10
|
mutex.synchronize do
|
|
11
11
|
break if @eager_loaded
|
|
12
12
|
raise Zeitwerk::SetupRequired unless @setup
|
|
13
13
|
|
|
14
|
-
log
|
|
14
|
+
log { "eager load start" }
|
|
15
15
|
|
|
16
16
|
actual_roots.each do |root_dir, root_namespace|
|
|
17
17
|
actual_eager_load_dir(root_dir, root_namespace, force: force)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
autoloaded_dirs.each do |autoloaded_dir|
|
|
21
|
-
Zeitwerk::Registry.
|
|
21
|
+
Zeitwerk::Registry.autoloads.unregister(autoloaded_dir)
|
|
22
22
|
end
|
|
23
23
|
autoloaded_dirs.clear
|
|
24
24
|
|
|
25
25
|
@eager_loaded = true
|
|
26
26
|
|
|
27
|
-
log
|
|
27
|
+
log { "eager load end" }
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
#: (String | Pathname) -> void
|
|
32
32
|
def eager_load_dir(path)
|
|
33
33
|
raise Zeitwerk::SetupRequired unless @setup
|
|
34
34
|
|
|
35
35
|
abspath = File.expand_path(path)
|
|
36
36
|
|
|
37
|
-
raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
|
|
37
|
+
raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
paths = []
|
|
40
40
|
|
|
41
41
|
root_namespace = nil
|
|
42
|
-
walk_up(abspath) do |dir|
|
|
42
|
+
@fs.walk_up(abspath) do |dir|
|
|
43
43
|
return if ignored_path?(dir)
|
|
44
44
|
return if eager_load_exclusions.member?(dir)
|
|
45
45
|
|
|
46
46
|
break if root_namespace = roots[dir]
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
basename = File.basename(dir)
|
|
49
|
+
return if @fs.hidden?(basename)
|
|
50
|
+
|
|
51
|
+
paths << [basename, dir] unless collapse?(dir)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
|
@@ -56,11 +56,12 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
56
56
|
return if @eager_loaded
|
|
57
57
|
|
|
58
58
|
namespace = root_namespace
|
|
59
|
-
|
|
59
|
+
paths.reverse_each do |basename, dir|
|
|
60
|
+
cname = cname_for(basename, dir)
|
|
60
61
|
# Can happen if there are no Ruby files. This is not an error condition,
|
|
61
62
|
# the directory is actually managed. Could have Ruby files later.
|
|
62
|
-
return unless
|
|
63
|
-
namespace =
|
|
63
|
+
return unless namespace.const_defined?(cname, false)
|
|
64
|
+
namespace = namespace.const_get(cname, false)
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
# A shortcircuiting test depends on the invocation of this method. Please
|
|
@@ -68,7 +69,7 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
68
69
|
actual_eager_load_dir(abspath, namespace)
|
|
69
70
|
end
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
#: (Module) -> void
|
|
72
73
|
def eager_load_namespace(mod)
|
|
73
74
|
raise Zeitwerk::SetupRequired unless @setup
|
|
74
75
|
|
|
@@ -82,7 +83,7 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
82
83
|
return unless mod_name
|
|
83
84
|
|
|
84
85
|
actual_roots.each do |root_dir, root_namespace|
|
|
85
|
-
if
|
|
86
|
+
if Object.equal?(mod)
|
|
86
87
|
# A shortcircuiting test depends on the invocation of this method.
|
|
87
88
|
# Please keep them in sync if refactored.
|
|
88
89
|
actual_eager_load_dir(root_dir, root_namespace)
|
|
@@ -110,82 +111,83 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
110
111
|
# The method is implemented as `constantize` for files, in a sense, to be able
|
|
111
112
|
# to descend orderly and make sure the file is loadable.
|
|
112
113
|
#
|
|
113
|
-
|
|
114
|
+
#: (String | Pathname) -> void
|
|
114
115
|
def load_file(path)
|
|
115
116
|
abspath = File.expand_path(path)
|
|
116
117
|
|
|
117
118
|
raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
|
118
|
-
raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if
|
|
119
|
+
raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
|
|
119
120
|
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
file_basename = File.basename(abspath, ".rb")
|
|
123
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
|
|
123
124
|
|
|
124
125
|
root_namespace = nil
|
|
125
|
-
|
|
126
|
+
paths = []
|
|
126
127
|
|
|
127
|
-
walk_up(File.dirname(abspath)) do |dir|
|
|
128
|
+
@fs.walk_up(File.dirname(abspath)) do |dir|
|
|
128
129
|
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(dir)
|
|
129
130
|
|
|
130
131
|
break if root_namespace = roots[dir]
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
basename = File.basename(dir)
|
|
134
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)
|
|
135
|
+
|
|
136
|
+
paths << [basename, dir] unless collapse?(dir)
|
|
136
137
|
end
|
|
137
138
|
|
|
138
139
|
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
|
139
140
|
|
|
141
|
+
base_cname = cname_for(file_basename, abspath)
|
|
142
|
+
|
|
140
143
|
namespace = root_namespace
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
paths.reverse_each do |basename, dir|
|
|
145
|
+
cname = cname_for(basename, dir)
|
|
146
|
+
namespace = namespace.const_get(cname, false)
|
|
143
147
|
end
|
|
144
148
|
|
|
145
149
|
raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
|
|
146
150
|
|
|
147
|
-
|
|
151
|
+
namespace.const_get(base_cname, false)
|
|
148
152
|
end
|
|
149
153
|
|
|
150
154
|
# The caller is responsible for making sure `namespace` is the namespace that
|
|
151
155
|
# corresponds to `dir`.
|
|
152
156
|
#
|
|
153
|
-
|
|
157
|
+
#: (String, Module, ?force: boolish) -> void
|
|
154
158
|
private def actual_eager_load_dir(dir, namespace, force: false)
|
|
155
159
|
honour_exclusions = !force
|
|
156
160
|
return if honour_exclusions && excluded_from_eager_load?(dir)
|
|
157
161
|
|
|
158
|
-
log
|
|
162
|
+
log { "eager load directory #{dir} start" }
|
|
159
163
|
|
|
160
164
|
queue = [[dir, namespace]]
|
|
161
|
-
while
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
ls(dir) do |basename, abspath|
|
|
165
|
+
while (current_dir, namespace = queue.shift)
|
|
166
|
+
@fs.ls(current_dir) do |basename, abspath, ftype|
|
|
165
167
|
next if honour_exclusions && eager_load_exclusions.member?(abspath)
|
|
166
168
|
|
|
167
|
-
if
|
|
168
|
-
if (cref = autoloads[abspath])
|
|
169
|
-
|
|
169
|
+
if ftype == :file
|
|
170
|
+
if (cref = autoloads[abspath])
|
|
171
|
+
cref.get
|
|
170
172
|
end
|
|
171
173
|
else
|
|
172
174
|
if collapse?(abspath)
|
|
173
175
|
queue << [abspath, namespace]
|
|
174
176
|
else
|
|
175
|
-
cname =
|
|
176
|
-
queue << [abspath,
|
|
177
|
+
cname = cname_for(basename, abspath)
|
|
178
|
+
queue << [abspath, namespace.const_get(cname, false)]
|
|
177
179
|
end
|
|
178
180
|
end
|
|
179
181
|
end
|
|
180
182
|
end
|
|
181
183
|
|
|
182
|
-
log
|
|
184
|
+
log { "eager load directory #{dir} end" }
|
|
183
185
|
end
|
|
184
186
|
|
|
185
187
|
# In order to invoke this method, the caller has to ensure `child` is a
|
|
186
188
|
# strict namespace descendant of `root_namespace`.
|
|
187
189
|
#
|
|
188
|
-
|
|
190
|
+
#: (Module, String, String, Module) -> void
|
|
189
191
|
private def eager_load_child_namespace(child, child_name, root_dir, root_namespace)
|
|
190
192
|
suffix = child_name
|
|
191
193
|
unless root_namespace.equal?(Object)
|
|
@@ -203,13 +205,13 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
203
205
|
next_dirs = []
|
|
204
206
|
|
|
205
207
|
suffix.split("::").each do |segment|
|
|
206
|
-
while dir = dirs.shift
|
|
207
|
-
ls(dir) do |basename, abspath|
|
|
208
|
-
next unless
|
|
208
|
+
while (dir = dirs.shift)
|
|
209
|
+
@fs.ls(dir) do |basename, abspath, ftype|
|
|
210
|
+
next unless ftype == :directory
|
|
209
211
|
|
|
210
212
|
if collapse?(abspath)
|
|
211
213
|
dirs << abspath
|
|
212
|
-
elsif segment ==
|
|
214
|
+
elsif segment == cname_for(basename, abspath).to_s
|
|
213
215
|
next_dirs << abspath
|
|
214
216
|
end
|
|
215
217
|
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This private class encapsulates interactions with the file system.
|
|
4
|
+
#
|
|
5
|
+
# It is used to list directories and check file types, and it encodes the
|
|
6
|
+
# conventions documented in the README.
|
|
7
|
+
#
|
|
8
|
+
# @private
|
|
9
|
+
class Zeitwerk::Loader::FileSystem # :nodoc:
|
|
10
|
+
#: (Zeitwerk::Loader) -> void
|
|
11
|
+
def initialize(loader)
|
|
12
|
+
@loader = loader
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
16
|
+
def ls(dir)
|
|
17
|
+
children = relevant_dir_entries(dir)
|
|
18
|
+
|
|
19
|
+
# The order in which a directory is listed depends on the file system.
|
|
20
|
+
#
|
|
21
|
+
# Since client code may run on different platforms, it seems convenient to
|
|
22
|
+
# sort directory entries. This provides more deterministic behavior, with
|
|
23
|
+
# consistent eager loading in particular.
|
|
24
|
+
children.sort_by!(&:first)
|
|
25
|
+
|
|
26
|
+
children.each do |basename, abspath, ftype|
|
|
27
|
+
if :directory == ftype && !has_at_least_one_ruby_file?(abspath)
|
|
28
|
+
@loader.__log { "directory #{abspath} is ignored because it has no Ruby files" }
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
yield basename, abspath, ftype
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#: (String) { (String) -> void } -> void
|
|
37
|
+
def walk_up(abspath)
|
|
38
|
+
loop do
|
|
39
|
+
yield abspath
|
|
40
|
+
abspath, basename = File.split(abspath)
|
|
41
|
+
break if basename == "/"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Encodes the documented conventions.
|
|
46
|
+
#
|
|
47
|
+
#: (String) -> Symbol?
|
|
48
|
+
def supported_ftype?(abspath)
|
|
49
|
+
if rb_extension?(abspath)
|
|
50
|
+
:file # By convention, we can avoid a syscall here.
|
|
51
|
+
elsif dir?(abspath)
|
|
52
|
+
:directory
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#: (String) -> bool
|
|
57
|
+
def rb_extension?(path)
|
|
58
|
+
path.end_with?(".rb")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#: (String) -> bool
|
|
62
|
+
def dir?(path)
|
|
63
|
+
File.directory?(path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#: (String) -> bool
|
|
67
|
+
def hidden?(basename)
|
|
68
|
+
basename.start_with?(".")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Looks for a Ruby file using breadth-first search. This type of search is
|
|
74
|
+
# important to list as less directories as possible and return fast in the
|
|
75
|
+
# common case in which there are Ruby files in the passed directory.
|
|
76
|
+
#
|
|
77
|
+
#: (String) -> bool
|
|
78
|
+
def has_at_least_one_ruby_file?(dir)
|
|
79
|
+
to_visit = [dir]
|
|
80
|
+
|
|
81
|
+
while (dir = to_visit.shift)
|
|
82
|
+
relevant_dir_entries(dir) do |_, abspath, ftype|
|
|
83
|
+
return true if :file == ftype
|
|
84
|
+
to_visit << abspath
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
92
|
+
#: (String) -> [[String, String, Symbol]]
|
|
93
|
+
def relevant_dir_entries(dir)
|
|
94
|
+
return enum_for(__method__, dir).to_a unless block_given?
|
|
95
|
+
|
|
96
|
+
each_ruby_file_or_directory(dir) do |basename, abspath, ftype|
|
|
97
|
+
next if @loader.__ignored_path?(abspath)
|
|
98
|
+
|
|
99
|
+
if :link == ftype
|
|
100
|
+
begin
|
|
101
|
+
ftype = File.stat(abspath).ftype.to_sym
|
|
102
|
+
rescue Errno::ENOENT
|
|
103
|
+
warn "ignoring broken symlink #{abspath}"
|
|
104
|
+
next
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if :file == ftype
|
|
109
|
+
yield basename, abspath, ftype if rb_extension?(basename)
|
|
110
|
+
elsif :directory == ftype
|
|
111
|
+
# Conceptually, root directories represent a separate project tree.
|
|
112
|
+
yield basename, abspath, ftype unless @loader.__root_dir?(abspath)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Dir.scan is more efficient in common platforms, but it is going to take a
|
|
118
|
+
# while for it to be available.
|
|
119
|
+
#
|
|
120
|
+
# The following compatibility methods have the same semantics but are written
|
|
121
|
+
# to favor the performance of the Ruby fallback, which can save syscalls.
|
|
122
|
+
#
|
|
123
|
+
# In particular, by convention, any directory entry with a .rb extension is
|
|
124
|
+
# assumed to be a file or a symlink to a file.
|
|
125
|
+
#
|
|
126
|
+
# These methods also freeze abspaths because that saves allocations when
|
|
127
|
+
# passed later to File methods. See https://github.com/fxn/zeitwerk/pull/125.
|
|
128
|
+
|
|
129
|
+
if Dir.respond_to?(:scan) # Available in Ruby 4.1.
|
|
130
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
131
|
+
def each_ruby_file_or_directory(dir)
|
|
132
|
+
Dir.scan(dir) do |basename, ftype|
|
|
133
|
+
next if hidden?(basename)
|
|
134
|
+
|
|
135
|
+
if rb_extension?(basename)
|
|
136
|
+
abspath = File.join(dir, basename).freeze
|
|
137
|
+
yield basename, abspath, :file # By convention.
|
|
138
|
+
elsif :directory == ftype
|
|
139
|
+
abspath = File.join(dir, basename).freeze
|
|
140
|
+
yield basename, abspath, :directory
|
|
141
|
+
elsif :link == ftype
|
|
142
|
+
abspath = File.join(dir, basename).freeze
|
|
143
|
+
yield basename, abspath, :directory if dir?(abspath)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
149
|
+
def each_ruby_file_or_directory(dir)
|
|
150
|
+
Dir.each_child(dir) do |basename|
|
|
151
|
+
next if hidden?(basename)
|
|
152
|
+
|
|
153
|
+
if rb_extension?(basename)
|
|
154
|
+
abspath = File.join(dir, basename).freeze
|
|
155
|
+
yield basename, abspath, :file # By convention.
|
|
156
|
+
else
|
|
157
|
+
abspath = File.join(dir, basename).freeze
|
|
158
|
+
if dir?(abspath)
|
|
159
|
+
yield basename, abspath, :directory
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|