zeitwerk 2.7.2 → 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: 1a07f90eb2f155582d05f58527ffcbc2f4d76c9a1983260ca8d527becaeb7972
4
- data.tar.gz: 65e8dc78ca8e6de674f0fc7d88aad5c9bad0d7687bc9ed26f93d6fa0e6d18e90
3
+ metadata.gz: 1500c4f54c6ac7e64eef74c8702682764fa845533a5b16c67a48ee11781ef3e0
4
+ data.tar.gz: f7201576b8b59ab3786cd4bc6fd58f3cd3afa3d8dcc1e86477121000f06ac50a
5
5
  SHA512:
6
- metadata.gz: d7b9d13e3d3d5bf0497ec259bf0817256586e245f7d47c951b8392784f715bc71c20d0ec3c9e465077da6d8e729ca6888fbaaa24820fe4459771e29340ee6d05
7
- data.tar.gz: 8b1322d36bc9115a56b6abab6be9549c868e0edd2025fe82dd2c5d0abb082fac8532c82ed03f895d34f2875f27b160f4861185112c4aef2a3129e46569115c0f
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,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk::ConstAdded # :nodoc:
4
- # @sig (Symbol) -> void
4
+ #: (Symbol) -> void
5
5
  def const_added(cname)
6
- if loader = Zeitwerk::Registry::ExplicitNamespaces.__loader_for(self, cname)
6
+ if loader = Zeitwerk::Registry.explicit_namespaces.loader_for(self, cname)
7
7
  namespace = const_get(cname, false)
8
+ cref = Zeitwerk::Cref.new(self, cname)
8
9
 
9
10
  unless namespace.is_a?(Module)
10
- cref = Zeitwerk::Cref.new(self, cname)
11
11
  raise Zeitwerk::Error, "#{cref} is expected to be a namespace, should be a class or module (got #{namespace.class})"
12
12
  end
13
13
 
14
- loader.__on_namespace_loaded(Zeitwerk::Cref.new(self, cname), namespace)
14
+ loader.__on_namespace_loaded(cref, namespace)
15
15
  end
16
16
  super
17
17
  end
@@ -1,11 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Description of the structure
4
+ # ----------------------------
5
+ #
3
6
  # This class emulates a hash table whose keys are of type Zeitwerk::Cref.
4
7
  #
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.
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.
9
16
  #
10
17
  # For example, if we store values 0, 1, and 2 for the crefs that would
11
18
  # correspond to `M::X`, `M::Y`, and `N::Z`, the map will look like this:
@@ -14,36 +21,64 @@
14
21
  #
15
22
  # This structure is internal, so only the needed interface is implemented.
16
23
  #
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:
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.
20
33
  #
21
- # https://github.com/fxn/zeitwerk/issues/188
34
+ # 2. Write a custom `hash`/`eql?` in Zeitwerk::Cref. Hash code would be
22
35
  #
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:
36
+ # real_mod_hash(@mod) ^ @cname.hash
25
37
  #
26
- # { "M::X" => 0, "M::Y" => 1, "N::Z" => 2 }
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.
27
40
  #
28
- # The gem used this approach for several years.
41
+ # 3. Similar to 2, but use
29
42
  #
30
- # Another option would be to make crefs hashable. I tried with hash code
43
+ # @mod.object_id ^ @cname.object_id
31
44
  #
32
- # real_mod_hash(mod) ^ cname.hash
45
+ # as hash code instead.
33
46
  #
34
- # and the matching eql?, but that was about 1.8x slower.
47
+ # Benchamrks
48
+ # ----------
35
49
  #
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.
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]
39
73
  class Zeitwerk::Cref::Map # :nodoc: all
74
+ #: () -> void
40
75
  def initialize
41
76
  @map = {}
42
77
  @map.compare_by_identity
43
78
  @mutex = Mutex.new
44
79
  end
45
80
 
46
- # @sig (Zeitwerk::Cref, V) -> V
81
+ #: (Zeitwerk::Cref, Value) -> Value
47
82
  def []=(cref, value)
48
83
  @mutex.synchronize do
49
84
  cnames = (@map[cref.mod] ||= {})
@@ -51,14 +86,14 @@ class Zeitwerk::Cref::Map # :nodoc: all
51
86
  end
52
87
  end
53
88
 
54
- # @sig (Zeitwerk::Cref) -> top?
89
+ #: (Zeitwerk::Cref) -> Value?
55
90
  def [](cref)
56
91
  @mutex.synchronize do
57
92
  @map[cref.mod]&.[](cref.cname)
58
93
  end
59
94
  end
60
95
 
61
- # @sig (Zeitwerk::Cref, { () -> V }) -> V
96
+ #: (Zeitwerk::Cref, { () -> Value }) -> Value
62
97
  def get_or_set(cref, &block)
63
98
  @mutex.synchronize do
64
99
  cnames = (@map[cref.mod] ||= {})
@@ -66,7 +101,7 @@ class Zeitwerk::Cref::Map # :nodoc: all
66
101
  end
67
102
  end
68
103
 
69
- # @sig (Zeitwerk::Cref) -> top?
104
+ #: (Zeitwerk::Cref) -> Value?
70
105
  def delete(cref)
71
106
  delete_mod_cname(cref.mod, cref.cname)
72
107
  end
@@ -74,7 +109,7 @@ class Zeitwerk::Cref::Map # :nodoc: all
74
109
  # Ad-hoc for loader_for, called from const_added. That is a hot path, I prefer
75
110
  # to not create a cref in every call, since that is global.
76
111
  #
77
- # @sig (Module, Symbol) -> top?
112
+ #: (Module, Symbol) -> Value?
78
113
  def delete_mod_cname(mod, cname)
79
114
  @mutex.synchronize do
80
115
  if cnames = @map[mod]
@@ -85,7 +120,7 @@ class Zeitwerk::Cref::Map # :nodoc: all
85
120
  end
86
121
  end
87
122
 
88
- # @sig (top) -> void
123
+ #: (Value) -> void
89
124
  def delete_by_value(value)
90
125
  @mutex.synchronize do
91
126
  @map.delete_if do |mod, cnames|
@@ -97,7 +132,7 @@ class Zeitwerk::Cref::Map # :nodoc: all
97
132
 
98
133
  # Order of yielded crefs is undefined.
99
134
  #
100
- # @sig () { (Zeitwerk::Cref) -> void } -> void
135
+ #: () { (Zeitwerk::Cref) -> void } -> void
101
136
  def each_key
102
137
  @mutex.synchronize do
103
138
  @map.each do |mod, cnames|
@@ -108,14 +143,14 @@ class Zeitwerk::Cref::Map # :nodoc: all
108
143
  end
109
144
  end
110
145
 
111
- # @sig () -> void
146
+ #: () -> void
112
147
  def clear
113
148
  @mutex.synchronize do
114
149
  @map.clear
115
150
  end
116
151
  end
117
152
 
118
- # @sig () -> bool
153
+ #: () -> bool
119
154
  def empty? # for tests
120
155
  @mutex.synchronize do
121
156
  @map.empty?
data/lib/zeitwerk/cref.rb CHANGED
@@ -2,69 +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. 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
14
  require_relative "cref/map"
15
15
 
16
16
  include Zeitwerk::RealModName
17
17
 
18
- # @sig Module
18
+ #: Module
19
19
  attr_reader :mod
20
20
 
21
- # @sig Symbol
21
+ #: Symbol
22
22
  attr_reader :cname
23
23
 
24
24
  # The type of the first argument is Module because Class < Module, class
25
25
  # objects are also valid.
26
26
  #
27
- # @sig (Module, Symbol) -> void
27
+ #: (Module, Symbol) -> void
28
28
  def initialize(mod, cname)
29
29
  @mod = mod
30
30
  @cname = cname
31
31
  @path = nil
32
32
  end
33
33
 
34
- # @sig () -> String
34
+ #: () -> String
35
35
  def path
36
- @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
37
37
  end
38
38
  alias to_s path
39
39
 
40
- # @sig () -> String?
40
+ #: () -> String?
41
41
  def autoload?
42
42
  @mod.autoload?(@cname, false)
43
43
  end
44
44
 
45
- # @sig (String) -> bool
45
+ #: (String) -> nil
46
46
  def autoload(abspath)
47
47
  @mod.autoload(@cname, abspath)
48
48
  end
49
49
 
50
- # @sig () -> bool
50
+ #: () -> bool
51
51
  def defined?
52
52
  @mod.const_defined?(@cname, false)
53
53
  end
54
54
 
55
- # @sig (top) -> top
55
+ #: (top) -> top
56
56
  def set(value)
57
57
  @mod.const_set(@cname, value)
58
58
  end
59
59
 
60
- # @raise [NameError]
61
- # @sig () -> top
60
+ #: () -> top ! NameError
62
61
  def get
63
62
  @mod.const_get(@cname, false)
64
63
  end
65
64
 
66
- # @raise [NameError]
67
- # @sig () -> void
65
+ #: () -> void ! NameError
68
66
  def remove
69
67
  @mod.__send__(:remove_const, @cname)
70
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,7 +2,7 @@
2
2
 
3
3
  # This is a private module.
4
4
  module Zeitwerk::Internal
5
- # @sig (Symbol) -> void
5
+ #: (Symbol) -> void
6
6
  def internal(method_name)
7
7
  private method_name
8
8
 
@@ -5,12 +5,11 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
5
5
 
6
6
  # Invoked from our decorated Kernel#require when a managed file is autoloaded.
7
7
  #
8
- # @raise [Zeitwerk::NameError]
9
- # @sig (String) -> void
8
+ #: (String) -> void ! Zeitwerk::NameError
10
9
  internal def on_file_autoloaded(file)
11
10
  cref = autoloads.delete(file)
12
11
 
13
- Zeitwerk::Registry.unregister_autoload(file)
12
+ Zeitwerk::Registry.autoloads.unregister(file)
14
13
 
15
14
  if cref.defined?
16
15
  log("constant #{cref} loaded from file #{file}") if logger
@@ -36,7 +35,7 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
36
35
  # Invoked from our decorated Kernel#require when a managed directory is
37
36
  # autoloaded.
38
37
  #
39
- # @sig (String) -> void
38
+ #: (String) -> void
40
39
  internal def on_dir_autoloaded(dir)
41
40
  # Module#autoload does not serialize concurrent requires in CRuby < 3.2, and
42
41
  # we handle directories ourselves without going through Kernel#require, so
@@ -53,8 +52,7 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
53
52
  dirs_autoload_monitor.synchronize do
54
53
  if cref = autoloads.delete(dir)
55
54
  implicit_namespace = cref.set(Module.new)
56
- cpath = implicit_namespace.name
57
- log("module #{cpath} autovivified from directory #{dir}") if logger
55
+ log("module #{cref} autovivified from directory #{dir}") if logger
58
56
 
59
57
  to_unload[dir] = cref if reloading_enabled?
60
58
 
@@ -66,7 +64,7 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
66
64
 
67
65
  on_namespace_loaded(cref, implicit_namespace)
68
66
 
69
- 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?
70
68
  end
71
69
  end
72
70
  end
@@ -75,7 +73,7 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
75
73
  # autovivification. If the namespace has matching subdirectories, we descend
76
74
  # into them now.
77
75
  #
78
- # @sig (Zeitwerk::Cref, Module) -> void
76
+ #: (Zeitwerk::Cref, Module) -> void
79
77
  internal def on_namespace_loaded(cref, namespace)
80
78
  if dirs = namespace_dirs.delete(cref)
81
79
  dirs.each do |dir|
@@ -86,7 +84,7 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
86
84
 
87
85
  private
88
86
 
89
- # @sig (String, top, String) -> void
87
+ #: (String, top, String) -> void
90
88
  def run_on_load_callbacks(cpath, value, abspath)
91
89
  # Order matters. If present, run the most specific one.
92
90
  callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)