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.
- checksums.yaml +4 -4
- data/README.md +473 -50
- data/lib/zeitwerk/cref.rb +99 -0
- data/lib/zeitwerk/error.rb +9 -0
- data/lib/zeitwerk/explicit_namespace.rb +14 -10
- data/lib/zeitwerk/gem_inflector.rb +2 -2
- data/lib/zeitwerk/gem_loader.rb +68 -0
- data/lib/zeitwerk/internal.rb +12 -0
- data/lib/zeitwerk/kernel.rb +6 -7
- data/lib/zeitwerk/loader/callbacks.rb +34 -27
- data/lib/zeitwerk/loader/config.rb +95 -52
- data/lib/zeitwerk/loader/eager_load.rb +232 -0
- data/lib/zeitwerk/loader/helpers.rb +106 -55
- data/lib/zeitwerk/loader.rb +287 -197
- data/lib/zeitwerk/null_inflector.rb +5 -0
- data/lib/zeitwerk/registry.rb +14 -18
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +4 -0
- metadata +8 -3
@@ -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
|
data/lib/zeitwerk/error.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
9
|
-
@version_file = File.join(
|
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
|
data/lib/zeitwerk/kernel.rb
CHANGED
@@ -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.
|
27
|
+
loader.__on_file_autoloaded(path) if required
|
29
28
|
required
|
30
29
|
else
|
31
|
-
loader.
|
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.
|
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
|
12
|
-
|
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
|
22
|
-
log("constant #{
|
23
|
-
|
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
|
-
|
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
|
36
|
-
# directories ourselves
|
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
|
46
|
-
|
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
|
-
|
49
|
-
cpath =
|
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(
|
67
|
+
on_namespace_loaded(implicit_namespace)
|
61
68
|
|
62
|
-
run_on_load_callbacks(cpath,
|
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
|
75
|
-
|
76
|
-
|
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
|