zeitwerk 2.6.13 → 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: 5d4ce4eb7136dafe17130fc5d895e525d80ae947f02dd9420b5bc85a4d6f0dfd
4
- data.tar.gz: edcac638955fef258ad36a358b78da6831a715ed46446848e39f9f1d8fb7de0b
3
+ metadata.gz: 1a07f90eb2f155582d05f58527ffcbc2f4d76c9a1983260ca8d527becaeb7972
4
+ data.tar.gz: 65e8dc78ca8e6de674f0fc7d88aad5c9bad0d7687bc9ed26f93d6fa0e6d18e90
5
5
  SHA512:
6
- metadata.gz: f0a6e64c56635c9c6a8c9b24bdeb7509e4a7f14489f32aae18b02221c23b95b34096184c78d2d1509130ce75031ed0f3a7751b78e43d9b72122440f07cf980bc
7
- data.tar.gz: 06440147c101a95ac2c8b7dcd43920a3efc3da2f58b902c157730a449d57b6ef74e0d3db7f3d2735be816a74ceb774e2d4075741556c7e2ba7fa00b8130c1723
6
+ metadata.gz: d7b9d13e3d3d5bf0497ec259bf0817256586e245f7d47c951b8392784f715bc71c20d0ec3c9e465077da6d8e729ca6888fbaaa24820fe4459771e29340ee6d05
7
+ data.tar.gz: 8b1322d36bc9115a56b6abab6be9549c868e0edd2025fe82dd2c5d0abb082fac8532c82ed03f895d34f2875f27b160f4861185112c4aef2a3129e46569115c0f
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,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
@@ -0,0 +1,71 @@
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. 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
+ require_relative "cref/map"
15
+
16
+ include Zeitwerk::RealModName
17
+
18
+ # @sig Module
19
+ attr_reader :mod
20
+
21
+ # @sig Symbol
22
+ attr_reader :cname
23
+
24
+ # The type of the first argument is Module because Class < Module, class
25
+ # objects are also valid.
26
+ #
27
+ # @sig (Module, Symbol) -> void
28
+ def initialize(mod, cname)
29
+ @mod = mod
30
+ @cname = cname
31
+ @path = nil
32
+ end
33
+
34
+ # @sig () -> String
35
+ def path
36
+ @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}".freeze
37
+ end
38
+ alias to_s path
39
+
40
+ # @sig () -> String?
41
+ def autoload?
42
+ @mod.autoload?(@cname, false)
43
+ end
44
+
45
+ # @sig (String) -> bool
46
+ def autoload(abspath)
47
+ @mod.autoload(@cname, abspath)
48
+ end
49
+
50
+ # @sig () -> bool
51
+ def defined?
52
+ @mod.const_defined?(@cname, false)
53
+ end
54
+
55
+ # @sig (top) -> top
56
+ def set(value)
57
+ @mod.const_set(@cname, value)
58
+ end
59
+
60
+ # @raise [NameError]
61
+ # @sig () -> top
62
+ def get
63
+ @mod.const_get(@cname, false)
64
+ end
65
+
66
+ # @raise [NameError]
67
+ # @sig () -> void
68
+ def remove
69
+ @mod.__send__(:remove_const, @cname)
70
+ end
71
+ 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}
@@ -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,36 +1,35 @@
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
- cref = autoloads.delete(file)
12
- cpath = cpath(*cref)
11
+ cref = autoloads.delete(file)
13
12
 
14
13
  Zeitwerk::Registry.unregister_autoload(file)
15
14
 
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?
15
+ if cref.defined?
16
+ log("constant #{cref} loaded from file #{file}") if logger
17
+ to_unload[file] = cref if reloading_enabled?
18
+ run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
20
19
  else
21
- msg = "expected file #{file} to define constant #{cpath}, but didn't"
20
+ msg = "expected file #{file} to define constant #{cref}, but didn't"
22
21
  log(msg) if logger
23
22
 
24
23
  # Ruby still keeps the autoload defined, but we remove it because the
25
24
  # contract in Zeitwerk is more strict.
26
- crem(*cref)
25
+ cref.remove
27
26
 
28
27
  # Since the expected constant was not defined, there is nothing to unload.
29
28
  # However, if the exception is rescued and reloading is enabled, we still
30
29
  # need to deleted the file from $LOADED_FEATURES.
31
- to_unload[cpath] = [file, cref] if reloading_enabled?
30
+ to_unload[file] = cref if reloading_enabled?
32
31
 
33
- raise Zeitwerk::NameError.new(msg, cref.last)
32
+ raise Zeitwerk::NameError.new(msg, cref.cname)
34
33
  end
35
34
  end
36
35
 
@@ -53,11 +52,11 @@ module Zeitwerk::Loader::Callbacks
53
52
  # children, since t1 would have correctly deleted its namespace_dirs entry.
54
53
  dirs_autoload_monitor.synchronize do
55
54
  if cref = autoloads.delete(dir)
56
- autovivified_module = cref[0].const_set(cref[1], Module.new)
57
- cpath = autovivified_module.name
55
+ implicit_namespace = cref.set(Module.new)
56
+ cpath = implicit_namespace.name
58
57
  log("module #{cpath} autovivified from directory #{dir}") if logger
59
58
 
60
- to_unload[cpath] = [dir, cref] if reloading_enabled?
59
+ to_unload[dir] = cref if reloading_enabled?
61
60
 
62
61
  # We don't unregister `dir` in the registry because concurrent threads
63
62
  # wouldn't find a loader associated to it in Kernel#require and would
@@ -65,21 +64,20 @@ module Zeitwerk::Loader::Callbacks
65
64
  # these to be able to unregister later if eager loading.
66
65
  autoloaded_dirs << dir
67
66
 
68
- on_namespace_loaded(autovivified_module)
67
+ on_namespace_loaded(cref, implicit_namespace)
69
68
 
70
- run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
69
+ run_on_load_callbacks(cpath, implicit_namespace, dir) unless on_load_callbacks.empty?
71
70
  end
72
71
  end
73
72
  end
74
73
 
75
- # Invoked when a class or module is created or reopened, either from the
76
- # tracer or from module autovivification. If the namespace has matching
77
- # subdirectories, we descend into them now.
74
+ # Invoked when a namespace is created, either from const_added or from module
75
+ # autovivification. If the namespace has matching subdirectories, we descend
76
+ # into them now.
78
77
  #
79
- # @private
80
- # @sig (Module) -> void
81
- def on_namespace_loaded(namespace)
82
- if dirs = namespace_dirs.delete(real_mod_name(namespace))
78
+ # @sig (Zeitwerk::Cref, Module) -> void
79
+ internal def on_namespace_loaded(cref, namespace)
80
+ if dirs = namespace_dirs.delete(cref)
83
81
  dirs.each do |dir|
84
82
  define_autoloads_for_dir(dir, namespace)
85
83
  end
@@ -88,7 +86,7 @@ module Zeitwerk::Loader::Callbacks
88
86
 
89
87
  private
90
88
 
91
- # @sig (String, Object) -> void
89
+ # @sig (String, top, String) -> void
92
90
  def run_on_load_callbacks(cpath, value, abspath)
93
91
  # Order matters. If present, run the most specific one.
94
92
  callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
@@ -71,15 +71,15 @@ module Zeitwerk::Loader::Config
71
71
 
72
72
  # User-oriented callbacks to be fired when a constant is loaded.
73
73
  #
74
- # @sig Hash[String, Array[{ (Object, String) -> void }]]
75
- # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
74
+ # @sig Hash[String, Array[{ (top, String) -> void }]]
75
+ # Hash[Symbol, Array[{ (String, top, String) -> void }]]
76
76
  attr_reader :on_load_callbacks
77
77
  private :on_load_callbacks
78
78
 
79
79
  # User-oriented callbacks to be fired before constants are removed.
80
80
  #
81
- # @sig Hash[String, Array[{ (Object, String) -> void }]]
82
- # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
81
+ # @sig Hash[String, Array[{ (top, String) -> void }]]
82
+ # Hash[Symbol, Array[{ (String, top, String) -> void }]]
83
83
  attr_reader :on_unload_callbacks
84
84
  private :on_unload_callbacks
85
85
 
@@ -247,8 +247,8 @@ module Zeitwerk::Loader::Config
247
247
  # end
248
248
  #
249
249
  # @raise [TypeError]
250
- # @sig (String) { (Object, String) -> void } -> void
251
- # (:ANY) { (String, Object, String) -> void } -> void
250
+ # @sig (String) { (top, String) -> void } -> void
251
+ # (:ANY) { (String, top, String) -> void } -> void
252
252
  def on_load(cpath = :ANY, &block)
253
253
  raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
254
254
 
@@ -272,8 +272,8 @@ module Zeitwerk::Loader::Config
272
272
  # end
273
273
  #
274
274
  # @raise [TypeError]
275
- # @sig (String) { (Object) -> void } -> void
276
- # (:ANY) { (String, Object) -> void } -> void
275
+ # @sig (String) { (top) -> void } -> void
276
+ # (:ANY) { (String, top) -> void } -> void
277
277
  def on_unload(cpath = :ANY, &block)
278
278
  raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
279
279
 
@@ -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