opal-zeitwerk 0.4.4 → 0.4.6

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.
@@ -1,471 +1,471 @@
1
- require "set"
2
-
3
- module Zeitwerk
4
- class Loader
5
- require_relative "loader/helpers"
6
- require_relative "loader/callbacks"
7
- require_relative "loader/config"
8
-
9
- include RealModName
10
- include Callbacks
11
- include Helpers
12
- include Config
13
-
14
- # Maps absolute paths for which an autoload has been set ---and not
15
- # executed--- to their corresponding parent class or module and constant
16
- # name.
17
- #
18
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
19
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
20
- # ...
21
- #
22
- # @private
23
- # @sig Hash[String, [Module, Symbol]]
24
- attr_reader :autoloads
25
-
26
- # We keep track of autoloaded directories to remove them from the registry
27
- # at the end of eager loading.
28
- #
29
- # Files are removed as they are autoloaded, but directories need to wait due
30
- # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
31
- #
32
- # @private
33
- # @sig Array[String]
34
- attr_reader :autoloaded_dirs
35
-
36
- # Stores metadata needed for unloading. Its entries look like this:
37
- #
38
- # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
39
- #
40
- # The cpath as key helps implementing unloadable_cpath? The file name is
41
- # stored in order to be able to delete it from $LOADED_FEATURES, and the
42
- # pair [Module, Symbol] is used to remove_const the constant from the class
43
- # or module object.
44
- #
45
- # If reloading is enabled, this hash is filled as constants are autoloaded
46
- # or eager loaded. Otherwise, the collection remains empty.
47
- #
48
- # @private
49
- # @sig Hash[String, [String, [Module, Symbol]]]
50
- attr_reader :to_unload
51
-
52
- # Maps constant paths of namespaces to arrays of corresponding directories.
53
- #
54
- # For example, given this mapping:
55
- #
56
- # "Admin" => [
57
- # "/Users/fxn/blog/app/controllers/admin",
58
- # "/Users/fxn/blog/app/models/admin",
59
- # ...
60
- # ]
61
- #
62
- # when `Admin` gets defined we know that it plays the role of a namespace and
63
- # that its children are spread over those directories. We'll visit them to set
64
- # up the corresponding autoloads.
65
- #
66
- # @private
67
- # @sig Hash[String, Array[String]]
68
- attr_reader :lazy_subdirs
69
-
70
- def initialize
71
- super
72
-
73
- @autoloads = {}
74
- @autoloaded_dirs = []
75
- @to_unload = {}
76
- @lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
77
- @setup = false
78
- @eager_loaded = false
79
- @module_paths = nil
80
-
81
- Registry.register_loader(self)
82
- end
83
-
84
- # Sets autoloads in the root namespace.
85
- #
86
- # @sig () -> void
87
- def setup
88
- return if @setup
89
-
90
- actual_root_dirs.each do |root_dir, namespace|
91
- set_autoloads_in_dir(root_dir, namespace)
92
- end
93
-
94
- on_setup_callbacks.each(&:call)
95
-
96
- @setup = true
97
- end
98
-
99
- # Removes loaded constants and configured autoloads.
100
- #
101
- # The objects the constants stored are no longer reachable through them. In
102
- # addition, since said objects are normally not referenced from anywhere
103
- # else, they are eligible for garbage collection, which would effectively
104
- # unload them.
105
- #
106
- # This method is public but undocumented. Main interface is `reload`, which
107
- # means `unload` + `setup`. This one is avaiable to be used together with
108
- # `unregister`, which is undocumented too.
109
- #
110
- # @sig () -> void
111
- def unload
112
- # We are going to keep track of the files that were required by our
113
- # autoloads to later remove them from $LOADED_FEATURES, thus making them
114
- # loadable by Kernel#require again.
115
- #
116
- # Directories are not stored in $LOADED_FEATURES, keeping track of files
117
- # is enough.
118
- unloaded_files = Set.new
119
-
120
- autoloads.each do |abspath, (parent, cname)|
121
- if parent.autoload?(cname)
122
- unload_autoload(parent, cname)
123
- else
124
- # Could happen if loaded with require_relative. That is unsupported,
125
- # and the constant path would escape unloadable_cpath? This is just
126
- # defensive code to clean things up as much as we are able to.
127
- unload_cref(parent, cname)
128
- unloaded_files.add(abspath) if ruby?(abspath)
129
- end
130
- end
131
-
132
- to_unload.each do |cpath, (abspath, (parent, cname))|
133
- # We have to check cdef? in this condition. Reason is, constants whose
134
- # file does not define them have to be kept in to_unload as explained
135
- # in the implementation of on_file_autoloaded.
136
- #
137
- # If the constant is not defined, on_unload should not be triggered
138
- # for it.
139
- if !on_unload_callbacks.empty? && cdef?(parent, cname)
140
- value = parent.const_get(cname)
141
- run_on_unload_callbacks(cpath, value, abspath)
142
- end
143
-
144
- unload_cref(parent, cname)
145
- unloaded_files.add(abspath) if ruby?(abspath)
146
- end
147
-
148
- unless unloaded_files.empty?
149
- # Bootsnap decorates Kernel#require to speed it up using a cache and
150
- # this optimization does not check if $LOADED_FEATURES has the file.
151
- #
152
- # To make it aware of changes, the gem defines singleton methods in
153
- # $LOADED_FEATURES:
154
- #
155
- # https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
156
- #
157
- # Rails applications may depend on bootsnap, so for unloading to work
158
- # in that setting it is preferable that we restrict our API choice to
159
- # one of those methods.
160
- $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
161
- end
162
-
163
- autoloads.clear
164
- autoloaded_dirs.clear
165
- to_unload.clear
166
- lazy_subdirs.clear
167
-
168
- Registry.on_unload(self)
169
- ExplicitNamespace.unregister_loader(self)
170
-
171
- @setup = false
172
- @eager_loaded = false
173
- end
174
-
175
- # Unloads all loaded code, and calls setup again so that the loader is able
176
- # to pick any changes in the file system.
177
- #
178
- # This method is not thread-safe, please see how this can be achieved by
179
- # client code in the README of the project.
180
- #
181
- # @raise [Zeitwerk::Error]
182
- # @sig () -> void
183
- def reload
184
- if reloading_enabled?
185
- unload
186
- recompute_ignored_paths
187
- recompute_collapse_dirs
188
- setup
189
- else
190
- raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
191
- end
192
- end
193
-
194
- # Eager loads all files in the root directories, recursively. Files do not
195
- # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
196
- # are not eager loaded. You can opt-out specifically in specific files and
197
- # directories with `do_not_eager_load`, and that can be overridden passing
198
- # `force: true`.
199
- #
200
- # @sig (true | false) -> void
201
- def eager_load(force: false)
202
- return if @eager_loaded
203
-
204
- honour_exclusions = !force
205
-
206
- queue = []
207
- actual_root_dirs.each do |root_dir, namespace|
208
- queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
209
- end
210
-
211
- while to_eager_load = queue.shift
212
- namespace, dir = to_eager_load
213
-
214
- ls(dir) do |basename, abspath|
215
- next if honour_exclusions && excluded_from_eager_load?(abspath)
216
-
217
- if ruby?(abspath)
218
- if cref = autoloads[abspath]
219
- cget(*cref)
220
- end
221
- elsif dir?(abspath) && !root_dirs.key?(abspath)
222
- if collapse?(abspath)
223
- queue << [namespace, abspath]
224
- else
225
- cname = inflector.camelize(basename, abspath)
226
- queue << [cget(namespace, cname), abspath]
227
- end
228
- end
229
- end
230
- end
231
-
232
- autoloaded_dirs.each do |autoloaded_dir|
233
- Registry.unregister_autoload(autoloaded_dir)
234
- end
235
- autoloaded_dirs.clear
236
-
237
- @eager_loaded = true
238
- end
239
-
240
- # Says if the given constant path would be unloaded on reload. This
241
- # predicate returns `false` if reloading is disabled.
242
- #
243
- # @sig (String) -> bool
244
- def unloadable_cpath?(cpath)
245
- to_unload.key?(cpath)
246
- end
247
-
248
- # Returns an array with the constant paths that would be unloaded on reload.
249
- # This predicate returns an empty array if reloading is disabled.
250
- #
251
- # @sig () -> Array[String]
252
- def unloadable_cpaths
253
- to_unload.keys
254
- end
255
-
256
- # This is a dangerous method.
257
- #
258
- # @experimental
259
- # @sig () -> void
260
- def unregister
261
- Registry.unregister_loader(self)
262
- ExplicitNamespace.unregister_loader(self)
263
- end
264
-
265
- # --- Class methods ---------------------------------------------------------------------------
266
-
267
- class << self
268
- # @sig #call | #debug | nil
269
- attr_accessor :default_logger
270
-
271
- # This is a shortcut for
272
- #
273
- # require "zeitwerk"
274
- # loader = Zeitwerk::Loader.new
275
- # loader.tag = File.basename(__FILE__, ".rb")
276
- # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
277
- # loader.push_dir(__dir__)
278
- #
279
- # except that this method returns the same object in subsequent calls from
280
- # the same file, in the unlikely case the gem wants to be able to reload.
281
- #
282
- # @sig () -> Zeitwerk::Loader
283
- def for_gem(called_from)
284
- Registry.loader_for_gem(called_from)
285
- end
286
-
287
- # Broadcasts `eager_load` to all loaders.
288
- #
289
- # @sig () -> void
290
- def eager_load_all
291
- Registry.loaders.each(&:eager_load)
292
- end
293
-
294
- # Returns an array with the absolute paths of the root directories of all
295
- # registered loaders. This is a read-only collection.
296
- #
297
- # @sig () -> Array[String]
298
- def all_dirs
299
- Registry.loaders.flat_map(&:dirs)
300
- end
301
- end
302
-
303
- private # -------------------------------------------------------------------------------------
304
-
305
- # @sig (String, Module) -> void
306
- def set_autoloads_in_dir(dir, parent)
307
- ls(dir) do |basename, abspath|
308
- begin
309
- if ruby?(abspath)
310
- # basename.delete_suffix!(".rb")
311
- cname = inflector.camelize(basename, abspath).to_sym
312
- autoload_file(parent, cname, abspath)
313
- elsif dir?(abspath)
314
- # In a Rails application, `app/models/concerns` is a subdirectory of
315
- # `app/models`, but both of them are root directories.
316
- #
317
- # To resolve the ambiguity file name -> constant path this introduces,
318
- # the `app/models/concerns` directory is totally ignored as a namespace,
319
- # it counts only as root. The guard checks that.
320
- unless root_dir?(abspath)
321
- cname = inflector.camelize(basename, abspath).to_sym
322
- if collapse?(abspath)
323
- set_autoloads_in_dir(abspath, parent)
324
- else
325
- autoload_subdir(parent, cname, abspath)
326
- end
327
- end
328
- end
329
- rescue ::NameError => error
330
- path_type = ruby?(abspath) ? "file" : "directory"
331
-
332
- raise NameError.new(<<~MESSAGE, error.name)
333
- #{error.message} inferred by #{inflector.class} from #{path_type}
334
-
335
- #{abspath}
336
-
337
- Possible ways to address this:
338
-
339
- * Tell Zeitwerk to ignore this particular #{path_type}.
340
- * Tell Zeitwerk to ignore one of its parent directories.
341
- * Rename the #{path_type} to comply with the naming conventions.
342
- * Modify the inflector to handle this case.
343
- MESSAGE
344
- end
345
- end
346
- end
347
-
348
- # @sig (Module, Symbol, String) -> void
349
- def autoload_subdir(parent, cname, subdir)
350
- if autoload_path = autoload_path_set_by_me_for?(parent, cname)
351
- cpath = cpath(parent, cname)
352
- register_explicit_namespace(cpath) if ruby?(autoload_path)
353
- # We do not need to issue another autoload, the existing one is enough
354
- # no matter if it is for a file or a directory. Just remember the
355
- # subdirectory has to be visited if the namespace is used.
356
- lazy_subdirs[cpath] << subdir
357
- elsif !cdef?(parent, cname)
358
- # First time we find this namespace, set an autoload for it.
359
- lazy_subdirs[cpath(parent, cname)] << subdir
360
- set_autoload(parent, cname, subdir)
361
- else
362
- # For whatever reason the constant that corresponds to this namespace has
363
- # already been defined, we have to recurse.
364
- set_autoloads_in_dir(subdir, cget(parent, cname))
365
- end
366
- end
367
-
368
- # @sig (Module, Symbol, String) -> void
369
- def autoload_file(parent, cname, file)
370
- if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
371
- # First autoload for a Ruby file wins, just ignore subsequent ones.
372
- if ruby?(autoload_path)
373
- # "file #{file} is ignored because #{autoload_path} has precedence"
374
- else
375
- promote_namespace_from_implicit_to_explicit(
376
- dir: autoload_path,
377
- file: file,
378
- parent: parent,
379
- cname: cname
380
- )
381
- end
382
- elsif cdef?(parent, cname)
383
- # "file #{file} is ignored because #{cpath(parent, cname)} is already defined"
384
- else
385
- set_autoload(parent, cname, file)
386
- end
387
- end
388
-
389
- # `dir` is the directory that would have autovivified a namespace. `file` is
390
- # the file where we've found the namespace is explicitly defined.
391
- #
392
- # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
393
- def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
394
- autoloads.delete(dir)
395
- Registry.unregister_autoload(dir)
396
-
397
- set_autoload(parent, cname, file)
398
- register_explicit_namespace(cpath(parent, cname))
399
- end
400
-
401
- # @sig (Module, Symbol, String) -> void
402
- def set_autoload(parent, cname, abspath)
403
- parent.autoload(cname, abspath)
404
-
405
- autoloads[abspath] = [parent, cname]
406
- Registry.register_autoload(self, abspath)
407
-
408
- # See why in the documentation of Zeitwerk::Registry.inceptions.
409
- unless parent.autoload?(cname)
410
- Registry.register_inception(cpath(parent, cname), abspath, self)
411
- end
412
- end
413
-
414
- # @sig (Module, Symbol) -> String?
415
- def autoload_path_set_by_me_for?(parent, cname)
416
- if autoload_path = strict_autoload_path(parent, cname)
417
- autoload_path if autoloads.key?(autoload_path)
418
- else
419
- Registry.inception?(cpath(parent, cname))
420
- end
421
- end
422
-
423
- # @sig (String) -> void
424
- def register_explicit_namespace(cpath)
425
- ExplicitNamespace.register(cpath, self)
426
- end
427
-
428
- # @sig (String) -> void
429
- def raise_if_conflicting_directory(dir)
430
- Registry.loaders.each do |loader|
431
- next if loader == self
432
- next if loader.ignores?(dir)
433
-
434
- dir = dir + "/"
435
- loader.root_dirs.each do |root_dir, _namespace|
436
- next if ignores?(root_dir)
437
-
438
- root_dir = root_dir + "/"
439
- if dir.start_with?(root_dir) || root_dir.start_with?(dir)
440
- raise Error,
441
- "loader\n\n#{loader}\n\nwants to manage directory #{dir.chop}," \
442
- " which is already managed by\n\n#{loader}\n"
443
- EOS
444
- end
445
- end
446
- end
447
- end
448
-
449
- # @sig (String, Object, String) -> void
450
- def run_on_unload_callbacks(cpath, value, abspath)
451
- # Order matters. If present, run the most specific one.
452
- on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
453
- on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
454
- end
455
-
456
- # @sig (Module, Symbol) -> void
457
- def unload_autoload(parent, cname)
458
- parent.__send__(:remove_const, cname)
459
- end
460
-
461
- # @sig (Module, Symbol) -> void
462
- def unload_cref(parent, cname)
463
- # Let's optimistically remove_const. The way we use it, this is going to
464
- # succeed always if all is good.
465
- parent.__send__(:remove_const, cname)
466
- rescue ::NameError
467
- # There are a few edge scenarios in which this may happen. If the constant
468
- # is gone, that is OK, anyway.
469
- end
470
- end
471
- end
1
+ require "set"
2
+
3
+ module Zeitwerk
4
+ class Loader
5
+ require_relative "loader/helpers"
6
+ require_relative "loader/callbacks"
7
+ require_relative "loader/config"
8
+
9
+ include RealModName
10
+ include Callbacks
11
+ include Helpers
12
+ include Config
13
+
14
+ # Maps absolute paths for which an autoload has been set ---and not
15
+ # executed--- to their corresponding parent class or module and constant
16
+ # name.
17
+ #
18
+ # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
19
+ # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
20
+ # ...
21
+ #
22
+ # @private
23
+ # @sig Hash[String, [Module, Symbol]]
24
+ attr_reader :autoloads
25
+
26
+ # We keep track of autoloaded directories to remove them from the registry
27
+ # at the end of eager loading.
28
+ #
29
+ # Files are removed as they are autoloaded, but directories need to wait due
30
+ # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
31
+ #
32
+ # @private
33
+ # @sig Array[String]
34
+ attr_reader :autoloaded_dirs
35
+
36
+ # Stores metadata needed for unloading. Its entries look like this:
37
+ #
38
+ # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
39
+ #
40
+ # The cpath as key helps implementing unloadable_cpath? The file name is
41
+ # stored in order to be able to delete it from $LOADED_FEATURES, and the
42
+ # pair [Module, Symbol] is used to remove_const the constant from the class
43
+ # or module object.
44
+ #
45
+ # If reloading is enabled, this hash is filled as constants are autoloaded
46
+ # or eager loaded. Otherwise, the collection remains empty.
47
+ #
48
+ # @private
49
+ # @sig Hash[String, [String, [Module, Symbol]]]
50
+ attr_reader :to_unload
51
+
52
+ # Maps constant paths of namespaces to arrays of corresponding directories.
53
+ #
54
+ # For example, given this mapping:
55
+ #
56
+ # "Admin" => [
57
+ # "/Users/fxn/blog/app/controllers/admin",
58
+ # "/Users/fxn/blog/app/models/admin",
59
+ # ...
60
+ # ]
61
+ #
62
+ # when `Admin` gets defined we know that it plays the role of a namespace and
63
+ # that its children are spread over those directories. We'll visit them to set
64
+ # up the corresponding autoloads.
65
+ #
66
+ # @private
67
+ # @sig Hash[String, Array[String]]
68
+ attr_reader :lazy_subdirs
69
+
70
+ def initialize
71
+ super
72
+
73
+ @autoloads = {}
74
+ @autoloaded_dirs = []
75
+ @to_unload = {}
76
+ @lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
77
+ @setup = false
78
+ @eager_loaded = false
79
+ @module_paths = nil
80
+
81
+ Registry.register_loader(self)
82
+ end
83
+
84
+ # Sets autoloads in the root namespace.
85
+ #
86
+ # @sig () -> void
87
+ def setup
88
+ return if @setup
89
+
90
+ actual_root_dirs.each do |root_dir, namespace|
91
+ set_autoloads_in_dir(root_dir, namespace)
92
+ end
93
+
94
+ on_setup_callbacks.each(&:call)
95
+
96
+ @setup = true
97
+ end
98
+
99
+ # Removes loaded constants and configured autoloads.
100
+ #
101
+ # The objects the constants stored are no longer reachable through them. In
102
+ # addition, since said objects are normally not referenced from anywhere
103
+ # else, they are eligible for garbage collection, which would effectively
104
+ # unload them.
105
+ #
106
+ # This method is public but undocumented. Main interface is `reload`, which
107
+ # means `unload` + `setup`. This one is avaiable to be used together with
108
+ # `unregister`, which is undocumented too.
109
+ #
110
+ # @sig () -> void
111
+ def unload
112
+ # We are going to keep track of the files that were required by our
113
+ # autoloads to later remove them from $LOADED_FEATURES, thus making them
114
+ # loadable by Kernel#require again.
115
+ #
116
+ # Directories are not stored in $LOADED_FEATURES, keeping track of files
117
+ # is enough.
118
+ unloaded_files = Set.new
119
+
120
+ autoloads.each do |abspath, (parent, cname)|
121
+ if parent.autoload?(cname)
122
+ unload_autoload(parent, cname)
123
+ else
124
+ # Could happen if loaded with require_relative. That is unsupported,
125
+ # and the constant path would escape unloadable_cpath? This is just
126
+ # defensive code to clean things up as much as we are able to.
127
+ unload_cref(parent, cname)
128
+ unloaded_files.add(abspath) if ruby?(abspath)
129
+ end
130
+ end
131
+
132
+ to_unload.each do |cpath, (abspath, (parent, cname))|
133
+ # We have to check cdef? in this condition. Reason is, constants whose
134
+ # file does not define them have to be kept in to_unload as explained
135
+ # in the implementation of on_file_autoloaded.
136
+ #
137
+ # If the constant is not defined, on_unload should not be triggered
138
+ # for it.
139
+ if !on_unload_callbacks.empty? && cdef?(parent, cname)
140
+ value = parent.const_get(cname)
141
+ run_on_unload_callbacks(cpath, value, abspath)
142
+ end
143
+
144
+ unload_cref(parent, cname)
145
+ unloaded_files.add(abspath) if ruby?(abspath)
146
+ end
147
+
148
+ unless unloaded_files.empty?
149
+ # Bootsnap decorates Kernel#require to speed it up using a cache and
150
+ # this optimization does not check if $LOADED_FEATURES has the file.
151
+ #
152
+ # To make it aware of changes, the gem defines singleton methods in
153
+ # $LOADED_FEATURES:
154
+ #
155
+ # https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
156
+ #
157
+ # Rails applications may depend on bootsnap, so for unloading to work
158
+ # in that setting it is preferable that we restrict our API choice to
159
+ # one of those methods.
160
+ $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
161
+ end
162
+
163
+ autoloads.clear
164
+ autoloaded_dirs.clear
165
+ to_unload.clear
166
+ lazy_subdirs.clear
167
+
168
+ Registry.on_unload(self)
169
+ ExplicitNamespace.unregister_loader(self)
170
+
171
+ @setup = false
172
+ @eager_loaded = false
173
+ end
174
+
175
+ # Unloads all loaded code, and calls setup again so that the loader is able
176
+ # to pick any changes in the file system.
177
+ #
178
+ # This method is not thread-safe, please see how this can be achieved by
179
+ # client code in the README of the project.
180
+ #
181
+ # @raise [Zeitwerk::Error]
182
+ # @sig () -> void
183
+ def reload
184
+ if reloading_enabled?
185
+ unload
186
+ recompute_ignored_paths
187
+ recompute_collapse_dirs
188
+ setup
189
+ else
190
+ raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
191
+ end
192
+ end
193
+
194
+ # Eager loads all files in the root directories, recursively. Files do not
195
+ # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
196
+ # are not eager loaded. You can opt-out specifically in specific files and
197
+ # directories with `do_not_eager_load`, and that can be overridden passing
198
+ # `force: true`.
199
+ #
200
+ # @sig (true | false) -> void
201
+ def eager_load(force: false)
202
+ return if @eager_loaded
203
+
204
+ honour_exclusions = !force
205
+
206
+ queue = []
207
+ actual_root_dirs.each do |root_dir, namespace|
208
+ queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
209
+ end
210
+
211
+ while to_eager_load = queue.shift
212
+ namespace, dir = to_eager_load
213
+
214
+ ls(dir) do |basename, abspath|
215
+ next if honour_exclusions && excluded_from_eager_load?(abspath)
216
+
217
+ if ruby?(abspath)
218
+ if cref = autoloads[abspath]
219
+ cget(*cref)
220
+ end
221
+ elsif dir?(abspath) && !root_dirs.key?(abspath)
222
+ if collapse?(abspath)
223
+ queue << [namespace, abspath]
224
+ else
225
+ cname = inflector.camelize(basename, abspath)
226
+ queue << [cget(namespace, cname), abspath]
227
+ end
228
+ end
229
+ end
230
+ end
231
+
232
+ autoloaded_dirs.each do |autoloaded_dir|
233
+ Registry.unregister_autoload(autoloaded_dir)
234
+ end
235
+ autoloaded_dirs.clear
236
+
237
+ @eager_loaded = true
238
+ end
239
+
240
+ # Says if the given constant path would be unloaded on reload. This
241
+ # predicate returns `false` if reloading is disabled.
242
+ #
243
+ # @sig (String) -> bool
244
+ def unloadable_cpath?(cpath)
245
+ to_unload.key?(cpath)
246
+ end
247
+
248
+ # Returns an array with the constant paths that would be unloaded on reload.
249
+ # This predicate returns an empty array if reloading is disabled.
250
+ #
251
+ # @sig () -> Array[String]
252
+ def unloadable_cpaths
253
+ to_unload.keys
254
+ end
255
+
256
+ # This is a dangerous method.
257
+ #
258
+ # @experimental
259
+ # @sig () -> void
260
+ def unregister
261
+ Registry.unregister_loader(self)
262
+ ExplicitNamespace.unregister_loader(self)
263
+ end
264
+
265
+ # --- Class methods ---------------------------------------------------------------------------
266
+
267
+ class << self
268
+ # @sig #call | #debug | nil
269
+ attr_accessor :default_logger
270
+
271
+ # This is a shortcut for
272
+ #
273
+ # require "zeitwerk"
274
+ # loader = Zeitwerk::Loader.new
275
+ # loader.tag = File.basename(__FILE__, ".rb")
276
+ # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
277
+ # loader.push_dir(__dir__)
278
+ #
279
+ # except that this method returns the same object in subsequent calls from
280
+ # the same file, in the unlikely case the gem wants to be able to reload.
281
+ #
282
+ # @sig () -> Zeitwerk::Loader
283
+ def for_gem(called_from)
284
+ Registry.loader_for_gem(called_from)
285
+ end
286
+
287
+ # Broadcasts `eager_load` to all loaders.
288
+ #
289
+ # @sig () -> void
290
+ def eager_load_all
291
+ Registry.loaders.each(&:eager_load)
292
+ end
293
+
294
+ # Returns an array with the absolute paths of the root directories of all
295
+ # registered loaders. This is a read-only collection.
296
+ #
297
+ # @sig () -> Array[String]
298
+ def all_dirs
299
+ Registry.loaders.flat_map(&:dirs)
300
+ end
301
+ end
302
+
303
+ private # -------------------------------------------------------------------------------------
304
+
305
+ # @sig (String, Module) -> void
306
+ def set_autoloads_in_dir(dir, parent)
307
+ ls(dir) do |basename, abspath|
308
+ begin
309
+ if ruby?(abspath)
310
+ # basename.delete_suffix!(".rb")
311
+ cname = inflector.camelize(basename, abspath).to_sym
312
+ autoload_file(parent, cname, abspath)
313
+ elsif dir?(abspath)
314
+ # In a Rails application, `app/models/concerns` is a subdirectory of
315
+ # `app/models`, but both of them are root directories.
316
+ #
317
+ # To resolve the ambiguity file name -> constant path this introduces,
318
+ # the `app/models/concerns` directory is totally ignored as a namespace,
319
+ # it counts only as root. The guard checks that.
320
+ unless root_dir?(abspath)
321
+ cname = inflector.camelize(basename, abspath).to_sym
322
+ if collapse?(abspath)
323
+ set_autoloads_in_dir(abspath, parent)
324
+ else
325
+ autoload_subdir(parent, cname, abspath)
326
+ end
327
+ end
328
+ end
329
+ rescue ::NameError => error
330
+ path_type = ruby?(abspath) ? "file" : "directory"
331
+
332
+ raise NameError.new(<<~MESSAGE, error.name)
333
+ #{error.message} inferred by #{inflector.class} from #{path_type}
334
+
335
+ #{abspath}
336
+
337
+ Possible ways to address this:
338
+
339
+ * Tell Zeitwerk to ignore this particular #{path_type}.
340
+ * Tell Zeitwerk to ignore one of its parent directories.
341
+ * Rename the #{path_type} to comply with the naming conventions.
342
+ * Modify the inflector to handle this case.
343
+ MESSAGE
344
+ end
345
+ end
346
+ end
347
+
348
+ # @sig (Module, Symbol, String) -> void
349
+ def autoload_subdir(parent, cname, subdir)
350
+ if autoload_path = autoload_path_set_by_me_for?(parent, cname)
351
+ cpath = cpath(parent, cname)
352
+ register_explicit_namespace(cpath) if ruby?(autoload_path)
353
+ # We do not need to issue another autoload, the existing one is enough
354
+ # no matter if it is for a file or a directory. Just remember the
355
+ # subdirectory has to be visited if the namespace is used.
356
+ lazy_subdirs[cpath] << subdir
357
+ elsif !cdef?(parent, cname)
358
+ # First time we find this namespace, set an autoload for it.
359
+ lazy_subdirs[cpath(parent, cname)] << subdir
360
+ set_autoload(parent, cname, subdir)
361
+ else
362
+ # For whatever reason the constant that corresponds to this namespace has
363
+ # already been defined, we have to recurse.
364
+ set_autoloads_in_dir(subdir, cget(parent, cname))
365
+ end
366
+ end
367
+
368
+ # @sig (Module, Symbol, String) -> void
369
+ def autoload_file(parent, cname, file)
370
+ if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
371
+ # First autoload for a Ruby file wins, just ignore subsequent ones.
372
+ if ruby?(autoload_path)
373
+ # "file #{file} is ignored because #{autoload_path} has precedence"
374
+ else
375
+ promote_namespace_from_implicit_to_explicit(
376
+ dir: autoload_path,
377
+ file: file,
378
+ parent: parent,
379
+ cname: cname
380
+ )
381
+ end
382
+ elsif cdef?(parent, cname)
383
+ # "file #{file} is ignored because #{cpath(parent, cname)} is already defined"
384
+ else
385
+ set_autoload(parent, cname, file)
386
+ end
387
+ end
388
+
389
+ # `dir` is the directory that would have autovivified a namespace. `file` is
390
+ # the file where we've found the namespace is explicitly defined.
391
+ #
392
+ # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
393
+ def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
394
+ autoloads.delete(dir)
395
+ Registry.unregister_autoload(dir)
396
+
397
+ set_autoload(parent, cname, file)
398
+ register_explicit_namespace(cpath(parent, cname))
399
+ end
400
+
401
+ # @sig (Module, Symbol, String) -> void
402
+ def set_autoload(parent, cname, abspath)
403
+ parent.autoload(cname, abspath)
404
+
405
+ autoloads[abspath] = [parent, cname]
406
+ Registry.register_autoload(self, abspath)
407
+
408
+ # See why in the documentation of Zeitwerk::Registry.inceptions.
409
+ unless parent.autoload?(cname)
410
+ Registry.register_inception(cpath(parent, cname), abspath, self)
411
+ end
412
+ end
413
+
414
+ # @sig (Module, Symbol) -> String?
415
+ def autoload_path_set_by_me_for?(parent, cname)
416
+ if autoload_path = strict_autoload_path(parent, cname)
417
+ autoload_path if autoloads.key?(autoload_path)
418
+ else
419
+ Registry.inception?(cpath(parent, cname))
420
+ end
421
+ end
422
+
423
+ # @sig (String) -> void
424
+ def register_explicit_namespace(cpath)
425
+ ExplicitNamespace.register(cpath, self)
426
+ end
427
+
428
+ # @sig (String) -> void
429
+ def raise_if_conflicting_directory(dir)
430
+ Registry.loaders.each do |loader|
431
+ next if loader == self
432
+ next if loader.ignores?(dir)
433
+
434
+ dir = dir + "/"
435
+ loader.root_dirs.each do |root_dir, _namespace|
436
+ next if ignores?(root_dir)
437
+
438
+ root_dir = root_dir + "/"
439
+ if dir.start_with?(root_dir) || root_dir.start_with?(dir)
440
+ raise Error,
441
+ "loader\n\n#{loader}\n\nwants to manage directory #{dir.chop}," \
442
+ " which is already managed by\n\n#{loader}\n"
443
+ EOS
444
+ end
445
+ end
446
+ end
447
+ end
448
+
449
+ # @sig (String, Object, String) -> void
450
+ def run_on_unload_callbacks(cpath, value, abspath)
451
+ # Order matters. If present, run the most specific one.
452
+ on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
453
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
454
+ end
455
+
456
+ # @sig (Module, Symbol) -> void
457
+ def unload_autoload(parent, cname)
458
+ parent.__send__(:remove_const, cname)
459
+ end
460
+
461
+ # @sig (Module, Symbol) -> void
462
+ def unload_cref(parent, cname)
463
+ # Let's optimistically remove_const. The way we use it, this is going to
464
+ # succeed always if all is good.
465
+ parent.__send__(:remove_const, cname)
466
+ rescue ::NameError
467
+ # There are a few edge scenarios in which this may happen. If the constant
468
+ # is gone, that is OK, anyway.
469
+ end
470
+ end
471
+ end