zeitwerk 2.6.13 → 2.7.1

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: 5d4ce4eb7136dafe17130fc5d895e525d80ae947f02dd9420b5bc85a4d6f0dfd
4
- data.tar.gz: edcac638955fef258ad36a358b78da6831a715ed46446848e39f9f1d8fb7de0b
3
+ metadata.gz: aba46812169c8e26085b099c708093b00a29bfd39e8d8210d29bc99e7df0e7fa
4
+ data.tar.gz: 0fc20386009d52d21a0cb79a64c5d800f7aecfce5a38e8430a10dc5d04c2609d
5
5
  SHA512:
6
- metadata.gz: f0a6e64c56635c9c6a8c9b24bdeb7509e4a7f14489f32aae18b02221c23b95b34096184c78d2d1509130ce75031ed0f3a7751b78e43d9b72122440f07cf980bc
7
- data.tar.gz: 06440147c101a95ac2c8b7dcd43920a3efc3da2f58b902c157730a449d57b6ef74e0d3db7f3d2735be816a74ceb774e2d4075741556c7e2ba7fa00b8130c1723
6
+ metadata.gz: 3edac5ad6f940caa70c4cac093bf271b8399b078d7124f450513c963ec5099e9b9082841ceb9893b81f1edf8e34dc642ec811403415fb230211670c63a950766
7
+ data.tar.gz: 47339a8b35dc06108fde9aadd62e83b1ea4e4ef22854c31849069f09ece1115dce07d63ec354fe96fbc599bba411ecff48db6a8b0c8b150ad7ee016787e9d75c
data/README.md CHANGED
@@ -54,15 +54,14 @@
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)
61
60
  - [`Zeitwerk::Loader#dirs`](#zeitwerkloaderdirs)
62
61
  - [`Zeitwerk::Loader#cpath_expected_at`](#zeitwerkloadercpath_expected_at)
62
+ - [`Zeitwerk::Loader#all_expected_cpaths`](#zeitwerkloaderall_expected_cpaths)
63
63
  - [Encodings](#encodings)
64
64
  - [Rules of thumb](#rules-of-thumb)
65
- - [Debuggers](#debuggers)
66
65
  - [Pronunciation](#pronunciation)
67
66
  - [Supported Ruby versions](#supported-ruby-versions)
68
67
  - [Testing](#testing)
@@ -259,7 +258,7 @@ app/controllers/admin/users_controller.rb -> Admin::UsersController
259
258
 
260
259
  and does not have a file called `admin.rb`, Zeitwerk automatically creates an `Admin` module on your behalf the first time `Admin` is used.
261
260
 
262
- To trigger this behavior, the directory must contain non-ignored Ruby files with the `.rb` extension, either directly or recursively. Otherwise, the directory is ignored. This condition is reevaluated during reloads.
261
+ To trigger this behavior, the directory must contain non-ignored Ruby files with the ".rb" extension, either directly or recursively. Otherwise, the directory is ignored. This condition is reevaluated during reloads.
263
262
 
264
263
  <a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
265
264
  ### Explicit namespaces
@@ -282,6 +281,8 @@ class Hotel < ApplicationRecord
282
281
  end
283
282
  ```
284
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
+
285
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.
286
287
 
287
288
  <a id="markdown-collapsing-directories" name="collapsing-directories"></a>
@@ -1064,7 +1065,7 @@ However, sometimes it might still be convenient to tell Zeitwerk to completely i
1064
1065
 
1065
1066
  You can ignore file names, directory names, and glob patterns. Glob patterns are expanded when they are added and again on each reload.
1066
1067
 
1067
- There is an edge case related to nested root directories. Conceptually, root directories are independent source trees. If you ignore a parent of a nested root directory, the nested root directory is not affected. You need to ignore it explictly if you want it ignored too.
1068
+ There is an edge case related to nested root directories. Conceptually, root directories are independent source trees. If you ignore a parent of a nested root directory, the nested root directory is not affected. You need to ignore it explicitly if you want it ignored too.
1068
1069
 
1069
1070
  Let's see some use cases.
1070
1071
 
@@ -1177,36 +1178,6 @@ file #{file} is ignored because #{constant_path} is already defined
1177
1178
 
1178
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).
1179
1180
 
1180
- <a id="markdown-edge-cases" name="edge-cases"></a>
1181
- ### Edge cases
1182
-
1183
- [Explicit namespaces](#explicit-namespaces) like `Trip` here:
1184
-
1185
- ```ruby
1186
- # trip.rb
1187
- class Trip
1188
- include Geolocation
1189
- end
1190
-
1191
- # trip/geolocation.rb
1192
- module Trip::Geolocation
1193
- ...
1194
- end
1195
- ```
1196
-
1197
- have to be defined with the `class`/`module` keywords, as in the example above.
1198
-
1199
- For technical reasons, raw constant assignment is not supported:
1200
-
1201
- ```ruby
1202
- # trip.rb
1203
- Trip = Class { ...} # NOT SUPPORTED
1204
- Trip = Struct.new { ... } # NOT SUPPORTED
1205
- Trip = Data.define { ... } # NOT SUPPORTED
1206
- ```
1207
-
1208
- This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
1209
-
1210
1181
  <a id="markdown-beware-of-circular-dependencies" name="beware-of-circular-dependencies"></a>
1211
1182
  ### Beware of circular dependencies
1212
1183
 
@@ -1330,6 +1301,54 @@ loader.cpath_expected_at("8.rb") # => Zeitwerk::NameError
1330
1301
 
1331
1302
  This method does not parse file contents and does not guarantee files define the returned constant path. It just says which is the _expected_ one.
1332
1303
 
1304
+ `Zeitwerk::Loader#cpath_expected_at` is designed to be used with individual paths. If you want to know all the expected constant paths in the project, please use `Zeitwerk::Loader#all_expected_cpaths`, documented next.
1305
+
1306
+ <a id="markdown-zeitwerkloaderall_expected_cpaths" name="zeitwerkloaderall_expected_cpaths"></a>
1307
+ #### `Zeitwerk::Loader#all_expected_cpaths`
1308
+
1309
+ The method `Zeitwerk::Loader#all_expected_cpaths` returns a hash that maps the absolute paths of the files and directories managed by the receiver to their expected constant paths.
1310
+
1311
+ Ignored files, hidden files, and files whose extension is not ".rb" are not included in the result. Same for directories, hidden or ignored directories are not included in the result. Additionally, directories that contain no files with extension ".rb" (recursively) are also excluded, since those are not considered to represent Ruby namespaces.
1312
+
1313
+ For example, if `lib` is the root directory of a gem with the following contents:
1314
+
1315
+ ```
1316
+ lib/.DS_Store
1317
+ lib/my_gem.rb
1318
+ lib/my_gem/version.rb
1319
+ lib/my_gem/ignored.rb
1320
+ lib/my_gem/drivers/unix.rb
1321
+ lib/my_gem/drivers/windows.rb
1322
+ lib/my_gem/collapsed/foo.rb
1323
+ lib/tasks/my_gem.rake
1324
+ ```
1325
+
1326
+ `Zeitwerk::Loader#all_expected_cpaths` would return (maybe in a different order):
1327
+
1328
+ ```ruby
1329
+ {
1330
+ "/.../lib" => "Object",
1331
+ "/.../lib/my_gem.rb" => "MyGem",
1332
+ "/.../lib/my_gem" => "MyGem",
1333
+ "/.../lib/my_gem/version.rb" => "MyGem::VERSION",
1334
+ "/.../lib/my_gem/drivers" => "MyGem::Drivers",
1335
+ "/.../lib/my_gem/drivers/unix.rb" => "MyGem::Drivers::Unix",
1336
+ "/.../lib/my_gem/drivers/windows.rb" => "MyGem::Drivers::Windows",
1337
+ "/.../lib/my_gem/collapsed" => "MyGem"
1338
+ "/.../lib/my_gem/collapsed/foo.rb" => "MyGem::Foo"
1339
+ }
1340
+ ```
1341
+
1342
+ In the previous example we assume `lib/my_gem/ignored.rb` is ignored, and therefore it is not present in the returned hash. Also, `lib/my_gem/collapsed` is a collapsed directory, so the expected namespace at that level is still `MyGem` (this is an edge case).
1343
+
1344
+ The file `lib/.DS_Store` is hidden, hence excluded. The directory `lib/tasks` is also not present because it contains no files with extension ".rb".
1345
+
1346
+ Directory paths do not have trailing slashes.
1347
+
1348
+ The order of the hash entries is undefined.
1349
+
1350
+ This method does not parse or execute file contents and does not guarantee files define the corresponding constant paths. It just says which are the _expected_ ones.
1351
+
1333
1352
  <a id="markdown-encodings" name="encodings"></a>
1334
1353
  ### Encodings
1335
1354
 
@@ -1357,15 +1376,6 @@ The test suite passes on Windows with codepage `Windows-1252` if all the involve
1357
1376
 
1358
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.
1359
1378
 
1360
- <a id="markdown-debuggers" name="debuggers"></a>
1361
- ### Debuggers
1362
-
1363
- 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)).
1364
-
1365
- [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.
1366
-
1367
- [Break](https://github.com/gsamokovarov/break) is fully compatible.
1368
-
1369
1379
  <a id="markdown-pronunciation" name="pronunciation"></a>
1370
1380
  ## Pronunciation
1371
1381
 
@@ -1374,9 +1384,12 @@ Zeitwerk and [debug.rb](https://github.com/ruby/debug) are fully compatible if C
1374
1384
  <a id="markdown-supported-ruby-versions" name="supported-ruby-versions"></a>
1375
1385
  ## Supported Ruby versions
1376
1386
 
1377
- Zeitwerk works with CRuby 2.5 and above.
1387
+ Starting with version 2.7, Zeitwerk requires Ruby 3.2 or newer.
1378
1388
 
1379
- 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.
1380
1393
 
1381
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.)
1382
1395
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk::ConstAdded
4
+ def const_added(cname)
5
+ if loader = Zeitwerk::ExplicitNamespace.__loader_for(self, cname)
6
+ namespace = const_get(cname, false)
7
+
8
+ 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
+ end
12
+
13
+ loader.on_namespace_loaded(namespace)
14
+ end
15
+ super
16
+ end
17
+
18
+ Module.prepend(self)
19
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This private class encapsulates pairs (mod, cname).
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:
7
+ #
8
+ # cref.path
9
+ # cref.set(value)
10
+ # cref.get
11
+ #
12
+ # The constant may or may not exist in mod.
13
+ class Zeitwerk::Cref
14
+ include Zeitwerk::RealModName
15
+
16
+ # @sig Module
17
+ attr_reader :mod
18
+
19
+ # @sig Symbol
20
+ attr_reader :cname
21
+
22
+ # The type of the first argument is Module because Class < Module, class
23
+ # objects are also valid.
24
+ #
25
+ # @sig (Module, Symbol) -> void
26
+ def initialize(mod, cname)
27
+ @mod = mod
28
+ @cname = cname
29
+ @path = nil
30
+ end
31
+
32
+ # @sig () -> String
33
+ def path
34
+ @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}".freeze
35
+ end
36
+
37
+ # @sig () -> String?
38
+ def autoload?
39
+ @mod.autoload?(@cname, false)
40
+ end
41
+
42
+ # @sig (String) -> bool
43
+ def autoload(abspath)
44
+ @mod.autoload(@cname, abspath)
45
+ end
46
+
47
+ # @sig () -> bool
48
+ def defined?
49
+ @mod.const_defined?(@cname, false)
50
+ end
51
+
52
+ # @sig (Object) -> Object
53
+ def set(value)
54
+ @mod.const_set(@cname, value)
55
+ end
56
+
57
+ # @raise [NameError]
58
+ # @sig () -> Object
59
+ def get
60
+ @mod.const_get(@cname, false)
61
+ end
62
+
63
+ # @raise [NameError]
64
+ # @sig () -> void
65
+ def remove
66
+ @mod.__send__(:remove_const, @cname)
67
+ end
68
+ end
@@ -1,93 +1,113 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
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.
7
15
  #
8
16
  # The implementation assumes an explicit namespace is managed by one loader.
9
17
  # Loaders that reopen namespaces owned by other projects are responsible for
10
18
  # loading their constant before setup. This is documented.
11
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
+
12
40
  class << self
13
41
  include RealModName
14
42
  extend Internal
15
43
 
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.
44
+ # Registers `cref` as being the constant path of an explicit namespace
45
+ # managed by `loader`.
33
46
  #
34
47
  # @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?
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}")
41
62
  end
42
63
  end
43
64
 
44
65
  # @sig (Zeitwerk::Loader) -> void
45
66
  internal def unregister_loader(loader)
46
- cpaths.delete_if { |_cpath, l| l == loader }
47
- disable_tracer_if_unneeded
67
+ @loaders.delete_if { _2.equal?(loader) }
48
68
  end
49
69
 
50
70
  # This is an internal method only used by the test suite.
51
71
  #
52
- # @sig (String) -> bool
53
- internal def registered?(cpath)
54
- cpaths.key?(cpath)
72
+ # @sig (String) -> Zeitwerk::Loader?
73
+ internal def registered?(cname_or_cpath)
74
+ @loaders[cname_or_cpath]
55
75
  end
56
76
 
77
+ # This is an internal method only used by the test suite.
78
+ #
57
79
  # @sig () -> void
58
- private def disable_tracer_if_unneeded
59
- mutex.synchronize do
60
- tracer.disable if cpaths.empty?
61
- end
80
+ internal def clear
81
+ @loaders.clear
62
82
  end
63
83
 
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?
84
+ module Synchronized
85
+ extend Internal
86
+
87
+ MUTEX = Mutex.new
70
88
 
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
89
+ internal def register(...)
90
+ MUTEX.synchronize { super }
82
91
  end
83
- end
84
- end
85
92
 
86
- @cpaths = {}
87
- @mutex = Mutex.new
93
+ internal def loader_for(...)
94
+ MUTEX.synchronize { super }
95
+ end
96
+
97
+ internal def unregister_loader(...)
98
+ MUTEX.synchronize { super }
99
+ end
88
100
 
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))
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
92
112
  end
93
113
  end
@@ -42,13 +42,12 @@ module Zeitwerk
42
42
  def warn_on_extra_files
43
43
  expected_namespace_dir = @root_file.delete_suffix(".rb")
44
44
 
45
- ls(@root_dir) do |basename, abspath|
45
+ ls(@root_dir) do |basename, abspath, ftype|
46
46
  next if abspath == @root_file
47
47
  next if abspath == expected_namespace_dir
48
48
 
49
49
  basename_without_ext = basename.delete_suffix(".rb")
50
50
  cname = inflector.camelize(basename_without_ext, abspath).to_sym
51
- ftype = dir?(abspath) ? "directory" : "file"
52
51
 
53
52
  warn(<<~EOS)
54
53
  WARNING: Zeitwerk defines the constant #{cname} after the #{ftype}
@@ -6,31 +6,31 @@ module Zeitwerk::Loader::Callbacks
6
6
 
7
7
  # Invoked from our decorated Kernel#require when a managed file is autoloaded.
8
8
  #
9
+ # @raise [Zeitwerk::NameError]
9
10
  # @sig (String) -> void
10
11
  internal def on_file_autoloaded(file)
11
- cref = autoloads.delete(file)
12
- cpath = cpath(*cref)
12
+ cref = autoloads.delete(file)
13
13
 
14
14
  Zeitwerk::Registry.unregister_autoload(file)
15
15
 
16
- if cdef?(*cref)
17
- log("constant #{cpath} loaded from file #{file}") if logger
18
- to_unload[cpath] = [file, cref] if reloading_enabled?
19
- run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
16
+ if cref.defined?
17
+ log("constant #{cref.path} loaded from file #{file}") if logger
18
+ to_unload[cref.path] = [file, cref] if reloading_enabled?
19
+ run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
20
20
  else
21
- msg = "expected file #{file} to define constant #{cpath}, but didn't"
21
+ msg = "expected file #{file} to define constant #{cref.path}, but didn't"
22
22
  log(msg) if logger
23
23
 
24
24
  # Ruby still keeps the autoload defined, but we remove it because the
25
25
  # contract in Zeitwerk is more strict.
26
- crem(*cref)
26
+ cref.remove
27
27
 
28
28
  # Since the expected constant was not defined, there is nothing to unload.
29
29
  # However, if the exception is rescued and reloading is enabled, we still
30
30
  # need to deleted the file from $LOADED_FEATURES.
31
- to_unload[cpath] = [file, cref] if reloading_enabled?
31
+ to_unload[cref.path] = [file, cref] if reloading_enabled?
32
32
 
33
- raise Zeitwerk::NameError.new(msg, cref.last)
33
+ raise Zeitwerk::NameError.new(msg, cref.cname)
34
34
  end
35
35
  end
36
36
 
@@ -53,8 +53,8 @@ module Zeitwerk::Loader::Callbacks
53
53
  # children, since t1 would have correctly deleted its namespace_dirs entry.
54
54
  dirs_autoload_monitor.synchronize do
55
55
  if cref = autoloads.delete(dir)
56
- autovivified_module = cref[0].const_set(cref[1], Module.new)
57
- cpath = autovivified_module.name
56
+ implicit_namespace = cref.set(Module.new)
57
+ cpath = implicit_namespace.name
58
58
  log("module #{cpath} autovivified from directory #{dir}") if logger
59
59
 
60
60
  to_unload[cpath] = [dir, cref] if reloading_enabled?
@@ -65,15 +65,15 @@ module Zeitwerk::Loader::Callbacks
65
65
  # these to be able to unregister later if eager loading.
66
66
  autoloaded_dirs << dir
67
67
 
68
- on_namespace_loaded(autovivified_module)
68
+ on_namespace_loaded(implicit_namespace)
69
69
 
70
- run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
70
+ run_on_load_callbacks(cpath, implicit_namespace, dir) unless on_load_callbacks.empty?
71
71
  end
72
72
  end
73
73
  end
74
74
 
75
75
  # Invoked when a class or module is created or reopened, either from the
76
- # tracer or from module autovivification. If the namespace has matching
76
+ # const_added or from module autovivification. If the namespace has matching
77
77
  # subdirectories, we descend into them now.
78
78
  #
79
79
  # @private
@@ -61,8 +61,8 @@ module Zeitwerk::Loader::EagerLoad
61
61
  cnames.reverse_each do |cname|
62
62
  # Can happen if there are no Ruby files. This is not an error condition,
63
63
  # the directory is actually managed. Could have Ruby files later.
64
- return unless cdef?(namespace, cname)
65
- namespace = cget(namespace, cname)
64
+ return unless namespace.const_defined?(cname, false)
65
+ namespace = namespace.const_get(cname, false)
66
66
  end
67
67
 
68
68
  # A shortcircuiting test depends on the invocation of this method. Please
@@ -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)
@@ -145,12 +145,12 @@ module Zeitwerk::Loader::EagerLoad
145
145
 
146
146
  namespace = root_namespace
147
147
  cnames.reverse_each do |cname|
148
- namespace = cget(namespace, cname)
148
+ namespace = namespace.const_get(cname, false)
149
149
  end
150
150
 
151
151
  raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
152
152
 
153
- cget(namespace, base_cname)
153
+ namespace.const_get(base_cname, false)
154
154
  end
155
155
 
156
156
  # The caller is responsible for making sure `namespace` is the namespace that
@@ -164,22 +164,20 @@ module Zeitwerk::Loader::EagerLoad
164
164
  log("eager load directory #{dir} start") if logger
165
165
 
166
166
  queue = [[dir, namespace]]
167
- while to_eager_load = queue.shift
168
- dir, namespace = to_eager_load
169
-
170
- ls(dir) do |basename, abspath|
167
+ while (current_dir, namespace = queue.shift)
168
+ ls(current_dir) do |basename, abspath, ftype|
171
169
  next if honour_exclusions && eager_load_exclusions.member?(abspath)
172
170
 
173
- if ruby?(abspath)
171
+ if ftype == :file
174
172
  if (cref = autoloads[abspath])
175
- cget(*cref)
173
+ cref.get
176
174
  end
177
175
  else
178
176
  if collapse?(abspath)
179
177
  queue << [abspath, namespace]
180
178
  else
181
179
  cname = inflector.camelize(basename, abspath).to_sym
182
- queue << [abspath, cget(namespace, cname)]
180
+ queue << [abspath, namespace.const_get(cname, false)]
183
181
  end
184
182
  end
185
183
  end
@@ -209,9 +207,9 @@ module Zeitwerk::Loader::EagerLoad
209
207
  next_dirs = []
210
208
 
211
209
  suffix.split("::").each do |segment|
212
- while dir = dirs.shift
213
- ls(dir) do |basename, abspath|
214
- next unless dir?(abspath)
210
+ while (dir = dirs.shift)
211
+ ls(dir) do |basename, abspath, ftype|
212
+ next unless ftype == :directory
215
213
 
216
214
  if collapse?(abspath)
217
215
  dirs << abspath
@@ -30,27 +30,43 @@ module Zeitwerk::Loader::Helpers
30
30
 
31
31
  if dir?(abspath)
32
32
  next if roots.key?(abspath)
33
- next if !has_at_least_one_ruby_file?(abspath)
33
+
34
+ if !has_at_least_one_ruby_file?(abspath)
35
+ log("directory #{abspath} is ignored because it has no Ruby files") if logger
36
+ next
37
+ end
38
+
39
+ ftype = :directory
34
40
  else
35
41
  next unless ruby?(abspath)
42
+ ftype = :file
36
43
  end
37
44
 
38
45
  # We freeze abspath because that saves allocations when passed later to
39
46
  # File methods. See #125.
40
- yield basename, abspath.freeze
47
+ yield basename, abspath.freeze, ftype
41
48
  end
42
49
  end
43
50
 
51
+ # Looks for a Ruby file using breadth-first search. This type of search is
52
+ # important to list as less directories as possible and return fast in the
53
+ # common case in which there are Ruby files.
54
+ #
44
55
  # @sig (String) -> bool
45
56
  private def has_at_least_one_ruby_file?(dir)
46
57
  to_visit = [dir]
47
58
 
48
- while dir = to_visit.shift
49
- ls(dir) do |_basename, abspath|
59
+ while (dir = to_visit.shift)
60
+ Dir.each_child(dir) do |basename|
61
+ next if hidden?(basename)
62
+
63
+ abspath = File.join(dir, basename)
64
+ next if ignored_path?(abspath)
65
+
50
66
  if dir?(abspath)
51
- to_visit << abspath
67
+ to_visit << abspath unless roots.key?(abspath)
52
68
  else
53
- return true
69
+ return true if ruby?(abspath)
54
70
  end
55
71
  end
56
72
  end
@@ -82,64 +98,7 @@ module Zeitwerk::Loader::Helpers
82
98
  end
83
99
  end
84
100
 
85
- # --- Constants ---------------------------------------------------------------------------------
86
-
87
- # The autoload? predicate takes into account the ancestor chain of the
88
- # receiver, like const_defined? and other methods in the constants API do.
89
- #
90
- # For example, given
91
- #
92
- # class A
93
- # autoload :X, "x.rb"
94
- # end
95
- #
96
- # class B < A
97
- # end
98
- #
99
- # B.autoload?(:X) returns "x.rb".
100
- #
101
- # We need a way to strictly check in parent ignoring ancestors.
102
- #
103
- # @sig (Module, Symbol) -> String?
104
- if method(:autoload?).arity == 1
105
- private def strict_autoload_path(parent, cname)
106
- parent.autoload?(cname) if cdef?(parent, cname)
107
- end
108
- else
109
- private def strict_autoload_path(parent, cname)
110
- parent.autoload?(cname, false)
111
- end
112
- end
113
-
114
- # @sig (Module, Symbol) -> String
115
- if Symbol.method_defined?(:name)
116
- # Symbol#name was introduced in Ruby 3.0. It returns always the same
117
- # frozen object, so we may save a few string allocations.
118
- private def cpath(parent, cname)
119
- Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
120
- end
121
- else
122
- private def cpath(parent, cname)
123
- Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
124
- end
125
- end
126
-
127
- # @sig (Module, Symbol) -> bool
128
- private def cdef?(parent, cname)
129
- parent.const_defined?(cname, false)
130
- end
131
-
132
- # @raise [NameError]
133
- # @sig (Module, Symbol) -> Object
134
- private def cget(parent, cname)
135
- parent.const_get(cname, false)
136
- end
137
-
138
- # @raise [NameError]
139
- # @sig (Module, Symbol) -> Object
140
- private def crem(parent, cname)
141
- parent.__send__(:remove_const, cname)
142
- end
101
+ # --- Inflection --------------------------------------------------------------------------------
143
102
 
144
103
  CNAME_VALIDATOR = Module.new
145
104
  private_constant :CNAME_VALIDATOR
@@ -22,14 +22,13 @@ module Zeitwerk
22
22
  private_constant :MUTEX
23
23
 
24
24
  # Maps absolute paths for which an autoload has been set ---and not
25
- # executed--- to their corresponding parent class or module and constant
26
- # name.
25
+ # executed--- to their corresponding Zeitwerk::Cref object.
27
26
  #
28
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
29
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
27
+ # "/Users/fxn/blog/app/models/user.rb" => #<Zeitwerk::Cref:... @mod=Object, @cname=:User, ...>,
28
+ # "/Users/fxn/blog/app/models/hotel/pricing.rb" => #<Zeitwerk::Cref:... @mod=Hotel, @cname=:Pricing, ...>,
30
29
  # ...
31
30
  #
32
- # @sig Hash[String, [Module, Symbol]]
31
+ # @sig Hash[String, Zeitwerk::Cref]
33
32
  attr_reader :autoloads
34
33
  internal :autoloads
35
34
 
@@ -45,17 +44,19 @@ module Zeitwerk
45
44
 
46
45
  # Stores metadata needed for unloading. Its entries look like this:
47
46
  #
48
- # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
47
+ # "Admin::Role" => [
48
+ # ".../admin/role.rb",
49
+ # #<Zeitwerk::Cref:... @mod=Admin, @cname=:Role, ...>
50
+ # ]
49
51
  #
50
52
  # The cpath as key helps implementing unloadable_cpath? The file name is
51
53
  # stored in order to be able to delete it from $LOADED_FEATURES, and the
52
- # pair [Module, Symbol] is used to remove_const the constant from the class
53
- # or module object.
54
+ # cref is used to remove the constant from the parent class or module.
54
55
  #
55
56
  # If reloading is enabled, this hash is filled as constants are autoloaded
56
57
  # or eager loaded. Otherwise, the collection remains empty.
57
58
  #
58
- # @sig Hash[String, [String, [Module, Symbol]]]
59
+ # @sig Hash[String, [String, Zeitwerk::Cref]]
59
60
  attr_reader :to_unload
60
61
  internal :to_unload
61
62
 
@@ -154,22 +155,22 @@ module Zeitwerk
154
155
  # is enough.
155
156
  unloaded_files = Set.new
156
157
 
157
- autoloads.each do |abspath, (parent, cname)|
158
- if parent.autoload?(cname)
159
- unload_autoload(parent, cname)
158
+ autoloads.each do |abspath, cref|
159
+ if cref.autoload?
160
+ unload_autoload(cref)
160
161
  else
161
162
  # Could happen if loaded with require_relative. That is unsupported,
162
163
  # and the constant path would escape unloadable_cpath? This is just
163
164
  # defensive code to clean things up as much as we are able to.
164
- unload_cref(parent, cname)
165
+ unload_cref(cref)
165
166
  unloaded_files.add(abspath) if ruby?(abspath)
166
167
  end
167
168
  end
168
169
 
169
- to_unload.each do |cpath, (abspath, (parent, cname))|
170
+ to_unload.each do |cpath, (abspath, cref)|
170
171
  unless on_unload_callbacks.empty?
171
172
  begin
172
- value = cget(parent, cname)
173
+ value = cref.get
173
174
  rescue ::NameError
174
175
  # Perhaps the user deleted the constant by hand, or perhaps an
175
176
  # autoload failed to define the expected constant but the user
@@ -179,7 +180,7 @@ module Zeitwerk
179
180
  end
180
181
  end
181
182
 
182
- unload_cref(parent, cname)
183
+ unload_cref(cref)
183
184
  unloaded_files.add(abspath) if ruby?(abspath)
184
185
  end
185
186
 
@@ -230,53 +231,86 @@ module Zeitwerk
230
231
  setup
231
232
  end
232
233
 
233
- # @sig (String | Pathname) -> String?
234
- def cpath_expected_at(path)
235
- abspath = File.expand_path(path)
234
+ # Returns a hash that maps the absolute paths of the managed files and
235
+ # directories to their respective expected constant paths.
236
+ #
237
+ # @sig () -> Hash[String, String]
238
+ def all_expected_cpaths
239
+ result = {}
236
240
 
237
- raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
241
+ actual_roots.each do |root_dir, root_namespace|
242
+ queue = [[root_dir, real_mod_name(root_namespace)]]
238
243
 
239
- return unless dir?(abspath) || ruby?(abspath)
240
- return if ignored_path?(abspath)
244
+ while (dir, cpath = queue.shift)
245
+ result[dir] = cpath
241
246
 
242
- paths = []
247
+ prefix = cpath == "Object" ? "" : cpath + "::"
243
248
 
244
- if ruby?(abspath)
245
- basename = File.basename(abspath, ".rb")
246
- return if hidden?(basename)
249
+ ls(dir) do |basename, abspath, ftype|
250
+ if ftype == :file
251
+ basename.delete_suffix!(".rb")
252
+ result[abspath] = prefix + inflector.camelize(basename, abspath)
253
+ else
254
+ if collapse?(abspath)
255
+ queue << [abspath, cpath]
256
+ else
257
+ queue << [abspath, prefix + inflector.camelize(basename, abspath)]
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
247
263
 
248
- paths << [basename, abspath]
249
- walk_up_from = File.dirname(abspath)
250
- else
251
- walk_up_from = abspath
264
+ result
252
265
  end
253
266
 
254
- root_namespace = nil
267
+ # @sig (String | Pathname) -> String?
268
+ def cpath_expected_at(path)
269
+ abspath = File.expand_path(path)
255
270
 
256
- walk_up(walk_up_from) do |dir|
257
- break if root_namespace = roots[dir]
258
- return if ignored_path?(dir)
271
+ raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
259
272
 
260
- basename = File.basename(dir)
261
- return if hidden?(basename)
273
+ return unless dir?(abspath) || ruby?(abspath)
274
+ return if ignored_path?(abspath)
262
275
 
263
- paths << [basename, abspath] unless collapse?(dir)
264
- end
276
+ paths = []
265
277
 
266
- return unless root_namespace
278
+ if ruby?(abspath)
279
+ basename = File.basename(abspath, ".rb")
280
+ return if hidden?(basename)
267
281
 
268
- if paths.empty?
269
- real_mod_name(root_namespace)
270
- else
271
- cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
282
+ paths << [basename, abspath]
283
+ walk_up_from = File.dirname(abspath)
284
+ else
285
+ walk_up_from = abspath
286
+ end
272
287
 
273
- if root_namespace == Object
274
- cnames.join("::")
288
+ root_namespace = nil
289
+
290
+ walk_up(walk_up_from) do |dir|
291
+ break if root_namespace = roots[dir]
292
+ return if ignored_path?(dir)
293
+
294
+ basename = File.basename(dir)
295
+ return if hidden?(basename)
296
+
297
+ paths << [basename, abspath] unless collapse?(dir)
298
+ end
299
+
300
+ return unless root_namespace
301
+
302
+ if paths.empty?
303
+ real_mod_name(root_namespace)
275
304
  else
276
- "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
305
+ cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
306
+
307
+ if root_namespace == Object
308
+ cnames.join("::")
309
+ else
310
+ "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
311
+ end
277
312
  end
278
313
  end
279
- end
280
314
 
281
315
  # Says if the given constant path would be unloaded on reload. This
282
316
  # predicate returns `false` if reloading is disabled.
@@ -408,24 +442,25 @@ module Zeitwerk
408
442
 
409
443
  # @sig (String, Module) -> void
410
444
  private def define_autoloads_for_dir(dir, parent)
411
- ls(dir) do |basename, abspath|
412
- if ruby?(basename)
445
+ ls(dir) do |basename, abspath, ftype|
446
+ if ftype == :file
413
447
  basename.delete_suffix!(".rb")
414
- autoload_file(parent, cname_for(basename, abspath), abspath)
448
+ cref = Cref.new(parent, cname_for(basename, abspath))
449
+ autoload_file(cref, abspath)
415
450
  else
416
451
  if collapse?(abspath)
417
452
  define_autoloads_for_dir(abspath, parent)
418
453
  else
419
- autoload_subdir(parent, cname_for(basename, abspath), abspath)
454
+ cref = Cref.new(parent, cname_for(basename, abspath))
455
+ autoload_subdir(cref, abspath)
420
456
  end
421
457
  end
422
458
  end
423
459
  end
424
460
 
425
461
  # @sig (Module, Symbol, String) -> void
426
- private def autoload_subdir(parent, cname, subdir)
427
- if autoload_path = autoload_path_set_by_me_for?(parent, cname)
428
- cpath = cpath(parent, cname)
462
+ private def autoload_subdir(cref, subdir)
463
+ if autoload_path = autoload_path_set_by_me_for?(cref)
429
464
  if ruby?(autoload_path)
430
465
  # Scanning visited a Ruby file first, and now a directory for the same
431
466
  # constant has been found. This means we are dealing with an explicit
@@ -434,94 +469,91 @@ module Zeitwerk
434
469
  # Registering is idempotent, and we have to keep the autoload pointing
435
470
  # to the file. This may run again if more directories are found later
436
471
  # on, no big deal.
437
- register_explicit_namespace(cpath)
472
+ register_explicit_namespace(cref)
438
473
  end
439
474
  # If the existing autoload points to a file, it has to be preserved, if
440
475
  # not, it is fine as it is. In either case, we do not need to override.
441
476
  # Just remember the subdirectory conforms this namespace.
442
- namespace_dirs[cpath] << subdir
443
- elsif !cdef?(parent, cname)
477
+ namespace_dirs[cref.path] << subdir
478
+ elsif !cref.defined?
444
479
  # First time we find this namespace, set an autoload for it.
445
- namespace_dirs[cpath(parent, cname)] << subdir
446
- define_autoload(parent, cname, subdir)
480
+ namespace_dirs[cref.path] << subdir
481
+ define_autoload(cref, subdir)
447
482
  else
448
483
  # For whatever reason the constant that corresponds to this namespace has
449
484
  # already been defined, we have to recurse.
450
- log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
451
- define_autoloads_for_dir(subdir, cget(parent, cname))
485
+ log("the namespace #{cref.path} already exists, descending into #{subdir}") if logger
486
+ define_autoloads_for_dir(subdir, cref.get)
452
487
  end
453
488
  end
454
489
 
455
490
  # @sig (Module, Symbol, String) -> void
456
- private def autoload_file(parent, cname, file)
457
- if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
491
+ private def autoload_file(cref, file)
492
+ if autoload_path = cref.autoload? || Registry.inception?(cref.path)
458
493
  # First autoload for a Ruby file wins, just ignore subsequent ones.
459
494
  if ruby?(autoload_path)
460
495
  shadowed_files << file
461
496
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
462
497
  else
463
- promote_namespace_from_implicit_to_explicit(
464
- dir: autoload_path,
465
- file: file,
466
- parent: parent,
467
- cname: cname
468
- )
498
+ promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
469
499
  end
470
- elsif cdef?(parent, cname)
500
+ elsif cref.defined?
471
501
  shadowed_files << file
472
- log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
502
+ log("file #{file} is ignored because #{cref.path} is already defined") if logger
473
503
  else
474
- define_autoload(parent, cname, file)
504
+ define_autoload(cref, file)
475
505
  end
476
506
  end
477
507
 
478
508
  # `dir` is the directory that would have autovivified a namespace. `file` is
479
509
  # the file where we've found the namespace is explicitly defined.
480
510
  #
481
- # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
482
- private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
511
+ # @sig (dir: String, file: String, cref: Zeitwerk::Cref) -> void
512
+ private def promote_namespace_from_implicit_to_explicit(dir:, file:, cref:)
483
513
  autoloads.delete(dir)
484
514
  Registry.unregister_autoload(dir)
485
515
 
486
- log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
516
+ log("earlier autoload for #{cref.path} discarded, it is actually an explicit namespace defined in #{file}") if logger
487
517
 
488
- define_autoload(parent, cname, file)
489
- register_explicit_namespace(cpath(parent, cname))
518
+ # Order matters: When Module#const_added is triggered by the autoload, we
519
+ # don't want the namespace to be registered yet.
520
+ define_autoload(cref, file)
521
+ register_explicit_namespace(cref)
490
522
  end
491
523
 
492
524
  # @sig (Module, Symbol, String) -> void
493
- private def define_autoload(parent, cname, abspath)
494
- parent.autoload(cname, abspath)
525
+ private def define_autoload(cref, abspath)
526
+ cref.autoload(abspath)
495
527
 
496
528
  if logger
497
529
  if ruby?(abspath)
498
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
530
+ log("autoload set for #{cref.path}, to be loaded from #{abspath}")
499
531
  else
500
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
532
+ log("autoload set for #{cref.path}, to be autovivified from #{abspath}")
501
533
  end
502
534
  end
503
535
 
504
- autoloads[abspath] = [parent, cname]
536
+ autoloads[abspath] = cref
505
537
  Registry.register_autoload(self, abspath)
506
538
 
507
539
  # See why in the documentation of Zeitwerk::Registry.inceptions.
508
- unless parent.autoload?(cname)
509
- Registry.register_inception(cpath(parent, cname), abspath, self)
540
+ unless cref.autoload?
541
+ Registry.register_inception(cref.path, abspath, self)
510
542
  end
511
543
  end
512
544
 
513
545
  # @sig (Module, Symbol) -> String?
514
- private def autoload_path_set_by_me_for?(parent, cname)
515
- if autoload_path = strict_autoload_path(parent, cname)
546
+ private def autoload_path_set_by_me_for?(cref)
547
+ if autoload_path = cref.autoload?
516
548
  autoload_path if autoloads.key?(autoload_path)
517
549
  else
518
- Registry.inception?(cpath(parent, cname))
550
+ Registry.inception?(cref.path, self)
519
551
  end
520
552
  end
521
553
 
522
- # @sig (String) -> void
523
- private def register_explicit_namespace(cpath)
524
- ExplicitNamespace.__register(cpath, self)
554
+ # @sig (Zeitwerk::Cref) -> void
555
+ private def register_explicit_namespace(cref)
556
+ ExplicitNamespace.__register(cref, self)
525
557
  end
526
558
 
527
559
  # @sig (String) -> void
@@ -556,21 +588,21 @@ module Zeitwerk
556
588
  end
557
589
 
558
590
  # @sig (Module, Symbol) -> void
559
- private def unload_autoload(parent, cname)
560
- crem(parent, cname)
561
- log("autoload for #{cpath(parent, cname)} removed") if logger
591
+ private def unload_autoload(cref)
592
+ cref.remove
593
+ log("autoload for #{cref.path} removed") if logger
562
594
  end
563
595
 
564
596
  # @sig (Module, Symbol) -> void
565
- private def unload_cref(parent, cname)
597
+ private def unload_cref(cref)
566
598
  # Let's optimistically remove_const. The way we use it, this is going to
567
599
  # succeed always if all is good.
568
- crem(parent, cname)
600
+ cref.remove
569
601
  rescue ::NameError
570
602
  # There are a few edge scenarios in which this may happen. If the constant
571
603
  # is gone, that is OK, anyway.
572
604
  else
573
- log("#{cpath(parent, cname)} unloaded") if logger
605
+ log("#{cref.path} unloaded") if logger
574
606
  end
575
607
  end
576
608
  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
@@ -110,9 +110,12 @@ module Zeitwerk
110
110
 
111
111
  # @private
112
112
  # @sig (String) -> String?
113
- def inception?(cpath)
113
+ def inception?(cpath, registered_by_loader=nil)
114
114
  if pair = inceptions[cpath]
115
- pair.first
115
+ abspath, loader = pair
116
+ if registered_by_loader.nil? || registered_by_loader.equal?(loader)
117
+ abspath
118
+ end
116
119
  end
117
120
  end
118
121
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.6.13"
4
+ VERSION = "2.7.1"
5
5
  end
data/lib/zeitwerk.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  module Zeitwerk
4
4
  require_relative "zeitwerk/real_mod_name"
5
5
  require_relative "zeitwerk/internal"
6
+ require_relative "zeitwerk/cref"
6
7
  require_relative "zeitwerk/loader"
7
8
  require_relative "zeitwerk/gem_loader"
8
9
  require_relative "zeitwerk/registry"
@@ -10,10 +11,12 @@ module Zeitwerk
10
11
  require_relative "zeitwerk/inflector"
11
12
  require_relative "zeitwerk/gem_inflector"
12
13
  require_relative "zeitwerk/null_inflector"
13
- require_relative "zeitwerk/kernel"
14
14
  require_relative "zeitwerk/error"
15
15
  require_relative "zeitwerk/version"
16
16
 
17
+ require_relative "zeitwerk/core_ext/kernel"
18
+ require_relative "zeitwerk/core_ext/module"
19
+
17
20
  # This is a dangerous method.
18
21
  #
19
22
  # @experimental
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.13
4
+ version: 2.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-06 00:00:00.000000000 Z
11
+ date: 2024-10-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -23,13 +23,15 @@ files:
23
23
  - MIT-LICENSE
24
24
  - README.md
25
25
  - lib/zeitwerk.rb
26
+ - lib/zeitwerk/core_ext/kernel.rb
27
+ - lib/zeitwerk/core_ext/module.rb
28
+ - lib/zeitwerk/cref.rb
26
29
  - lib/zeitwerk/error.rb
27
30
  - lib/zeitwerk/explicit_namespace.rb
28
31
  - lib/zeitwerk/gem_inflector.rb
29
32
  - lib/zeitwerk/gem_loader.rb
30
33
  - lib/zeitwerk/inflector.rb
31
34
  - lib/zeitwerk/internal.rb
32
- - lib/zeitwerk/kernel.rb
33
35
  - lib/zeitwerk/loader.rb
34
36
  - lib/zeitwerk/loader/callbacks.rb
35
37
  - lib/zeitwerk/loader/config.rb
@@ -55,14 +57,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
55
57
  requirements:
56
58
  - - ">="
57
59
  - !ruby/object:Gem::Version
58
- version: '2.5'
60
+ version: '3.2'
59
61
  required_rubygems_version: !ruby/object:Gem::Requirement
60
62
  requirements:
61
63
  - - ">="
62
64
  - !ruby/object:Gem::Version
63
65
  version: '0'
64
66
  requirements: []
65
- rubygems_version: 3.5.5
67
+ rubygems_version: 3.5.21
66
68
  signing_key:
67
69
  specification_version: 4
68
70
  summary: Efficient and thread-safe constant autoloader
File without changes