zeitwerk 2.7.1 → 2.7.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aba46812169c8e26085b099c708093b00a29bfd39e8d8210d29bc99e7df0e7fa
4
- data.tar.gz: 0fc20386009d52d21a0cb79a64c5d800f7aecfce5a38e8430a10dc5d04c2609d
3
+ metadata.gz: 1a07f90eb2f155582d05f58527ffcbc2f4d76c9a1983260ca8d527becaeb7972
4
+ data.tar.gz: 65e8dc78ca8e6de674f0fc7d88aad5c9bad0d7687bc9ed26f93d6fa0e6d18e90
5
5
  SHA512:
6
- metadata.gz: 3edac5ad6f940caa70c4cac093bf271b8399b078d7124f450513c963ec5099e9b9082841ceb9893b81f1edf8e34dc642ec811403415fb230211670c63a950766
7
- data.tar.gz: 47339a8b35dc06108fde9aadd62e83b1ea4e4ef22854c31849069f09ece1115dce07d63ec354fe96fbc599bba411ecff48db6a8b0c8b150ad7ee016787e9d75c
6
+ metadata.gz: d7b9d13e3d3d5bf0497ec259bf0817256586e245f7d47c951b8392784f715bc71c20d0ec3c9e465077da6d8e729ca6888fbaaa24820fe4459771e29340ee6d05
7
+ data.tar.gz: 8b1322d36bc9115a56b6abab6be9549c868e0edd2025fe82dd2c5d0abb082fac8532c82ed03f895d34f2875f27b160f4861185112c4aef2a3129e46569115c0f
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Zeitwerk::ConstAdded
3
+ module Zeitwerk::ConstAdded # :nodoc:
4
+ # @sig (Symbol) -> void
4
5
  def const_added(cname)
5
- if loader = Zeitwerk::ExplicitNamespace.__loader_for(self, cname)
6
+ if loader = Zeitwerk::Registry::ExplicitNamespaces.__loader_for(self, cname)
6
7
  namespace = const_get(cname, false)
7
8
 
8
9
  unless namespace.is_a?(Module)
9
10
  cref = Zeitwerk::Cref.new(self, cname)
10
- raise Zeitwerk::Error, "#{cref.path} is expected to be a namespace, should be a class or module (got #{namespace.class})"
11
+ raise Zeitwerk::Error, "#{cref} is expected to be a namespace, should be a class or module (got #{namespace.class})"
11
12
  end
12
13
 
13
- loader.on_namespace_loaded(namespace)
14
+ loader.__on_namespace_loaded(Zeitwerk::Cref.new(self, cname), namespace)
14
15
  end
15
16
  super
16
17
  end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class emulates a hash table whose keys are of type Zeitwerk::Cref.
4
+ #
5
+ # It is a synchronized 2-level hash. The keys of the top one, stored in `@map`,
6
+ # are class and module objects, but their hash code is forced to be their object
7
+ # IDs (see why below). Then, each one of them stores a hash table keyed on
8
+ # constant names as symbols. We finally store the values in those.
9
+ #
10
+ # For example, if we store values 0, 1, and 2 for the crefs that would
11
+ # correspond to `M::X`, `M::Y`, and `N::Z`, the map will look like this:
12
+ #
13
+ # { M => { X: 0, :Y => 1 }, N => { Z: 2 } }
14
+ #
15
+ # This structure is internal, so only the needed interface is implemented.
16
+ #
17
+ # Why not use tables that map pairs [Module, Symbol] to their values? Because
18
+ # class and module objects are not guaranteed to be hashable, the `hash` method
19
+ # may have been overridden:
20
+ #
21
+ # https://github.com/fxn/zeitwerk/issues/188
22
+ #
23
+ # We can also use a 1-level hash whose keys are the corresponding class and
24
+ # module names. In the example above it would be:
25
+ #
26
+ # { "M::X" => 0, "M::Y" => 1, "N::Z" => 2 }
27
+ #
28
+ # The gem used this approach for several years.
29
+ #
30
+ # Another option would be to make crefs hashable. I tried with hash code
31
+ #
32
+ # real_mod_hash(mod) ^ cname.hash
33
+ #
34
+ # and the matching eql?, but that was about 1.8x slower.
35
+ #
36
+ # Finally, I came with this solution which is 1.6x faster than the previous one
37
+ # based on class and module names, even being synchronized. Also, client code
38
+ # feels natural, since crefs are central objects in Zeitwerk's implementation.
39
+ class Zeitwerk::Cref::Map # :nodoc: all
40
+ def initialize
41
+ @map = {}
42
+ @map.compare_by_identity
43
+ @mutex = Mutex.new
44
+ end
45
+
46
+ # @sig (Zeitwerk::Cref, V) -> V
47
+ def []=(cref, value)
48
+ @mutex.synchronize do
49
+ cnames = (@map[cref.mod] ||= {})
50
+ cnames[cref.cname] = value
51
+ end
52
+ end
53
+
54
+ # @sig (Zeitwerk::Cref) -> top?
55
+ def [](cref)
56
+ @mutex.synchronize do
57
+ @map[cref.mod]&.[](cref.cname)
58
+ end
59
+ end
60
+
61
+ # @sig (Zeitwerk::Cref, { () -> V }) -> V
62
+ def get_or_set(cref, &block)
63
+ @mutex.synchronize do
64
+ cnames = (@map[cref.mod] ||= {})
65
+ cnames.fetch(cref.cname) { cnames[cref.cname] = block.call }
66
+ end
67
+ end
68
+
69
+ # @sig (Zeitwerk::Cref) -> top?
70
+ def delete(cref)
71
+ delete_mod_cname(cref.mod, cref.cname)
72
+ end
73
+
74
+ # Ad-hoc for loader_for, called from const_added. That is a hot path, I prefer
75
+ # to not create a cref in every call, since that is global.
76
+ #
77
+ # @sig (Module, Symbol) -> top?
78
+ def delete_mod_cname(mod, cname)
79
+ @mutex.synchronize do
80
+ if cnames = @map[mod]
81
+ value = cnames.delete(cname)
82
+ @map.delete(mod) if cnames.empty?
83
+ value
84
+ end
85
+ end
86
+ end
87
+
88
+ # @sig (top) -> void
89
+ def delete_by_value(value)
90
+ @mutex.synchronize do
91
+ @map.delete_if do |mod, cnames|
92
+ cnames.delete_if { _2 == value }
93
+ cnames.empty?
94
+ end
95
+ end
96
+ end
97
+
98
+ # Order of yielded crefs is undefined.
99
+ #
100
+ # @sig () { (Zeitwerk::Cref) -> void } -> void
101
+ def each_key
102
+ @mutex.synchronize do
103
+ @map.each do |mod, cnames|
104
+ cnames.each_key do |cname|
105
+ yield Zeitwerk::Cref.new(mod, cname)
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # @sig () -> void
112
+ def clear
113
+ @mutex.synchronize do
114
+ @map.clear
115
+ end
116
+ end
117
+
118
+ # @sig () -> bool
119
+ def empty? # for tests
120
+ @mutex.synchronize do
121
+ @map.empty?
122
+ end
123
+ end
124
+ end
data/lib/zeitwerk/cref.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  # This private class encapsulates pairs (mod, cname).
4
4
  #
5
5
  # Objects represent the constant cname in the class or module object mod, and
6
- # have API to manage them that encapsulates the constants API. Examples:
6
+ # have API to manage them. Examples:
7
7
  #
8
8
  # cref.path
9
9
  # cref.set(value)
@@ -11,6 +11,8 @@
11
11
  #
12
12
  # The constant may or may not exist in mod.
13
13
  class Zeitwerk::Cref
14
+ require_relative "cref/map"
15
+
14
16
  include Zeitwerk::RealModName
15
17
 
16
18
  # @sig Module
@@ -33,6 +35,7 @@ class Zeitwerk::Cref
33
35
  def path
34
36
  @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}".freeze
35
37
  end
38
+ alias to_s path
36
39
 
37
40
  # @sig () -> String?
38
41
  def autoload?
@@ -49,13 +52,13 @@ class Zeitwerk::Cref
49
52
  @mod.const_defined?(@cname, false)
50
53
  end
51
54
 
52
- # @sig (Object) -> Object
55
+ # @sig (top) -> top
53
56
  def set(value)
54
57
  @mod.const_set(@cname, value)
55
58
  end
56
59
 
57
60
  # @raise [NameError]
58
- # @sig () -> Object
61
+ # @sig () -> top
59
62
  def get
60
63
  @mod.const_get(@cname, false)
61
64
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # This is a private module.
4
4
  module Zeitwerk::Internal
5
+ # @sig (Symbol) -> void
5
6
  def internal(method_name)
6
7
  private method_name
7
8
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Zeitwerk::Loader::Callbacks
4
- include Zeitwerk::RealModName
3
+ module Zeitwerk::Loader::Callbacks # :nodoc: all
5
4
  extend Zeitwerk::Internal
6
5
 
7
6
  # Invoked from our decorated Kernel#require when a managed file is autoloaded.
@@ -14,11 +13,11 @@ module Zeitwerk::Loader::Callbacks
14
13
  Zeitwerk::Registry.unregister_autoload(file)
15
14
 
16
15
  if cref.defined?
17
- log("constant #{cref.path} loaded from file #{file}") if logger
18
- to_unload[cref.path] = [file, cref] if reloading_enabled?
16
+ log("constant #{cref} loaded from file #{file}") if logger
17
+ to_unload[file] = cref if reloading_enabled?
19
18
  run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
20
19
  else
21
- msg = "expected file #{file} to define constant #{cref.path}, but didn't"
20
+ msg = "expected file #{file} to define constant #{cref}, but didn't"
22
21
  log(msg) if logger
23
22
 
24
23
  # Ruby still keeps the autoload defined, but we remove it because the
@@ -28,7 +27,7 @@ module Zeitwerk::Loader::Callbacks
28
27
  # Since the expected constant was not defined, there is nothing to unload.
29
28
  # However, if the exception is rescued and reloading is enabled, we still
30
29
  # need to deleted the file from $LOADED_FEATURES.
31
- to_unload[cref.path] = [file, cref] if reloading_enabled?
30
+ to_unload[file] = cref if reloading_enabled?
32
31
 
33
32
  raise Zeitwerk::NameError.new(msg, cref.cname)
34
33
  end
@@ -57,7 +56,7 @@ module Zeitwerk::Loader::Callbacks
57
56
  cpath = implicit_namespace.name
58
57
  log("module #{cpath} autovivified from directory #{dir}") if logger
59
58
 
60
- to_unload[cpath] = [dir, cref] if reloading_enabled?
59
+ to_unload[dir] = cref if reloading_enabled?
61
60
 
62
61
  # We don't unregister `dir` in the registry because concurrent threads
63
62
  # wouldn't find a loader associated to it in Kernel#require and would
@@ -65,21 +64,20 @@ module Zeitwerk::Loader::Callbacks
65
64
  # these to be able to unregister later if eager loading.
66
65
  autoloaded_dirs << dir
67
66
 
68
- on_namespace_loaded(implicit_namespace)
67
+ on_namespace_loaded(cref, implicit_namespace)
69
68
 
70
69
  run_on_load_callbacks(cpath, implicit_namespace, dir) unless on_load_callbacks.empty?
71
70
  end
72
71
  end
73
72
  end
74
73
 
75
- # Invoked when a class or module is created or reopened, either from the
76
- # const_added or from module autovivification. If the namespace has matching
77
- # subdirectories, we descend into them now.
74
+ # Invoked when a namespace is created, either from const_added or from module
75
+ # autovivification. If the namespace has matching subdirectories, we descend
76
+ # into them now.
78
77
  #
79
- # @private
80
- # @sig (Module) -> void
81
- def on_namespace_loaded(namespace)
82
- if dirs = namespace_dirs.delete(real_mod_name(namespace))
78
+ # @sig (Zeitwerk::Cref, Module) -> void
79
+ internal def on_namespace_loaded(cref, namespace)
80
+ if dirs = namespace_dirs.delete(cref)
83
81
  dirs.each do |dir|
84
82
  define_autoloads_for_dir(dir, namespace)
85
83
  end
@@ -88,7 +86,7 @@ module Zeitwerk::Loader::Callbacks
88
86
 
89
87
  private
90
88
 
91
- # @sig (String, Object) -> void
89
+ # @sig (String, top, String) -> void
92
90
  def run_on_load_callbacks(cpath, value, abspath)
93
91
  # Order matters. If present, run the most specific one.
94
92
  callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
@@ -71,15 +71,15 @@ module Zeitwerk::Loader::Config
71
71
 
72
72
  # User-oriented callbacks to be fired when a constant is loaded.
73
73
  #
74
- # @sig Hash[String, Array[{ (Object, String) -> void }]]
75
- # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
74
+ # @sig Hash[String, Array[{ (top, String) -> void }]]
75
+ # Hash[Symbol, Array[{ (String, top, String) -> void }]]
76
76
  attr_reader :on_load_callbacks
77
77
  private :on_load_callbacks
78
78
 
79
79
  # User-oriented callbacks to be fired before constants are removed.
80
80
  #
81
- # @sig Hash[String, Array[{ (Object, String) -> void }]]
82
- # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
81
+ # @sig Hash[String, Array[{ (top, String) -> void }]]
82
+ # Hash[Symbol, Array[{ (String, top, String) -> void }]]
83
83
  attr_reader :on_unload_callbacks
84
84
  private :on_unload_callbacks
85
85
 
@@ -247,8 +247,8 @@ module Zeitwerk::Loader::Config
247
247
  # end
248
248
  #
249
249
  # @raise [TypeError]
250
- # @sig (String) { (Object, String) -> void } -> void
251
- # (:ANY) { (String, Object, String) -> void } -> void
250
+ # @sig (String) { (top, String) -> void } -> void
251
+ # (:ANY) { (String, top, String) -> void } -> void
252
252
  def on_load(cpath = :ANY, &block)
253
253
  raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
254
254
 
@@ -272,8 +272,8 @@ module Zeitwerk::Loader::Config
272
272
  # end
273
273
  #
274
274
  # @raise [TypeError]
275
- # @sig (String) { (Object) -> void } -> void
276
- # (:ANY) { (String, Object) -> void } -> void
275
+ # @sig (String) { (top) -> void } -> void
276
+ # (:ANY) { (String, top) -> void } -> void
277
277
  def on_unload(cpath = :ANY, &block)
278
278
  raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
279
279
 
@@ -32,6 +32,30 @@ module Zeitwerk
32
32
  attr_reader :autoloads
33
33
  internal :autoloads
34
34
 
35
+ # When the path passed to Module#autoload is in the stack of features being
36
+ # loaded at the moment, Ruby passes. For example, Module#autoload? returns
37
+ # `nil` even if the autoload has not been attempted. See
38
+ #
39
+ # https://bugs.ruby-lang.org/issues/21035
40
+ #
41
+ # We call these "inceptions".
42
+ #
43
+ # A common case is the entry point of gems managed by Zeitwerk. Their main
44
+ # file is normally required and, while doing so, the loader sets an autoload
45
+ # on the gem namespace. That autoload hits this edge case.
46
+ #
47
+ # There is some logic that neeeds to know if an autoload for a given
48
+ # constant already exists. We check Module#autoload? first, and fallback to
49
+ # the inceptions just in case.
50
+ #
51
+ # This map keeps track of pairs (cref, autoload_path) found by the loader.
52
+ # The module Zeitwerk::Registry::Inceptions, on the other hand, acts as a
53
+ # global registry for them.
54
+ #
55
+ # @sig Zeitwerk::Cref::Map[String]
56
+ attr_reader :inceptions
57
+ internal :inceptions
58
+
35
59
  # We keep track of autoloaded directories to remove them from the registry
36
60
  # at the end of eager loading.
37
61
  #
@@ -42,48 +66,31 @@ module Zeitwerk
42
66
  attr_reader :autoloaded_dirs
43
67
  internal :autoloaded_dirs
44
68
 
45
- # Stores metadata needed for unloading. Its entries look like this:
69
+ # If reloading is enabled, this collection maps autoload paths to their
70
+ # autoloaded crefs.
46
71
  #
47
- # "Admin::Role" => [
48
- # ".../admin/role.rb",
49
- # #<Zeitwerk::Cref:... @mod=Admin, @cname=:Role, ...>
50
- # ]
72
+ # On unload, the autoload paths are passed to callbacks, files deleted from
73
+ # $LOADED_FEATURES, and the crefs are deleted.
51
74
  #
52
- # The cpath as key helps implementing unloadable_cpath? The file name is
53
- # stored in order to be able to delete it from $LOADED_FEATURES, and the
54
- # cref is used to remove the constant from the parent class or module.
55
- #
56
- # If reloading is enabled, this hash is filled as constants are autoloaded
57
- # or eager loaded. Otherwise, the collection remains empty.
58
- #
59
- # @sig Hash[String, [String, Zeitwerk::Cref]]
75
+ # @sig Hash[String, Zeitwerk::Cref]
60
76
  attr_reader :to_unload
61
77
  internal :to_unload
62
78
 
63
- # Maps namespace constant paths to their respective directories.
79
+ # Maps namespace crefs to the directories that conform the namespace.
64
80
  #
65
- # For example, given this mapping:
81
+ # When these crefs get defined we know their children are spread over those
82
+ # directories. We'll visit them to set up the corresponding autoloads.
66
83
  #
67
- # "Admin" => [
68
- # "/Users/fxn/blog/app/controllers/admin",
69
- # "/Users/fxn/blog/app/models/admin",
70
- # ...
71
- # ]
72
- #
73
- # when `Admin` gets defined we know that it plays the role of a namespace
74
- # and that its children are spread over those directories. We'll visit them
75
- # to set up the corresponding autoloads.
76
- #
77
- # @sig Hash[String, Array[String]]
84
+ # @sig Zeitwerk::Cref::Map[String]
78
85
  attr_reader :namespace_dirs
79
86
  internal :namespace_dirs
80
87
 
81
88
  # A shadowed file is a file managed by this loader that is ignored when
82
89
  # setting autoloads because its matching constant is already taken.
83
90
  #
84
- # This private set is populated as we descend. For example, if the loader
85
- # has only scanned the top-level, `shadowed_files` does not have shadowed
86
- # files that may exist deep in the project tree yet.
91
+ # This private set is populated lazily, as we descend. For example, if the
92
+ # loader has only scanned the top-level, `shadowed_files` does not have the
93
+ # shadowed files that may exist deep in the project tree.
87
94
  #
88
95
  # @sig Set[String]
89
96
  attr_reader :shadowed_files
@@ -101,9 +108,10 @@ module Zeitwerk
101
108
  super
102
109
 
103
110
  @autoloads = {}
111
+ @inceptions = Zeitwerk::Cref::Map.new
104
112
  @autoloaded_dirs = []
105
113
  @to_unload = {}
106
- @namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
114
+ @namespace_dirs = Zeitwerk::Cref::Map.new
107
115
  @shadowed_files = Set.new
108
116
  @setup = false
109
117
  @eager_loaded = false
@@ -167,7 +175,7 @@ module Zeitwerk
167
175
  end
168
176
  end
169
177
 
170
- to_unload.each do |cpath, (abspath, cref)|
178
+ to_unload.each do |abspath, cref|
171
179
  unless on_unload_callbacks.empty?
172
180
  begin
173
181
  value = cref.get
@@ -176,7 +184,7 @@ module Zeitwerk
176
184
  # autoload failed to define the expected constant but the user
177
185
  # rescued the exception.
178
186
  else
179
- run_on_unload_callbacks(cpath, value, abspath)
187
+ run_on_unload_callbacks(cref, value, abspath)
180
188
  end
181
189
  end
182
190
 
@@ -205,8 +213,10 @@ module Zeitwerk
205
213
  namespace_dirs.clear
206
214
  shadowed_files.clear
207
215
 
216
+ unregister_inceptions
217
+ unregister_explicit_namespaces
218
+
208
219
  Registry.on_unload(self)
209
- ExplicitNamespace.__unregister_loader(self)
210
220
 
211
221
  @setup = false
212
222
  @eager_loaded = false
@@ -315,17 +325,23 @@ module Zeitwerk
315
325
  # Says if the given constant path would be unloaded on reload. This
316
326
  # predicate returns `false` if reloading is disabled.
317
327
  #
328
+ # This is an undocumented method that I wrote to help transition from the
329
+ # classic autoloader in Rails. Its usage was removed from Rails in 7.0.
330
+ #
318
331
  # @sig (String) -> bool
319
332
  def unloadable_cpath?(cpath)
320
- to_unload.key?(cpath)
333
+ unloadable_cpaths.include?(cpath)
321
334
  end
322
335
 
323
336
  # Returns an array with the constant paths that would be unloaded on reload.
324
337
  # This predicate returns an empty array if reloading is disabled.
325
338
  #
339
+ # This is an undocumented method that I wrote to help transition from the
340
+ # classic autoloader in Rails. Its usage was removed from Rails in 7.0.
341
+ #
326
342
  # @sig () -> Array[String]
327
343
  def unloadable_cpaths
328
- to_unload.keys.freeze
344
+ to_unload.values.map(&:path)
329
345
  end
330
346
 
331
347
  # This is a dangerous method.
@@ -333,8 +349,9 @@ module Zeitwerk
333
349
  # @experimental
334
350
  # @sig () -> void
335
351
  def unregister
352
+ unregister_inceptions
353
+ unregister_explicit_namespaces
336
354
  Registry.unregister_loader(self)
337
- ExplicitNamespace.__unregister_loader(self)
338
355
  end
339
356
 
340
357
  # The return value of this predicate is only meaningful if the loader has
@@ -474,22 +491,22 @@ module Zeitwerk
474
491
  # If the existing autoload points to a file, it has to be preserved, if
475
492
  # not, it is fine as it is. In either case, we do not need to override.
476
493
  # Just remember the subdirectory conforms this namespace.
477
- namespace_dirs[cref.path] << subdir
494
+ namespace_dirs.get_or_set(cref) { [] } << subdir
478
495
  elsif !cref.defined?
479
496
  # First time we find this namespace, set an autoload for it.
480
- namespace_dirs[cref.path] << subdir
497
+ namespace_dirs.get_or_set(cref) { [] } << subdir
481
498
  define_autoload(cref, subdir)
482
499
  else
483
500
  # For whatever reason the constant that corresponds to this namespace has
484
501
  # already been defined, we have to recurse.
485
- log("the namespace #{cref.path} already exists, descending into #{subdir}") if logger
502
+ log("the namespace #{cref} already exists, descending into #{subdir}") if logger
486
503
  define_autoloads_for_dir(subdir, cref.get)
487
504
  end
488
505
  end
489
506
 
490
507
  # @sig (Module, Symbol, String) -> void
491
508
  private def autoload_file(cref, file)
492
- if autoload_path = cref.autoload? || Registry.inception?(cref.path)
509
+ if autoload_path = cref.autoload? || Registry::Inceptions.registered?(cref)
493
510
  # First autoload for a Ruby file wins, just ignore subsequent ones.
494
511
  if ruby?(autoload_path)
495
512
  shadowed_files << file
@@ -499,7 +516,7 @@ module Zeitwerk
499
516
  end
500
517
  elsif cref.defined?
501
518
  shadowed_files << file
502
- log("file #{file} is ignored because #{cref.path} is already defined") if logger
519
+ log("file #{file} is ignored because #{cref} is already defined") if logger
503
520
  else
504
521
  define_autoload(cref, file)
505
522
  end
@@ -513,7 +530,7 @@ module Zeitwerk
513
530
  autoloads.delete(dir)
514
531
  Registry.unregister_autoload(dir)
515
532
 
516
- log("earlier autoload for #{cref.path} discarded, it is actually an explicit namespace defined in #{file}") if logger
533
+ log("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
517
534
 
518
535
  # Order matters: When Module#const_added is triggered by the autoload, we
519
536
  # don't want the namespace to be registered yet.
@@ -527,19 +544,16 @@ module Zeitwerk
527
544
 
528
545
  if logger
529
546
  if ruby?(abspath)
530
- log("autoload set for #{cref.path}, to be loaded from #{abspath}")
547
+ log("autoload set for #{cref}, to be loaded from #{abspath}")
531
548
  else
532
- log("autoload set for #{cref.path}, to be autovivified from #{abspath}")
549
+ log("autoload set for #{cref}, to be autovivified from #{abspath}")
533
550
  end
534
551
  end
535
552
 
536
553
  autoloads[abspath] = cref
537
554
  Registry.register_autoload(self, abspath)
538
555
 
539
- # See why in the documentation of Zeitwerk::Registry.inceptions.
540
- unless cref.autoload?
541
- Registry.register_inception(cref.path, abspath, self)
542
- end
556
+ register_inception(cref, abspath) unless cref.autoload?
543
557
  end
544
558
 
545
559
  # @sig (Module, Symbol) -> String?
@@ -547,13 +561,32 @@ module Zeitwerk
547
561
  if autoload_path = cref.autoload?
548
562
  autoload_path if autoloads.key?(autoload_path)
549
563
  else
550
- Registry.inception?(cref.path, self)
564
+ inceptions[cref]
551
565
  end
552
566
  end
553
567
 
554
568
  # @sig (Zeitwerk::Cref) -> void
555
569
  private def register_explicit_namespace(cref)
556
- ExplicitNamespace.__register(cref, self)
570
+ Registry::ExplicitNamespaces.__register(cref, self)
571
+ end
572
+
573
+ # @sig () -> void
574
+ private def unregister_explicit_namespaces
575
+ Registry::ExplicitNamespaces.__unregister_loader(self)
576
+ end
577
+
578
+ # @sig (Zeitwerk::Cref, String) -> void
579
+ private def register_inception(cref, abspath)
580
+ inceptions[cref] = abspath
581
+ Registry::Inceptions.register(cref, abspath)
582
+ end
583
+
584
+ # @sig () -> void
585
+ private def unregister_inceptions
586
+ inceptions.each_key do |cref|
587
+ Registry::Inceptions.unregister(cref)
588
+ end
589
+ inceptions.clear
557
590
  end
558
591
 
559
592
  # @sig (String) -> void
@@ -580,17 +613,17 @@ module Zeitwerk
580
613
  end
581
614
  end
582
615
 
583
- # @sig (String, Object, String) -> void
584
- private def run_on_unload_callbacks(cpath, value, abspath)
616
+ # @sig (String, top, String) -> void
617
+ private def run_on_unload_callbacks(cref, value, abspath)
585
618
  # Order matters. If present, run the most specific one.
586
- on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
587
- on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
619
+ on_unload_callbacks[cref.path]&.each { |c| c.call(value, abspath) }
620
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cref.path, value, abspath) }
588
621
  end
589
622
 
590
623
  # @sig (Module, Symbol) -> void
591
624
  private def unload_autoload(cref)
592
625
  cref.remove
593
- log("autoload for #{cref.path} removed") if logger
626
+ log("autoload for #{cref} removed") if logger
594
627
  end
595
628
 
596
629
  # @sig (Module, Symbol) -> void
@@ -602,7 +635,7 @@ module Zeitwerk
602
635
  # There are a few edge scenarios in which this may happen. If the constant
603
636
  # is gone, that is OK, anyway.
604
637
  else
605
- log("#{cref.path} unloaded") if logger
638
+ log("#{cref} unloaded") if logger
606
639
  end
607
640
  end
608
641
  end
@@ -1,4 +1,5 @@
1
1
  class Zeitwerk::NullInflector
2
+ # @sig (String, String) -> String
2
3
  def camelize(basename, _abspath)
3
4
  basename
4
5
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk::Registry
4
+ # This module is a registry for explicit namespaces.
5
+ #
6
+ # When a loader determines that a certain file should define an explicit
7
+ # namespace, it registers it here, associating its cref with itself.
8
+ #
9
+ # If the namespace is autoloaded, our const_added callback retrieves its
10
+ # loader by calling loader_for. That way, the loader is able to scan the
11
+ # subdirectories that conform the namespace and set autoloads for their
12
+ # expected constants just in time.
13
+ #
14
+ # Once autoloaded, the namespace is unregistered.
15
+ #
16
+ # The implementation assumes an explicit namespace is managed by one loader.
17
+ # Loaders that reopen namespaces owned by other projects are responsible for
18
+ # loading their constant before setup. This is documented.
19
+ module ExplicitNamespaces # :nodoc: all
20
+ # Maps crefs of explicit namespaces with their corresponding loader.
21
+ #
22
+ # Entries are added as the namespaces are found, and removed as they are
23
+ # autoloaded.
24
+ #
25
+ # @sig Zeitwerk::Cref::Map[Zeitwerk::Loader]
26
+ @loaders = Zeitwerk::Cref::Map.new
27
+
28
+ class << self
29
+ extend Zeitwerk::Internal
30
+
31
+ # Registers `cref` as being the constant path of an explicit namespace
32
+ # managed by `loader`.
33
+ #
34
+ # @sig (Zeitwerk::Cref, Zeitwerk::Loader) -> void
35
+ internal def register(cref, loader)
36
+ @loaders[cref] = loader
37
+ end
38
+
39
+ # @sig (Module, Symbol) -> Zeitwerk::Loader?
40
+ internal def loader_for(mod, cname)
41
+ @loaders.delete_mod_cname(mod, cname)
42
+ end
43
+
44
+ # @sig (Zeitwerk::Loader) -> void
45
+ internal def unregister_loader(loader)
46
+ @loaders.delete_by_value(loader)
47
+ end
48
+
49
+ # This is an internal method only used by the test suite.
50
+ #
51
+ # @sig (Symbol | String) -> Zeitwerk::Loader?
52
+ internal def registered?(cref)
53
+ @loaders[cref]
54
+ end
55
+
56
+ # This is an internal method only used by the test suite.
57
+ #
58
+ # @sig () -> void
59
+ internal def clear
60
+ @loaders.clear
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,31 @@
1
+ module Zeitwerk::Registry
2
+ # Loaders know their own inceptions, but there is a use case in which we need
3
+ # to know if a given cpath is an inception globally. This is what this
4
+ # registry is for.
5
+ module Inceptions # :nodoc: all
6
+ # @sig Zeitwerk::Cref::Map[String]
7
+ @inceptions = Zeitwerk::Cref::Map.new
8
+
9
+ class << self
10
+ # @sig (Zeitwerk::Cref, String) -> void
11
+ def register(cref, autoload_path)
12
+ @inceptions[cref] = autoload_path
13
+ end
14
+
15
+ # @sig (String) -> String?
16
+ def registered?(cref)
17
+ @inceptions[cref]
18
+ end
19
+
20
+ # @sig (String) -> void
21
+ def unregister(cref)
22
+ @inceptions.delete(cref)
23
+ end
24
+
25
+ # @sig () -> void
26
+ def clear # for tests
27
+ @inceptions.clear
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Zeitwerk
4
4
  module Registry # :nodoc: all
5
+ require_relative "registry/explicit_namespaces"
6
+ require_relative "registry/inceptions"
7
+
5
8
  class << self
6
9
  # Keeps track of all loaders. Useful to broadcast messages and to prevent
7
10
  # them from being garbage collected.
@@ -25,45 +28,6 @@ module Zeitwerk
25
28
  # @sig Hash[String, Zeitwerk::Loader]
26
29
  attr_reader :autoloads
27
30
 
28
- # This hash table addresses an edge case in which an autoload is ignored.
29
- #
30
- # For example, let's suppose we want to autoload in a gem like this:
31
- #
32
- # # lib/my_gem.rb
33
- # loader = Zeitwerk::Loader.new
34
- # loader.push_dir(__dir__)
35
- # loader.setup
36
- #
37
- # module MyGem
38
- # end
39
- #
40
- # if you require "my_gem", as Bundler would do, this happens while setting
41
- # up autoloads:
42
- #
43
- # 1. Object.autoload?(:MyGem) returns `nil` because the autoload for
44
- # the constant is issued by Zeitwerk while the same file is being
45
- # required.
46
- # 2. The constant `MyGem` is undefined while setup runs.
47
- #
48
- # Therefore, a directory `lib/my_gem` would autovivify a module according to
49
- # the existing information. But that would be wrong.
50
- #
51
- # To overcome this fundamental limitation, we keep track of the constant
52
- # paths that are in this situation ---in the example above, "MyGem"--- and
53
- # take this collection into account for the autovivification logic.
54
- #
55
- # Note that you cannot generally address this by moving the setup code
56
- # below the constant definition, because we want libraries to be able to
57
- # use managed constants in the module body:
58
- #
59
- # module MyGem
60
- # include MyConcern
61
- # end
62
- #
63
- # @private
64
- # @sig Hash[String, [String, Zeitwerk::Loader]]
65
- attr_reader :inceptions
66
-
67
31
  # Registers a loader.
68
32
  #
69
33
  # @private
@@ -78,7 +42,6 @@ module Zeitwerk
78
42
  loaders.delete(loader)
79
43
  gem_loaders_by_root_file.delete_if { |_, l| l == loader }
80
44
  autoloads.delete_if { |_, l| l == loader }
81
- inceptions.delete_if { |_, (_, l)| l == loader }
82
45
  end
83
46
 
84
47
  # This method returns always a loader, the same instance for the same root
@@ -102,23 +65,6 @@ module Zeitwerk
102
65
  autoloads.delete(abspath)
103
66
  end
104
67
 
105
- # @private
106
- # @sig (String, String, Zeitwerk::Loader) -> void
107
- def register_inception(cpath, abspath, loader)
108
- inceptions[cpath] = [abspath, loader]
109
- end
110
-
111
- # @private
112
- # @sig (String) -> String?
113
- def inception?(cpath, registered_by_loader=nil)
114
- if pair = inceptions[cpath]
115
- abspath, loader = pair
116
- if registered_by_loader.nil? || registered_by_loader.equal?(loader)
117
- abspath
118
- end
119
- end
120
- end
121
-
122
68
  # @private
123
69
  # @sig (String) -> Zeitwerk::Loader?
124
70
  def loader_for(path)
@@ -129,13 +75,11 @@ module Zeitwerk
129
75
  # @sig (Zeitwerk::Loader) -> void
130
76
  def on_unload(loader)
131
77
  autoloads.delete_if { |_path, object| object == loader }
132
- inceptions.delete_if { |_cpath, (_path, object)| object == loader }
133
78
  end
134
79
  end
135
80
 
136
81
  @loaders = []
137
82
  @gem_loaders_by_root_file = {}
138
83
  @autoloads = {}
139
- @inceptions = {}
140
84
  end
141
85
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.7.1"
4
+ VERSION = "2.7.2"
5
5
  end
data/lib/zeitwerk.rb CHANGED
@@ -7,7 +7,6 @@ module Zeitwerk
7
7
  require_relative "zeitwerk/loader"
8
8
  require_relative "zeitwerk/gem_loader"
9
9
  require_relative "zeitwerk/registry"
10
- require_relative "zeitwerk/explicit_namespace"
11
10
  require_relative "zeitwerk/inflector"
12
11
  require_relative "zeitwerk/gem_inflector"
13
12
  require_relative "zeitwerk/null_inflector"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.1
4
+ version: 2.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-10-18 00:00:00.000000000 Z
10
+ date: 2025-02-18 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: |2
14
13
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -26,8 +25,8 @@ files:
26
25
  - lib/zeitwerk/core_ext/kernel.rb
27
26
  - lib/zeitwerk/core_ext/module.rb
28
27
  - lib/zeitwerk/cref.rb
28
+ - lib/zeitwerk/cref/map.rb
29
29
  - lib/zeitwerk/error.rb
30
- - lib/zeitwerk/explicit_namespace.rb
31
30
  - lib/zeitwerk/gem_inflector.rb
32
31
  - lib/zeitwerk/gem_loader.rb
33
32
  - lib/zeitwerk/inflector.rb
@@ -40,6 +39,8 @@ files:
40
39
  - lib/zeitwerk/null_inflector.rb
41
40
  - lib/zeitwerk/real_mod_name.rb
42
41
  - lib/zeitwerk/registry.rb
42
+ - lib/zeitwerk/registry/explicit_namespaces.rb
43
+ - lib/zeitwerk/registry/inceptions.rb
43
44
  - lib/zeitwerk/version.rb
44
45
  homepage: https://github.com/fxn/zeitwerk
45
46
  licenses:
@@ -49,7 +50,6 @@ metadata:
49
50
  changelog_uri: https://github.com/fxn/zeitwerk/blob/master/CHANGELOG.md
50
51
  source_code_uri: https://github.com/fxn/zeitwerk
51
52
  bug_tracker_uri: https://github.com/fxn/zeitwerk/issues
52
- post_install_message:
53
53
  rdoc_options: []
54
54
  require_paths:
55
55
  - lib
@@ -64,8 +64,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
64
  - !ruby/object:Gem::Version
65
65
  version: '0'
66
66
  requirements: []
67
- rubygems_version: 3.5.21
68
- signing_key:
67
+ rubygems_version: 3.6.4
69
68
  specification_version: 4
70
69
  summary: Efficient and thread-safe constant autoloader
71
70
  test_files: []
@@ -1,113 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zeitwerk
4
- # This module is essentially a registry for explicit namespaces.
5
- #
6
- # When a loader determines that a certain file should define an explicit
7
- # namespace, it registers it here, associating its cref with itself.
8
- #
9
- # If the namespace is autoloaded, our const_added callback retrieves its
10
- # loader by calling loader_for. That way, the loader is able to scan the
11
- # subdirectories that conform the namespace and set autoloads for their
12
- # expected constants just in time.
13
- #
14
- # Once autoloaded, the namespace is unregistered.
15
- #
16
- # The implementation assumes an explicit namespace is managed by one loader.
17
- # Loaders that reopen namespaces owned by other projects are responsible for
18
- # loading their constant before setup. This is documented.
19
- module ExplicitNamespace # :nodoc: all
20
- # Maps cnames or cpaths of explicit namespaces with their corresponding
21
- # loader. They are symbols for top-level ones, and strings for nested ones:
22
- #
23
- # {
24
- # :Admin => #<Zeitwerk::Loader:...>,
25
- # "Hotel::Pricing" => #<Zeitwerk::Loader:...>
26
- # }
27
- #
28
- # There are two types of keys to make loader_for as fast as possible, since
29
- # it is invoked by our const_added for all autoloads and constant actually
30
- # added. Globally. With this trick, for top-level constants we do not need
31
- # to call Symbol#name and perform a string lookup. Instead, we can directly
32
- # perform a fast symbol lookup.
33
- #
34
- # Entries are added as the namespaces are found, and removed as they are
35
- # autoloaded.
36
- #
37
- # @sig Hash[(Symbol | String) => Zeitwerk::Loader]
38
- @loaders = {}
39
-
40
- class << self
41
- include RealModName
42
- extend Internal
43
-
44
- # Registers `cref` as being the constant path of an explicit namespace
45
- # managed by `loader`.
46
- #
47
- # @sig (String, Zeitwerk::Loader) -> void
48
- internal def register(cref, loader)
49
- if Object.equal?(cref.mod)
50
- @loaders[cref.cname] = loader
51
- else
52
- @loaders[cref.path] = loader
53
- end
54
- end
55
-
56
- # @sig (Module, Symbol) -> Zeitwerk::Loader?
57
- internal def loader_for(mod, cname)
58
- if Object.equal?(mod)
59
- @loaders.delete(cname)
60
- else
61
- @loaders.delete("#{real_mod_name(mod)}::#{cname}")
62
- end
63
- end
64
-
65
- # @sig (Zeitwerk::Loader) -> void
66
- internal def unregister_loader(loader)
67
- @loaders.delete_if { _2.equal?(loader) }
68
- end
69
-
70
- # This is an internal method only used by the test suite.
71
- #
72
- # @sig (String) -> Zeitwerk::Loader?
73
- internal def registered?(cname_or_cpath)
74
- @loaders[cname_or_cpath]
75
- end
76
-
77
- # This is an internal method only used by the test suite.
78
- #
79
- # @sig () -> void
80
- internal def clear
81
- @loaders.clear
82
- end
83
-
84
- module Synchronized
85
- extend Internal
86
-
87
- MUTEX = Mutex.new
88
-
89
- internal def register(...)
90
- MUTEX.synchronize { super }
91
- end
92
-
93
- internal def loader_for(...)
94
- MUTEX.synchronize { super }
95
- end
96
-
97
- internal def unregister_loader(...)
98
- MUTEX.synchronize { super }
99
- end
100
-
101
- internal def registered?(...)
102
- MUTEX.synchronize { super }
103
- end
104
-
105
- internal def clear
106
- MUTEX.synchronize { super }
107
- end
108
- end
109
-
110
- prepend Synchronized unless RUBY_ENGINE == "ruby"
111
- end
112
- end
113
- end