zeitwerk 2.6.15 → 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: 8177bcca0fd5895bbe28b19dc5fd04a810e2905f21f33924786a33a317c96207
4
- data.tar.gz: 7e441521285fe28230dadfb4c5c9d40564e376dd651ddae043190c5fdd3ee526
3
+ metadata.gz: aba46812169c8e26085b099c708093b00a29bfd39e8d8210d29bc99e7df0e7fa
4
+ data.tar.gz: 0fc20386009d52d21a0cb79a64c5d800f7aecfce5a38e8430a10dc5d04c2609d
5
5
  SHA512:
6
- metadata.gz: 57e0e401c952a13d1b96a8993bbee930132f2b63252ff7cd6646ceeeb5a3de886d1c36a8cdbc06e9add3e16e76bacd08cc8e32b58289fa01e9eabea4dad25581
7
- data.tar.gz: e56116c8325ab38f751b8497f8270544543e950f92624c755b5d3d7bb296b48ce4afa668e1b4daae546561ddfa344fd9c650c7114c9cd2c637a514f0ebd38c98
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>
@@ -1065,7 +1065,7 @@ However, sometimes it might still be convenient to tell Zeitwerk to completely i
1065
1065
 
1066
1066
  You can ignore file names, directory names, and glob patterns. Glob patterns are expanded when they are added and again on each reload.
1067
1067
 
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 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.
1069
1069
 
1070
1070
  Let's see some use cases.
1071
1071
 
@@ -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
@@ -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
- # 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,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 = autoloads.delete(file)
12
- cpath = cpath(*cref)
12
+ cref = autoloads.delete(file)
13
13
 
14
14
  Zeitwerk::Registry.unregister_autoload(file)
15
15
 
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?
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 #{cpath}, but didn't"
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
- crem(*cref)
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[cpath] = [file, cref] if reloading_enabled?
31
+ to_unload[cref.path] = [file, cref] if reloading_enabled?
32
32
 
33
- raise Zeitwerk::NameError.new(msg, cref.last)
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
- autovivified_module = cref[0].const_set(cref[1], Module.new)
57
- cpath = autovivified_module.name
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(autovivified_module)
68
+ on_namespace_loaded(implicit_namespace)
69
69
 
70
- run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
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
- # tracer or from module autovivification. If the namespace has matching
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 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
@@ -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)
@@ -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
- until queue.empty?
168
- dir, namespace = queue.shift
169
-
170
- ls(dir) do |basename, abspath, ftype|
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
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
@@ -30,7 +30,12 @@ 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
+
34
39
  ftype = :directory
35
40
  else
36
41
  next unless ruby?(abspath)
@@ -52,9 +57,7 @@ module Zeitwerk::Loader::Helpers
52
57
  to_visit = [dir]
53
58
 
54
59
  while (dir = to_visit.shift)
55
- children = Dir.children(dir)
56
-
57
- children.each do |basename|
60
+ Dir.each_child(dir) do |basename|
58
61
  next if hidden?(basename)
59
62
 
60
63
  abspath = File.join(dir, basename)
@@ -95,64 +98,7 @@ module Zeitwerk::Loader::Helpers
95
98
  end
96
99
  end
97
100
 
98
- # --- Constants ---------------------------------------------------------------------------------
99
-
100
- # The autoload? predicate takes into account the ancestor chain of the
101
- # receiver, like const_defined? and other methods in the constants API do.
102
- #
103
- # For example, given
104
- #
105
- # class A
106
- # autoload :X, "x.rb"
107
- # end
108
- #
109
- # class B < A
110
- # end
111
- #
112
- # B.autoload?(:X) returns "x.rb".
113
- #
114
- # We need a way to strictly check in parent ignoring ancestors.
115
- #
116
- # @sig (Module, Symbol) -> String?
117
- if method(:autoload?).arity == 1
118
- private def strict_autoload_path(parent, cname)
119
- parent.autoload?(cname) if cdef?(parent, cname)
120
- end
121
- else
122
- private def strict_autoload_path(parent, cname)
123
- parent.autoload?(cname, false)
124
- end
125
- end
126
-
127
- # @sig (Module, Symbol) -> String
128
- if Symbol.method_defined?(:name)
129
- # Symbol#name was introduced in Ruby 3.0. It returns always the same
130
- # frozen object, so we may save a few string allocations.
131
- private def cpath(parent, cname)
132
- Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
133
- end
134
- else
135
- private def cpath(parent, cname)
136
- Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
137
- end
138
- end
139
-
140
- # @sig (Module, Symbol) -> bool
141
- private def cdef?(parent, cname)
142
- parent.const_defined?(cname, false)
143
- end
144
-
145
- # @raise [NameError]
146
- # @sig (Module, Symbol) -> Object
147
- private def cget(parent, cname)
148
- parent.const_get(cname, false)
149
- end
150
-
151
- # @raise [NameError]
152
- # @sig (Module, Symbol) -> Object
153
- private def crem(parent, cname)
154
- parent.__send__(:remove_const, cname)
155
- end
101
+ # --- Inflection --------------------------------------------------------------------------------
156
102
 
157
103
  CNAME_VALIDATOR = Module.new
158
104
  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
 
@@ -240,8 +241,7 @@ module Zeitwerk
240
241
  actual_roots.each do |root_dir, root_namespace|
241
242
  queue = [[root_dir, real_mod_name(root_namespace)]]
242
243
 
243
- until queue.empty?
244
- dir, cpath = queue.shift
244
+ while (dir, cpath = queue.shift)
245
245
  result[dir] = cpath
246
246
 
247
247
  prefix = cpath == "Object" ? "" : cpath + "::"
@@ -445,21 +445,22 @@ module Zeitwerk
445
445
  ls(dir) do |basename, abspath, ftype|
446
446
  if ftype == :file
447
447
  basename.delete_suffix!(".rb")
448
- autoload_file(parent, cname_for(basename, abspath), abspath)
448
+ cref = Cref.new(parent, cname_for(basename, abspath))
449
+ autoload_file(cref, abspath)
449
450
  else
450
451
  if collapse?(abspath)
451
452
  define_autoloads_for_dir(abspath, parent)
452
453
  else
453
- autoload_subdir(parent, cname_for(basename, abspath), abspath)
454
+ cref = Cref.new(parent, cname_for(basename, abspath))
455
+ autoload_subdir(cref, abspath)
454
456
  end
455
457
  end
456
458
  end
457
459
  end
458
460
 
459
461
  # @sig (Module, Symbol, String) -> void
460
- private def autoload_subdir(parent, cname, subdir)
461
- if autoload_path = autoload_path_set_by_me_for?(parent, cname)
462
- cpath = cpath(parent, cname)
462
+ private def autoload_subdir(cref, subdir)
463
+ if autoload_path = autoload_path_set_by_me_for?(cref)
463
464
  if ruby?(autoload_path)
464
465
  # Scanning visited a Ruby file first, and now a directory for the same
465
466
  # constant has been found. This means we are dealing with an explicit
@@ -468,94 +469,91 @@ module Zeitwerk
468
469
  # Registering is idempotent, and we have to keep the autoload pointing
469
470
  # to the file. This may run again if more directories are found later
470
471
  # on, no big deal.
471
- register_explicit_namespace(cpath)
472
+ register_explicit_namespace(cref)
472
473
  end
473
474
  # If the existing autoload points to a file, it has to be preserved, if
474
475
  # not, it is fine as it is. In either case, we do not need to override.
475
476
  # Just remember the subdirectory conforms this namespace.
476
- namespace_dirs[cpath] << subdir
477
- elsif !cdef?(parent, cname)
477
+ namespace_dirs[cref.path] << subdir
478
+ elsif !cref.defined?
478
479
  # First time we find this namespace, set an autoload for it.
479
- namespace_dirs[cpath(parent, cname)] << subdir
480
- define_autoload(parent, cname, subdir)
480
+ namespace_dirs[cref.path] << subdir
481
+ define_autoload(cref, subdir)
481
482
  else
482
483
  # For whatever reason the constant that corresponds to this namespace has
483
484
  # already been defined, we have to recurse.
484
- log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
485
- 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)
486
487
  end
487
488
  end
488
489
 
489
490
  # @sig (Module, Symbol, String) -> void
490
- private def autoload_file(parent, cname, file)
491
- 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)
492
493
  # First autoload for a Ruby file wins, just ignore subsequent ones.
493
494
  if ruby?(autoload_path)
494
495
  shadowed_files << file
495
496
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
496
497
  else
497
- promote_namespace_from_implicit_to_explicit(
498
- dir: autoload_path,
499
- file: file,
500
- parent: parent,
501
- cname: cname
502
- )
498
+ promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
503
499
  end
504
- elsif cdef?(parent, cname)
500
+ elsif cref.defined?
505
501
  shadowed_files << file
506
- 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
507
503
  else
508
- define_autoload(parent, cname, file)
504
+ define_autoload(cref, file)
509
505
  end
510
506
  end
511
507
 
512
508
  # `dir` is the directory that would have autovivified a namespace. `file` is
513
509
  # the file where we've found the namespace is explicitly defined.
514
510
  #
515
- # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
516
- 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:)
517
513
  autoloads.delete(dir)
518
514
  Registry.unregister_autoload(dir)
519
515
 
520
- 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
521
517
 
522
- define_autoload(parent, cname, file)
523
- register_explicit_namespace(cpath(parent, cname))
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)
524
522
  end
525
523
 
526
524
  # @sig (Module, Symbol, String) -> void
527
- private def define_autoload(parent, cname, abspath)
528
- parent.autoload(cname, abspath)
525
+ private def define_autoload(cref, abspath)
526
+ cref.autoload(abspath)
529
527
 
530
528
  if logger
531
529
  if ruby?(abspath)
532
- log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
530
+ log("autoload set for #{cref.path}, to be loaded from #{abspath}")
533
531
  else
534
- log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
532
+ log("autoload set for #{cref.path}, to be autovivified from #{abspath}")
535
533
  end
536
534
  end
537
535
 
538
- autoloads[abspath] = [parent, cname]
536
+ autoloads[abspath] = cref
539
537
  Registry.register_autoload(self, abspath)
540
538
 
541
539
  # See why in the documentation of Zeitwerk::Registry.inceptions.
542
- unless parent.autoload?(cname)
543
- Registry.register_inception(cpath(parent, cname), abspath, self)
540
+ unless cref.autoload?
541
+ Registry.register_inception(cref.path, abspath, self)
544
542
  end
545
543
  end
546
544
 
547
545
  # @sig (Module, Symbol) -> String?
548
- private def autoload_path_set_by_me_for?(parent, cname)
549
- if autoload_path = strict_autoload_path(parent, cname)
546
+ private def autoload_path_set_by_me_for?(cref)
547
+ if autoload_path = cref.autoload?
550
548
  autoload_path if autoloads.key?(autoload_path)
551
549
  else
552
- Registry.inception?(cpath(parent, cname))
550
+ Registry.inception?(cref.path, self)
553
551
  end
554
552
  end
555
553
 
556
- # @sig (String) -> void
557
- private def register_explicit_namespace(cpath)
558
- ExplicitNamespace.__register(cpath, self)
554
+ # @sig (Zeitwerk::Cref) -> void
555
+ private def register_explicit_namespace(cref)
556
+ ExplicitNamespace.__register(cref, self)
559
557
  end
560
558
 
561
559
  # @sig (String) -> void
@@ -590,21 +588,21 @@ module Zeitwerk
590
588
  end
591
589
 
592
590
  # @sig (Module, Symbol) -> void
593
- private def unload_autoload(parent, cname)
594
- crem(parent, cname)
595
- log("autoload for #{cpath(parent, cname)} removed") if logger
591
+ private def unload_autoload(cref)
592
+ cref.remove
593
+ log("autoload for #{cref.path} removed") if logger
596
594
  end
597
595
 
598
596
  # @sig (Module, Symbol) -> void
599
- private def unload_cref(parent, cname)
597
+ private def unload_cref(cref)
600
598
  # Let's optimistically remove_const. The way we use it, this is going to
601
599
  # succeed always if all is good.
602
- crem(parent, cname)
600
+ cref.remove
603
601
  rescue ::NameError
604
602
  # There are a few edge scenarios in which this may happen. If the constant
605
603
  # is gone, that is OK, anyway.
606
604
  else
607
- log("#{cpath(parent, cname)} unloaded") if logger
605
+ log("#{cref.path} unloaded") if logger
608
606
  end
609
607
  end
610
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
- 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.15"
4
+ VERSION = "2.7.1"
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"
@@ -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.6.15
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-05-26 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,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.5'
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.7
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