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 +4 -4
- data/README.md +58 -45
- data/lib/zeitwerk/core_ext/module.rb +20 -0
- data/lib/zeitwerk/cref/map.rb +124 -0
- data/lib/zeitwerk/cref.rb +71 -0
- data/lib/zeitwerk/gem_loader.rb +1 -2
- data/lib/zeitwerk/internal.rb +1 -0
- data/lib/zeitwerk/loader/callbacks.rb +23 -25
- data/lib/zeitwerk/loader/config.rb +8 -8
- data/lib/zeitwerk/loader/eager_load.rb +13 -15
- data/lib/zeitwerk/loader/helpers.rb +23 -64
- data/lib/zeitwerk/loader.rb +196 -131
- data/lib/zeitwerk/null_inflector.rb +1 -0
- data/lib/zeitwerk/real_mod_name.rb +2 -8
- data/lib/zeitwerk/registry/explicit_namespaces.rb +64 -0
- data/lib/zeitwerk/registry/inceptions.rb +31 -0
- data/lib/zeitwerk/registry.rb +3 -56
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +4 -2
- metadata +10 -9
- data/lib/zeitwerk/explicit_namespace.rb +0 -93
- /data/lib/zeitwerk/{kernel.rb → core_ext/kernel.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a07f90eb2f155582d05f58527ffcbc2f4d76c9a1983260ca8d527becaeb7972
|
4
|
+
data.tar.gz: 65e8dc78ca8e6de674f0fc7d88aad5c9bad0d7687bc9ed26f93d6fa0e6d18e90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
-
|
1387
|
+
Starting with version 2.7, Zeitwerk requires Ruby 3.2 or newer.
|
1378
1388
|
|
1379
|
-
|
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
|
data/lib/zeitwerk/gem_loader.rb
CHANGED
@@ -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}
|
data/lib/zeitwerk/internal.rb
CHANGED
@@ -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
|
12
|
-
cpath = cpath(*cref)
|
11
|
+
cref = autoloads.delete(file)
|
13
12
|
|
14
13
|
Zeitwerk::Registry.unregister_autoload(file)
|
15
14
|
|
16
|
-
if
|
17
|
-
log("constant #{
|
18
|
-
to_unload[
|
19
|
-
run_on_load_callbacks(
|
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 #{
|
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
|
-
|
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[
|
30
|
+
to_unload[file] = cref if reloading_enabled?
|
32
31
|
|
33
|
-
raise Zeitwerk::NameError.new(msg, cref.
|
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
|
-
|
57
|
-
cpath =
|
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[
|
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(
|
67
|
+
on_namespace_loaded(cref, implicit_namespace)
|
69
68
|
|
70
|
-
run_on_load_callbacks(cpath,
|
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
|
76
|
-
#
|
77
|
-
#
|
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
|
-
# @
|
80
|
-
|
81
|
-
|
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,
|
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[{ (
|
75
|
-
# Hash[Symbol, Array[{ (String,
|
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[{ (
|
82
|
-
# Hash[Symbol, Array[{ (String,
|
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) { (
|
251
|
-
# (:ANY) { (String,
|
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) { (
|
276
|
-
# (:ANY) { (String,
|
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
|
65
|
-
namespace =
|
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
|
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 =
|
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
|
-
|
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
|
168
|
-
|
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
|
171
|
+
if ftype == :file
|
174
172
|
if (cref = autoloads[abspath])
|
175
|
-
|
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,
|
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
|
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
|