zeitwerk 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk
2
4
  # Centralizes the logic for the trace point used to detect the creation of
3
5
  # explicit namespaces, needed to descend into matching subdirectories right
@@ -14,24 +16,22 @@ module Zeitwerk
14
16
  # the file system, to the loader responsible for them.
15
17
  #
16
18
  # @private
17
- # @return [{String => Zeitwerk::Loader}]
19
+ # @sig Hash[String, Zeitwerk::Loader]
18
20
  attr_reader :cpaths
19
21
 
20
22
  # @private
21
- # @return [Mutex]
23
+ # @sig Mutex
22
24
  attr_reader :mutex
23
25
 
24
26
  # @private
25
- # @return [TracePoint]
27
+ # @sig TracePoint
26
28
  attr_reader :tracer
27
29
 
28
30
  # Asserts `cpath` corresponds to an explicit namespace for which `loader`
29
31
  # is responsible.
30
32
  #
31
33
  # @private
32
- # @param cpath [String]
33
- # @param loader [Zeitwerk::Loader]
34
- # @return [void]
34
+ # @sig (String, Zeitwerk::Loader) -> void
35
35
  def register(cpath, loader)
36
36
  mutex.synchronize do
37
37
  cpaths[cpath] = loader
@@ -42,27 +42,36 @@ module Zeitwerk
42
42
  end
43
43
 
44
44
  # @private
45
- # @param loader [Zeitwerk::Loader]
46
- # @return [void]
47
- def unregister(loader)
45
+ # @sig (Zeitwerk::Loader) -> void
46
+ def unregister_loader(loader)
48
47
  cpaths.delete_if { |_cpath, l| l == loader }
49
48
  disable_tracer_if_unneeded
50
49
  end
51
50
 
51
+ private
52
+
53
+ # @sig () -> void
52
54
  def disable_tracer_if_unneeded
53
55
  mutex.synchronize do
54
56
  tracer.disable if cpaths.empty?
55
57
  end
56
58
  end
57
59
 
60
+ # @sig (TracePoint) -> void
58
61
  def tracepoint_class_callback(event)
59
62
  # If the class is a singleton class, we won't do anything with it so we
60
63
  # can bail out immediately. This is several orders of magnitude faster
61
64
  # than accessing its name.
62
65
  return if event.self.singleton_class?
63
66
 
64
- # Note that it makes sense to compute the hash code unconditionally,
65
- # because the trace point is disabled if cpaths is empty.
67
+ # It might be tempting to return if name.nil?, to avoid the computation
68
+ # of a hash code and delete call. But Ruby does not trigger the :class
69
+ # event on Class.new or Module.new, so that would incur in an extra call
70
+ # for nothing.
71
+ #
72
+ # On the other hand, if we were called, cpaths is not empty. Otherwise
73
+ # the tracer is disabled. So we do need to go ahead with the hash code
74
+ # computation and delete call.
66
75
  if loader = cpaths.delete(real_mod_name(event.self))
67
76
  loader.on_namespace_loaded(event.self)
68
77
  disable_tracer_if_unneeded
@@ -2,16 +2,14 @@
2
2
 
3
3
  module Zeitwerk
4
4
  class GemInflector < Inflector
5
- # @param root_file [String]
5
+ # @sig (String) -> void
6
6
  def initialize(root_file)
7
7
  namespace = File.basename(root_file, ".rb")
8
8
  lib_dir = File.dirname(root_file)
9
9
  @version_file = File.join(lib_dir, namespace, "version.rb")
10
10
  end
11
11
 
12
- # @param basename [String]
13
- # @param abspath [String]
14
- # @return [String]
12
+ # @sig (String, String) -> String
15
13
  def camelize(basename, abspath)
16
14
  abspath == @version_file ? "VERSION" : super
17
15
  end
@@ -11,9 +11,7 @@ module Zeitwerk
11
11
  #
12
12
  # Takes into account hard-coded mappings configured with `inflect`.
13
13
  #
14
- # @param basename [String]
15
- # @param _abspath [String]
16
- # @return [String]
14
+ # @sig (String, String) -> String
17
15
  def camelize(basename, _abspath)
18
16
  overrides[basename] || basename.split('_').each(&:capitalize!).join
19
17
  end
@@ -30,8 +28,7 @@ module Zeitwerk
30
28
  # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
31
29
  # inflector.camelize("users_controller", abspath) # => "UsersController"
32
30
  #
33
- # @param inflections [{String => String}]
34
- # @return [void]
31
+ # @sig (Hash[String, String]) -> void
35
32
  def inflect(inflections)
36
33
  overrides.merge!(inflections)
37
34
  end
@@ -41,7 +38,7 @@ module Zeitwerk
41
38
  # Hard-coded basename to constant name user maps that override the default
42
39
  # inflection logic.
43
40
  #
44
- # @return [{String => String}]
41
+ # @sig () -> Hash[String, String]
45
42
  def overrides
46
43
  @overrides ||= {}
47
44
  end
@@ -3,7 +3,7 @@
3
3
  module Kernel
4
4
  module_function
5
5
 
6
- # We are going to decorate Kerner#require with two goals.
6
+ # We are going to decorate Kernel#require with two goals.
7
7
  #
8
8
  # First, by intercepting Kernel#require calls, we are able to autovivify
9
9
  # modules on required directories, and also do internal housekeeping when
@@ -12,15 +12,15 @@ module Kernel
12
12
  # On the other hand, if you publish a new version of a gem that is now managed
13
13
  # by Zeitwerk, client code can reference directly your classes and modules and
14
14
  # should not require anything. But if someone has legacy require calls around,
15
- # they will work as expected, and in a compatible way.
15
+ # they will work as expected, and in a compatible way. This feature is by now
16
+ # EXPERIMENTAL and UNDOCUMENTED.
16
17
  #
17
18
  # We cannot decorate with prepend + super because Kernel has already been
18
19
  # included in Object, and changes in ancestors don't get propagated into
19
20
  # already existing ancestor chains.
20
21
  alias_method :zeitwerk_original_require, :require
21
22
 
22
- # @param path [String]
23
- # @return [Boolean]
23
+ # @sig (String) -> true | false
24
24
  def require(path)
25
25
  if loader = Zeitwerk::Registry.loader_for(path)
26
26
  if path.end_with?(".rb")
@@ -29,13 +29,14 @@ module Kernel
29
29
  end
30
30
  else
31
31
  loader.on_dir_autoloaded(path)
32
+ true
32
33
  end
33
34
  else
34
35
  zeitwerk_original_require(path).tap do |required|
35
36
  if required
36
- realpath = $LOADED_FEATURES.last
37
- if loader = Zeitwerk::Registry.loader_for(realpath)
38
- loader.on_file_autoloaded(realpath)
37
+ abspath = $LOADED_FEATURES.last
38
+ if loader = Zeitwerk::Registry.loader_for(abspath)
39
+ loader.on_file_autoloaded(abspath)
39
40
  end
40
41
  end
41
42
  end
@@ -1,20 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk::Loader::Callbacks
2
4
  include Zeitwerk::RealModName
3
5
 
4
6
  # Invoked from our decorated Kernel#require when a managed file is autoloaded.
5
7
  #
6
8
  # @private
7
- # @param file [String]
8
- # @return [void]
9
+ # @sig (String) -> void
9
10
  def on_file_autoloaded(file)
10
- cref = autoloads.delete(file)
11
- to_unload[cpath(*cref)] = [file, cref] if reloading_enabled?
11
+ cref = autoloads.delete(file)
12
+ cpath = cpath(*cref)
13
+
14
+ # If reloading is enabled, we need to put this constant for unloading
15
+ # regardless of what cdef? says. In Ruby < 3.1 the internal state is not
16
+ # fully cleared. Module#constants still includes it, and you need to
17
+ # remove_const. See https://github.com/ruby/ruby/pull/4715.
18
+ to_unload[cpath] = [file, cref] if reloading_enabled?
12
19
  Zeitwerk::Registry.unregister_autoload(file)
13
20
 
14
- if logger && cdef?(*cref)
15
- log("constant #{cpath(*cref)} loaded from file #{file}")
16
- elsif !cdef?(*cref)
17
- raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath(*cref)}, but didn't", cref.last)
21
+ if cdef?(*cref)
22
+ log("constant #{cpath} loaded from file #{file}") if logger
23
+ run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
24
+ else
25
+ raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
18
26
  end
19
27
  end
20
28
 
@@ -22,8 +30,7 @@ module Zeitwerk::Loader::Callbacks
22
30
  # autoloaded.
23
31
  #
24
32
  # @private
25
- # @param dir [String]
26
- # @return [void]
33
+ # @sig (String) -> void
27
34
  def on_dir_autoloaded(dir)
28
35
  # Module#autoload does not serialize concurrent requires, and we handle
29
36
  # directories ourselves, so the callback needs to account for concurrency.
@@ -39,9 +46,10 @@ module Zeitwerk::Loader::Callbacks
39
46
  mutex2.synchronize do
40
47
  if cref = autoloads.delete(dir)
41
48
  autovivified_module = cref[0].const_set(cref[1], Module.new)
42
- log("module #{autovivified_module.name} autovivified from directory #{dir}") if logger
49
+ cpath = autovivified_module.name
50
+ log("module #{cpath} autovivified from directory #{dir}") if logger
43
51
 
44
- to_unload[autovivified_module.name] = [dir, cref] if reloading_enabled?
52
+ to_unload[cpath] = [dir, cref] if reloading_enabled?
45
53
 
46
54
  # We don't unregister `dir` in the registry because concurrent threads
47
55
  # wouldn't find a loader associated to it in Kernel#require and would
@@ -50,6 +58,8 @@ module Zeitwerk::Loader::Callbacks
50
58
  autoloaded_dirs << dir
51
59
 
52
60
  on_namespace_loaded(autovivified_module)
61
+
62
+ run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
53
63
  end
54
64
  end
55
65
  end
@@ -59,8 +69,7 @@ module Zeitwerk::Loader::Callbacks
59
69
  # subdirectories, we descend into them now.
60
70
  #
61
71
  # @private
62
- # @param namespace [Module]
63
- # @return [void]
72
+ # @sig (Module) -> void
64
73
  def on_namespace_loaded(namespace)
65
74
  if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
66
75
  subdirs.each do |subdir|
@@ -68,4 +77,16 @@ module Zeitwerk::Loader::Callbacks
68
77
  end
69
78
  end
70
79
  end
80
+
81
+ private
82
+
83
+ # @sig (String, Object) -> void
84
+ def run_on_load_callbacks(cpath, value, abspath)
85
+ # Order matters. If present, run the most specific one.
86
+ callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
87
+ callbacks&.each { |c| c.call(value, abspath) }
88
+
89
+ callbacks = on_load_callbacks[:ANY]
90
+ callbacks&.each { |c| c.call(cpath, value, abspath) }
91
+ end
71
92
  end
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "securerandom"
5
+
6
+ module Zeitwerk::Loader::Config
7
+ # Absolute paths of the root directories. Stored in a hash to preserve
8
+ # order, easily handle duplicates, and also be able to have a fast lookup,
9
+ # needed for detecting nested paths.
10
+ #
11
+ # "/Users/fxn/blog/app/assets" => true,
12
+ # "/Users/fxn/blog/app/channels" => true,
13
+ # ...
14
+ #
15
+ # This is a private collection maintained by the loader. The public
16
+ # interface for it is `push_dir` and `dirs`.
17
+ #
18
+ # @private
19
+ # @sig Hash[String, true]
20
+ attr_reader :root_dirs
21
+
22
+ # @sig #camelize
23
+ attr_accessor :inflector
24
+
25
+ # Absolute paths of files, directories, or glob patterns to be totally
26
+ # ignored.
27
+ #
28
+ # @private
29
+ # @sig Set[String]
30
+ attr_reader :ignored_glob_patterns
31
+
32
+ # The actual collection of absolute file and directory names at the time the
33
+ # ignored glob patterns were expanded. Computed on setup, and recomputed on
34
+ # reload.
35
+ #
36
+ # @private
37
+ # @sig Set[String]
38
+ attr_reader :ignored_paths
39
+
40
+ # Absolute paths of directories or glob patterns to be collapsed.
41
+ #
42
+ # @private
43
+ # @sig Set[String]
44
+ attr_reader :collapse_glob_patterns
45
+
46
+ # The actual collection of absolute directory names at the time the collapse
47
+ # glob patterns were expanded. Computed on setup, and recomputed on reload.
48
+ #
49
+ # @private
50
+ # @sig Set[String]
51
+ attr_reader :collapse_dirs
52
+
53
+ # Absolute paths of files or directories not to be eager loaded.
54
+ #
55
+ # @private
56
+ # @sig Set[String]
57
+ attr_reader :eager_load_exclusions
58
+
59
+ # User-oriented callbacks to be fired on setup and on reload.
60
+ #
61
+ # @private
62
+ # @sig Array[{ () -> void }]
63
+ attr_reader :on_setup_callbacks
64
+
65
+ # User-oriented callbacks to be fired when a constant is loaded.
66
+ #
67
+ # @private
68
+ # @sig Hash[String, Array[{ (Object, String) -> void }]]
69
+ # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
70
+ attr_reader :on_load_callbacks
71
+
72
+ # User-oriented callbacks to be fired before constants are removed.
73
+ #
74
+ # @private
75
+ # @sig Hash[String, Array[{ (Object, String) -> void }]]
76
+ # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
77
+ attr_reader :on_unload_callbacks
78
+
79
+ # @sig #call | #debug | nil
80
+ attr_accessor :logger
81
+
82
+ def initialize
83
+ @initialized_at = Time.now
84
+ @root_dirs = {}
85
+ @inflector = Zeitwerk::Inflector.new
86
+ @ignored_glob_patterns = Set.new
87
+ @ignored_paths = Set.new
88
+ @collapse_glob_patterns = Set.new
89
+ @collapse_dirs = Set.new
90
+ @eager_load_exclusions = Set.new
91
+ @reloading_enabled = false
92
+ @on_setup_callbacks = []
93
+ @on_load_callbacks = {}
94
+ @on_unload_callbacks = {}
95
+ @logger = self.class.default_logger
96
+ @tag = SecureRandom.hex(3)
97
+ end
98
+
99
+ # Pushes `path` to the list of root directories.
100
+ #
101
+ # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
102
+ # the same process already manages that directory or one of its ascendants or
103
+ # descendants.
104
+ #
105
+ # @raise [Zeitwerk::Error]
106
+ # @sig (String | Pathname, Module) -> void
107
+ def push_dir(path, namespace: Object)
108
+ # Note that Class < Module.
109
+ unless namespace.is_a?(Module)
110
+ raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
111
+ end
112
+
113
+ abspath = File.expand_path(path)
114
+ if dir?(abspath)
115
+ raise_if_conflicting_directory(abspath)
116
+ root_dirs[abspath] = namespace
117
+ else
118
+ raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
119
+ end
120
+ end
121
+
122
+ # Returns the loader's tag.
123
+ #
124
+ # Implemented as a method instead of via attr_reader for symmetry with the
125
+ # writer below.
126
+ #
127
+ # @sig () -> String
128
+ def tag
129
+ @tag
130
+ end
131
+
132
+ # Sets a tag for the loader, useful for logging.
133
+ #
134
+ # @param tag [#to_s]
135
+ # @sig (#to_s) -> void
136
+ def tag=(tag)
137
+ @tag = tag.to_s
138
+ end
139
+
140
+ # Absolute paths of the root directories. This is a read-only collection,
141
+ # please push here via `push_dir`.
142
+ #
143
+ # @sig () -> Array[String]
144
+ def dirs
145
+ root_dirs.keys.freeze
146
+ end
147
+
148
+ # You need to call this method before setup in order to be able to reload.
149
+ # There is no way to undo this, either you want to reload or you don't.
150
+ #
151
+ # @raise [Zeitwerk::Error]
152
+ # @sig () -> void
153
+ def enable_reloading
154
+ mutex.synchronize do
155
+ break if @reloading_enabled
156
+
157
+ if @setup
158
+ raise Zeitwerk::Error, "cannot enable reloading after setup"
159
+ else
160
+ @reloading_enabled = true
161
+ end
162
+ end
163
+ end
164
+
165
+ # @sig () -> bool
166
+ def reloading_enabled?
167
+ @reloading_enabled
168
+ end
169
+
170
+ # Let eager load ignore the given files or directories. The constants defined
171
+ # in those files are still autoloadable.
172
+ #
173
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
174
+ def do_not_eager_load(*paths)
175
+ mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
176
+ end
177
+
178
+ # Configure files, directories, or glob patterns to be totally ignored.
179
+ #
180
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
181
+ def ignore(*glob_patterns)
182
+ glob_patterns = expand_paths(glob_patterns)
183
+ mutex.synchronize do
184
+ ignored_glob_patterns.merge(glob_patterns)
185
+ ignored_paths.merge(expand_glob_patterns(glob_patterns))
186
+ end
187
+ end
188
+
189
+ # Configure directories or glob patterns to be collapsed.
190
+ #
191
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
192
+ def collapse(*glob_patterns)
193
+ glob_patterns = expand_paths(glob_patterns)
194
+ mutex.synchronize do
195
+ collapse_glob_patterns.merge(glob_patterns)
196
+ collapse_dirs.merge(expand_glob_patterns(glob_patterns))
197
+ end
198
+ end
199
+
200
+ # Configure a block to be called after setup and on each reload.
201
+ # If setup was already done, the block runs immediately.
202
+ #
203
+ # @sig () { () -> void } -> void
204
+ def on_setup(&block)
205
+ mutex.synchronize do
206
+ on_setup_callbacks << block
207
+ block.call if @setup
208
+ end
209
+ end
210
+
211
+ # Configure a block to be invoked once a certain constant path is loaded.
212
+ # Supports multiple callbacks, and if there are many, they are executed in
213
+ # the order in which they were defined.
214
+ #
215
+ # loader.on_load("SomeApiClient") do |klass, _abspath|
216
+ # klass.endpoint = "https://api.dev"
217
+ # end
218
+ #
219
+ # Can also be configured for any constant loaded:
220
+ #
221
+ # loader.on_load do |cpath, value, abspath|
222
+ # # ...
223
+ # end
224
+ #
225
+ # @raise [TypeError]
226
+ # @sig (String) { (Object, String) -> void } -> void
227
+ # (:ANY) { (String, Object, String) -> void } -> void
228
+ def on_load(cpath = :ANY, &block)
229
+ raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
230
+
231
+ mutex.synchronize do
232
+ (on_load_callbacks[cpath] ||= []) << block
233
+ end
234
+ end
235
+
236
+ # Configure a block to be invoked right before a certain constant is removed.
237
+ # Supports multiple callbacks, and if there are many, they are executed in the
238
+ # order in which they were defined.
239
+ #
240
+ # loader.on_unload("Country") do |klass, _abspath|
241
+ # klass.clear_cache
242
+ # end
243
+ #
244
+ # Can also be configured for any removed constant:
245
+ #
246
+ # loader.on_unload do |cpath, value, abspath|
247
+ # # ...
248
+ # end
249
+ #
250
+ # @raise [TypeError]
251
+ # @sig (String) { (Object) -> void } -> void
252
+ # (:ANY) { (String, Object) -> void } -> void
253
+ def on_unload(cpath = :ANY, &block)
254
+ raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
255
+
256
+ mutex.synchronize do
257
+ (on_unload_callbacks[cpath] ||= []) << block
258
+ end
259
+ end
260
+
261
+ # Logs to `$stdout`, handy shortcut for debugging.
262
+ #
263
+ # @sig () -> void
264
+ def log!
265
+ @logger = ->(msg) { puts msg }
266
+ end
267
+
268
+ # @private
269
+ # @sig (String) -> bool
270
+ def ignores?(abspath)
271
+ ignored_paths.any? do |ignored_path|
272
+ ignored_path == abspath || (dir?(ignored_path) && abspath.start_with?(ignored_path + "/"))
273
+ end
274
+ end
275
+
276
+ private
277
+
278
+ # @sig () -> Array[String]
279
+ def actual_root_dirs
280
+ root_dirs.reject do |root_dir, _namespace|
281
+ !dir?(root_dir) || ignored_paths.member?(root_dir)
282
+ end
283
+ end
284
+
285
+ # @sig (String) -> bool
286
+ def root_dir?(dir)
287
+ root_dirs.key?(dir)
288
+ end
289
+
290
+ # @sig (String) -> bool
291
+ def excluded_from_eager_load?(abspath)
292
+ eager_load_exclusions.member?(abspath)
293
+ end
294
+
295
+ # @sig (String) -> bool
296
+ def collapse?(dir)
297
+ collapse_dirs.member?(dir)
298
+ end
299
+
300
+ # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
301
+ def expand_paths(paths)
302
+ paths.flatten.map! { |path| File.expand_path(path) }
303
+ end
304
+
305
+ # @sig (Array[String]) -> Array[String]
306
+ def expand_glob_patterns(glob_patterns)
307
+ # Note that Dir.glob works with regular file names just fine. That is,
308
+ # glob patterns technically need no wildcards.
309
+ glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
310
+ end
311
+
312
+ # @sig () -> void
313
+ def recompute_ignored_paths
314
+ ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
315
+ end
316
+
317
+ # @sig () -> void
318
+ def recompute_collapse_dirs
319
+ collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
320
+ end
321
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk::Loader::Helpers
4
+ private
5
+
6
+ # --- Logging -----------------------------------------------------------------------------------
7
+
8
+ # @sig (String) -> void
9
+ def log(message)
10
+ method_name = logger.respond_to?(:debug) ? :debug : :call
11
+ logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
12
+ end
13
+
14
+ # --- Files and directories ---------------------------------------------------------------------
15
+
16
+ # @sig (String) { (String, String) -> void } -> void
17
+ def ls(dir)
18
+ Dir.each_child(dir) do |basename|
19
+ next if hidden?(basename)
20
+
21
+ abspath = File.join(dir, basename)
22
+ next if ignored_paths.member?(abspath)
23
+
24
+ # We freeze abspath because that saves allocations when passed later to
25
+ # File methods. See #125.
26
+ yield basename, abspath.freeze
27
+ end
28
+ end
29
+
30
+ # @sig (String) -> bool
31
+ def ruby?(path)
32
+ path.end_with?(".rb")
33
+ end
34
+
35
+ # @sig (String) -> bool
36
+ def dir?(path)
37
+ File.directory?(path)
38
+ end
39
+
40
+ # @sig String -> bool
41
+ def hidden?(basename)
42
+ basename.start_with?(".")
43
+ end
44
+
45
+ # --- Constants ---------------------------------------------------------------------------------
46
+
47
+ # The autoload? predicate takes into account the ancestor chain of the
48
+ # receiver, like const_defined? and other methods in the constants API do.
49
+ #
50
+ # For example, given
51
+ #
52
+ # class A
53
+ # autoload :X, "x.rb"
54
+ # end
55
+ #
56
+ # class B < A
57
+ # end
58
+ #
59
+ # B.autoload?(:X) returns "x.rb".
60
+ #
61
+ # We need a way to strictly check in parent ignoring ancestors.
62
+ #
63
+ # @sig (Module, Symbol) -> String?
64
+ if method(:autoload?).arity == 1
65
+ def strict_autoload_path(parent, cname)
66
+ parent.autoload?(cname) if cdef?(parent, cname)
67
+ end
68
+ else
69
+ def strict_autoload_path(parent, cname)
70
+ parent.autoload?(cname, false)
71
+ end
72
+ end
73
+
74
+ # @sig (Module, Symbol) -> String
75
+ if Symbol.method_defined?(:name)
76
+ # Symbol#name was introduced in Ruby 3.0. It returns always the same
77
+ # frozen object, so we may save a few string allocations.
78
+ def cpath(parent, cname)
79
+ Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
80
+ end
81
+ else
82
+ def cpath(parent, cname)
83
+ Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
84
+ end
85
+ end
86
+
87
+ # @sig (Module, Symbol) -> bool
88
+ def cdef?(parent, cname)
89
+ parent.const_defined?(cname, false)
90
+ end
91
+
92
+ # @raise [NameError]
93
+ # @sig (Module, Symbol) -> Object
94
+ def cget(parent, cname)
95
+ parent.const_get(cname, false)
96
+ end
97
+ end