zeitwerk 2.6.18 → 2.7.2

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: a1fa60f08c282d8471eaf321deba97207ce56d2cba32619cff78f09e65f2f258
4
- data.tar.gz: 1c803846848fc8830913acfd8fbf04bfa59f26c3c8806d4fa5bed9c751f48c5a
3
+ metadata.gz: 1a07f90eb2f155582d05f58527ffcbc2f4d76c9a1983260ca8d527becaeb7972
4
+ data.tar.gz: 65e8dc78ca8e6de674f0fc7d88aad5c9bad0d7687bc9ed26f93d6fa0e6d18e90
5
5
  SHA512:
6
- metadata.gz: abb7134976f1ae00cafd77dcaa39fe2239632639c136d866ac75e55c8ec15f1c157ff0e434969dade286ee36e6422856799acd3e86dcd61e4ef39d1fd23909a4
7
- data.tar.gz: 42e2309bd69f0bba3bfc74acecf2fcf3b3206a6cd26583afdc1012d34a063431bc03ec4ec42e1cf452f41bfa0de43a4d171cb2e99c7ca3e64b61412fbef83819
6
+ metadata.gz: d7b9d13e3d3d5bf0497ec259bf0817256586e245f7d47c951b8392784f715bc71c20d0ec3c9e465077da6d8e729ca6888fbaaa24820fe4459771e29340ee6d05
7
+ data.tar.gz: 8b1322d36bc9115a56b6abab6be9549c868e0edd2025fe82dd2c5d0abb082fac8532c82ed03f895d34f2875f27b160f4861185112c4aef2a3129e46569115c0f
data/README.md CHANGED
@@ -54,7 +54,6 @@
54
54
  - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
55
55
  - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
56
56
  - [Shadowed files](#shadowed-files)
57
- - [Edge cases](#edge-cases)
58
57
  - [Beware of circular dependencies](#beware-of-circular-dependencies)
59
58
  - [Reopening third-party namespaces](#reopening-third-party-namespaces)
60
59
  - [Introspection](#introspection)
@@ -63,7 +62,6 @@
63
62
  - [`Zeitwerk::Loader#all_expected_cpaths`](#zeitwerkloaderall_expected_cpaths)
64
63
  - [Encodings](#encodings)
65
64
  - [Rules of thumb](#rules-of-thumb)
66
- - [Debuggers](#debuggers)
67
65
  - [Pronunciation](#pronunciation)
68
66
  - [Supported Ruby versions](#supported-ruby-versions)
69
67
  - [Testing](#testing)
@@ -283,6 +281,8 @@ class Hotel < ApplicationRecord
283
281
  end
284
282
  ```
285
283
 
284
+ When autoloaded, Zeitwerk verifies the expected constant (`Hotel` in the example) stores a class or module object. If it doesn't, `Zeitwerk::Error` is raised.
285
+
286
286
  An explicit namespace must be managed by one single loader. Loaders that reopen namespaces owned by other projects are responsible for loading their constants before setup.
287
287
 
288
288
  <a id="markdown-collapsing-directories" name="collapsing-directories"></a>
@@ -1178,36 +1178,6 @@ file #{file} is ignored because #{constant_path} is already defined
1178
1178
 
1179
1179
  Shadowing only applies to Ruby files, namespace definition can be spread over multiple directories. And you can also reopen third-party namespaces if done [orderly](#reopening-third-party-namespaces).
1180
1180
 
1181
- <a id="markdown-edge-cases" name="edge-cases"></a>
1182
- ### Edge cases
1183
-
1184
- [Explicit namespaces](#explicit-namespaces) like `Trip` here:
1185
-
1186
- ```ruby
1187
- # trip.rb
1188
- class Trip
1189
- include Geolocation
1190
- end
1191
-
1192
- # trip/geolocation.rb
1193
- module Trip::Geolocation
1194
- ...
1195
- end
1196
- ```
1197
-
1198
- have to be defined with the `class`/`module` keywords, as in the example above.
1199
-
1200
- For technical reasons, raw constant assignment is not supported:
1201
-
1202
- ```ruby
1203
- # trip.rb
1204
- Trip = Class { ...} # NOT SUPPORTED
1205
- Trip = Struct.new { ... } # NOT SUPPORTED
1206
- Trip = Data.define { ... } # NOT SUPPORTED
1207
- ```
1208
-
1209
- This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
1210
-
1211
1181
  <a id="markdown-beware-of-circular-dependencies" name="beware-of-circular-dependencies"></a>
1212
1182
  ### Beware of circular dependencies
1213
1183
 
@@ -1406,15 +1376,6 @@ The test suite passes on Windows with codepage `Windows-1252` if all the involve
1406
1376
 
1407
1377
  6. In a given process, ideally, there should be at most one loader with reloading enabled. Technically, you can have more, but it may get tricky if one refers to constants managed by the other one. Do that only if you know what you are doing.
1408
1378
 
1409
- <a id="markdown-debuggers" name="debuggers"></a>
1410
- ### Debuggers
1411
-
1412
- Zeitwerk and [debug.rb](https://github.com/ruby/debug) are fully compatible if CRuby is ≥ 3.1 (see [ruby/debug#558](https://github.com/ruby/debug/pull/558)).
1413
-
1414
- [Byebug](https://github.com/deivid-rodriguez/byebug) is compatible except for an edge case explained in [deivid-rodriguez/byebug#564](https://github.com/deivid-rodriguez/byebug/issues/564). Prior to CRuby 3.1, `debug.rb` has a similar edge incompatibility.
1415
-
1416
- [Break](https://github.com/gsamokovarov/break) is fully compatible.
1417
-
1418
1379
  <a id="markdown-pronunciation" name="pronunciation"></a>
1419
1380
  ## Pronunciation
1420
1381
 
@@ -1423,9 +1384,12 @@ Zeitwerk and [debug.rb](https://github.com/ruby/debug) are fully compatible if C
1423
1384
  <a id="markdown-supported-ruby-versions" name="supported-ruby-versions"></a>
1424
1385
  ## Supported Ruby versions
1425
1386
 
1426
- Zeitwerk works with CRuby 2.5 and above.
1387
+ Starting with version 2.7, Zeitwerk requires Ruby 3.2 or newer.
1427
1388
 
1428
- On TruffleRuby all is good except for thread-safety. Right now, in TruffleRuby `Module#autoload` does not block threads accessing a constant that is being autoloaded. CRuby prevents such access to avoid concurrent threads from seeing partial evaluations of the corresponding file. Zeitwerk inherits autoloading thread-safety from this property. This is not an issue if your project gets eager loaded, or if you lazy load in single-threaded environments. (See https://github.com/oracle/truffleruby/issues/2431.)
1389
+ Zeitwerk 2.7 requires TruffleRuby 24.1.2+ due to https://github.com/oracle/truffleruby/issues/3683.
1390
+ Alternatively, TruffleRuby users can use a `< 2.7` version constraint for the `zeitwerk` gem.
1391
+ As of this writing, [autoloading is not fully thread-safe yet on TruffleRuby](https://github.com/oracle/truffleruby/issues/2431).
1392
+ If your program is multi-threaded, you need to eager load before threads are created.
1429
1393
 
1430
1394
  JRuby 9.3.0.0 is almost there. As of this writing, the test suite of Zeitwerk passes on JRuby except for three tests. (See https://github.com/jruby/jruby/issues/6781.)
1431
1395
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk::ConstAdded # :nodoc:
4
+ # @sig (Symbol) -> void
5
+ def const_added(cname)
6
+ if loader = Zeitwerk::Registry::ExplicitNamespaces.__loader_for(self, cname)
7
+ namespace = const_get(cname, false)
8
+
9
+ unless namespace.is_a?(Module)
10
+ cref = Zeitwerk::Cref.new(self, cname)
11
+ raise Zeitwerk::Error, "#{cref} is expected to be a namespace, should be a class or module (got #{namespace.class})"
12
+ end
13
+
14
+ loader.__on_namespace_loaded(Zeitwerk::Cref.new(self, cname), namespace)
15
+ end
16
+ super
17
+ end
18
+
19
+ Module.prepend(self)
20
+ 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,8 +11,13 @@
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
 
18
+ # @sig Module
19
+ attr_reader :mod
20
+
16
21
  # @sig Symbol
17
22
  attr_reader :cname
18
23
 
@@ -26,48 +31,15 @@ class Zeitwerk::Cref
26
31
  @path = nil
27
32
  end
28
33
 
29
- if Symbol.method_defined?(:name)
30
- # Symbol#name was introduced in Ruby 3.0. It returns always the same
31
- # frozen object, so we may save a few string allocations.
32
- #
33
- # @sig () -> String
34
- def path
35
- @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}"
36
- end
37
- else
38
- # @sig () -> String
39
- def path
40
- @path ||= Object.equal?(@mod) ? @cname.to_s : "#{real_mod_name(@mod)}::#{@cname}"
41
- end
34
+ # @sig () -> String
35
+ def path
36
+ @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}".freeze
42
37
  end
38
+ alias to_s path
43
39
 
44
- # The autoload? predicate takes into account the ancestor chain of the
45
- # receiver, like const_defined? and other methods in the constants API do.
46
- #
47
- # For example, given
48
- #
49
- # class A
50
- # autoload :X, "x.rb"
51
- # end
52
- #
53
- # class B < A
54
- # end
55
- #
56
- # B.autoload?(:X) returns "x.rb".
57
- #
58
- # We need a way to retrieve it ignoring ancestors.
59
- #
60
40
  # @sig () -> String?
61
- if method(:autoload?).arity == 1
62
- # @sig () -> String?
63
- def autoload?
64
- @mod.autoload?(@cname) if self.defined?
65
- end
66
- else
67
- # @sig () -> String?
68
- def autoload?
69
- @mod.autoload?(@cname, false)
70
- end
41
+ def autoload?
42
+ @mod.autoload?(@cname, false)
71
43
  end
72
44
 
73
45
  # @sig (String) -> bool
@@ -80,13 +52,13 @@ class Zeitwerk::Cref
80
52
  @mod.const_defined?(@cname, false)
81
53
  end
82
54
 
83
- # @sig (Object) -> Object
55
+ # @sig (top) -> top
84
56
  def set(value)
85
57
  @mod.const_set(@cname, value)
86
58
  end
87
59
 
88
60
  # @raise [NameError]
89
- # @sig () -> Object
61
+ # @sig () -> top
90
62
  def get
91
63
  @mod.const_get(@cname, false)
92
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,11 +1,11 @@
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
  #
8
+ # @raise [Zeitwerk::NameError]
9
9
  # @sig (String) -> void
10
10
  internal def on_file_autoloaded(file)
11
11
  cref = autoloads.delete(file)
@@ -13,11 +13,11 @@ module Zeitwerk::Loader::Callbacks
13
13
  Zeitwerk::Registry.unregister_autoload(file)
14
14
 
15
15
  if cref.defined?
16
- log("constant #{cref.path} loaded from file #{file}") if logger
17
- 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?
18
18
  run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
19
19
  else
20
- msg = "expected file #{file} to define constant #{cref.path}, but didn't"
20
+ msg = "expected file #{file} to define constant #{cref}, but didn't"
21
21
  log(msg) if logger
22
22
 
23
23
  # Ruby still keeps the autoload defined, but we remove it because the
@@ -27,7 +27,7 @@ module Zeitwerk::Loader::Callbacks
27
27
  # Since the expected constant was not defined, there is nothing to unload.
28
28
  # However, if the exception is rescued and reloading is enabled, we still
29
29
  # need to deleted the file from $LOADED_FEATURES.
30
- to_unload[cref.path] = [file, cref] if reloading_enabled?
30
+ to_unload[file] = cref if reloading_enabled?
31
31
 
32
32
  raise Zeitwerk::NameError.new(msg, cref.cname)
33
33
  end
@@ -56,7 +56,7 @@ module Zeitwerk::Loader::Callbacks
56
56
  cpath = implicit_namespace.name
57
57
  log("module #{cpath} autovivified from directory #{dir}") if logger
58
58
 
59
- to_unload[cpath] = [dir, cref] if reloading_enabled?
59
+ to_unload[dir] = cref if reloading_enabled?
60
60
 
61
61
  # We don't unregister `dir` in the registry because concurrent threads
62
62
  # wouldn't find a loader associated to it in Kernel#require and would
@@ -64,21 +64,20 @@ module Zeitwerk::Loader::Callbacks
64
64
  # these to be able to unregister later if eager loading.
65
65
  autoloaded_dirs << dir
66
66
 
67
- on_namespace_loaded(implicit_namespace)
67
+ on_namespace_loaded(cref, implicit_namespace)
68
68
 
69
69
  run_on_load_callbacks(cpath, implicit_namespace, dir) unless on_load_callbacks.empty?
70
70
  end
71
71
  end
72
72
  end
73
73
 
74
- # Invoked when a class or module is created or reopened, either from the
75
- # tracer or from module autovivification. If the namespace has matching
76
- # 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.
77
77
  #
78
- # @private
79
- # @sig (Module) -> void
80
- def on_namespace_loaded(namespace)
81
- 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)
82
81
  dirs.each do |dir|
83
82
  define_autoloads_for_dir(dir, namespace)
84
83
  end
@@ -87,7 +86,7 @@ module Zeitwerk::Loader::Callbacks
87
86
 
88
87
  private
89
88
 
90
- # @sig (String, Object) -> void
89
+ # @sig (String, top, String) -> void
91
90
  def run_on_load_callbacks(cpath, value, abspath)
92
91
  # Order matters. If present, run the most specific one.
93
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
 
@@ -84,7 +84,7 @@ module Zeitwerk::Loader::EagerLoad
84
84
  return unless mod_name
85
85
 
86
86
  actual_roots.each do |root_dir, root_namespace|
87
- if mod.equal?(Object)
87
+ if Object.equal?(mod)
88
88
  # A shortcircuiting test depends on the invocation of this method.
89
89
  # Please keep them in sync if refactored.
90
90
  actual_eager_load_dir(root_dir, root_namespace)
@@ -57,9 +57,7 @@ module Zeitwerk::Loader::Helpers
57
57
  to_visit = [dir]
58
58
 
59
59
  while (dir = to_visit.shift)
60
- children = Dir.children(dir)
61
-
62
- children.each do |basename|
60
+ Dir.each_child(dir) do |basename|
63
61
  next if hidden?(basename)
64
62
 
65
63
  abspath = File.join(dir, basename)
@@ -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
@@ -469,27 +486,27 @@ module Zeitwerk
469
486
  # Registering is idempotent, and we have to keep the autoload pointing
470
487
  # to the file. This may run again if more directories are found later
471
488
  # on, no big deal.
472
- register_explicit_namespace(cref.path)
489
+ register_explicit_namespace(cref)
473
490
  end
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,10 +530,12 @@ 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
 
535
+ # Order matters: When Module#const_added is triggered by the autoload, we
536
+ # don't want the namespace to be registered yet.
518
537
  define_autoload(cref, file)
519
- register_explicit_namespace(cref.path)
538
+ register_explicit_namespace(cref)
520
539
  end
521
540
 
522
541
  # @sig (Module, Symbol, String) -> void
@@ -525,19 +544,16 @@ module Zeitwerk
525
544
 
526
545
  if logger
527
546
  if ruby?(abspath)
528
- log("autoload set for #{cref.path}, to be loaded from #{abspath}")
547
+ log("autoload set for #{cref}, to be loaded from #{abspath}")
529
548
  else
530
- log("autoload set for #{cref.path}, to be autovivified from #{abspath}")
549
+ log("autoload set for #{cref}, to be autovivified from #{abspath}")
531
550
  end
532
551
  end
533
552
 
534
553
  autoloads[abspath] = cref
535
554
  Registry.register_autoload(self, abspath)
536
555
 
537
- # See why in the documentation of Zeitwerk::Registry.inceptions.
538
- unless cref.autoload?
539
- Registry.register_inception(cref.path, abspath, self)
540
- end
556
+ register_inception(cref, abspath) unless cref.autoload?
541
557
  end
542
558
 
543
559
  # @sig (Module, Symbol) -> String?
@@ -545,13 +561,32 @@ module Zeitwerk
545
561
  if autoload_path = cref.autoload?
546
562
  autoload_path if autoloads.key?(autoload_path)
547
563
  else
548
- Registry.inception?(cref.path, self)
564
+ inceptions[cref]
549
565
  end
550
566
  end
551
567
 
552
- # @sig (String) -> void
553
- private def register_explicit_namespace(cpath)
554
- ExplicitNamespace.__register(cpath, self)
568
+ # @sig (Zeitwerk::Cref) -> void
569
+ private def register_explicit_namespace(cref)
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
555
590
  end
556
591
 
557
592
  # @sig (String) -> void
@@ -578,17 +613,17 @@ module Zeitwerk
578
613
  end
579
614
  end
580
615
 
581
- # @sig (String, Object, String) -> void
582
- 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)
583
618
  # Order matters. If present, run the most specific one.
584
- on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
585
- 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) }
586
621
  end
587
622
 
588
623
  # @sig (Module, Symbol) -> void
589
624
  private def unload_autoload(cref)
590
625
  cref.remove
591
- log("autoload for #{cref.path} removed") if logger
626
+ log("autoload for #{cref} removed") if logger
592
627
  end
593
628
 
594
629
  # @sig (Module, Symbol) -> void
@@ -600,7 +635,7 @@ module Zeitwerk
600
635
  # There are a few edge scenarios in which this may happen. If the constant
601
636
  # is gone, that is OK, anyway.
602
637
  else
603
- log("#{cref.path} unloaded") if logger
638
+ log("#{cref} unloaded") if logger
604
639
  end
605
640
  end
606
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
@@ -10,13 +10,7 @@ module Zeitwerk::RealModName
10
10
  # The name method can be overridden, hence the indirection in this method.
11
11
  #
12
12
  # @sig (Module) -> String?
13
- if UnboundMethod.method_defined?(:bind_call)
14
- def real_mod_name(mod)
15
- UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
16
- end
17
- else
18
- def real_mod_name(mod)
19
- UNBOUND_METHOD_MODULE_NAME.bind(mod).call
20
- end
13
+ def real_mod_name(mod)
14
+ UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
21
15
  end
22
16
  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.6.18"
4
+ VERSION = "2.7.2"
5
5
  end
data/lib/zeitwerk.rb CHANGED
@@ -7,14 +7,15 @@ 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"
14
- require_relative "zeitwerk/kernel"
15
13
  require_relative "zeitwerk/error"
16
14
  require_relative "zeitwerk/version"
17
15
 
16
+ require_relative "zeitwerk/core_ext/kernel"
17
+ require_relative "zeitwerk/core_ext/module"
18
+
18
19
  # This is a dangerous method.
19
20
  #
20
21
  # @experimental
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.6.18
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-09-02 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
@@ -23,14 +22,15 @@ files:
23
22
  - MIT-LICENSE
24
23
  - README.md
25
24
  - lib/zeitwerk.rb
25
+ - lib/zeitwerk/core_ext/kernel.rb
26
+ - lib/zeitwerk/core_ext/module.rb
26
27
  - lib/zeitwerk/cref.rb
28
+ - lib/zeitwerk/cref/map.rb
27
29
  - lib/zeitwerk/error.rb
28
- - lib/zeitwerk/explicit_namespace.rb
29
30
  - lib/zeitwerk/gem_inflector.rb
30
31
  - lib/zeitwerk/gem_loader.rb
31
32
  - lib/zeitwerk/inflector.rb
32
33
  - lib/zeitwerk/internal.rb
33
- - lib/zeitwerk/kernel.rb
34
34
  - lib/zeitwerk/loader.rb
35
35
  - lib/zeitwerk/loader/callbacks.rb
36
36
  - lib/zeitwerk/loader/config.rb
@@ -39,6 +39,8 @@ files:
39
39
  - lib/zeitwerk/null_inflector.rb
40
40
  - lib/zeitwerk/real_mod_name.rb
41
41
  - lib/zeitwerk/registry.rb
42
+ - lib/zeitwerk/registry/explicit_namespaces.rb
43
+ - lib/zeitwerk/registry/inceptions.rb
42
44
  - lib/zeitwerk/version.rb
43
45
  homepage: https://github.com/fxn/zeitwerk
44
46
  licenses:
@@ -48,7 +50,6 @@ metadata:
48
50
  changelog_uri: https://github.com/fxn/zeitwerk/blob/master/CHANGELOG.md
49
51
  source_code_uri: https://github.com/fxn/zeitwerk
50
52
  bug_tracker_uri: https://github.com/fxn/zeitwerk/issues
51
- post_install_message:
52
53
  rdoc_options: []
53
54
  require_paths:
54
55
  - lib
@@ -56,15 +57,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
56
57
  requirements:
57
58
  - - ">="
58
59
  - !ruby/object:Gem::Version
59
- version: '2.5'
60
+ version: '3.2'
60
61
  required_rubygems_version: !ruby/object:Gem::Requirement
61
62
  requirements:
62
63
  - - ">="
63
64
  - !ruby/object:Gem::Version
64
65
  version: '0'
65
66
  requirements: []
66
- rubygems_version: 3.5.11
67
- signing_key:
67
+ rubygems_version: 3.6.4
68
68
  specification_version: 4
69
69
  summary: Efficient and thread-safe constant autoloader
70
70
  test_files: []
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zeitwerk
4
- # Centralizes the logic for the trace point used to detect the creation of
5
- # explicit namespaces, needed to descend into matching subdirectories right
6
- # after the constant has been defined.
7
- #
8
- # The implementation assumes an explicit namespace is managed by one loader.
9
- # Loaders that reopen namespaces owned by other projects are responsible for
10
- # loading their constant before setup. This is documented.
11
- module ExplicitNamespace # :nodoc: all
12
- class << self
13
- include RealModName
14
- extend Internal
15
-
16
- # Maps constant paths that correspond to explicit namespaces according to
17
- # the file system, to the loader responsible for them.
18
- #
19
- # @sig Hash[String, Zeitwerk::Loader]
20
- attr_reader :cpaths
21
- private :cpaths
22
-
23
- # @sig Mutex
24
- attr_reader :mutex
25
- private :mutex
26
-
27
- # @sig TracePoint
28
- attr_reader :tracer
29
- private :tracer
30
-
31
- # Asserts `cpath` corresponds to an explicit namespace for which `loader`
32
- # is responsible.
33
- #
34
- # @sig (String, Zeitwerk::Loader) -> void
35
- internal def register(cpath, loader)
36
- mutex.synchronize do
37
- cpaths[cpath] = loader
38
- # We check enabled? because, looking at the C source code, enabling an
39
- # enabled tracer does not seem to be a simple no-op.
40
- tracer.enable unless tracer.enabled?
41
- end
42
- end
43
-
44
- # @sig (Zeitwerk::Loader) -> void
45
- internal def unregister_loader(loader)
46
- cpaths.delete_if { |_cpath, l| l == loader }
47
- disable_tracer_if_unneeded
48
- end
49
-
50
- # This is an internal method only used by the test suite.
51
- #
52
- # @sig (String) -> bool
53
- internal def registered?(cpath)
54
- cpaths.key?(cpath)
55
- end
56
-
57
- # @sig () -> void
58
- private def disable_tracer_if_unneeded
59
- mutex.synchronize do
60
- tracer.disable if cpaths.empty?
61
- end
62
- end
63
-
64
- # @sig (TracePoint) -> void
65
- private def tracepoint_class_callback(event)
66
- # If the class is a singleton class, we won't do anything with it so we
67
- # can bail out immediately. This is several orders of magnitude faster
68
- # than accessing its name.
69
- return if event.self.singleton_class?
70
-
71
- # It might be tempting to return if name.nil?, to avoid the computation
72
- # of a hash code and delete call. But Ruby does not trigger the :class
73
- # event on Class.new or Module.new, so that would incur in an extra call
74
- # for nothing.
75
- #
76
- # On the other hand, if we were called, cpaths is not empty. Otherwise
77
- # the tracer is disabled. So we do need to go ahead with the hash code
78
- # computation and delete call.
79
- if loader = cpaths.delete(real_mod_name(event.self))
80
- loader.on_namespace_loaded(event.self)
81
- disable_tracer_if_unneeded
82
- end
83
- end
84
- end
85
-
86
- @cpaths = {}
87
- @mutex = Mutex.new
88
-
89
- # We go through a method instead of defining a block mainly to have a better
90
- # label when profiling.
91
- @tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
92
- end
93
- end
File without changes