zeitwerk 2.6.17 → 2.7.1

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: 51e7aed4aef39ce0a2caaea2f23852ea4f2e8fcb7b65819d14a4d92e7aec70d0
4
- data.tar.gz: 7e310bac85d85018cb840339a42958b9fbc6fac566fee51e6a5c599a3cb132e3
3
+ metadata.gz: aba46812169c8e26085b099c708093b00a29bfd39e8d8210d29bc99e7df0e7fa
4
+ data.tar.gz: 0fc20386009d52d21a0cb79a64c5d800f7aecfce5a38e8430a10dc5d04c2609d
5
5
  SHA512:
6
- metadata.gz: 0274e4685362f6585b9fb90291561926c8b45434ea3df71858ed99c5dfbffd54e814ed6dfcc4c6e8bfd8a9f266dbe4261bf6010856558474ad7e85045851e4cd
7
- data.tar.gz: 210f457fad164472582fc67264bed2bd981612588bd3a8cdce265b2f2c67b12d10d9d2b9abb555dead008df20d5a73955072e703305e8f55b9d4dd5877d9b693
6
+ metadata.gz: 3edac5ad6f940caa70c4cac093bf271b8399b078d7124f450513c963ec5099e9b9082841ceb9893b81f1edf8e34dc642ec811403415fb230211670c63a950766
7
+ data.tar.gz: 47339a8b35dc06108fde9aadd62e83b1ea4e4ef22854c31849069f09ece1115dce07d63ec354fe96fbc599bba411ecff48db6a8b0c8b150ad7ee016787e9d75c
data/README.md CHANGED
@@ -54,7 +54,6 @@
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)
@@ -63,7 +62,6 @@
63
62
  - [`Zeitwerk::Loader#all_expected_cpaths`](#zeitwerkloaderall_expected_cpaths)
64
63
  - [Encodings](#encodings)
65
64
  - [Rules of thumb](#rules-of-thumb)
66
- - [Debuggers](#debuggers)
67
65
  - [Pronunciation](#pronunciation)
68
66
  - [Supported Ruby versions](#supported-ruby-versions)
69
67
  - [Testing](#testing)
@@ -283,6 +281,8 @@ class Hotel < ApplicationRecord
283
281
  end
284
282
  ```
285
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
+
286
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.
287
287
 
288
288
  <a id="markdown-collapsing-directories" name="collapsing-directories"></a>
@@ -1178,36 +1178,6 @@ file #{file} is ignored because #{constant_path} is already defined
1178
1178
 
1179
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).
1180
1180
 
1181
- <a id="markdown-edge-cases" name="edge-cases"></a>
1182
- ### Edge cases
1183
-
1184
- [Explicit namespaces](#explicit-namespaces) like `Trip` here:
1185
-
1186
- ```ruby
1187
- # trip.rb
1188
- class Trip
1189
- include Geolocation
1190
- end
1191
-
1192
- # trip/geolocation.rb
1193
- module Trip::Geolocation
1194
- ...
1195
- end
1196
- ```
1197
-
1198
- have to be defined with the `class`/`module` keywords, as in the example above.
1199
-
1200
- For technical reasons, raw constant assignment is not supported:
1201
-
1202
- ```ruby
1203
- # trip.rb
1204
- Trip = Class { ...} # NOT SUPPORTED
1205
- Trip = Struct.new { ... } # NOT SUPPORTED
1206
- Trip = Data.define { ... } # NOT SUPPORTED
1207
- ```
1208
-
1209
- This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
1210
-
1211
1181
  <a id="markdown-beware-of-circular-dependencies" name="beware-of-circular-dependencies"></a>
1212
1182
  ### Beware of circular dependencies
1213
1183
 
@@ -1406,15 +1376,6 @@ The test suite passes on Windows with codepage `Windows-1252` if all the involve
1406
1376
 
1407
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.
1408
1378
 
1409
- <a id="markdown-debuggers" name="debuggers"></a>
1410
- ### Debuggers
1411
-
1412
- 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)).
1413
-
1414
- [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.
1415
-
1416
- [Break](https://github.com/gsamokovarov/break) is fully compatible.
1417
-
1418
1379
  <a id="markdown-pronunciation" name="pronunciation"></a>
1419
1380
  ## Pronunciation
1420
1381
 
@@ -1423,9 +1384,12 @@ Zeitwerk and [debug.rb](https://github.com/ruby/debug) are fully compatible if C
1423
1384
  <a id="markdown-supported-ruby-versions" name="supported-ruby-versions"></a>
1424
1385
  ## Supported Ruby versions
1425
1386
 
1426
- Zeitwerk works with CRuby 2.5 and above.
1387
+ Starting with version 2.7, Zeitwerk requires Ruby 3.2 or newer.
1427
1388
 
1428
- On TruffleRuby all is good except for thread-safety. Right now, in TruffleRuby `Module#autoload` does not block threads accessing a constant that is being autoloaded. CRuby prevents such access to avoid concurrent threads from seeing partial evaluations of the corresponding file. Zeitwerk inherits autoloading thread-safety from this property. This is not an issue if your project gets eager loaded, or if you lazy load in single-threaded environments. (See https://github.com/oracle/truffleruby/issues/2431.)
1389
+ Zeitwerk 2.7 requires TruffleRuby 24.1.2+ due to https://github.com/oracle/truffleruby/issues/3683.
1390
+ Alternatively, TruffleRuby users can use a `< 2.7` version constraint for the `zeitwerk` gem.
1391
+ As of this writing, [autoloading is not fully thread-safe yet on TruffleRuby](https://github.com/oracle/truffleruby/issues/2431).
1392
+ If your program is multi-threaded, you need to eager load before threads are created.
1429
1393
 
1430
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.)
1431
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
data/lib/zeitwerk/cref.rb CHANGED
@@ -13,6 +13,9 @@
13
13
  class Zeitwerk::Cref
14
14
  include Zeitwerk::RealModName
15
15
 
16
+ # @sig Module
17
+ attr_reader :mod
18
+
16
19
  # @sig Symbol
17
20
  attr_reader :cname
18
21
 
@@ -26,48 +29,14 @@ class Zeitwerk::Cref
26
29
  @path = nil
27
30
  end
28
31
 
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
32
+ # @sig () -> String
33
+ def path
34
+ @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}".freeze
42
35
  end
43
36
 
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
37
  # @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
38
+ def autoload?
39
+ @mod.autoload?(@cname, false)
71
40
  end
72
41
 
73
42
  # @sig (String) -> bool
@@ -1,93 +1,113 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- # Centralizes the logic for the trace point used to detect the creation of
5
- # explicit namespaces, needed to descend into matching subdirectories right
6
- # after the constant has been defined.
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
- # Maps constant paths that correspond to explicit namespaces according to
17
- # the file system, to the loader responsible for them.
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(cpath, loader)
36
- mutex.synchronize do
37
- cpaths[cpath] = loader
38
- # We check enabled? because, looking at the C source code, enabling an
39
- # enabled tracer does not seem to be a simple no-op.
40
- tracer.enable unless tracer.enabled?
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
- cpaths.delete_if { |_cpath, l| l == loader }
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) -> bool
53
- internal def registered?(cpath)
54
- cpaths.key?(cpath)
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
- private def disable_tracer_if_unneeded
59
- mutex.synchronize do
60
- tracer.disable if cpaths.empty?
61
- end
80
+ internal def clear
81
+ @loaders.clear
62
82
  end
63
83
 
64
- # @sig (TracePoint) -> void
65
- private def tracepoint_class_callback(event)
66
- # If the class is a singleton class, we won't do anything with it so we
67
- # can bail out immediately. This is several orders of magnitude faster
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
- # It might be tempting to return if name.nil?, to avoid the computation
72
- # of a hash code and delete call. But Ruby does not trigger the :class
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
- @cpaths = {}
87
- @mutex = Mutex.new
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
- # We go through a method instead of defining a block mainly to have a better
90
- # label when profiling.
91
- @tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
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
@@ -6,6 +6,7 @@ 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
12
  cref = autoloads.delete(file)
@@ -72,7 +73,7 @@ module Zeitwerk::Loader::Callbacks
72
73
  end
73
74
 
74
75
  # Invoked when a class or module is created or reopened, either from the
75
- # tracer or from module autovivification. If the namespace has matching
76
+ # const_added or from module autovivification. If the namespace has matching
76
77
  # subdirectories, we descend into them now.
77
78
  #
78
79
  # @private
@@ -84,7 +84,7 @@ module Zeitwerk::Loader::EagerLoad
84
84
  return unless mod_name
85
85
 
86
86
  actual_roots.each do |root_dir, root_namespace|
87
- if mod.equal?(Object)
87
+ if Object.equal?(mod)
88
88
  # A shortcircuiting test depends on the invocation of this method.
89
89
  # Please keep them in sync if refactored.
90
90
  actual_eager_load_dir(root_dir, root_namespace)
@@ -57,9 +57,7 @@ module Zeitwerk::Loader::Helpers
57
57
  to_visit = [dir]
58
58
 
59
59
  while (dir = to_visit.shift)
60
- children = Dir.children(dir)
61
-
62
- children.each do |basename|
60
+ Dir.each_child(dir) do |basename|
63
61
  next if hidden?(basename)
64
62
 
65
63
  abspath = File.join(dir, basename)
@@ -469,7 +469,7 @@ module Zeitwerk
469
469
  # Registering is idempotent, and we have to keep the autoload pointing
470
470
  # to the file. This may run again if more directories are found later
471
471
  # on, no big deal.
472
- register_explicit_namespace(cref.path)
472
+ register_explicit_namespace(cref)
473
473
  end
474
474
  # If the existing autoload points to a file, it has to be preserved, if
475
475
  # not, it is fine as it is. In either case, we do not need to override.
@@ -515,8 +515,10 @@ module Zeitwerk
515
515
 
516
516
  log("earlier autoload for #{cref.path} discarded, it is actually an explicit namespace defined in #{file}") if logger
517
517
 
518
+ # Order matters: When Module#const_added is triggered by the autoload, we
519
+ # don't want the namespace to be registered yet.
518
520
  define_autoload(cref, file)
519
- register_explicit_namespace(cref.path)
521
+ register_explicit_namespace(cref)
520
522
  end
521
523
 
522
524
  # @sig (Module, Symbol, String) -> void
@@ -545,13 +547,13 @@ module Zeitwerk
545
547
  if autoload_path = cref.autoload?
546
548
  autoload_path if autoloads.key?(autoload_path)
547
549
  else
548
- Registry.inception?(cref.path)
550
+ Registry.inception?(cref.path, self)
549
551
  end
550
552
  end
551
553
 
552
- # @sig (String) -> void
553
- private def register_explicit_namespace(cpath)
554
- ExplicitNamespace.__register(cpath, self)
554
+ # @sig (Zeitwerk::Cref) -> void
555
+ private def register_explicit_namespace(cref)
556
+ ExplicitNamespace.__register(cref, self)
555
557
  end
556
558
 
557
559
  # @sig (String) -> void
@@ -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
- if UnboundMethod.method_defined?(:bind_call)
14
- def real_mod_name(mod)
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
@@ -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.first
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.6.17"
4
+ VERSION = "2.7.1"
5
5
  end
data/lib/zeitwerk.rb CHANGED
@@ -11,10 +11,12 @@ module Zeitwerk
11
11
  require_relative "zeitwerk/inflector"
12
12
  require_relative "zeitwerk/gem_inflector"
13
13
  require_relative "zeitwerk/null_inflector"
14
- require_relative "zeitwerk/kernel"
15
14
  require_relative "zeitwerk/error"
16
15
  require_relative "zeitwerk/version"
17
16
 
17
+ require_relative "zeitwerk/core_ext/kernel"
18
+ require_relative "zeitwerk/core_ext/module"
19
+
18
20
  # This is a dangerous method.
19
21
  #
20
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.6.17
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-07-29 00:00:00.000000000 Z
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,6 +23,8 @@ 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
26
28
  - lib/zeitwerk/cref.rb
27
29
  - lib/zeitwerk/error.rb
28
30
  - lib/zeitwerk/explicit_namespace.rb
@@ -30,7 +32,6 @@ files:
30
32
  - lib/zeitwerk/gem_loader.rb
31
33
  - lib/zeitwerk/inflector.rb
32
34
  - lib/zeitwerk/internal.rb
33
- - lib/zeitwerk/kernel.rb
34
35
  - lib/zeitwerk/loader.rb
35
36
  - lib/zeitwerk/loader/callbacks.rb
36
37
  - lib/zeitwerk/loader/config.rb
@@ -56,14 +57,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
56
57
  requirements:
57
58
  - - ">="
58
59
  - !ruby/object:Gem::Version
59
- version: '2.5'
60
+ version: '3.2'
60
61
  required_rubygems_version: !ruby/object:Gem::Requirement
61
62
  requirements:
62
63
  - - ">="
63
64
  - !ruby/object:Gem::Version
64
65
  version: '0'
65
66
  requirements: []
66
- rubygems_version: 3.5.15
67
+ rubygems_version: 3.5.21
67
68
  signing_key:
68
69
  specification_version: 4
69
70
  summary: Efficient and thread-safe constant autoloader
File without changes