zeitwerk 2.6.7 → 2.6.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.17"
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.17
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-07-29 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.15
65
67
  signing_key:
66
68
  specification_version: 4
67
69
  summary: Efficient and thread-safe constant autoloader