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 +4 -4
- data/README.md +58 -45
- data/lib/zeitwerk/core_ext/module.rb +19 -0
- data/lib/zeitwerk/cref.rb +68 -0
- data/lib/zeitwerk/explicit_namespace.rb +79 -59
- data/lib/zeitwerk/gem_loader.rb +1 -2
- data/lib/zeitwerk/loader/callbacks.rb +15 -15
- data/lib/zeitwerk/loader/eager_load.rb +13 -15
- data/lib/zeitwerk/loader/helpers.rb +23 -64
- data/lib/zeitwerk/loader.rb +128 -96
- data/lib/zeitwerk/real_mod_name.rb +2 -8
- data/lib/zeitwerk/registry.rb +5 -2
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +4 -1
- metadata +7 -5
- /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: aba46812169c8e26085b099c708093b00a29bfd39e8d8210d29bc99e7df0e7fa
|
4
|
+
data.tar.gz: 0fc20386009d52d21a0cb79a64c5d800f7aecfce5a38e8430a10dc5d04c2609d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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,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
|
-
#
|
5
|
-
#
|
6
|
-
#
|
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
|
-
#
|
17
|
-
#
|
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(
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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) ->
|
53
|
-
internal def registered?(
|
54
|
-
|
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
|
-
|
59
|
-
|
60
|
-
tracer.disable if cpaths.empty?
|
61
|
-
end
|
80
|
+
internal def clear
|
81
|
+
@loaders.clear
|
62
82
|
end
|
63
83
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
87
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
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
|
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}
|
@@ -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
|
12
|
-
cpath = cpath(*cref)
|
12
|
+
cref = autoloads.delete(file)
|
13
13
|
|
14
14
|
Zeitwerk::Registry.unregister_autoload(file)
|
15
15
|
|
16
|
-
if
|
17
|
-
log("constant #{
|
18
|
-
to_unload[
|
19
|
-
run_on_load_callbacks(
|
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 #{
|
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
|
-
|
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[
|
31
|
+
to_unload[cref.path] = [file, cref] if reloading_enabled?
|
32
32
|
|
33
|
-
raise Zeitwerk::NameError.new(msg, cref.
|
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
|
-
|
57
|
-
cpath =
|
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(
|
68
|
+
on_namespace_loaded(implicit_namespace)
|
69
69
|
|
70
|
-
run_on_load_callbacks(cpath,
|
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
|
-
#
|
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
|
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
|
@@ -30,27 +30,43 @@ module Zeitwerk::Loader::Helpers
|
|
30
30
|
|
31
31
|
if dir?(abspath)
|
32
32
|
next if roots.key?(abspath)
|
33
|
-
|
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
|
-
|
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
|
-
# ---
|
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
|
data/lib/zeitwerk/loader.rb
CHANGED
@@ -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
|
26
|
-
# name.
|
25
|
+
# executed--- to their corresponding Zeitwerk::Cref object.
|
27
26
|
#
|
28
|
-
# "/Users/fxn/blog/app/models/user.rb" =>
|
29
|
-
# "/Users/fxn/blog/app/models/hotel/pricing.rb" =>
|
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,
|
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" => [
|
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
|
-
#
|
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,
|
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,
|
158
|
-
if
|
159
|
-
unload_autoload(
|
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(
|
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,
|
170
|
+
to_unload.each do |cpath, (abspath, cref)|
|
170
171
|
unless on_unload_callbacks.empty?
|
171
172
|
begin
|
172
|
-
value =
|
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(
|
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
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
241
|
+
actual_roots.each do |root_dir, root_namespace|
|
242
|
+
queue = [[root_dir, real_mod_name(root_namespace)]]
|
238
243
|
|
239
|
-
|
240
|
-
|
244
|
+
while (dir, cpath = queue.shift)
|
245
|
+
result[dir] = cpath
|
241
246
|
|
242
|
-
|
247
|
+
prefix = cpath == "Object" ? "" : cpath + "::"
|
243
248
|
|
244
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
249
|
-
walk_up_from = File.dirname(abspath)
|
250
|
-
else
|
251
|
-
walk_up_from = abspath
|
264
|
+
result
|
252
265
|
end
|
253
266
|
|
254
|
-
|
267
|
+
# @sig (String | Pathname) -> String?
|
268
|
+
def cpath_expected_at(path)
|
269
|
+
abspath = File.expand_path(path)
|
255
270
|
|
256
|
-
|
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
|
-
|
261
|
-
return if
|
273
|
+
return unless dir?(abspath) || ruby?(abspath)
|
274
|
+
return if ignored_path?(abspath)
|
262
275
|
|
263
|
-
paths
|
264
|
-
end
|
276
|
+
paths = []
|
265
277
|
|
266
|
-
|
278
|
+
if ruby?(abspath)
|
279
|
+
basename = File.basename(abspath, ".rb")
|
280
|
+
return if hidden?(basename)
|
267
281
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
282
|
+
paths << [basename, abspath]
|
283
|
+
walk_up_from = File.dirname(abspath)
|
284
|
+
else
|
285
|
+
walk_up_from = abspath
|
286
|
+
end
|
272
287
|
|
273
|
-
|
274
|
-
|
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
|
-
|
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
|
445
|
+
ls(dir) do |basename, abspath, ftype|
|
446
|
+
if ftype == :file
|
413
447
|
basename.delete_suffix!(".rb")
|
414
|
-
|
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
|
-
|
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(
|
427
|
-
if autoload_path = autoload_path_set_by_me_for?(
|
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(
|
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[
|
443
|
-
elsif !
|
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[
|
446
|
-
define_autoload(
|
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 #{
|
451
|
-
define_autoloads_for_dir(subdir,
|
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(
|
457
|
-
if autoload_path =
|
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
|
500
|
+
elsif cref.defined?
|
471
501
|
shadowed_files << file
|
472
|
-
log("file #{file} is ignored because #{
|
502
|
+
log("file #{file} is ignored because #{cref.path} is already defined") if logger
|
473
503
|
else
|
474
|
-
define_autoload(
|
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,
|
482
|
-
private def promote_namespace_from_implicit_to_explicit(dir:, file:,
|
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 #{
|
516
|
+
log("earlier autoload for #{cref.path} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
487
517
|
|
488
|
-
|
489
|
-
|
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(
|
494
|
-
|
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 #{
|
530
|
+
log("autoload set for #{cref.path}, to be loaded from #{abspath}")
|
499
531
|
else
|
500
|
-
log("autoload set for #{
|
532
|
+
log("autoload set for #{cref.path}, to be autovivified from #{abspath}")
|
501
533
|
end
|
502
534
|
end
|
503
535
|
|
504
|
-
autoloads[abspath] =
|
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
|
509
|
-
Registry.register_inception(
|
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?(
|
515
|
-
if autoload_path =
|
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?(
|
550
|
+
Registry.inception?(cref.path, self)
|
519
551
|
end
|
520
552
|
end
|
521
553
|
|
522
|
-
# @sig (
|
523
|
-
private def register_explicit_namespace(
|
524
|
-
ExplicitNamespace.__register(
|
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(
|
560
|
-
|
561
|
-
log("autoload for #{
|
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(
|
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
|
-
|
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("#{
|
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
|
-
|
14
|
-
|
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
|
data/lib/zeitwerk/registry.rb
CHANGED
@@ -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
|
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
|
|
data/lib/zeitwerk/version.rb
CHANGED
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.
|
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-
|
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
|
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.
|
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
|