zeitwerk 2.7.1 → 2.7.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aba46812169c8e26085b099c708093b00a29bfd39e8d8210d29bc99e7df0e7fa
4
- data.tar.gz: 0fc20386009d52d21a0cb79a64c5d800f7aecfce5a38e8430a10dc5d04c2609d
3
+ metadata.gz: 1500c4f54c6ac7e64eef74c8702682764fa845533a5b16c67a48ee11781ef3e0
4
+ data.tar.gz: f7201576b8b59ab3786cd4bc6fd58f3cd3afa3d8dcc1e86477121000f06ac50a
5
5
  SHA512:
6
- metadata.gz: 3edac5ad6f940caa70c4cac093bf271b8399b078d7124f450513c963ec5099e9b9082841ceb9893b81f1edf8e34dc642ec811403415fb230211670c63a950766
7
- data.tar.gz: 47339a8b35dc06108fde9aadd62e83b1ea4e4ef22854c31849069f09ece1115dce07d63ec354fe96fbc599bba411ecff48db6a8b0c8b150ad7ee016787e9d75c
6
+ metadata.gz: ab113d90c9a42ec5c75c6ebe68ab5a04395f1b33228de42de6952f2e673a329f30de85477977ec11d582df82f40c4dc5c08e8ba2305f46a6d07ac4e88c564887
7
+ data.tar.gz: 73fb9dc78a9a7148119b306f93a513f841e4888d421fbf178963bbfa1368a8c7c66cd8661a28e8d95d14f51dab16fb4ff886f23183ec13a74847c45c1850f30d
@@ -19,9 +19,9 @@ module Kernel
19
19
  alias_method :zeitwerk_original_require, :require
20
20
  end
21
21
 
22
- # @sig (String) -> true | false
22
+ #: (String) -> bool
23
23
  def require(path)
24
- if loader = Zeitwerk::Registry.loader_for(path)
24
+ if loader = Zeitwerk::Registry.autoloads.registered?(path)
25
25
  if path.end_with?(".rb")
26
26
  required = zeitwerk_original_require(path)
27
27
  loader.__on_file_autoloaded(path) if required
@@ -34,7 +34,7 @@ module Kernel
34
34
  required = zeitwerk_original_require(path)
35
35
  if required
36
36
  abspath = $LOADED_FEATURES.last
37
- if loader = Zeitwerk::Registry.loader_for(abspath)
37
+ if loader = Zeitwerk::Registry.autoloads.registered?(abspath)
38
38
  loader.__on_file_autoloaded(abspath)
39
39
  end
40
40
  end
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Zeitwerk::ConstAdded
3
+ module Zeitwerk::ConstAdded # :nodoc:
4
+ #: (Symbol) -> void
4
5
  def const_added(cname)
5
- if loader = Zeitwerk::ExplicitNamespace.__loader_for(self, cname)
6
+ if loader = Zeitwerk::Registry.explicit_namespaces.loader_for(self, cname)
6
7
  namespace = const_get(cname, false)
8
+ cref = Zeitwerk::Cref.new(self, cname)
7
9
 
8
10
  unless namespace.is_a?(Module)
9
- 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(cref, namespace)
14
15
  end
15
16
  super
16
17
  end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Description of the structure
4
+ # ----------------------------
5
+ #
6
+ # This class emulates a hash table whose keys are of type Zeitwerk::Cref.
7
+ #
8
+ # It is a synchronized 2-level hash.
9
+ #
10
+ # The keys of the top one, stored in `@map`, are class and module objects, but
11
+ # their hash code is forced to be their object IDs because class and module
12
+ # objects may not be hashable (https://github.com/fxn/zeitwerk/issues/188).
13
+ #
14
+ # Then, each one of them stores a hash table with their constants and values.
15
+ # Constants are stored as symbols.
16
+ #
17
+ # For example, if we store values 0, 1, and 2 for the crefs that would
18
+ # correspond to `M::X`, `M::Y`, and `N::Z`, the map will look like this:
19
+ #
20
+ # { M => { X: 0, :Y => 1 }, N => { Z: 2 } }
21
+ #
22
+ # This structure is internal, so only the needed interface is implemented.
23
+ #
24
+ # Alternative approaches
25
+ # -----------------------
26
+ #
27
+ # 1. We could also use a 1-level hash whose keys are constant paths. In the
28
+ # example above it would be:
29
+ #
30
+ # { "M::X" => 0, "M::Y" => 1, "N::Z" => 2 }
31
+ #
32
+ # The gem used this approach for several years.
33
+ #
34
+ # 2. Write a custom `hash`/`eql?` in Zeitwerk::Cref. Hash code would be
35
+ #
36
+ # real_mod_hash(@mod) ^ @cname.hash
37
+ #
38
+ # where `real_mod_hash(@mod)` would actually be a call to the real `hash`
39
+ # method in Module. Like what we do for module names to bypass overrides.
40
+ #
41
+ # 3. Similar to 2, but use
42
+ #
43
+ # @mod.object_id ^ @cname.object_id
44
+ #
45
+ # as hash code instead.
46
+ #
47
+ # Benchamrks
48
+ # ----------
49
+ #
50
+ # Writing:
51
+ #
52
+ # map - baseline
53
+ # (3) - 1.74x slower
54
+ # (2) - 2.91x slower
55
+ # (1) - 3.87x slower
56
+ #
57
+ # Reading:
58
+ #
59
+ # map - baseline
60
+ # (3) - 1.99x slower
61
+ # (2) - 2.80x slower
62
+ # (1) - 3.48x slower
63
+ #
64
+ # Extra ball
65
+ # ----------
66
+ #
67
+ # In addition to that, the map is synchronized and provides `delete_mod_cname`,
68
+ # which is ad-hoc for the hot path in `const_added`, we do not need to create
69
+ # unnecessary cref objects for constants we do not manage (but we do not know in
70
+ # advance there).
71
+
72
+ #: [Value]
73
+ class Zeitwerk::Cref::Map # :nodoc: all
74
+ #: () -> void
75
+ def initialize
76
+ @map = {}
77
+ @map.compare_by_identity
78
+ @mutex = Mutex.new
79
+ end
80
+
81
+ #: (Zeitwerk::Cref, Value) -> Value
82
+ def []=(cref, value)
83
+ @mutex.synchronize do
84
+ cnames = (@map[cref.mod] ||= {})
85
+ cnames[cref.cname] = value
86
+ end
87
+ end
88
+
89
+ #: (Zeitwerk::Cref) -> Value?
90
+ def [](cref)
91
+ @mutex.synchronize do
92
+ @map[cref.mod]&.[](cref.cname)
93
+ end
94
+ end
95
+
96
+ #: (Zeitwerk::Cref, { () -> Value }) -> Value
97
+ def get_or_set(cref, &block)
98
+ @mutex.synchronize do
99
+ cnames = (@map[cref.mod] ||= {})
100
+ cnames.fetch(cref.cname) { cnames[cref.cname] = block.call }
101
+ end
102
+ end
103
+
104
+ #: (Zeitwerk::Cref) -> Value?
105
+ def delete(cref)
106
+ delete_mod_cname(cref.mod, cref.cname)
107
+ end
108
+
109
+ # Ad-hoc for loader_for, called from const_added. That is a hot path, I prefer
110
+ # to not create a cref in every call, since that is global.
111
+ #
112
+ #: (Module, Symbol) -> Value?
113
+ def delete_mod_cname(mod, cname)
114
+ @mutex.synchronize do
115
+ if cnames = @map[mod]
116
+ value = cnames.delete(cname)
117
+ @map.delete(mod) if cnames.empty?
118
+ value
119
+ end
120
+ end
121
+ end
122
+
123
+ #: (Value) -> void
124
+ def delete_by_value(value)
125
+ @mutex.synchronize do
126
+ @map.delete_if do |mod, cnames|
127
+ cnames.delete_if { _2 == value }
128
+ cnames.empty?
129
+ end
130
+ end
131
+ end
132
+
133
+ # Order of yielded crefs is undefined.
134
+ #
135
+ #: () { (Zeitwerk::Cref) -> void } -> void
136
+ def each_key
137
+ @mutex.synchronize do
138
+ @map.each do |mod, cnames|
139
+ cnames.each_key do |cname|
140
+ yield Zeitwerk::Cref.new(mod, cname)
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ #: () -> void
147
+ def clear
148
+ @mutex.synchronize do
149
+ @map.clear
150
+ end
151
+ end
152
+
153
+ #: () -> bool
154
+ def empty? # for tests
155
+ @mutex.synchronize do
156
+ @map.empty?
157
+ end
158
+ end
159
+ end
data/lib/zeitwerk/cref.rb CHANGED
@@ -2,66 +2,67 @@
2
2
 
3
3
  # This private class encapsulates pairs (mod, cname).
4
4
  #
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:
5
+ # Objects represent the constant `cname` in the class or module object `mod`,
6
+ # and have API to manage them. Examples:
7
7
  #
8
8
  # cref.path
9
9
  # cref.set(value)
10
10
  # cref.get
11
11
  #
12
- # The constant may or may not exist in mod.
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
- # @sig Module
18
+ #: Module
17
19
  attr_reader :mod
18
20
 
19
- # @sig Symbol
21
+ #: Symbol
20
22
  attr_reader :cname
21
23
 
22
24
  # The type of the first argument is Module because Class < Module, class
23
25
  # objects are also valid.
24
26
  #
25
- # @sig (Module, Symbol) -> void
27
+ #: (Module, Symbol) -> void
26
28
  def initialize(mod, cname)
27
29
  @mod = mod
28
30
  @cname = cname
29
31
  @path = nil
30
32
  end
31
33
 
32
- # @sig () -> String
34
+ #: () -> String
33
35
  def path
34
- @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}".freeze
36
+ @path ||= Object == @mod ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}".freeze
35
37
  end
38
+ alias to_s path
36
39
 
37
- # @sig () -> String?
40
+ #: () -> String?
38
41
  def autoload?
39
42
  @mod.autoload?(@cname, false)
40
43
  end
41
44
 
42
- # @sig (String) -> bool
45
+ #: (String) -> nil
43
46
  def autoload(abspath)
44
47
  @mod.autoload(@cname, abspath)
45
48
  end
46
49
 
47
- # @sig () -> bool
50
+ #: () -> bool
48
51
  def defined?
49
52
  @mod.const_defined?(@cname, false)
50
53
  end
51
54
 
52
- # @sig (Object) -> Object
55
+ #: (top) -> top
53
56
  def set(value)
54
57
  @mod.const_set(@cname, value)
55
58
  end
56
59
 
57
- # @raise [NameError]
58
- # @sig () -> Object
60
+ #: () -> top ! NameError
59
61
  def get
60
62
  @mod.const_get(@cname, false)
61
63
  end
62
64
 
63
- # @raise [NameError]
64
- # @sig () -> void
65
+ #: () -> void ! NameError
65
66
  def remove
66
67
  @mod.__send__(:remove_const, @cname)
67
68
  end
@@ -5,6 +5,7 @@ module Zeitwerk
5
5
  end
6
6
 
7
7
  class ReloadingDisabledError < Error
8
+ #: () -> void
8
9
  def initialize
9
10
  super("can't reload, please call loader.enable_reloading before setup")
10
11
  end
@@ -14,6 +15,7 @@ module Zeitwerk
14
15
  end
15
16
 
16
17
  class SetupRequired < Error
18
+ #: () -> void
17
19
  def initialize
18
20
  super("please, finish your configuration and call Zeitwerk::Loader#setup once all is ready")
19
21
  end
@@ -2,14 +2,14 @@
2
2
 
3
3
  module Zeitwerk
4
4
  class GemInflector < Inflector
5
- # @sig (String) -> void
5
+ #: (String) -> void
6
6
  def initialize(root_file)
7
7
  namespace = File.basename(root_file, ".rb")
8
8
  root_dir = File.dirname(root_file)
9
9
  @version_file = File.join(root_dir, namespace, "version.rb")
10
10
  end
11
11
 
12
- # @sig (String, String) -> String
12
+ #: (String, String) -> String
13
13
  def camelize(basename, abspath)
14
14
  abspath == @version_file ? "VERSION" : super
15
15
  end
@@ -10,12 +10,12 @@ module Zeitwerk
10
10
  private_class_method :new
11
11
 
12
12
  # @private
13
- # @sig (String, bool) -> Zeitwerk::GemLoader
13
+ #: (String, namespace: Module, warn_on_extra_files: boolish) -> Zeitwerk::GemLoader
14
14
  def self.__new(root_file, namespace:, warn_on_extra_files:)
15
15
  new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
16
16
  end
17
17
 
18
- # @sig (String, bool) -> void
18
+ #: (String, namespace: Module, warn_on_extra_files: boolish) -> void
19
19
  def initialize(root_file, namespace:, warn_on_extra_files:)
20
20
  super()
21
21
 
@@ -30,7 +30,7 @@ module Zeitwerk
30
30
  push_dir(@root_dir, namespace: namespace)
31
31
  end
32
32
 
33
- # @sig () -> void
33
+ #: () -> void
34
34
  def setup
35
35
  warn_on_extra_files if @warn_on_extra_files
36
36
  super
@@ -38,7 +38,7 @@ module Zeitwerk
38
38
 
39
39
  private
40
40
 
41
- # @sig () -> void
41
+ #: () -> void
42
42
  def warn_on_extra_files
43
43
  expected_namespace_dir = @root_file.delete_suffix(".rb")
44
44
 
@@ -11,7 +11,7 @@ module Zeitwerk
11
11
  #
12
12
  # Takes into account hard-coded mappings configured with `inflect`.
13
13
  #
14
- # @sig (String, String) -> String
14
+ #: (String, String) -> String
15
15
  def camelize(basename, _abspath)
16
16
  overrides[basename] || basename.split('_').each(&:capitalize!).join
17
17
  end
@@ -28,7 +28,7 @@ module Zeitwerk
28
28
  # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
29
29
  # inflector.camelize("users_controller", abspath) # => "UsersController"
30
30
  #
31
- # @sig (Hash[String, String]) -> void
31
+ #: (Hash[String, String]) -> void
32
32
  def inflect(inflections)
33
33
  overrides.merge!(inflections)
34
34
  end
@@ -38,7 +38,7 @@ module Zeitwerk
38
38
  # Hard-coded basename to constant name user maps that override the default
39
39
  # inflection logic.
40
40
  #
41
- # @sig () -> Hash[String, String]
41
+ #: () -> Hash[String, String]
42
42
  def overrides
43
43
  @overrides ||= {}
44
44
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # This is a private module.
4
4
  module Zeitwerk::Internal
5
+ #: (Symbol) -> void
5
6
  def internal(method_name)
6
7
  private method_name
7
8
 
@@ -1,24 +1,22 @@
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.
8
7
  #
9
- # @raise [Zeitwerk::NameError]
10
- # @sig (String) -> void
8
+ #: (String) -> void ! Zeitwerk::NameError
11
9
  internal def on_file_autoloaded(file)
12
10
  cref = autoloads.delete(file)
13
11
 
14
- Zeitwerk::Registry.unregister_autoload(file)
12
+ Zeitwerk::Registry.autoloads.unregister(file)
15
13
 
16
14
  if cref.defined?
17
- log("constant #{cref.path} loaded from file #{file}") if logger
18
- to_unload[cref.path] = [file, cref] if reloading_enabled?
15
+ log("constant #{cref} loaded from file #{file}") if logger
16
+ to_unload[file] = cref if reloading_enabled?
19
17
  run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
20
18
  else
21
- msg = "expected file #{file} to define constant #{cref.path}, but didn't"
19
+ msg = "expected file #{file} to define constant #{cref}, but didn't"
22
20
  log(msg) if logger
23
21
 
24
22
  # Ruby still keeps the autoload defined, but we remove it because the
@@ -28,7 +26,7 @@ module Zeitwerk::Loader::Callbacks
28
26
  # Since the expected constant was not defined, there is nothing to unload.
29
27
  # However, if the exception is rescued and reloading is enabled, we still
30
28
  # need to deleted the file from $LOADED_FEATURES.
31
- to_unload[cref.path] = [file, cref] if reloading_enabled?
29
+ to_unload[file] = cref if reloading_enabled?
32
30
 
33
31
  raise Zeitwerk::NameError.new(msg, cref.cname)
34
32
  end
@@ -37,7 +35,7 @@ module Zeitwerk::Loader::Callbacks
37
35
  # Invoked from our decorated Kernel#require when a managed directory is
38
36
  # autoloaded.
39
37
  #
40
- # @sig (String) -> void
38
+ #: (String) -> void
41
39
  internal def on_dir_autoloaded(dir)
42
40
  # Module#autoload does not serialize concurrent requires in CRuby < 3.2, and
43
41
  # we handle directories ourselves without going through Kernel#require, so
@@ -54,10 +52,9 @@ module Zeitwerk::Loader::Callbacks
54
52
  dirs_autoload_monitor.synchronize do
55
53
  if cref = autoloads.delete(dir)
56
54
  implicit_namespace = cref.set(Module.new)
57
- cpath = implicit_namespace.name
58
- log("module #{cpath} autovivified from directory #{dir}") if logger
55
+ log("module #{cref} autovivified from directory #{dir}") if logger
59
56
 
60
- to_unload[cpath] = [dir, cref] if reloading_enabled?
57
+ to_unload[dir] = cref if reloading_enabled?
61
58
 
62
59
  # We don't unregister `dir` in the registry because concurrent threads
63
60
  # wouldn't find a loader associated to it in Kernel#require and would
@@ -65,21 +62,20 @@ module Zeitwerk::Loader::Callbacks
65
62
  # these to be able to unregister later if eager loading.
66
63
  autoloaded_dirs << dir
67
64
 
68
- on_namespace_loaded(implicit_namespace)
65
+ on_namespace_loaded(cref, implicit_namespace)
69
66
 
70
- run_on_load_callbacks(cpath, implicit_namespace, dir) unless on_load_callbacks.empty?
67
+ run_on_load_callbacks(cref.path, implicit_namespace, dir) unless on_load_callbacks.empty?
71
68
  end
72
69
  end
73
70
  end
74
71
 
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.
72
+ # Invoked when a namespace is created, either from const_added or from module
73
+ # autovivification. If the namespace has matching subdirectories, we descend
74
+ # into them now.
78
75
  #
79
- # @private
80
- # @sig (Module) -> void
81
- def on_namespace_loaded(namespace)
82
- if dirs = namespace_dirs.delete(real_mod_name(namespace))
76
+ #: (Zeitwerk::Cref, Module) -> void
77
+ internal def on_namespace_loaded(cref, namespace)
78
+ if dirs = namespace_dirs.delete(cref)
83
79
  dirs.each do |dir|
84
80
  define_autoloads_for_dir(dir, namespace)
85
81
  end
@@ -88,7 +84,7 @@ module Zeitwerk::Loader::Callbacks
88
84
 
89
85
  private
90
86
 
91
- # @sig (String, Object) -> void
87
+ #: (String, top, String) -> void
92
88
  def run_on_load_callbacks(cpath, value, abspath)
93
89
  # Order matters. If present, run the most specific one.
94
90
  callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)