zeitwerk 2.3.0

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.
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk
4
+ require_relative "zeitwerk/real_mod_name"
5
+ require_relative "zeitwerk/loader"
6
+ require_relative "zeitwerk/registry"
7
+ require_relative "zeitwerk/explicit_namespace"
8
+ require_relative "zeitwerk/inflector"
9
+ require_relative "zeitwerk/gem_inflector"
10
+ require_relative "zeitwerk/kernel"
11
+ require_relative "zeitwerk/error"
12
+ end
@@ -0,0 +1,10 @@
1
+ module Zeitwerk
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ReloadingDisabledError < Error
6
+ end
7
+
8
+ class NameError < ::NameError
9
+ end
10
+ end
@@ -0,0 +1,80 @@
1
+ module Zeitwerk
2
+ # Centralizes the logic for the trace point used to detect the creation of
3
+ # explicit namespaces, needed to descend into matching subdirectories right
4
+ # after the constant has been defined.
5
+ #
6
+ # The implementation assumes an explicit namespace is managed by one loader.
7
+ # Loaders that reopen namespaces owned by other projects are responsible for
8
+ # loading their constant before setup. This is documented.
9
+ module ExplicitNamespace # :nodoc: all
10
+ class << self
11
+ include RealModName
12
+
13
+ # Maps constant paths that correspond to explicit namespaces according to
14
+ # the file system, to the loader responsible for them.
15
+ #
16
+ # @private
17
+ # @return [{String => Zeitwerk::Loader}]
18
+ attr_reader :cpaths
19
+
20
+ # @private
21
+ # @return [Mutex]
22
+ attr_reader :mutex
23
+
24
+ # @private
25
+ # @return [TracePoint]
26
+ attr_reader :tracer
27
+
28
+ # Asserts `cpath` corresponds to an explicit namespace for which `loader`
29
+ # is responsible.
30
+ #
31
+ # @private
32
+ # @param cpath [String]
33
+ # @param loader [Zeitwerk::Loader]
34
+ # @return [void]
35
+ def register(cpath, loader)
36
+ mutex.synchronize do
37
+ cpaths[cpath] = loader
38
+ # We check enabled? because, looking at the C source code, enabling an
39
+ # enabled tracer does not seem to be a simple no-op.
40
+ tracer.enable unless tracer.enabled?
41
+ end
42
+ end
43
+
44
+ # @private
45
+ # @param loader [Zeitwerk::Loader]
46
+ # @return [void]
47
+ def unregister(loader)
48
+ cpaths.delete_if { |_cpath, l| l == loader }
49
+ disable_tracer_if_unneeded
50
+ end
51
+
52
+ def disable_tracer_if_unneeded
53
+ mutex.synchronize do
54
+ tracer.disable if cpaths.empty?
55
+ end
56
+ end
57
+
58
+ def tracepoint_class_callback(event)
59
+ # If the class is a singleton class, we won't do anything with it so we
60
+ # can bail out immediately. This is several orders of magnitude faster
61
+ # than accessing its name.
62
+ return if event.self.singleton_class?
63
+
64
+ # Note that it makes sense to compute the hash code unconditionally,
65
+ # because the trace point is disabled if cpaths is empty.
66
+ if loader = cpaths.delete(real_mod_name(event.self))
67
+ loader.on_namespace_loaded(event.self)
68
+ disable_tracer_if_unneeded
69
+ end
70
+ end
71
+ end
72
+
73
+ @cpaths = {}
74
+ @mutex = Mutex.new
75
+
76
+ # We go through a method instead of defining a block mainly to have a better
77
+ # label when profiling.
78
+ @tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk
4
+ class GemInflector < Inflector
5
+ # @param root_file [String]
6
+ def initialize(root_file)
7
+ namespace = File.basename(root_file, ".rb")
8
+ lib_dir = File.dirname(root_file)
9
+ @version_file = File.join(lib_dir, namespace, "version.rb")
10
+ end
11
+
12
+ # @param basename [String]
13
+ # @param abspath [String]
14
+ # @return [String]
15
+ def camelize(basename, abspath)
16
+ abspath == @version_file ? "VERSION" : super
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk
4
+ class Inflector
5
+ # Very basic snake case -> camel case conversion.
6
+ #
7
+ # inflector = Zeitwerk::Inflector.new
8
+ # inflector.camelize("post", ...) # => "Post"
9
+ # inflector.camelize("users_controller", ...) # => "UsersController"
10
+ # inflector.camelize("api", ...) # => "Api"
11
+ #
12
+ # Takes into account hard-coded mappings configured with `inflect`.
13
+ #
14
+ # @param basename [String]
15
+ # @param _abspath [String]
16
+ # @return [String]
17
+ def camelize(basename, _abspath)
18
+ overrides[basename] || basename.split('_').map!(&:capitalize).join
19
+ end
20
+
21
+ # Configures hard-coded inflections:
22
+ #
23
+ # inflector = Zeitwerk::Inflector.new
24
+ # inflector.inflect(
25
+ # "html_parser" => "HTMLParser",
26
+ # "mysql_adapter" => "MySQLAdapter"
27
+ # )
28
+ #
29
+ # inflector.camelize("html_parser", abspath) # => "HTMLParser"
30
+ # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
31
+ # inflector.camelize("users_controller", abspath) # => "UsersController"
32
+ #
33
+ # @param inflections [{String => String}]
34
+ # @return [void]
35
+ def inflect(inflections)
36
+ overrides.merge!(inflections)
37
+ end
38
+
39
+ private
40
+
41
+ # Hard-coded basename to constant name user maps that override the default
42
+ # inflection logic.
43
+ #
44
+ # @return [{String => String}]
45
+ def overrides
46
+ @overrides ||= {}
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kernel
4
+ module_function
5
+
6
+ # We cannot decorate with prepend + super because Kernel has already been
7
+ # included in Object, and changes in ancestors don't get propagated into
8
+ # already existing ancestor chains.
9
+ alias_method :zeitwerk_original_require, :require
10
+
11
+ # @param path [String]
12
+ # @return [Boolean]
13
+ def require(path)
14
+ if loader = Zeitwerk::Registry.loader_for(path)
15
+ if path.end_with?(".rb")
16
+ zeitwerk_original_require(path).tap do |required|
17
+ loader.on_file_autoloaded(path) if required
18
+ end
19
+ else
20
+ loader.on_dir_autoloaded(path)
21
+ end
22
+ else
23
+ zeitwerk_original_require(path).tap do |required|
24
+ if required
25
+ realpath = $LOADED_FEATURES.last
26
+ if loader = Zeitwerk::Registry.loader_for(realpath)
27
+ loader.on_file_autoloaded(realpath)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,808 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "securerandom"
5
+
6
+ module Zeitwerk
7
+ class Loader
8
+ require_relative "loader/callbacks"
9
+ include Callbacks
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
35
+
36
+ # Absolute paths of files or directories that have to be preloaded.
37
+ #
38
+ # @private
39
+ # @return [<String>]
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
48
+
49
+ # The actual collection of absolute file and directory names at the time the
50
+ # ignored glob patterns were expanded. Computed on setup, and recomputed on
51
+ # reload.
52
+ #
53
+ # @private
54
+ # @return [Set<String>]
55
+ attr_reader :ignored_paths
56
+
57
+ # Absolute paths of directories or glob patterns to be collapsed.
58
+ #
59
+ # @private
60
+ # @return [Set<String>]
61
+ attr_reader :collapse_glob_patterns
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.
65
+ #
66
+ # @private
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.
73
+ #
74
+ # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
75
+ # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
76
+ # ...
77
+ #
78
+ # @private
79
+ # @return [{String => (Module, Symbol)}]
80
+ attr_reader :autoloads
81
+
82
+ # We keep track of autoloaded directories to remove them from the registry
83
+ # at the end of eager loading.
84
+ #
85
+ # Files are removed as they are autoloaded, but directories need to wait due
86
+ # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
87
+ #
88
+ # @private
89
+ # @return [<String>]
90
+ attr_reader :autoloaded_dirs
91
+
92
+ # Stores metadata needed for unloading. Its entries look like this:
93
+ #
94
+ # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
95
+ #
96
+ # The cpath as key helps implementing unloadable_cpath? The real file name
97
+ # is stored in order to be able to delete it from $LOADED_FEATURES, and the
98
+ # pair [Module, Symbol] is used to remove_const the constant from the class
99
+ # or module object.
100
+ #
101
+ # If reloading is enabled, this hash is filled as constants are autoloaded
102
+ # or eager loaded. Otherwise, the collection remains empty.
103
+ #
104
+ # @private
105
+ # @return [{String => (String, (Module, Symbol))}]
106
+ attr_reader :to_unload
107
+
108
+ # Maps constant paths of namespaces to arrays of corresponding directories.
109
+ #
110
+ # For example, given this mapping:
111
+ #
112
+ # "Admin" => [
113
+ # "/Users/fxn/blog/app/controllers/admin",
114
+ # "/Users/fxn/blog/app/models/admin",
115
+ # ...
116
+ # ]
117
+ #
118
+ # when `Admin` gets defined we know that it plays the role of a namespace and
119
+ # that its children are spread over those directories. We'll visit them to set
120
+ # up the corresponding autoloads.
121
+ #
122
+ # @private
123
+ # @return [{String => <String>}]
124
+ attr_reader :lazy_subdirs
125
+
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
+ # @private
133
+ # @return [Mutex]
134
+ attr_reader :mutex
135
+
136
+ # @private
137
+ # @return [Mutex]
138
+ attr_reader :mutex2
139
+
140
+ def initialize
141
+ @initialized_at = Time.now
142
+
143
+ @tag = SecureRandom.hex(3)
144
+ @inflector = Inflector.new
145
+ @logger = self.class.default_logger
146
+
147
+ @root_dirs = {}
148
+ @preloads = []
149
+ @ignored_glob_patterns = Set.new
150
+ @ignored_paths = Set.new
151
+ @collapse_glob_patterns = Set.new
152
+ @collapse_dirs = Set.new
153
+ @autoloads = {}
154
+ @autoloaded_dirs = []
155
+ @to_unload = {}
156
+ @lazy_subdirs = {}
157
+ @eager_load_exclusions = Set.new
158
+
159
+ # TODO: find a better name for these mutexes.
160
+ @mutex = Mutex.new
161
+ @mutex2 = Mutex.new
162
+ @setup = false
163
+ @eager_loaded = false
164
+
165
+ @reloading_enabled = false
166
+
167
+ Registry.register_loader(self)
168
+ end
169
+
170
+ # Sets a tag for the loader, useful for logging.
171
+ #
172
+ # @param tag [#to_s]
173
+ # @return [void]
174
+ def tag=(tag)
175
+ @tag = tag.to_s
176
+ end
177
+
178
+ # Absolute paths of the root directories. This is a read-only collection,
179
+ # please push here via `push_dir`.
180
+ #
181
+ # @return [<String>]
182
+ def dirs
183
+ root_dirs.keys.freeze
184
+ end
185
+
186
+ # Pushes `path` to the list of root directories.
187
+ #
188
+ # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
189
+ # the same process already manages that directory or one of its ascendants
190
+ # or descendants.
191
+ #
192
+ # @param path [<String, Pathname>]
193
+ # @raise [Zeitwerk::Error]
194
+ # @return [void]
195
+ def push_dir(path)
196
+ abspath = File.expand_path(path)
197
+ if dir?(abspath)
198
+ raise_if_conflicting_directory(abspath)
199
+ root_dirs[abspath] = true
200
+ else
201
+ raise Error, "the root directory #{abspath} does not exist"
202
+ end
203
+ end
204
+
205
+ # You need to call this method before setup in order to be able to reload.
206
+ # There is no way to undo this, either you want to reload or you don't.
207
+ #
208
+ # @raise [Zeitwerk::Error]
209
+ # @return [void]
210
+ def enable_reloading
211
+ mutex.synchronize do
212
+ break if @reloading_enabled
213
+
214
+ if @setup
215
+ raise Error, "cannot enable reloading after setup"
216
+ else
217
+ @reloading_enabled = true
218
+ end
219
+ end
220
+ end
221
+
222
+ # @return [Boolean]
223
+ def reloading_enabled?
224
+ @reloading_enabled
225
+ end
226
+
227
+ # Files or directories to be preloaded instead of lazy loaded.
228
+ #
229
+ # @param paths [<String, Pathname, <String, Pathname>>]
230
+ # @return [void]
231
+ def preload(*paths)
232
+ mutex.synchronize do
233
+ expand_paths(paths).each do |abspath|
234
+ preloads << abspath
235
+ do_preload_abspath(abspath) if @setup
236
+ end
237
+ end
238
+ end
239
+
240
+ # Configure files, directories, or glob patterns to be totally ignored.
241
+ #
242
+ # @param paths [<String, Pathname, <String, Pathname>>]
243
+ # @return [void]
244
+ def ignore(*glob_patterns)
245
+ glob_patterns = expand_paths(glob_patterns)
246
+ mutex.synchronize do
247
+ ignored_glob_patterns.merge(glob_patterns)
248
+ ignored_paths.merge(expand_glob_patterns(glob_patterns))
249
+ end
250
+ end
251
+
252
+ # Configure directories or glob patterns to be collapsed.
253
+ #
254
+ # @param paths [<String, Pathname, <String, Pathname>>]
255
+ # @return [void]
256
+ def collapse(*glob_patterns)
257
+ glob_patterns = expand_paths(glob_patterns)
258
+ mutex.synchronize do
259
+ collapse_glob_patterns.merge(glob_patterns)
260
+ collapse_dirs.merge(expand_glob_patterns(glob_patterns))
261
+ end
262
+ end
263
+
264
+ # Sets autoloads in the root namespace and preloads files, if any.
265
+ #
266
+ # @return [void]
267
+ def setup
268
+ mutex.synchronize do
269
+ break if @setup
270
+
271
+ actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
272
+ do_preload
273
+
274
+ @setup = true
275
+ end
276
+ end
277
+
278
+ # Removes loaded constants and configured autoloads.
279
+ #
280
+ # The objects the constants stored are no longer reachable through them. In
281
+ # addition, since said objects are normally not referenced from anywhere
282
+ # else, they are eligible for garbage collection, which would effectively
283
+ # unload them.
284
+ #
285
+ # @private
286
+ # @return [void]
287
+ def unload
288
+ mutex.synchronize do
289
+ # We are going to keep track of the files that were required by our
290
+ # autoloads to later remove them from $LOADED_FEATURES, thus making them
291
+ # loadable by Kernel#require again.
292
+ #
293
+ # Directories are not stored in $LOADED_FEATURES, keeping track of files
294
+ # is enough.
295
+ unloaded_files = Set.new
296
+
297
+ autoloads.each do |realpath, (parent, cname)|
298
+ if parent.autoload?(cname)
299
+ unload_autoload(parent, cname)
300
+ else
301
+ # Could happen if loaded with require_relative. That is unsupported,
302
+ # and the constant path would escape unloadable_cpath? This is just
303
+ # defensive code to clean things up as much as we are able to.
304
+ unload_cref(parent, cname) if cdef?(parent, cname)
305
+ unloaded_files.add(realpath) if ruby?(realpath)
306
+ end
307
+ end
308
+
309
+ to_unload.each_value do |(realpath, (parent, cname))|
310
+ unload_cref(parent, cname) if cdef?(parent, cname)
311
+ unloaded_files.add(realpath) if ruby?(realpath)
312
+ end
313
+
314
+ unless unloaded_files.empty?
315
+ # Bootsnap decorates Kernel#require to speed it up using a cache and
316
+ # this optimization does not check if $LOADED_FEATURES has the file.
317
+ #
318
+ # To make it aware of changes, the gem defines singleton methods in
319
+ # $LOADED_FEATURES:
320
+ #
321
+ # https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
322
+ #
323
+ # Rails applications may depend on bootsnap, so for unloading to work
324
+ # in that setting it is preferable that we restrict our API choice to
325
+ # one of those methods.
326
+ $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
327
+ end
328
+
329
+ autoloads.clear
330
+ autoloaded_dirs.clear
331
+ to_unload.clear
332
+ lazy_subdirs.clear
333
+
334
+ Registry.on_unload(self)
335
+ ExplicitNamespace.unregister(self)
336
+
337
+ @setup = false
338
+ @eager_loaded = false
339
+ end
340
+ end
341
+
342
+ # Unloads all loaded code, and calls setup again so that the loader is able
343
+ # to pick any changes in the file system.
344
+ #
345
+ # This method is not thread-safe, please see how this can be achieved by
346
+ # client code in the README of the project.
347
+ #
348
+ # @raise [Zeitwerk::Error]
349
+ # @return [void]
350
+ def reload
351
+ if reloading_enabled?
352
+ unload
353
+ recompute_ignored_paths
354
+ recompute_collapse_dirs
355
+ setup
356
+ else
357
+ raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
358
+ end
359
+ end
360
+
361
+ # Eager loads all files in the root directories, recursively. Files do not
362
+ # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
363
+ # are not eager loaded. You can opt-out specifically in specific files and
364
+ # directories with `do_not_eager_load`.
365
+ #
366
+ # @return [void]
367
+ def eager_load
368
+ mutex.synchronize do
369
+ break if @eager_loaded
370
+
371
+ queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
372
+ queue.map! { |dir| [Object, dir] }
373
+ while to_eager_load = queue.shift
374
+ namespace, dir = to_eager_load
375
+
376
+ ls(dir) do |basename, abspath|
377
+ next if eager_load_exclusions.member?(abspath)
378
+
379
+ if ruby?(abspath)
380
+ if cref = autoloads[File.realpath(abspath)]
381
+ cref[0].const_get(cref[1], false)
382
+ end
383
+ elsif dir?(abspath) && !root_dirs.key?(abspath)
384
+ if collapse_dirs.member?(abspath)
385
+ queue << [namespace, abspath]
386
+ else
387
+ cname = inflector.camelize(basename, abspath)
388
+ queue << [namespace.const_get(cname, false), abspath]
389
+ end
390
+ end
391
+ end
392
+ end
393
+
394
+ autoloaded_dirs.each do |autoloaded_dir|
395
+ Registry.unregister_autoload(autoloaded_dir)
396
+ end
397
+ autoloaded_dirs.clear
398
+
399
+ @eager_loaded = true
400
+ end
401
+ end
402
+
403
+ # Let eager load ignore the given files or directories. The constants
404
+ # defined in those files are still autoloadable.
405
+ #
406
+ # @param paths [<String, Pathname, <String, Pathname>>]
407
+ # @return [void]
408
+ def do_not_eager_load(*paths)
409
+ mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
410
+ end
411
+
412
+ # Says if the given constant path would be unloaded on reload. This
413
+ # predicate returns `false` if reloading is disabled.
414
+ #
415
+ # @param cpath [String]
416
+ # @return [Boolean]
417
+ def unloadable_cpath?(cpath)
418
+ to_unload.key?(cpath)
419
+ end
420
+
421
+ # Returns an array with the constant paths that would be unloaded on reload.
422
+ # This predicate returns an empty array if reloading is disabled.
423
+ #
424
+ # @return [<String>]
425
+ def unloadable_cpaths
426
+ to_unload.keys.freeze
427
+ end
428
+
429
+ # Logs to `$stdout`, handy shortcut for debugging.
430
+ #
431
+ # @return [void]
432
+ def log!
433
+ @logger = ->(msg) { puts msg }
434
+ end
435
+
436
+ # @private
437
+ # @param dir [String]
438
+ # @return [Boolean]
439
+ def manages?(dir)
440
+ dir = dir + "/"
441
+ ignored_paths.each do |ignored_path|
442
+ return false if dir.start_with?(ignored_path + "/")
443
+ end
444
+
445
+ root_dirs.each_key do |root_dir|
446
+ return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
447
+ end
448
+
449
+ false
450
+ end
451
+
452
+ # --- Class methods ---------------------------------------------------------------------------
453
+
454
+ class << self
455
+ # @return [#call, #debug, nil]
456
+ attr_accessor :default_logger
457
+
458
+ # @private
459
+ # @return [Mutex]
460
+ attr_accessor :mutex
461
+
462
+ # This is a shortcut for
463
+ #
464
+ # require "zeitwerk"
465
+ # loader = Zeitwerk::Loader.new
466
+ # loader.tag = File.basename(__FILE__, ".rb")
467
+ # loader.inflector = Zeitwerk::GemInflector.new
468
+ # loader.push_dir(__dir__)
469
+ #
470
+ # except that this method returns the same object in subsequent calls from
471
+ # the same file, in the unlikely case the gem wants to be able to reload.
472
+ #
473
+ # @return [Zeitwerk::Loader]
474
+ def for_gem
475
+ called_from = caller_locations(1, 1).first.path
476
+ Registry.loader_for_gem(called_from)
477
+ end
478
+
479
+ # Broadcasts `eager_load` to all loaders.
480
+ #
481
+ # @return [void]
482
+ def eager_load_all
483
+ Registry.loaders.each(&:eager_load)
484
+ end
485
+
486
+ # Returns an array with the absolute paths of the root directories of all
487
+ # registered loaders. This is a read-only collection.
488
+ #
489
+ # @return [<String>]
490
+ def all_dirs
491
+ Registry.loaders.flat_map(&:dirs).freeze
492
+ end
493
+ end
494
+
495
+ self.mutex = Mutex.new
496
+
497
+ private # -------------------------------------------------------------------------------------
498
+
499
+ # @return [<String>]
500
+ def actual_root_dirs
501
+ root_dirs.keys.delete_if do |root_dir|
502
+ !dir?(root_dir) || ignored_paths.member?(root_dir)
503
+ end
504
+ end
505
+
506
+ # @param dir [String]
507
+ # @param parent [Module]
508
+ # @return [void]
509
+ def set_autoloads_in_dir(dir, parent)
510
+ ls(dir) do |basename, abspath|
511
+ begin
512
+ if ruby?(basename)
513
+ basename[-3..-1] = ''
514
+ cname = inflector.camelize(basename, abspath).to_sym
515
+ autoload_file(parent, cname, abspath)
516
+ elsif dir?(abspath)
517
+ # In a Rails application, `app/models/concerns` is a subdirectory of
518
+ # `app/models`, but both of them are root directories.
519
+ #
520
+ # To resolve the ambiguity file name -> constant path this introduces,
521
+ # the `app/models/concerns` directory is totally ignored as a namespace,
522
+ # it counts only as root. The guard checks that.
523
+ unless root_dirs.key?(abspath)
524
+ cname = inflector.camelize(basename, abspath).to_sym
525
+ if collapse_dirs.member?(abspath)
526
+ set_autoloads_in_dir(abspath, parent)
527
+ else
528
+ autoload_subdir(parent, cname, abspath)
529
+ end
530
+ end
531
+ end
532
+ rescue ::NameError => error
533
+ path_type = ruby?(abspath) ? "file" : "directory"
534
+
535
+ raise NameError.new(<<~MESSAGE, error.name)
536
+ #{error.message} inferred by #{inflector.class} from #{path_type}
537
+
538
+ #{abspath}
539
+
540
+ Possible ways to address this:
541
+
542
+ * Tell Zeitwerk to ignore this particular #{path_type}.
543
+ * Tell Zeitwerk to ignore one of its parent directories.
544
+ * Rename the #{path_type} to comply with the naming conventions.
545
+ * Modify the inflector to handle this case.
546
+ MESSAGE
547
+ end
548
+ end
549
+ end
550
+
551
+ # @param parent [Module]
552
+ # @param cname [Symbol]
553
+ # @param subdir [String]
554
+ # @return [void]
555
+ def autoload_subdir(parent, cname, subdir)
556
+ if autoload_path = autoload_for?(parent, cname)
557
+ cpath = cpath(parent, cname)
558
+ register_explicit_namespace(cpath) if ruby?(autoload_path)
559
+ # We do not need to issue another autoload, the existing one is enough
560
+ # no matter if it is for a file or a directory. Just remember the
561
+ # subdirectory has to be visited if the namespace is used.
562
+ (lazy_subdirs[cpath] ||= []) << subdir
563
+ elsif !cdef?(parent, cname)
564
+ # First time we find this namespace, set an autoload for it.
565
+ (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
566
+ set_autoload(parent, cname, subdir)
567
+ else
568
+ # For whatever reason the constant that corresponds to this namespace has
569
+ # already been defined, we have to recurse.
570
+ set_autoloads_in_dir(subdir, parent.const_get(cname))
571
+ end
572
+ end
573
+
574
+ # @param parent [Module]
575
+ # @param cname [Symbol]
576
+ # @param file [String]
577
+ # @return [void]
578
+ def autoload_file(parent, cname, file)
579
+ if autoload_path = autoload_for?(parent, cname)
580
+ # First autoload for a Ruby file wins, just ignore subsequent ones.
581
+ if ruby?(autoload_path)
582
+ log("file #{file} is ignored because #{autoload_path} has precedence") if logger
583
+ else
584
+ promote_namespace_from_implicit_to_explicit(
585
+ dir: autoload_path,
586
+ file: file,
587
+ parent: parent,
588
+ cname: cname
589
+ )
590
+ end
591
+ elsif cdef?(parent, cname)
592
+ log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
593
+ else
594
+ set_autoload(parent, cname, file)
595
+ end
596
+ end
597
+
598
+ # @param dir [String] directory that would have autovivified a module
599
+ # @param file [String] the file where the namespace is explicitly defined
600
+ # @param parent [Module]
601
+ # @param cname [Symbol]
602
+ # @return [void]
603
+ def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
604
+ autoloads.delete(dir)
605
+ Registry.unregister_autoload(dir)
606
+
607
+ set_autoload(parent, cname, file)
608
+ register_explicit_namespace(cpath(parent, cname))
609
+ end
610
+
611
+ # @param parent [Module]
612
+ # @param cname [Symbol]
613
+ # @param abspath [String]
614
+ # @return [void]
615
+ def set_autoload(parent, cname, abspath)
616
+ # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
617
+ # real path to be able to delete it from $LOADED_FEATURES on unload, and to
618
+ # be able to do a lookup later in Kernel#require for manual require calls.
619
+ realpath = File.realpath(abspath)
620
+ parent.autoload(cname, realpath)
621
+ if logger
622
+ if ruby?(realpath)
623
+ log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
624
+ else
625
+ log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
626
+ end
627
+ end
628
+
629
+ autoloads[realpath] = [parent, cname]
630
+ Registry.register_autoload(self, realpath)
631
+
632
+ # See why in the documentation of Zeitwerk::Registry.inceptions.
633
+ unless parent.autoload?(cname)
634
+ Registry.register_inception(cpath(parent, cname), realpath, self)
635
+ end
636
+ end
637
+
638
+ # @param parent [Module]
639
+ # @param cname [Symbol]
640
+ # @return [String, nil]
641
+ def autoload_for?(parent, cname)
642
+ strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
643
+ end
644
+
645
+ # The autoload? predicate takes into account the ancestor chain of the
646
+ # receiver, like const_defined? and other methods in the constants API do.
647
+ #
648
+ # For example, given
649
+ #
650
+ # class A
651
+ # autoload :X, "x.rb"
652
+ # end
653
+ #
654
+ # class B < A
655
+ # end
656
+ #
657
+ # B.autoload?(:X) returns "x.rb".
658
+ #
659
+ # We need a way to strictly check in parent ignoring ancestors.
660
+ #
661
+ # @param parent [Module]
662
+ # @param cname [Symbol]
663
+ # @return [String, nil]
664
+ if method(:autoload?).arity == 1
665
+ def strict_autoload_path(parent, cname)
666
+ parent.autoload?(cname) if cdef?(parent, cname)
667
+ end
668
+ else
669
+ def strict_autoload_path(parent, cname)
670
+ parent.autoload?(cname, false)
671
+ end
672
+ end
673
+
674
+ # This method is called this way because I prefer `preload` to be the method
675
+ # name to configure preloads in the public interface.
676
+ #
677
+ # @return [void]
678
+ def do_preload
679
+ preloads.each do |abspath|
680
+ do_preload_abspath(abspath)
681
+ end
682
+ end
683
+
684
+ # @param abspath [String]
685
+ # @return [void]
686
+ def do_preload_abspath(abspath)
687
+ if ruby?(abspath)
688
+ do_preload_file(abspath)
689
+ elsif dir?(abspath)
690
+ do_preload_dir(abspath)
691
+ end
692
+ end
693
+
694
+ # @param dir [String]
695
+ # @return [void]
696
+ def do_preload_dir(dir)
697
+ ls(dir) do |_basename, abspath|
698
+ do_preload_abspath(abspath)
699
+ end
700
+ end
701
+
702
+ # @param file [String]
703
+ # @return [Boolean]
704
+ def do_preload_file(file)
705
+ log("preloading #{file}") if logger
706
+ require file
707
+ end
708
+
709
+ # @param parent [Module]
710
+ # @param cname [Symbol]
711
+ # @return [String]
712
+ def cpath(parent, cname)
713
+ parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
714
+ end
715
+
716
+ # @param dir [String]
717
+ # @yieldparam path [String, String]
718
+ # @return [void]
719
+ def ls(dir)
720
+ Dir.foreach(dir) do |basename|
721
+ next if basename.start_with?(".")
722
+ abspath = File.join(dir, basename)
723
+ yield basename, abspath unless ignored_paths.member?(abspath)
724
+ end
725
+ end
726
+
727
+ # @param path [String]
728
+ # @return [Boolean]
729
+ def ruby?(path)
730
+ path.end_with?(".rb")
731
+ end
732
+
733
+ # @param path [String]
734
+ # @return [Boolean]
735
+ def dir?(path)
736
+ File.directory?(path)
737
+ end
738
+
739
+ # @param paths [<String, Pathname, <String, Pathname>>]
740
+ # @return [<String>]
741
+ def expand_paths(paths)
742
+ paths.flatten.map! { |path| File.expand_path(path) }
743
+ end
744
+
745
+ # @param glob_patterns [<String>]
746
+ # @return [<String>]
747
+ def expand_glob_patterns(glob_patterns)
748
+ # Note that Dir.glob works with regular file names just fine. That is,
749
+ # glob patterns technically need no wildcards.
750
+ glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
751
+ end
752
+
753
+ # @return [void]
754
+ def recompute_ignored_paths
755
+ ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
756
+ end
757
+
758
+ # @return [void]
759
+ def recompute_collapse_dirs
760
+ collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
761
+ end
762
+
763
+ # @param message [String]
764
+ # @return [void]
765
+ def log(message)
766
+ method_name = logger.respond_to?(:debug) ? :debug : :call
767
+ logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
768
+ end
769
+
770
+ def cdef?(parent, cname)
771
+ parent.const_defined?(cname, false)
772
+ end
773
+
774
+ def register_explicit_namespace(cpath)
775
+ ExplicitNamespace.register(cpath, self)
776
+ end
777
+
778
+ def raise_if_conflicting_directory(dir)
779
+ self.class.mutex.synchronize do
780
+ Registry.loaders.each do |loader|
781
+ if loader != self && loader.manages?(dir)
782
+ require "pp"
783
+ raise Error,
784
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
785
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
786
+ EOS
787
+ end
788
+ end
789
+ end
790
+ end
791
+
792
+ # @param parent [Module]
793
+ # @param cname [Symbol]
794
+ # @return [void]
795
+ def unload_autoload(parent, cname)
796
+ parent.send(:remove_const, cname)
797
+ log("autoload for #{cpath(parent, cname)} removed") if logger
798
+ end
799
+
800
+ # @param parent [Module]
801
+ # @param cname [Symbol]
802
+ # @return [void]
803
+ def unload_cref(parent, cname)
804
+ parent.send(:remove_const, cname)
805
+ log("#{cpath(parent, cname)} unloaded") if logger
806
+ end
807
+ end
808
+ end