zeitwerk 2.6.7 → 2.6.16

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,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
3
4
  require "set"
4
5
 
5
6
  module Zeitwerk
@@ -21,14 +22,13 @@ module Zeitwerk
21
22
  private_constant :MUTEX
22
23
 
23
24
  # Maps absolute paths for which an autoload has been set ---and not
24
- # executed--- to their corresponding parent class or module and constant
25
- # name.
25
+ # executed--- to their corresponding Zeitwerk::Cref object.
26
26
  #
27
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
28
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
27
+ # "/Users/fxn/blog/app/models/user.rb" => #<Zeitwerk::Cref:... @mod=Object, @cname=:User, ...>,
28
+ # "/Users/fxn/blog/app/models/hotel/pricing.rb" => #<Zeitwerk::Cref:... @mod=Hotel, @cname=:Pricing, ...>,
29
29
  # ...
30
30
  #
31
- # @sig Hash[String, [Module, Symbol]]
31
+ # @sig Hash[String, Zeitwerk::Cref]
32
32
  attr_reader :autoloads
33
33
  internal :autoloads
34
34
 
@@ -44,17 +44,19 @@ module Zeitwerk
44
44
 
45
45
  # Stores metadata needed for unloading. Its entries look like this:
46
46
  #
47
- # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
47
+ # "Admin::Role" => [
48
+ # ".../admin/role.rb",
49
+ # #<Zeitwerk::Cref:... @mod=Admin, @cname=:Role, ...>
50
+ # ]
48
51
  #
49
52
  # The cpath as key helps implementing unloadable_cpath? The file name is
50
53
  # stored in order to be able to delete it from $LOADED_FEATURES, and the
51
- # pair [Module, Symbol] is used to remove_const the constant from the class
52
- # or module object.
54
+ # cref is used to remove the constant from the parent class or module.
53
55
  #
54
56
  # If reloading is enabled, this hash is filled as constants are autoloaded
55
57
  # or eager loaded. Otherwise, the collection remains empty.
56
58
  #
57
- # @sig Hash[String, [String, [Module, Symbol]]]
59
+ # @sig Hash[String, [String, Zeitwerk::Cref]]
58
60
  attr_reader :to_unload
59
61
  internal :to_unload
60
62
 
@@ -91,9 +93,9 @@ module Zeitwerk
91
93
  attr_reader :mutex
92
94
  private :mutex
93
95
 
94
- # @sig Mutex
95
- attr_reader :mutex2
96
- private :mutex2
96
+ # @sig Monitor
97
+ attr_reader :dirs_autoload_monitor
98
+ private :dirs_autoload_monitor
97
99
 
98
100
  def initialize
99
101
  super
@@ -103,11 +105,12 @@ module Zeitwerk
103
105
  @to_unload = {}
104
106
  @namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
105
107
  @shadowed_files = Set.new
106
- @mutex = Mutex.new
107
- @mutex2 = Mutex.new
108
108
  @setup = false
109
109
  @eager_loaded = false
110
110
 
111
+ @mutex = Mutex.new
112
+ @dirs_autoload_monitor = Monitor.new
113
+
111
114
  Registry.register_loader(self)
112
115
  end
113
116
 
@@ -119,7 +122,7 @@ module Zeitwerk
119
122
  break if @setup
120
123
 
121
124
  actual_roots.each do |root_dir, root_namespace|
122
- set_autoloads_in_dir(root_dir, root_namespace)
125
+ define_autoloads_for_dir(root_dir, root_namespace)
123
126
  end
124
127
 
125
128
  on_setup_callbacks.each(&:call)
@@ -152,22 +155,22 @@ module Zeitwerk
152
155
  # is enough.
153
156
  unloaded_files = Set.new
154
157
 
155
- autoloads.each do |abspath, (parent, cname)|
156
- if parent.autoload?(cname)
157
- unload_autoload(parent, cname)
158
+ autoloads.each do |abspath, cref|
159
+ if cref.autoload?
160
+ unload_autoload(cref)
158
161
  else
159
162
  # Could happen if loaded with require_relative. That is unsupported,
160
163
  # and the constant path would escape unloadable_cpath? This is just
161
164
  # defensive code to clean things up as much as we are able to.
162
- unload_cref(parent, cname)
165
+ unload_cref(cref)
163
166
  unloaded_files.add(abspath) if ruby?(abspath)
164
167
  end
165
168
  end
166
169
 
167
- to_unload.each do |cpath, (abspath, (parent, cname))|
170
+ to_unload.each do |cpath, (abspath, cref)|
168
171
  unless on_unload_callbacks.empty?
169
172
  begin
170
- value = cget(parent, cname)
173
+ value = cref.get
171
174
  rescue ::NameError
172
175
  # Perhaps the user deleted the constant by hand, or perhaps an
173
176
  # autoload failed to define the expected constant but the user
@@ -177,7 +180,7 @@ module Zeitwerk
177
180
  end
178
181
  end
179
182
 
180
- unload_cref(parent, cname)
183
+ unload_cref(cref)
181
184
  unloaded_files.add(abspath) if ruby?(abspath)
182
185
  end
183
186
 
@@ -228,6 +231,87 @@ module Zeitwerk
228
231
  setup
229
232
  end
230
233
 
234
+ # Returns a hash that maps the absolute paths of the managed files and
235
+ # directories to their respective expected constant paths.
236
+ #
237
+ # @sig () -> Hash[String, String]
238
+ def all_expected_cpaths
239
+ result = {}
240
+
241
+ actual_roots.each do |root_dir, root_namespace|
242
+ queue = [[root_dir, real_mod_name(root_namespace)]]
243
+
244
+ while (dir, cpath = queue.shift)
245
+ result[dir] = cpath
246
+
247
+ prefix = cpath == "Object" ? "" : cpath + "::"
248
+
249
+ ls(dir) do |basename, abspath, ftype|
250
+ if ftype == :file
251
+ basename.delete_suffix!(".rb")
252
+ result[abspath] = prefix + inflector.camelize(basename, abspath)
253
+ else
254
+ if collapse?(abspath)
255
+ queue << [abspath, cpath]
256
+ else
257
+ queue << [abspath, prefix + inflector.camelize(basename, abspath)]
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ result
265
+ end
266
+
267
+ # @sig (String | Pathname) -> String?
268
+ def cpath_expected_at(path)
269
+ abspath = File.expand_path(path)
270
+
271
+ raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
272
+
273
+ return unless dir?(abspath) || ruby?(abspath)
274
+ return if ignored_path?(abspath)
275
+
276
+ paths = []
277
+
278
+ if ruby?(abspath)
279
+ basename = File.basename(abspath, ".rb")
280
+ return if hidden?(basename)
281
+
282
+ paths << [basename, abspath]
283
+ walk_up_from = File.dirname(abspath)
284
+ else
285
+ walk_up_from = abspath
286
+ end
287
+
288
+ root_namespace = nil
289
+
290
+ walk_up(walk_up_from) do |dir|
291
+ break if root_namespace = roots[dir]
292
+ return if ignored_path?(dir)
293
+
294
+ basename = File.basename(dir)
295
+ return if hidden?(basename)
296
+
297
+ paths << [basename, abspath] unless collapse?(dir)
298
+ end
299
+
300
+ return unless root_namespace
301
+
302
+ if paths.empty?
303
+ real_mod_name(root_namespace)
304
+ else
305
+ cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
306
+
307
+ if root_namespace == Object
308
+ cnames.join("::")
309
+ else
310
+ "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
311
+ end
312
+ end
313
+ end
314
+
231
315
  # Says if the given constant path would be unloaded on reload. This
232
316
  # predicate returns `false` if reloading is disabled.
233
317
  #
@@ -264,12 +348,15 @@ module Zeitwerk
264
348
  # --- Class methods ---------------------------------------------------------------------------
265
349
 
266
350
  class << self
351
+ include RealModName
352
+
267
353
  # @sig #call | #debug | nil
268
354
  attr_accessor :default_logger
269
355
 
270
356
  # This is a shortcut for
271
357
  #
272
358
  # require "zeitwerk"
359
+ #
273
360
  # loader = Zeitwerk::Loader.new
274
361
  # loader.tag = File.basename(__FILE__, ".rb")
275
362
  # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
@@ -284,7 +371,36 @@ module Zeitwerk
284
371
  # @sig (bool) -> Zeitwerk::GemLoader
285
372
  def for_gem(warn_on_extra_files: true)
286
373
  called_from = caller_locations(1, 1).first.path
287
- Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
374
+ Registry.loader_for_gem(called_from, namespace: Object, warn_on_extra_files: warn_on_extra_files)
375
+ end
376
+
377
+ # This is a shortcut for
378
+ #
379
+ # require "zeitwerk"
380
+ #
381
+ # loader = Zeitwerk::Loader.new
382
+ # loader.tag = namespace.name + "-" + File.basename(__FILE__, ".rb")
383
+ # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
384
+ # loader.push_dir(__dir__, namespace: namespace)
385
+ #
386
+ # except that this method returns the same object in subsequent calls from
387
+ # the same file, in the unlikely case the gem wants to be able to reload.
388
+ #
389
+ # This method returns a subclass of Zeitwerk::Loader, but the exact type
390
+ # is private, client code can only rely on the interface.
391
+ #
392
+ # @sig (bool) -> Zeitwerk::GemLoader
393
+ def for_gem_extension(namespace)
394
+ unless namespace.is_a?(Module) # Note that Class < Module.
395
+ raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
396
+ end
397
+
398
+ unless real_mod_name(namespace)
399
+ raise Zeitwerk::Error, "extending anonymous namespaces is unsupported"
400
+ end
401
+
402
+ called_from = caller_locations(1, 1).first.path
403
+ Registry.loader_for_gem(called_from, namespace: namespace, warn_on_extra_files: false)
288
404
  end
289
405
 
290
406
  # Broadcasts `eager_load` to all loaders. Those that have not been setup
@@ -325,44 +441,26 @@ module Zeitwerk
325
441
  end
326
442
 
327
443
  # @sig (String, Module) -> void
328
- private def set_autoloads_in_dir(dir, parent)
329
- ls(dir) do |basename, abspath|
330
- begin
331
- if ruby?(basename)
332
- basename.delete_suffix!(".rb")
333
- cname = inflector.camelize(basename, abspath).to_sym
334
- autoload_file(parent, cname, abspath)
444
+ private def define_autoloads_for_dir(dir, parent)
445
+ ls(dir) do |basename, abspath, ftype|
446
+ if ftype == :file
447
+ basename.delete_suffix!(".rb")
448
+ cref = Cref.new(parent, cname_for(basename, abspath))
449
+ autoload_file(cref, abspath)
450
+ else
451
+ if collapse?(abspath)
452
+ define_autoloads_for_dir(abspath, parent)
335
453
  else
336
- if collapse?(abspath)
337
- set_autoloads_in_dir(abspath, parent)
338
- else
339
- cname = inflector.camelize(basename, abspath).to_sym
340
- autoload_subdir(parent, cname, abspath)
341
- end
454
+ cref = Cref.new(parent, cname_for(basename, abspath))
455
+ autoload_subdir(cref, abspath)
342
456
  end
343
- rescue ::NameError => error
344
- path_type = ruby?(abspath) ? "file" : "directory"
345
-
346
- raise NameError.new(<<~MESSAGE, error.name)
347
- #{error.message} inferred by #{inflector.class} from #{path_type}
348
-
349
- #{abspath}
350
-
351
- Possible ways to address this:
352
-
353
- * Tell Zeitwerk to ignore this particular #{path_type}.
354
- * Tell Zeitwerk to ignore one of its parent directories.
355
- * Rename the #{path_type} to comply with the naming conventions.
356
- * Modify the inflector to handle this case.
357
- MESSAGE
358
457
  end
359
458
  end
360
459
  end
361
460
 
362
461
  # @sig (Module, Symbol, String) -> void
363
- private def autoload_subdir(parent, cname, subdir)
364
- if autoload_path = autoload_path_set_by_me_for?(parent, cname)
365
- cpath = cpath(parent, cname)
462
+ private def autoload_subdir(cref, subdir)
463
+ if autoload_path = autoload_path_set_by_me_for?(cref)
366
464
  if ruby?(autoload_path)
367
465
  # Scanning visited a Ruby file first, and now a directory for the same
368
466
  # constant has been found. This means we are dealing with an explicit
@@ -371,88 +469,83 @@ module Zeitwerk
371
469
  # Registering is idempotent, and we have to keep the autoload pointing
372
470
  # to the file. This may run again if more directories are found later
373
471
  # on, no big deal.
374
- register_explicit_namespace(cpath)
472
+ register_explicit_namespace(cref.path)
375
473
  end
376
474
  # If the existing autoload points to a file, it has to be preserved, if
377
475
  # not, it is fine as it is. In either case, we do not need to override.
378
476
  # Just remember the subdirectory conforms this namespace.
379
- namespace_dirs[cpath] << subdir
380
- elsif !cdef?(parent, cname)
477
+ namespace_dirs[cref.path] << subdir
478
+ elsif !cref.defined?
381
479
  # First time we find this namespace, set an autoload for it.
382
- namespace_dirs[cpath(parent, cname)] << subdir
383
- set_autoload(parent, cname, subdir)
480
+ namespace_dirs[cref.path] << subdir
481
+ define_autoload(cref, subdir)
384
482
  else
385
483
  # For whatever reason the constant that corresponds to this namespace has
386
484
  # already been defined, we have to recurse.
387
- log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
388
- set_autoloads_in_dir(subdir, cget(parent, cname))
485
+ log("the namespace #{cref.path} already exists, descending into #{subdir}") if logger
486
+ define_autoloads_for_dir(subdir, cref.get)
389
487
  end
390
488
  end
391
489
 
392
490
  # @sig (Module, Symbol, String) -> void
393
- private def autoload_file(parent, cname, file)
394
- if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
491
+ private def autoload_file(cref, file)
492
+ if autoload_path = cref.autoload? || Registry.inception?(cref.path)
395
493
  # First autoload for a Ruby file wins, just ignore subsequent ones.
396
494
  if ruby?(autoload_path)
397
495
  shadowed_files << file
398
496
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
399
497
  else
400
- promote_namespace_from_implicit_to_explicit(
401
- dir: autoload_path,
402
- file: file,
403
- parent: parent,
404
- cname: cname
405
- )
498
+ promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
406
499
  end
407
- elsif cdef?(parent, cname)
500
+ elsif cref.defined?
408
501
  shadowed_files << file
409
- log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
502
+ log("file #{file} is ignored because #{cref.path} is already defined") if logger
410
503
  else
411
- set_autoload(parent, cname, file)
504
+ define_autoload(cref, file)
412
505
  end
413
506
  end
414
507
 
415
508
  # `dir` is the directory that would have autovivified a namespace. `file` is
416
509
  # the file where we've found the namespace is explicitly defined.
417
510
  #
418
- # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
419
- private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
511
+ # @sig (dir: String, file: String, cref: Zeitwerk::Cref) -> void
512
+ private def promote_namespace_from_implicit_to_explicit(dir:, file:, cref:)
420
513
  autoloads.delete(dir)
421
514
  Registry.unregister_autoload(dir)
422
515
 
423
- log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
516
+ log("earlier autoload for #{cref.path} discarded, it is actually an explicit namespace defined in #{file}") if logger
424
517
 
425
- set_autoload(parent, cname, file)
426
- register_explicit_namespace(cpath(parent, cname))
518
+ define_autoload(cref, file)
519
+ register_explicit_namespace(cref.path)
427
520
  end
428
521
 
429
522
  # @sig (Module, Symbol, String) -> void
430
- private def set_autoload(parent, cname, abspath)
431
- parent.autoload(cname, abspath)
523
+ private def define_autoload(cref, abspath)
524
+ cref.autoload(abspath)
432
525
 
433
526
  if logger
434
527
  if ruby?(abspath)
435
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
528
+ log("autoload set for #{cref.path}, to be loaded from #{abspath}")
436
529
  else
437
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
530
+ log("autoload set for #{cref.path}, to be autovivified from #{abspath}")
438
531
  end
439
532
  end
440
533
 
441
- autoloads[abspath] = [parent, cname]
534
+ autoloads[abspath] = cref
442
535
  Registry.register_autoload(self, abspath)
443
536
 
444
537
  # See why in the documentation of Zeitwerk::Registry.inceptions.
445
- unless parent.autoload?(cname)
446
- Registry.register_inception(cpath(parent, cname), abspath, self)
538
+ unless cref.autoload?
539
+ Registry.register_inception(cref.path, abspath, self)
447
540
  end
448
541
  end
449
542
 
450
543
  # @sig (Module, Symbol) -> String?
451
- private def autoload_path_set_by_me_for?(parent, cname)
452
- if autoload_path = strict_autoload_path(parent, cname)
544
+ private def autoload_path_set_by_me_for?(cref)
545
+ if autoload_path = cref.autoload?
453
546
  autoload_path if autoloads.key?(autoload_path)
454
547
  else
455
- Registry.inception?(cpath(parent, cname))
548
+ Registry.inception?(cref.path)
456
549
  end
457
550
  end
458
551
 
@@ -479,7 +572,6 @@ module Zeitwerk
479
572
  raise Error,
480
573
  "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
481
574
  " which is already managed by\n\n#{loader.pretty_inspect}\n"
482
- EOS
483
575
  end
484
576
  end
485
577
  end
@@ -494,21 +586,21 @@ module Zeitwerk
494
586
  end
495
587
 
496
588
  # @sig (Module, Symbol) -> void
497
- private def unload_autoload(parent, cname)
498
- crem(parent, cname)
499
- log("autoload for #{cpath(parent, cname)} removed") if logger
589
+ private def unload_autoload(cref)
590
+ cref.remove
591
+ log("autoload for #{cref.path} removed") if logger
500
592
  end
501
593
 
502
594
  # @sig (Module, Symbol) -> void
503
- private def unload_cref(parent, cname)
595
+ private def unload_cref(cref)
504
596
  # Let's optimistically remove_const. The way we use it, this is going to
505
597
  # succeed always if all is good.
506
- crem(parent, cname)
598
+ cref.remove
507
599
  rescue ::NameError
508
600
  # There are a few edge scenarios in which this may happen. If the constant
509
601
  # is gone, that is OK, anyway.
510
602
  else
511
- log("#{cpath(parent, cname)} unloaded") if logger
603
+ log("#{cref.path} unloaded") if logger
512
604
  end
513
605
  end
514
606
  end
@@ -0,0 +1,5 @@
1
+ class Zeitwerk::NullInflector
2
+ def camelize(basename, _abspath)
3
+ basename
4
+ end
5
+ end
@@ -86,8 +86,8 @@ module Zeitwerk
86
86
  #
87
87
  # @private
88
88
  # @sig (String) -> Zeitwerk::Loader
89
- def loader_for_gem(root_file, warn_on_extra_files:)
90
- gem_loaders_by_root_file[root_file] ||= GemLoader._new(root_file, warn_on_extra_files: warn_on_extra_files)
89
+ def loader_for_gem(root_file, namespace:, warn_on_extra_files:)
90
+ gem_loaders_by_root_file[root_file] ||= GemLoader.__new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
91
91
  end
92
92
 
93
93
  # @private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.6.7"
4
+ VERSION = "2.6.16"
5
5
  end
data/lib/zeitwerk.rb CHANGED
@@ -3,12 +3,14 @@
3
3
  module Zeitwerk
4
4
  require_relative "zeitwerk/real_mod_name"
5
5
  require_relative "zeitwerk/internal"
6
+ require_relative "zeitwerk/cref"
6
7
  require_relative "zeitwerk/loader"
7
8
  require_relative "zeitwerk/gem_loader"
8
9
  require_relative "zeitwerk/registry"
9
10
  require_relative "zeitwerk/explicit_namespace"
10
11
  require_relative "zeitwerk/inflector"
11
12
  require_relative "zeitwerk/gem_inflector"
13
+ require_relative "zeitwerk/null_inflector"
12
14
  require_relative "zeitwerk/kernel"
13
15
  require_relative "zeitwerk/error"
14
16
  require_relative "zeitwerk/version"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.7
4
+ version: 2.6.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-10 00:00:00.000000000 Z
11
+ date: 2024-06-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -23,6 +23,7 @@ files:
23
23
  - MIT-LICENSE
24
24
  - README.md
25
25
  - lib/zeitwerk.rb
26
+ - lib/zeitwerk/cref.rb
26
27
  - lib/zeitwerk/error.rb
27
28
  - lib/zeitwerk/explicit_namespace.rb
28
29
  - lib/zeitwerk/gem_inflector.rb
@@ -35,6 +36,7 @@ files:
35
36
  - lib/zeitwerk/loader/config.rb
36
37
  - lib/zeitwerk/loader/eager_load.rb
37
38
  - lib/zeitwerk/loader/helpers.rb
39
+ - lib/zeitwerk/null_inflector.rb
38
40
  - lib/zeitwerk/real_mod_name.rb
39
41
  - lib/zeitwerk/registry.rb
40
42
  - lib/zeitwerk/version.rb
@@ -61,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
63
  - !ruby/object:Gem::Version
62
64
  version: '0'
63
65
  requirements: []
64
- rubygems_version: 3.4.3
66
+ rubygems_version: 3.5.10
65
67
  signing_key:
66
68
  specification_version: 4
67
69
  summary: Efficient and thread-safe constant autoloader