zeitwerk 2.6.12 → 2.6.15

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: 5a9048a5ba05f448e5406080995aab9709a3dd7c8dd32aad3aa0ba4f544b69c4
4
- data.tar.gz: 581cf27f70c82b754a1baadf7a41f6c87c069ae2e78bd26b4ec6e02cfa2795b9
3
+ metadata.gz: 8177bcca0fd5895bbe28b19dc5fd04a810e2905f21f33924786a33a317c96207
4
+ data.tar.gz: 7e441521285fe28230dadfb4c5c9d40564e376dd651ddae043190c5fdd3ee526
5
5
  SHA512:
6
- metadata.gz: 45327b148bab210d941ffeae022b70e5c2f2be4372f2192a08859faeed81d2cc5702b05d6e4efcf676a8fb7b451c46200ac372051c68ba84cfc31ed9339f3ede
7
- data.tar.gz: 8e3266d7641f58b6a8f74abfa913c14ae4a79d4efd177578de4e7e144a21adc33d33279b7d2771cdc57937cfedfb1f5e193bb9e57c18ea90956b342b516e9bd9
6
+ metadata.gz: 57e0e401c952a13d1b96a8993bbee930132f2b63252ff7cd6646ceeeb5a3de886d1c36a8cdbc06e9add3e16e76bacd08cc8e32b58289fa01e9eabea4dad25581
7
+ data.tar.gz: e56116c8325ab38f751b8497f8270544543e950f92624c755b5d3d7bb296b48ce4afa668e1b4daae546561ddfa344fd9c650c7114c9cd2c637a514f0ebd38c98
data/README.md CHANGED
@@ -40,6 +40,7 @@
40
40
  - [Inflection](#inflection)
41
41
  - [Zeitwerk::Inflector](#zeitwerkinflector)
42
42
  - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
43
+ - [Zeitwerk::NullInflector](#zeitwerknullinflector)
43
44
  - [Custom inflector](#custom-inflector)
44
45
  - [Callbacks](#callbacks)
45
46
  - [The on_setup callback](#the-on_setup-callback)
@@ -59,6 +60,7 @@
59
60
  - [Introspection](#introspection)
60
61
  - [`Zeitwerk::Loader#dirs`](#zeitwerkloaderdirs)
61
62
  - [`Zeitwerk::Loader#cpath_expected_at`](#zeitwerkloadercpath_expected_at)
63
+ - [`Zeitwerk::Loader#all_expected_cpaths`](#zeitwerkloaderall_expected_cpaths)
62
64
  - [Encodings](#encodings)
63
65
  - [Rules of thumb](#rules-of-thumb)
64
66
  - [Debuggers](#debuggers)
@@ -258,7 +260,7 @@ app/controllers/admin/users_controller.rb -> Admin::UsersController
258
260
 
259
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.
260
262
 
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
+ 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.
262
264
 
263
265
  <a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
264
266
  ### Explicit namespaces
@@ -373,7 +375,7 @@ require "zeitwerk"
373
375
  loader = Zeitwerk::Loader.new
374
376
  loader.tag = File.basename(__FILE__, ".rb")
375
377
  loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
376
- loader.push_dir(__dir__)
378
+ loader.push_dir(File.dirname(__FILE__))
377
379
  ```
378
380
 
379
381
  If the main module references project constants at the top-level, Zeitwerk has to be ready to load them. Their definitions, in turn, may reference other project constants. And this is recursive. Therefore, it is important that the `setup` call happens above the main module definition:
@@ -579,7 +581,7 @@ root_dir2/my_app/routes
579
581
  root_dir3/my_app/routes
580
582
  ```
581
583
 
582
- where `root_directory{1,2,3}` are root directories, eager loading `MyApp::Routes` will eager load the contents of the three corresponding directories.
584
+ where `root_dir{1,2,3}` are root directories, eager loading `MyApp::Routes` will eager load the contents of the three corresponding directories.
583
585
 
584
586
  There might exist external source trees implementing part of the namespace. This happens routinely, because top-level constants are stored in the globally shared `Object`. It happens also when deliberately [reopening third-party namespaces](#reopening-third-party-namespaces). Such external code is not eager loaded, the implementation is carefully scoped to what the receiver manages to avoid side-effects elsewhere.
585
587
 
@@ -774,6 +776,31 @@ This inflector is like the basic one, except it expects `lib/my_gem/version.rb`
774
776
 
775
777
  The inflectors of different loaders are independent of each other. There are no global inflection rules or global configuration that can affect this inflector. It is deterministic.
776
778
 
779
+ <a id="markdown-zeitwerknullinflector" name="zeitwerknullinflector"></a>
780
+ #### Zeitwerk::NullInflector
781
+
782
+ This is an experimental inflector that simply returns its input unchanged.
783
+
784
+ ```ruby
785
+ loader.inflector = Zeitwerk::NullInflector.new
786
+ ```
787
+
788
+ In a project using this inflector, the names of files and directories are equal to the constants they define:
789
+
790
+ ```
791
+ User.rb -> User
792
+ HTMLParser.rb -> HTMLParser
793
+ Admin/Role.rb -> Admin::Role
794
+ ```
795
+
796
+ Point is, you think less. Names that typically need custom configuration like acronyms no longer require your attention. What you see is what you get, simple.
797
+
798
+ This inflector is experimental since Ruby usually goes for snake case in files and directories. But hey, if you fancy giving it a whirl, go for it!
799
+
800
+ The null inflector cannot be used in Rails applications because the `main` autoloader also manages engines. However, you could subclass the default inflector and override `camelize` to return the basename untouched if it starts with an uppercase letter. Generators would not create the expected file names, but you could still experiment to see how far this approach takes you.
801
+
802
+ In case-insensitive file systems, this inflector works as long as directory listings return the expected strings. Zeitwerk lists directories using Ruby APIs like `Dir.children` or `Dir.entries`.
803
+
777
804
  <a id="markdown-custom-inflector" name="custom-inflector"></a>
778
805
  #### Custom inflector
779
806
 
@@ -1006,7 +1033,7 @@ Zeitwerk::Loader.default_logger = method(:puts)
1006
1033
 
1007
1034
  If there is a logger configured, you'll see traces when autoloads are set, files loaded, and modules autovivified. While reloading, removed autoloads and unloaded objects are also traced.
1008
1035
 
1009
- As a curiosity, if your project has namespaces you'll notice in the traces Zeitwerk sets autoloads for _directories_. That's a technique used to be able to descend into subdirectories on demand, avoiding that way unnecessary tree walks.
1036
+ As a curiosity, if your project has namespaces you'll notice in the traces Zeitwerk sets autoloads for _directories_. This allows descending into subdirectories on demand, thus avoiding unnecessary tree walks.
1010
1037
 
1011
1038
  <a id="markdown-loader-tag" name="loader-tag"></a>
1012
1039
  #### Loader tag
@@ -1032,7 +1059,7 @@ Zeitwerk@my_gem: constant MyGem::Foo loaded from ...
1032
1059
  <a id="markdown-ignoring-parts-of-the-project" name="ignoring-parts-of-the-project"></a>
1033
1060
  ### Ignoring parts of the project
1034
1061
 
1035
- Zeitwerk ignores automatically any file or directory whose name starts with a dot, and any files that do not have extension ".rb".
1062
+ Zeitwerk ignores automatically any file or directory whose name starts with a dot, and any files that do not have the extension ".rb".
1036
1063
 
1037
1064
  However, sometimes it might still be convenient to tell Zeitwerk to completely ignore some particular Ruby file or directory. That is possible with `ignore`, which accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
1038
1065
 
@@ -1304,6 +1331,54 @@ loader.cpath_expected_at("8.rb") # => Zeitwerk::NameError
1304
1331
 
1305
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.
1306
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
+
1307
1382
  <a id="markdown-encodings" name="encodings"></a>
1308
1383
  ### Encodings
1309
1384
 
@@ -1325,7 +1400,7 @@ The test suite passes on Windows with codepage `Windows-1252` if all the involve
1325
1400
 
1326
1401
  3. In that line, if two loaders manage files that translate to the same constant in the same namespace, the first one wins, the rest are ignored. Similar to what happens with `require` and `$LOAD_PATH`, only the first occurrence matters.
1327
1402
 
1328
- 4. Projects that reopen a namespace defined by some dependency have to ensure said namespace is loaded before setup. That is, the project has to make sure it reopens, rather than define. This is often accomplished just loading the dependency.
1403
+ 4. Projects that reopen a namespace defined by some dependency have to ensure said namespace is loaded before setup. That is, the project has to make sure it reopens, rather than defines, the namespace. This is often accomplished by loading (e.g., `require`-ing) the dependency.
1329
1404
 
1330
1405
  5. Objects stored in reloadable constants should not be cached in places that are not reloaded. For example, non-reloadable classes should not subclass a reloadable class, or mixin a reloadable module. Otherwise, after reloading, those classes or module objects would become stale. Referring to constants in dynamic places like method calls or lambdas is fine.
1331
1406
 
@@ -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}
@@ -14,10 +14,6 @@ module Kernel
14
14
  # should not require anything. But if someone has legacy require calls around,
15
15
  # they will work as expected, and in a compatible way. This feature is by now
16
16
  # EXPERIMENTAL and UNDOCUMENTED.
17
- #
18
- # We cannot decorate with prepend + super because Kernel has already been
19
- # included in Object, and changes in ancestors don't get propagated into
20
- # already existing ancestor chains on Ruby < 3.0.
21
17
  alias_method :zeitwerk_original_require, :require
22
18
  class << self
23
19
  alias_method :zeitwerk_original_require, :require
@@ -164,13 +164,13 @@ 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
167
+ until queue.empty?
168
+ dir, namespace = queue.shift
169
169
 
170
- ls(dir) do |basename, abspath|
170
+ ls(dir) do |basename, abspath, ftype|
171
171
  next if honour_exclusions && eager_load_exclusions.member?(abspath)
172
172
 
173
- if ruby?(abspath)
173
+ if ftype == :file
174
174
  if (cref = autoloads[abspath])
175
175
  cget(*cref)
176
176
  end
@@ -209,9 +209,9 @@ module Zeitwerk::Loader::EagerLoad
209
209
  next_dirs = []
210
210
 
211
211
  suffix.split("::").each do |segment|
212
- while dir = dirs.shift
213
- ls(dir) do |basename, abspath|
214
- next unless dir?(abspath)
212
+ while (dir = dirs.shift)
213
+ ls(dir) do |basename, abspath, ftype|
214
+ next unless ftype == :directory
215
215
 
216
216
  if collapse?(abspath)
217
217
  dirs << abspath
@@ -31,26 +31,39 @@ module Zeitwerk::Loader::Helpers
31
31
  if dir?(abspath)
32
32
  next if roots.key?(abspath)
33
33
  next if !has_at_least_one_ruby_file?(abspath)
34
+ ftype = :directory
34
35
  else
35
36
  next unless ruby?(abspath)
37
+ ftype = :file
36
38
  end
37
39
 
38
40
  # We freeze abspath because that saves allocations when passed later to
39
41
  # File methods. See #125.
40
- yield basename, abspath.freeze
42
+ yield basename, abspath.freeze, ftype
41
43
  end
42
44
  end
43
45
 
46
+ # Looks for a Ruby file using breadth-first search. This type of search is
47
+ # important to list as less directories as possible and return fast in the
48
+ # common case in which there are Ruby files.
49
+ #
44
50
  # @sig (String) -> bool
45
51
  private def has_at_least_one_ruby_file?(dir)
46
52
  to_visit = [dir]
47
53
 
48
- while dir = to_visit.shift
49
- ls(dir) do |_basename, abspath|
54
+ while (dir = to_visit.shift)
55
+ children = Dir.children(dir)
56
+
57
+ children.each do |basename|
58
+ next if hidden?(basename)
59
+
60
+ abspath = File.join(dir, basename)
61
+ next if ignored_path?(abspath)
62
+
50
63
  if dir?(abspath)
51
- to_visit << abspath
64
+ to_visit << abspath unless roots.key?(abspath)
52
65
  else
53
- return true
66
+ return true if ruby?(abspath)
54
67
  end
55
68
  end
56
69
  end
@@ -230,53 +230,87 @@ module Zeitwerk
230
230
  setup
231
231
  end
232
232
 
233
- # @sig (String | Pathname) -> String?
234
- def cpath_expected_at(path)
235
- abspath = File.expand_path(path)
233
+ # Returns a hash that maps the absolute paths of the managed files and
234
+ # directories to their respective expected constant paths.
235
+ #
236
+ # @sig () -> Hash[String, String]
237
+ def all_expected_cpaths
238
+ result = {}
236
239
 
237
- raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
240
+ actual_roots.each do |root_dir, root_namespace|
241
+ queue = [[root_dir, real_mod_name(root_namespace)]]
238
242
 
239
- return unless dir?(abspath) || ruby?(abspath)
240
- return if ignored_path?(abspath)
243
+ until queue.empty?
244
+ dir, cpath = queue.shift
245
+ result[dir] = cpath
241
246
 
242
- paths = []
247
+ prefix = cpath == "Object" ? "" : cpath + "::"
243
248
 
244
- if ruby?(abspath)
245
- basename = File.basename(abspath, ".rb")
246
- return if hidden?(basename)
249
+ ls(dir) do |basename, abspath, ftype|
250
+ if ftype == :file
251
+ basename.delete_suffix!(".rb")
252
+ result[abspath] = prefix + inflector.camelize(basename, abspath)
253
+ else
254
+ if collapse?(abspath)
255
+ queue << [abspath, cpath]
256
+ else
257
+ queue << [abspath, prefix + inflector.camelize(basename, abspath)]
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
247
263
 
248
- paths << [basename, abspath]
249
- walk_up_from = File.dirname(abspath)
250
- else
251
- walk_up_from = abspath
264
+ result
252
265
  end
253
266
 
254
- root_namespace = nil
267
+ # @sig (String | Pathname) -> String?
268
+ def cpath_expected_at(path)
269
+ abspath = File.expand_path(path)
255
270
 
256
- walk_up(walk_up_from) do |dir|
257
- break if root_namespace = roots[dir]
258
- return if ignored_path?(dir)
271
+ raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
259
272
 
260
- basename = File.basename(dir)
261
- return if hidden?(basename)
273
+ return unless dir?(abspath) || ruby?(abspath)
274
+ return if ignored_path?(abspath)
262
275
 
263
- paths << [basename, abspath] unless collapse?(dir)
264
- end
276
+ paths = []
265
277
 
266
- return unless root_namespace
278
+ if ruby?(abspath)
279
+ basename = File.basename(abspath, ".rb")
280
+ return if hidden?(basename)
267
281
 
268
- if paths.empty?
269
- real_mod_name(root_namespace)
270
- else
271
- cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
282
+ paths << [basename, abspath]
283
+ walk_up_from = File.dirname(abspath)
284
+ else
285
+ walk_up_from = abspath
286
+ end
272
287
 
273
- if root_namespace == Object
274
- cnames.join("::")
288
+ root_namespace = nil
289
+
290
+ walk_up(walk_up_from) do |dir|
291
+ break if root_namespace = roots[dir]
292
+ return if ignored_path?(dir)
293
+
294
+ basename = File.basename(dir)
295
+ return if hidden?(basename)
296
+
297
+ paths << [basename, abspath] unless collapse?(dir)
298
+ end
299
+
300
+ return unless root_namespace
301
+
302
+ if paths.empty?
303
+ real_mod_name(root_namespace)
275
304
  else
276
- "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
305
+ cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
306
+
307
+ if root_namespace == Object
308
+ cnames.join("::")
309
+ else
310
+ "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
311
+ end
277
312
  end
278
313
  end
279
- end
280
314
 
281
315
  # Says if the given constant path would be unloaded on reload. This
282
316
  # predicate returns `false` if reloading is disabled.
@@ -408,8 +442,8 @@ 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
448
  autoload_file(parent, cname_for(basename, abspath), abspath)
415
449
  else
@@ -0,0 +1,5 @@
1
+ class Zeitwerk::NullInflector
2
+ def camelize(basename, _abspath)
3
+ basename
4
+ end
5
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.6.12"
4
+ VERSION = "2.6.15"
5
5
  end
data/lib/zeitwerk.rb CHANGED
@@ -9,6 +9,7 @@ module Zeitwerk
9
9
  require_relative "zeitwerk/explicit_namespace"
10
10
  require_relative "zeitwerk/inflector"
11
11
  require_relative "zeitwerk/gem_inflector"
12
+ require_relative "zeitwerk/null_inflector"
12
13
  require_relative "zeitwerk/kernel"
13
14
  require_relative "zeitwerk/error"
14
15
  require_relative "zeitwerk/version"
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.12
4
+ version: 2.6.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-25 00:00:00.000000000 Z
11
+ date: 2024-05-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -35,6 +35,7 @@ files:
35
35
  - lib/zeitwerk/loader/config.rb
36
36
  - lib/zeitwerk/loader/eager_load.rb
37
37
  - lib/zeitwerk/loader/helpers.rb
38
+ - lib/zeitwerk/null_inflector.rb
38
39
  - lib/zeitwerk/real_mod_name.rb
39
40
  - lib/zeitwerk/registry.rb
40
41
  - lib/zeitwerk/version.rb
@@ -61,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
62
  - !ruby/object:Gem::Version
62
63
  version: '0'
63
64
  requirements: []
64
- rubygems_version: 3.4.16
65
+ rubygems_version: 3.5.7
65
66
  signing_key:
66
67
  specification_version: 4
67
68
  summary: Efficient and thread-safe constant autoloader