zeitwerk 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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