zeitwerk 2.5.4 → 2.6.18

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.
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This private class encapsulates pairs (mod, cname).
4
+ #
5
+ # Objects represent the constant cname in the class or module object mod, and
6
+ # have API to manage them that encapsulates the constants API. Examples:
7
+ #
8
+ # cref.path
9
+ # cref.set(value)
10
+ # cref.get
11
+ #
12
+ # The constant may or may not exist in mod.
13
+ class Zeitwerk::Cref
14
+ include Zeitwerk::RealModName
15
+
16
+ # @sig Symbol
17
+ attr_reader :cname
18
+
19
+ # The type of the first argument is Module because Class < Module, class
20
+ # objects are also valid.
21
+ #
22
+ # @sig (Module, Symbol) -> void
23
+ def initialize(mod, cname)
24
+ @mod = mod
25
+ @cname = cname
26
+ @path = nil
27
+ end
28
+
29
+ if Symbol.method_defined?(:name)
30
+ # Symbol#name was introduced in Ruby 3.0. It returns always the same
31
+ # frozen object, so we may save a few string allocations.
32
+ #
33
+ # @sig () -> String
34
+ def path
35
+ @path ||= Object.equal?(@mod) ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}"
36
+ end
37
+ else
38
+ # @sig () -> String
39
+ def path
40
+ @path ||= Object.equal?(@mod) ? @cname.to_s : "#{real_mod_name(@mod)}::#{@cname}"
41
+ end
42
+ end
43
+
44
+ # The autoload? predicate takes into account the ancestor chain of the
45
+ # receiver, like const_defined? and other methods in the constants API do.
46
+ #
47
+ # For example, given
48
+ #
49
+ # class A
50
+ # autoload :X, "x.rb"
51
+ # end
52
+ #
53
+ # class B < A
54
+ # end
55
+ #
56
+ # B.autoload?(:X) returns "x.rb".
57
+ #
58
+ # We need a way to retrieve it ignoring ancestors.
59
+ #
60
+ # @sig () -> String?
61
+ if method(:autoload?).arity == 1
62
+ # @sig () -> String?
63
+ def autoload?
64
+ @mod.autoload?(@cname) if self.defined?
65
+ end
66
+ else
67
+ # @sig () -> String?
68
+ def autoload?
69
+ @mod.autoload?(@cname, false)
70
+ end
71
+ end
72
+
73
+ # @sig (String) -> bool
74
+ def autoload(abspath)
75
+ @mod.autoload(@cname, abspath)
76
+ end
77
+
78
+ # @sig () -> bool
79
+ def defined?
80
+ @mod.const_defined?(@cname, false)
81
+ end
82
+
83
+ # @sig (Object) -> Object
84
+ def set(value)
85
+ @mod.const_set(@cname, value)
86
+ end
87
+
88
+ # @raise [NameError]
89
+ # @sig () -> Object
90
+ def get
91
+ @mod.const_get(@cname, false)
92
+ end
93
+
94
+ # @raise [NameError]
95
+ # @sig () -> void
96
+ def remove
97
+ @mod.__send__(:remove_const, @cname)
98
+ end
99
+ end
@@ -5,8 +5,17 @@ module Zeitwerk
5
5
  end
6
6
 
7
7
  class ReloadingDisabledError < Error
8
+ def initialize
9
+ super("can't reload, please call loader.enable_reloading before setup")
10
+ end
8
11
  end
9
12
 
10
13
  class NameError < ::NameError
11
14
  end
15
+
16
+ class SetupRequired < Error
17
+ def initialize
18
+ super("please, finish your configuration and call Zeitwerk::Loader#setup once all is ready")
19
+ end
20
+ end
12
21
  end
@@ -11,28 +11,28 @@ module Zeitwerk
11
11
  module ExplicitNamespace # :nodoc: all
12
12
  class << self
13
13
  include RealModName
14
+ extend Internal
14
15
 
15
16
  # Maps constant paths that correspond to explicit namespaces according to
16
17
  # the file system, to the loader responsible for them.
17
18
  #
18
- # @private
19
19
  # @sig Hash[String, Zeitwerk::Loader]
20
20
  attr_reader :cpaths
21
+ private :cpaths
21
22
 
22
- # @private
23
23
  # @sig Mutex
24
24
  attr_reader :mutex
25
+ private :mutex
25
26
 
26
- # @private
27
27
  # @sig TracePoint
28
28
  attr_reader :tracer
29
+ private :tracer
29
30
 
30
31
  # Asserts `cpath` corresponds to an explicit namespace for which `loader`
31
32
  # is responsible.
32
33
  #
33
- # @private
34
34
  # @sig (String, Zeitwerk::Loader) -> void
35
- def register(cpath, loader)
35
+ internal def register(cpath, loader)
36
36
  mutex.synchronize do
37
37
  cpaths[cpath] = loader
38
38
  # We check enabled? because, looking at the C source code, enabling an
@@ -41,24 +41,28 @@ module Zeitwerk
41
41
  end
42
42
  end
43
43
 
44
- # @private
45
44
  # @sig (Zeitwerk::Loader) -> void
46
- def unregister_loader(loader)
45
+ internal def unregister_loader(loader)
47
46
  cpaths.delete_if { |_cpath, l| l == loader }
48
47
  disable_tracer_if_unneeded
49
48
  end
50
49
 
51
- private
50
+ # This is an internal method only used by the test suite.
51
+ #
52
+ # @sig (String) -> bool
53
+ internal def registered?(cpath)
54
+ cpaths.key?(cpath)
55
+ end
52
56
 
53
57
  # @sig () -> void
54
- def disable_tracer_if_unneeded
58
+ private def disable_tracer_if_unneeded
55
59
  mutex.synchronize do
56
60
  tracer.disable if cpaths.empty?
57
61
  end
58
62
  end
59
63
 
60
64
  # @sig (TracePoint) -> void
61
- def tracepoint_class_callback(event)
65
+ private def tracepoint_class_callback(event)
62
66
  # If the class is a singleton class, we won't do anything with it so we
63
67
  # can bail out immediately. This is several orders of magnitude faster
64
68
  # than accessing its name.
@@ -5,8 +5,8 @@ module Zeitwerk
5
5
  # @sig (String) -> void
6
6
  def initialize(root_file)
7
7
  namespace = File.basename(root_file, ".rb")
8
- lib_dir = File.dirname(root_file)
9
- @version_file = File.join(lib_dir, namespace, "version.rb")
8
+ root_dir = File.dirname(root_file)
9
+ @version_file = File.join(root_dir, namespace, "version.rb")
10
10
  end
11
11
 
12
12
  # @sig (String, String) -> String
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk
4
+ # @private
5
+ class GemLoader < Loader
6
+ include RealModName
7
+
8
+ # Users should not create instances directly, the public interface is
9
+ # `Zeitwerk::Loader.for_gem`.
10
+ private_class_method :new
11
+
12
+ # @private
13
+ # @sig (String, bool) -> Zeitwerk::GemLoader
14
+ def self.__new(root_file, namespace:, warn_on_extra_files:)
15
+ new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
16
+ end
17
+
18
+ # @sig (String, bool) -> void
19
+ def initialize(root_file, namespace:, warn_on_extra_files:)
20
+ super()
21
+
22
+ @tag = File.basename(root_file, ".rb")
23
+ @tag = real_mod_name(namespace) + "-" + @tag unless namespace.equal?(Object)
24
+
25
+ @inflector = GemInflector.new(root_file)
26
+ @root_file = File.expand_path(root_file)
27
+ @root_dir = File.dirname(root_file)
28
+ @warn_on_extra_files = warn_on_extra_files
29
+
30
+ push_dir(@root_dir, namespace: namespace)
31
+ end
32
+
33
+ # @sig () -> void
34
+ def setup
35
+ warn_on_extra_files if @warn_on_extra_files
36
+ super
37
+ end
38
+
39
+ private
40
+
41
+ # @sig () -> void
42
+ def warn_on_extra_files
43
+ expected_namespace_dir = @root_file.delete_suffix(".rb")
44
+
45
+ ls(@root_dir) do |basename, abspath, ftype|
46
+ next if abspath == @root_file
47
+ next if abspath == expected_namespace_dir
48
+
49
+ basename_without_ext = basename.delete_suffix(".rb")
50
+ cname = inflector.camelize(basename_without_ext, abspath).to_sym
51
+
52
+ warn(<<~EOS)
53
+ WARNING: Zeitwerk defines the constant #{cname} after the #{ftype}
54
+
55
+ #{abspath}
56
+
57
+ To prevent that, please configure the loader to ignore it:
58
+
59
+ loader.ignore("\#{__dir__}/#{basename}")
60
+
61
+ Otherwise, there is a flag to silence this warning:
62
+
63
+ Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
64
+ EOS
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is a private module.
4
+ module Zeitwerk::Internal
5
+ def internal(method_name)
6
+ private method_name
7
+
8
+ mangled = "__#{method_name}"
9
+ alias_method mangled, method_name
10
+ public mangled
11
+ end
12
+ end
@@ -14,21 +14,20 @@ 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
18
+ class << self
19
+ alias_method :zeitwerk_original_require, :require
20
+ end
22
21
 
23
22
  # @sig (String) -> true | false
24
23
  def require(path)
25
24
  if loader = Zeitwerk::Registry.loader_for(path)
26
25
  if path.end_with?(".rb")
27
26
  required = zeitwerk_original_require(path)
28
- loader.on_file_autoloaded(path) if required
27
+ loader.__on_file_autoloaded(path) if required
29
28
  required
30
29
  else
31
- loader.on_dir_autoloaded(path)
30
+ loader.__on_dir_autoloaded(path)
32
31
  true
33
32
  end
34
33
  else
@@ -36,7 +35,7 @@ module Kernel
36
35
  if required
37
36
  abspath = $LOADED_FEATURES.last
38
37
  if loader = Zeitwerk::Registry.loader_for(abspath)
39
- loader.on_file_autoloaded(abspath)
38
+ loader.__on_file_autoloaded(abspath)
40
39
  end
41
40
  end
42
41
  required
@@ -2,38 +2,45 @@
2
2
 
3
3
  module Zeitwerk::Loader::Callbacks
4
4
  include Zeitwerk::RealModName
5
+ extend Zeitwerk::Internal
5
6
 
6
7
  # Invoked from our decorated Kernel#require when a managed file is autoloaded.
7
8
  #
8
- # @private
9
9
  # @sig (String) -> void
10
- def on_file_autoloaded(file)
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?
10
+ internal def on_file_autoloaded(file)
11
+ cref = autoloads.delete(file)
12
+
19
13
  Zeitwerk::Registry.unregister_autoload(file)
20
14
 
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?
15
+ if cref.defined?
16
+ log("constant #{cref.path} loaded from file #{file}") if logger
17
+ to_unload[cref.path] = [file, cref] if reloading_enabled?
18
+ run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
24
19
  else
25
- raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
20
+ msg = "expected file #{file} to define constant #{cref.path}, but didn't"
21
+ log(msg) if logger
22
+
23
+ # Ruby still keeps the autoload defined, but we remove it because the
24
+ # contract in Zeitwerk is more strict.
25
+ cref.remove
26
+
27
+ # Since the expected constant was not defined, there is nothing to unload.
28
+ # However, if the exception is rescued and reloading is enabled, we still
29
+ # need to deleted the file from $LOADED_FEATURES.
30
+ to_unload[cref.path] = [file, cref] if reloading_enabled?
31
+
32
+ raise Zeitwerk::NameError.new(msg, cref.cname)
26
33
  end
27
34
  end
28
35
 
29
36
  # Invoked from our decorated Kernel#require when a managed directory is
30
37
  # autoloaded.
31
38
  #
32
- # @private
33
39
  # @sig (String) -> void
34
- def on_dir_autoloaded(dir)
35
- # Module#autoload does not serialize concurrent requires, and we handle
36
- # directories ourselves, so the callback needs to account for concurrency.
40
+ internal def on_dir_autoloaded(dir)
41
+ # Module#autoload does not serialize concurrent requires in CRuby < 3.2, and
42
+ # we handle directories ourselves without going through Kernel#require, so
43
+ # the callback needs to account for concurrency.
37
44
  #
38
45
  # Multi-threading would introduce a race condition here in which thread t1
39
46
  # autovivifies the module, and while autoloads for its children are being
@@ -42,11 +49,11 @@ module Zeitwerk::Loader::Callbacks
42
49
  # Without the mutex and subsequent delete call, t2 would reset the module.
43
50
  # That not only would reassign the constant (undesirable per se) but, worse,
44
51
  # the module object created by t2 wouldn't have any of the autoloads for its
45
- # children, since t1 would have correctly deleted its lazy_subdirs entry.
46
- mutex2.synchronize do
52
+ # children, since t1 would have correctly deleted its namespace_dirs entry.
53
+ dirs_autoload_monitor.synchronize do
47
54
  if cref = autoloads.delete(dir)
48
- autovivified_module = cref[0].const_set(cref[1], Module.new)
49
- cpath = autovivified_module.name
55
+ implicit_namespace = cref.set(Module.new)
56
+ cpath = implicit_namespace.name
50
57
  log("module #{cpath} autovivified from directory #{dir}") if logger
51
58
 
52
59
  to_unload[cpath] = [dir, cref] if reloading_enabled?
@@ -57,9 +64,9 @@ module Zeitwerk::Loader::Callbacks
57
64
  # these to be able to unregister later if eager loading.
58
65
  autoloaded_dirs << dir
59
66
 
60
- on_namespace_loaded(autovivified_module)
67
+ on_namespace_loaded(implicit_namespace)
61
68
 
62
- run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
69
+ run_on_load_callbacks(cpath, implicit_namespace, dir) unless on_load_callbacks.empty?
63
70
  end
64
71
  end
65
72
  end
@@ -71,9 +78,9 @@ module Zeitwerk::Loader::Callbacks
71
78
  # @private
72
79
  # @sig (Module) -> void
73
80
  def on_namespace_loaded(namespace)
74
- if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
75
- subdirs.each do |subdir|
76
- set_autoloads_in_dir(subdir, namespace)
81
+ if dirs = namespace_dirs.delete(real_mod_name(namespace))
82
+ dirs.each do |dir|
83
+ define_autoloads_for_dir(dir, namespace)
77
84
  end
78
85
  end
79
86
  end