zeitwerk 2.6.13 → 2.6.17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d4ce4eb7136dafe17130fc5d895e525d80ae947f02dd9420b5bc85a4d6f0dfd
4
- data.tar.gz: edcac638955fef258ad36a358b78da6831a715ed46446848e39f9f1d8fb7de0b
3
+ metadata.gz: 51e7aed4aef39ce0a2caaea2f23852ea4f2e8fcb7b65819d14a4d92e7aec70d0
4
+ data.tar.gz: 7e310bac85d85018cb840339a42958b9fbc6fac566fee51e6a5c599a3cb132e3
5
5
  SHA512:
6
- metadata.gz: f0a6e64c56635c9c6a8c9b24bdeb7509e4a7f14489f32aae18b02221c23b95b34096184c78d2d1509130ce75031ed0f3a7751b78e43d9b72122440f07cf980bc
7
- data.tar.gz: 06440147c101a95ac2c8b7dcd43920a3efc3da2f58b902c157730a449d57b6ef74e0d3db7f3d2735be816a74ceb774e2d4075741556c7e2ba7fa00b8130c1723
6
+ metadata.gz: 0274e4685362f6585b9fb90291561926c8b45434ea3df71858ed99c5dfbffd54e814ed6dfcc4c6e8bfd8a9f266dbe4261bf6010856558474ad7e85045851e4cd
7
+ data.tar.gz: 210f457fad164472582fc67264bed2bd981612588bd3a8cdce265b2f2c67b12d10d9d2b9abb555dead008df20d5a73955072e703305e8f55b9d4dd5877d9b693
data/README.md CHANGED
@@ -60,6 +60,7 @@
60
60
  - [Introspection](#introspection)
61
61
  - [`Zeitwerk::Loader#dirs`](#zeitwerkloaderdirs)
62
62
  - [`Zeitwerk::Loader#cpath_expected_at`](#zeitwerkloadercpath_expected_at)
63
+ - [`Zeitwerk::Loader#all_expected_cpaths`](#zeitwerkloaderall_expected_cpaths)
63
64
  - [Encodings](#encodings)
64
65
  - [Rules of thumb](#rules-of-thumb)
65
66
  - [Debuggers](#debuggers)
@@ -259,7 +260,7 @@ app/controllers/admin/users_controller.rb -> Admin::UsersController
259
260
 
260
261
  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
262
 
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.
263
+ 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
264
 
264
265
  <a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
265
266
  ### Explicit namespaces
@@ -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
 
@@ -1330,6 +1331,54 @@ loader.cpath_expected_at("8.rb") # => Zeitwerk::NameError
1330
1331
 
1331
1332
  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
1333
 
1334
+ `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.
1335
+
1336
+ <a id="markdown-zeitwerkloaderall_expected_cpaths" name="zeitwerkloaderall_expected_cpaths"></a>
1337
+ #### `Zeitwerk::Loader#all_expected_cpaths`
1338
+
1339
+ 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.
1340
+
1341
+ 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.
1342
+
1343
+ For example, if `lib` is the root directory of a gem with the following contents:
1344
+
1345
+ ```
1346
+ lib/.DS_Store
1347
+ lib/my_gem.rb
1348
+ lib/my_gem/version.rb
1349
+ lib/my_gem/ignored.rb
1350
+ lib/my_gem/drivers/unix.rb
1351
+ lib/my_gem/drivers/windows.rb
1352
+ lib/my_gem/collapsed/foo.rb
1353
+ lib/tasks/my_gem.rake
1354
+ ```
1355
+
1356
+ `Zeitwerk::Loader#all_expected_cpaths` would return (maybe in a different order):
1357
+
1358
+ ```ruby
1359
+ {
1360
+ "/.../lib" => "Object",
1361
+ "/.../lib/my_gem.rb" => "MyGem",
1362
+ "/.../lib/my_gem" => "MyGem",
1363
+ "/.../lib/my_gem/version.rb" => "MyGem::VERSION",
1364
+ "/.../lib/my_gem/drivers" => "MyGem::Drivers",
1365
+ "/.../lib/my_gem/drivers/unix.rb" => "MyGem::Drivers::Unix",
1366
+ "/.../lib/my_gem/drivers/windows.rb" => "MyGem::Drivers::Windows",
1367
+ "/.../lib/my_gem/collapsed" => "MyGem"
1368
+ "/.../lib/my_gem/collapsed/foo.rb" => "MyGem::Foo"
1369
+ }
1370
+ ```
1371
+
1372
+ 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).
1373
+
1374
+ 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".
1375
+
1376
+ Directory paths do not have trailing slashes.
1377
+
1378
+ The order of the hash entries is undefined.
1379
+
1380
+ 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.
1381
+
1333
1382
  <a id="markdown-encodings" name="encodings"></a>
1334
1383
  ### Encodings
1335
1384
 
@@ -0,0 +1,99 @@
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 Symbol
17
+ attr_reader :cname
18
+
19
+ # The type of the first argument is Module because Class < Module, class
20
+ # objects are also valid.
21
+ #
22
+ # @sig (Module, Symbol) -> void
23
+ def initialize(mod, cname)
24
+ @mod = mod
25
+ @cname = cname
26
+ @path = nil
27
+ end
28
+
29
+ if Symbol.method_defined?(:name)
30
+ # Symbol#name was introduced in Ruby 3.0. It returns always the same
31
+ # frozen object, so we may save a few string allocations.
32
+ #
33
+ # @sig () -> String
34
+ def path
35
+ @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}"
36
+ end
37
+ else
38
+ # @sig () -> String
39
+ def path
40
+ @path ||= Object.equal?(@mod) ? @cname.to_s : "#{real_mod_name(@mod)}::#{@cname}"
41
+ end
42
+ end
43
+
44
+ # The autoload? predicate takes into account the ancestor chain of the
45
+ # receiver, like const_defined? and other methods in the constants API do.
46
+ #
47
+ # For example, given
48
+ #
49
+ # class A
50
+ # autoload :X, "x.rb"
51
+ # end
52
+ #
53
+ # class B < A
54
+ # end
55
+ #
56
+ # B.autoload?(:X) returns "x.rb".
57
+ #
58
+ # We need a way to retrieve it ignoring ancestors.
59
+ #
60
+ # @sig () -> String?
61
+ if method(:autoload?).arity == 1
62
+ # @sig () -> String?
63
+ def autoload?
64
+ @mod.autoload?(@cname) if self.defined?
65
+ end
66
+ else
67
+ # @sig () -> String?
68
+ def autoload?
69
+ @mod.autoload?(@cname, false)
70
+ end
71
+ end
72
+
73
+ # @sig (String) -> bool
74
+ def autoload(abspath)
75
+ @mod.autoload(@cname, abspath)
76
+ end
77
+
78
+ # @sig () -> bool
79
+ def defined?
80
+ @mod.const_defined?(@cname, false)
81
+ end
82
+
83
+ # @sig (Object) -> Object
84
+ def set(value)
85
+ @mod.const_set(@cname, value)
86
+ end
87
+
88
+ # @raise [NameError]
89
+ # @sig () -> Object
90
+ def get
91
+ @mod.const_get(@cname, false)
92
+ end
93
+
94
+ # @raise [NameError]
95
+ # @sig () -> void
96
+ def remove
97
+ @mod.__send__(:remove_const, @cname)
98
+ end
99
+ 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}
@@ -8,29 +8,28 @@ module Zeitwerk::Loader::Callbacks
8
8
  #
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.path} loaded from file #{file}") if logger
17
+ to_unload[cref.path] = [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.path}, 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[cref.path] = [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,8 +52,8 @@ 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
59
  to_unload[cpath] = [dir, cref] if reloading_enabled?
@@ -65,9 +64,9 @@ 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(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
@@ -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
@@ -145,12 +145,12 @@ module Zeitwerk::Loader::EagerLoad
145
145
 
146
146
  namespace = root_namespace
147
147
  cnames.reverse_each do |cname|
148
- namespace = cget(namespace, cname)
148
+ namespace = namespace.const_get(cname, false)
149
149
  end
150
150
 
151
151
  raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
152
152
 
153
- cget(namespace, base_cname)
153
+ namespace.const_get(base_cname, false)
154
154
  end
155
155
 
156
156
  # The caller is responsible for making sure `namespace` is the namespace that
@@ -164,22 +164,20 @@ module Zeitwerk::Loader::EagerLoad
164
164
  log("eager load directory #{dir} start") if logger
165
165
 
166
166
  queue = [[dir, namespace]]
167
- while to_eager_load = queue.shift
168
- dir, namespace = to_eager_load
169
-
170
- ls(dir) do |basename, abspath|
167
+ while (current_dir, namespace = queue.shift)
168
+ ls(current_dir) do |basename, abspath, ftype|
171
169
  next if honour_exclusions && eager_load_exclusions.member?(abspath)
172
170
 
173
- if ruby?(abspath)
171
+ if ftype == :file
174
172
  if (cref = autoloads[abspath])
175
- cget(*cref)
173
+ cref.get
176
174
  end
177
175
  else
178
176
  if collapse?(abspath)
179
177
  queue << [abspath, namespace]
180
178
  else
181
179
  cname = inflector.camelize(basename, abspath).to_sym
182
- queue << [abspath, cget(namespace, cname)]
180
+ queue << [abspath, namespace.const_get(cname, false)]
183
181
  end
184
182
  end
185
183
  end
@@ -209,9 +207,9 @@ module Zeitwerk::Loader::EagerLoad
209
207
  next_dirs = []
210
208
 
211
209
  suffix.split("::").each do |segment|
212
- while dir = dirs.shift
213
- ls(dir) do |basename, abspath|
214
- next unless dir?(abspath)
210
+ while (dir = dirs.shift)
211
+ ls(dir) do |basename, abspath, ftype|
212
+ next unless ftype == :directory
215
213
 
216
214
  if collapse?(abspath)
217
215
  dirs << abspath
@@ -30,27 +30,45 @@ module Zeitwerk::Loader::Helpers
30
30
 
31
31
  if dir?(abspath)
32
32
  next if roots.key?(abspath)
33
- next if !has_at_least_one_ruby_file?(abspath)
33
+
34
+ if !has_at_least_one_ruby_file?(abspath)
35
+ log("directory #{abspath} is ignored because it has no Ruby files") if logger
36
+ next
37
+ end
38
+
39
+ ftype = :directory
34
40
  else
35
41
  next unless ruby?(abspath)
42
+ ftype = :file
36
43
  end
37
44
 
38
45
  # We freeze abspath because that saves allocations when passed later to
39
46
  # File methods. See #125.
40
- yield basename, abspath.freeze
47
+ yield basename, abspath.freeze, ftype
41
48
  end
42
49
  end
43
50
 
51
+ # Looks for a Ruby file using breadth-first search. This type of search is
52
+ # important to list as less directories as possible and return fast in the
53
+ # common case in which there are Ruby files.
54
+ #
44
55
  # @sig (String) -> bool
45
56
  private def has_at_least_one_ruby_file?(dir)
46
57
  to_visit = [dir]
47
58
 
48
- while dir = to_visit.shift
49
- ls(dir) do |_basename, abspath|
59
+ while (dir = to_visit.shift)
60
+ children = Dir.children(dir)
61
+
62
+ children.each do |basename|
63
+ next if hidden?(basename)
64
+
65
+ abspath = File.join(dir, basename)
66
+ next if ignored_path?(abspath)
67
+
50
68
  if dir?(abspath)
51
- to_visit << abspath
69
+ to_visit << abspath unless roots.key?(abspath)
52
70
  else
53
- return true
71
+ return true if ruby?(abspath)
54
72
  end
55
73
  end
56
74
  end
@@ -82,64 +100,7 @@ module Zeitwerk::Loader::Helpers
82
100
  end
83
101
  end
84
102
 
85
- # --- Constants ---------------------------------------------------------------------------------
86
-
87
- # The autoload? predicate takes into account the ancestor chain of the
88
- # receiver, like const_defined? and other methods in the constants API do.
89
- #
90
- # For example, given
91
- #
92
- # class A
93
- # autoload :X, "x.rb"
94
- # end
95
- #
96
- # class B < A
97
- # end
98
- #
99
- # B.autoload?(:X) returns "x.rb".
100
- #
101
- # We need a way to strictly check in parent ignoring ancestors.
102
- #
103
- # @sig (Module, Symbol) -> String?
104
- if method(:autoload?).arity == 1
105
- private def strict_autoload_path(parent, cname)
106
- parent.autoload?(cname) if cdef?(parent, cname)
107
- end
108
- else
109
- private def strict_autoload_path(parent, cname)
110
- parent.autoload?(cname, false)
111
- end
112
- end
113
-
114
- # @sig (Module, Symbol) -> String
115
- if Symbol.method_defined?(:name)
116
- # Symbol#name was introduced in Ruby 3.0. It returns always the same
117
- # frozen object, so we may save a few string allocations.
118
- private def cpath(parent, cname)
119
- Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
120
- end
121
- else
122
- private def cpath(parent, cname)
123
- Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
124
- end
125
- end
126
-
127
- # @sig (Module, Symbol) -> bool
128
- private def cdef?(parent, cname)
129
- parent.const_defined?(cname, false)
130
- end
131
-
132
- # @raise [NameError]
133
- # @sig (Module, Symbol) -> Object
134
- private def cget(parent, cname)
135
- parent.const_get(cname, false)
136
- end
137
-
138
- # @raise [NameError]
139
- # @sig (Module, Symbol) -> Object
140
- private def crem(parent, cname)
141
- parent.__send__(:remove_const, cname)
142
- end
103
+ # --- Inflection --------------------------------------------------------------------------------
143
104
 
144
105
  CNAME_VALIDATOR = Module.new
145
106
  private_constant :CNAME_VALIDATOR
@@ -22,14 +22,13 @@ module Zeitwerk
22
22
  private_constant :MUTEX
23
23
 
24
24
  # Maps absolute paths for which an autoload has been set ---and not
25
- # executed--- to their corresponding parent class or module and constant
26
- # name.
25
+ # executed--- to their corresponding Zeitwerk::Cref object.
27
26
  #
28
- # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
29
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
27
+ # "/Users/fxn/blog/app/models/user.rb" => #<Zeitwerk::Cref:... @mod=Object, @cname=:User, ...>,
28
+ # "/Users/fxn/blog/app/models/hotel/pricing.rb" => #<Zeitwerk::Cref:... @mod=Hotel, @cname=:Pricing, ...>,
30
29
  # ...
31
30
  #
32
- # @sig Hash[String, [Module, Symbol]]
31
+ # @sig Hash[String, Zeitwerk::Cref]
33
32
  attr_reader :autoloads
34
33
  internal :autoloads
35
34
 
@@ -45,17 +44,19 @@ module Zeitwerk
45
44
 
46
45
  # Stores metadata needed for unloading. Its entries look like this:
47
46
  #
48
- # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
47
+ # "Admin::Role" => [
48
+ # ".../admin/role.rb",
49
+ # #<Zeitwerk::Cref:... @mod=Admin, @cname=:Role, ...>
50
+ # ]
49
51
  #
50
52
  # The cpath as key helps implementing unloadable_cpath? The file name is
51
53
  # stored in order to be able to delete it from $LOADED_FEATURES, and the
52
- # pair [Module, Symbol] is used to remove_const the constant from the class
53
- # or module object.
54
+ # cref is used to remove the constant from the parent class or module.
54
55
  #
55
56
  # If reloading is enabled, this hash is filled as constants are autoloaded
56
57
  # or eager loaded. Otherwise, the collection remains empty.
57
58
  #
58
- # @sig Hash[String, [String, [Module, Symbol]]]
59
+ # @sig Hash[String, [String, Zeitwerk::Cref]]
59
60
  attr_reader :to_unload
60
61
  internal :to_unload
61
62
 
@@ -154,22 +155,22 @@ module Zeitwerk
154
155
  # is enough.
155
156
  unloaded_files = Set.new
156
157
 
157
- autoloads.each do |abspath, (parent, cname)|
158
- if parent.autoload?(cname)
159
- unload_autoload(parent, cname)
158
+ autoloads.each do |abspath, cref|
159
+ if cref.autoload?
160
+ unload_autoload(cref)
160
161
  else
161
162
  # Could happen if loaded with require_relative. That is unsupported,
162
163
  # and the constant path would escape unloadable_cpath? This is just
163
164
  # defensive code to clean things up as much as we are able to.
164
- unload_cref(parent, cname)
165
+ unload_cref(cref)
165
166
  unloaded_files.add(abspath) if ruby?(abspath)
166
167
  end
167
168
  end
168
169
 
169
- to_unload.each do |cpath, (abspath, (parent, cname))|
170
+ to_unload.each do |cpath, (abspath, cref)|
170
171
  unless on_unload_callbacks.empty?
171
172
  begin
172
- value = cget(parent, cname)
173
+ value = cref.get
173
174
  rescue ::NameError
174
175
  # Perhaps the user deleted the constant by hand, or perhaps an
175
176
  # autoload failed to define the expected constant but the user
@@ -179,7 +180,7 @@ module Zeitwerk
179
180
  end
180
181
  end
181
182
 
182
- unload_cref(parent, cname)
183
+ unload_cref(cref)
183
184
  unloaded_files.add(abspath) if ruby?(abspath)
184
185
  end
185
186
 
@@ -230,53 +231,86 @@ module Zeitwerk
230
231
  setup
231
232
  end
232
233
 
233
- # @sig (String | Pathname) -> String?
234
- def cpath_expected_at(path)
235
- abspath = File.expand_path(path)
234
+ # Returns a hash that maps the absolute paths of the managed files and
235
+ # directories to their respective expected constant paths.
236
+ #
237
+ # @sig () -> Hash[String, String]
238
+ def all_expected_cpaths
239
+ result = {}
236
240
 
237
- raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
241
+ actual_roots.each do |root_dir, root_namespace|
242
+ queue = [[root_dir, real_mod_name(root_namespace)]]
238
243
 
239
- return unless dir?(abspath) || ruby?(abspath)
240
- return if ignored_path?(abspath)
244
+ while (dir, cpath = queue.shift)
245
+ result[dir] = cpath
241
246
 
242
- paths = []
247
+ prefix = cpath == "Object" ? "" : cpath + "::"
243
248
 
244
- if ruby?(abspath)
245
- basename = File.basename(abspath, ".rb")
246
- return if hidden?(basename)
249
+ ls(dir) do |basename, abspath, ftype|
250
+ if ftype == :file
251
+ basename.delete_suffix!(".rb")
252
+ result[abspath] = prefix + inflector.camelize(basename, abspath)
253
+ else
254
+ if collapse?(abspath)
255
+ queue << [abspath, cpath]
256
+ else
257
+ queue << [abspath, prefix + inflector.camelize(basename, abspath)]
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
247
263
 
248
- paths << [basename, abspath]
249
- walk_up_from = File.dirname(abspath)
250
- else
251
- walk_up_from = abspath
264
+ result
252
265
  end
253
266
 
254
- root_namespace = nil
267
+ # @sig (String | Pathname) -> String?
268
+ def cpath_expected_at(path)
269
+ abspath = File.expand_path(path)
255
270
 
256
- walk_up(walk_up_from) do |dir|
257
- break if root_namespace = roots[dir]
258
- return if ignored_path?(dir)
271
+ raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
259
272
 
260
- basename = File.basename(dir)
261
- return if hidden?(basename)
273
+ return unless dir?(abspath) || ruby?(abspath)
274
+ return if ignored_path?(abspath)
262
275
 
263
- paths << [basename, abspath] unless collapse?(dir)
264
- end
276
+ paths = []
265
277
 
266
- return unless root_namespace
278
+ if ruby?(abspath)
279
+ basename = File.basename(abspath, ".rb")
280
+ return if hidden?(basename)
267
281
 
268
- if paths.empty?
269
- real_mod_name(root_namespace)
270
- else
271
- cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
282
+ paths << [basename, abspath]
283
+ walk_up_from = File.dirname(abspath)
284
+ else
285
+ walk_up_from = abspath
286
+ end
287
+
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
272
299
 
273
- if root_namespace == Object
274
- cnames.join("::")
300
+ return unless root_namespace
301
+
302
+ if paths.empty?
303
+ real_mod_name(root_namespace)
275
304
  else
276
- "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
305
+ cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
306
+
307
+ if root_namespace == Object
308
+ cnames.join("::")
309
+ else
310
+ "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
311
+ end
277
312
  end
278
313
  end
279
- end
280
314
 
281
315
  # Says if the given constant path would be unloaded on reload. This
282
316
  # predicate returns `false` if reloading is disabled.
@@ -408,24 +442,25 @@ module Zeitwerk
408
442
 
409
443
  # @sig (String, Module) -> void
410
444
  private def define_autoloads_for_dir(dir, parent)
411
- ls(dir) do |basename, abspath|
412
- if ruby?(basename)
445
+ ls(dir) do |basename, abspath, ftype|
446
+ if ftype == :file
413
447
  basename.delete_suffix!(".rb")
414
- autoload_file(parent, cname_for(basename, abspath), abspath)
448
+ cref = Cref.new(parent, cname_for(basename, abspath))
449
+ autoload_file(cref, abspath)
415
450
  else
416
451
  if collapse?(abspath)
417
452
  define_autoloads_for_dir(abspath, parent)
418
453
  else
419
- autoload_subdir(parent, cname_for(basename, abspath), abspath)
454
+ cref = Cref.new(parent, cname_for(basename, abspath))
455
+ autoload_subdir(cref, abspath)
420
456
  end
421
457
  end
422
458
  end
423
459
  end
424
460
 
425
461
  # @sig (Module, Symbol, String) -> void
426
- private def autoload_subdir(parent, cname, subdir)
427
- if autoload_path = autoload_path_set_by_me_for?(parent, cname)
428
- cpath = cpath(parent, cname)
462
+ private def autoload_subdir(cref, subdir)
463
+ if autoload_path = autoload_path_set_by_me_for?(cref)
429
464
  if ruby?(autoload_path)
430
465
  # Scanning visited a Ruby file first, and now a directory for the same
431
466
  # constant has been found. This means we are dealing with an explicit
@@ -434,88 +469,83 @@ module Zeitwerk
434
469
  # Registering is idempotent, and we have to keep the autoload pointing
435
470
  # to the file. This may run again if more directories are found later
436
471
  # on, no big deal.
437
- register_explicit_namespace(cpath)
472
+ register_explicit_namespace(cref.path)
438
473
  end
439
474
  # If the existing autoload points to a file, it has to be preserved, if
440
475
  # not, it is fine as it is. In either case, we do not need to override.
441
476
  # Just remember the subdirectory conforms this namespace.
442
- namespace_dirs[cpath] << subdir
443
- elsif !cdef?(parent, cname)
477
+ namespace_dirs[cref.path] << subdir
478
+ elsif !cref.defined?
444
479
  # First time we find this namespace, set an autoload for it.
445
- namespace_dirs[cpath(parent, cname)] << subdir
446
- define_autoload(parent, cname, subdir)
480
+ namespace_dirs[cref.path] << subdir
481
+ define_autoload(cref, subdir)
447
482
  else
448
483
  # For whatever reason the constant that corresponds to this namespace has
449
484
  # already been defined, we have to recurse.
450
- log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
451
- define_autoloads_for_dir(subdir, cget(parent, cname))
485
+ log("the namespace #{cref.path} already exists, descending into #{subdir}") if logger
486
+ define_autoloads_for_dir(subdir, cref.get)
452
487
  end
453
488
  end
454
489
 
455
490
  # @sig (Module, Symbol, String) -> void
456
- private def autoload_file(parent, cname, file)
457
- if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
491
+ private def autoload_file(cref, file)
492
+ if autoload_path = cref.autoload? || Registry.inception?(cref.path)
458
493
  # First autoload for a Ruby file wins, just ignore subsequent ones.
459
494
  if ruby?(autoload_path)
460
495
  shadowed_files << file
461
496
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
462
497
  else
463
- promote_namespace_from_implicit_to_explicit(
464
- dir: autoload_path,
465
- file: file,
466
- parent: parent,
467
- cname: cname
468
- )
498
+ promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
469
499
  end
470
- elsif cdef?(parent, cname)
500
+ elsif cref.defined?
471
501
  shadowed_files << file
472
- log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
502
+ log("file #{file} is ignored because #{cref.path} is already defined") if logger
473
503
  else
474
- define_autoload(parent, cname, file)
504
+ define_autoload(cref, file)
475
505
  end
476
506
  end
477
507
 
478
508
  # `dir` is the directory that would have autovivified a namespace. `file` is
479
509
  # the file where we've found the namespace is explicitly defined.
480
510
  #
481
- # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
482
- private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
511
+ # @sig (dir: String, file: String, cref: Zeitwerk::Cref) -> void
512
+ private def promote_namespace_from_implicit_to_explicit(dir:, file:, cref:)
483
513
  autoloads.delete(dir)
484
514
  Registry.unregister_autoload(dir)
485
515
 
486
- log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
516
+ log("earlier autoload for #{cref.path} discarded, it is actually an explicit namespace defined in #{file}") if logger
487
517
 
488
- define_autoload(parent, cname, file)
489
- register_explicit_namespace(cpath(parent, cname))
518
+ define_autoload(cref, file)
519
+ register_explicit_namespace(cref.path)
490
520
  end
491
521
 
492
522
  # @sig (Module, Symbol, String) -> void
493
- private def define_autoload(parent, cname, abspath)
494
- parent.autoload(cname, abspath)
523
+ private def define_autoload(cref, abspath)
524
+ cref.autoload(abspath)
495
525
 
496
526
  if logger
497
527
  if ruby?(abspath)
498
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
528
+ log("autoload set for #{cref.path}, to be loaded from #{abspath}")
499
529
  else
500
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
530
+ log("autoload set for #{cref.path}, to be autovivified from #{abspath}")
501
531
  end
502
532
  end
503
533
 
504
- autoloads[abspath] = [parent, cname]
534
+ autoloads[abspath] = cref
505
535
  Registry.register_autoload(self, abspath)
506
536
 
507
537
  # See why in the documentation of Zeitwerk::Registry.inceptions.
508
- unless parent.autoload?(cname)
509
- Registry.register_inception(cpath(parent, cname), abspath, self)
538
+ unless cref.autoload?
539
+ Registry.register_inception(cref.path, abspath, self)
510
540
  end
511
541
  end
512
542
 
513
543
  # @sig (Module, Symbol) -> String?
514
- private def autoload_path_set_by_me_for?(parent, cname)
515
- if autoload_path = strict_autoload_path(parent, cname)
544
+ private def autoload_path_set_by_me_for?(cref)
545
+ if autoload_path = cref.autoload?
516
546
  autoload_path if autoloads.key?(autoload_path)
517
547
  else
518
- Registry.inception?(cpath(parent, cname))
548
+ Registry.inception?(cref.path)
519
549
  end
520
550
  end
521
551
 
@@ -556,21 +586,21 @@ module Zeitwerk
556
586
  end
557
587
 
558
588
  # @sig (Module, Symbol) -> void
559
- private def unload_autoload(parent, cname)
560
- crem(parent, cname)
561
- log("autoload for #{cpath(parent, cname)} removed") if logger
589
+ private def unload_autoload(cref)
590
+ cref.remove
591
+ log("autoload for #{cref.path} removed") if logger
562
592
  end
563
593
 
564
594
  # @sig (Module, Symbol) -> void
565
- private def unload_cref(parent, cname)
595
+ private def unload_cref(cref)
566
596
  # Let's optimistically remove_const. The way we use it, this is going to
567
597
  # succeed always if all is good.
568
- crem(parent, cname)
598
+ cref.remove
569
599
  rescue ::NameError
570
600
  # There are a few edge scenarios in which this may happen. If the constant
571
601
  # is gone, that is OK, anyway.
572
602
  else
573
- log("#{cpath(parent, cname)} unloaded") if logger
603
+ log("#{cref.path} unloaded") if logger
574
604
  end
575
605
  end
576
606
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.6.13"
4
+ VERSION = "2.6.17"
5
5
  end
data/lib/zeitwerk.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  module Zeitwerk
4
4
  require_relative "zeitwerk/real_mod_name"
5
5
  require_relative "zeitwerk/internal"
6
+ require_relative "zeitwerk/cref"
6
7
  require_relative "zeitwerk/loader"
7
8
  require_relative "zeitwerk/gem_loader"
8
9
  require_relative "zeitwerk/registry"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.13
4
+ version: 2.6.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-06 00:00:00.000000000 Z
11
+ date: 2024-07-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -23,6 +23,7 @@ files:
23
23
  - MIT-LICENSE
24
24
  - README.md
25
25
  - lib/zeitwerk.rb
26
+ - lib/zeitwerk/cref.rb
26
27
  - lib/zeitwerk/error.rb
27
28
  - lib/zeitwerk/explicit_namespace.rb
28
29
  - lib/zeitwerk/gem_inflector.rb
@@ -62,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
63
  - !ruby/object:Gem::Version
63
64
  version: '0'
64
65
  requirements: []
65
- rubygems_version: 3.5.5
66
+ rubygems_version: 3.5.15
66
67
  signing_key:
67
68
  specification_version: 4
68
69
  summary: Efficient and thread-safe constant autoloader