zeitwerk 2.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +650 -0
- data/lib/zeitwerk.rb +12 -0
- data/lib/zeitwerk/error.rb +10 -0
- data/lib/zeitwerk/explicit_namespace.rb +80 -0
- data/lib/zeitwerk/gem_inflector.rb +19 -0
- data/lib/zeitwerk/inflector.rb +49 -0
- data/lib/zeitwerk/kernel.rb +33 -0
- data/lib/zeitwerk/loader.rb +765 -0
- data/lib/zeitwerk/loader/callbacks.rb +71 -0
- data/lib/zeitwerk/real_mod_name.rb +21 -0
- data/lib/zeitwerk/registry.rb +147 -0
- data/lib/zeitwerk/version.rb +5 -0
- metadata +59 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
module Zeitwerk::Loader::Callbacks
|
2
|
+
include Zeitwerk::RealModName
|
3
|
+
|
4
|
+
# Invoked from our decorated Kernel#require when a managed file is autoloaded.
|
5
|
+
#
|
6
|
+
# @private
|
7
|
+
# @param file [String]
|
8
|
+
# @return [void]
|
9
|
+
def on_file_autoloaded(file)
|
10
|
+
cref = autoloads.delete(file)
|
11
|
+
to_unload[cpath(*cref)] = [file, cref] if reloading_enabled?
|
12
|
+
Zeitwerk::Registry.unregister_autoload(file)
|
13
|
+
|
14
|
+
if logger && cdef?(*cref)
|
15
|
+
log("constant #{cpath(*cref)} loaded from file #{file}")
|
16
|
+
elsif !cdef?(*cref)
|
17
|
+
raise Zeitwerk::NameError, "expected file #{file} to define constant #{cpath(*cref)}, but didn't"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Invoked from our decorated Kernel#require when a managed directory is
|
22
|
+
# autoloaded.
|
23
|
+
#
|
24
|
+
# @private
|
25
|
+
# @param dir [String]
|
26
|
+
# @return [void]
|
27
|
+
def on_dir_autoloaded(dir)
|
28
|
+
# Module#autoload does not serialize concurrent requires, and we handle
|
29
|
+
# directories ourselves, so the callback needs to account for concurrency.
|
30
|
+
#
|
31
|
+
# Multi-threading would introduce a race condition here in which thread t1
|
32
|
+
# autovivifies the module, and while autoloads for its children are being
|
33
|
+
# set, thread t2 autoloads the same namespace.
|
34
|
+
#
|
35
|
+
# Without the mutex and subsequent delete call, t2 would reset the module.
|
36
|
+
# That not only would reassign the constant (undesirable per se) but, worse,
|
37
|
+
# the module object created by t2 wouldn't have any of the autoloads for its
|
38
|
+
# children, since t1 would have correctly deleted its lazy_subdirs entry.
|
39
|
+
mutex2.synchronize do
|
40
|
+
if cref = autoloads.delete(dir)
|
41
|
+
autovivified_module = cref[0].const_set(cref[1], Module.new)
|
42
|
+
log("module #{autovivified_module.name} autovivified from directory #{dir}") if logger
|
43
|
+
|
44
|
+
to_unload[autovivified_module.name] = [dir, cref] if reloading_enabled?
|
45
|
+
|
46
|
+
# We don't unregister `dir` in the registry because concurrent threads
|
47
|
+
# wouldn't find a loader associated to it in Kernel#require and would
|
48
|
+
# try to require the directory. Instead, we are going to keep track of
|
49
|
+
# these to be able to unregister later if eager loading.
|
50
|
+
autoloaded_dirs << dir
|
51
|
+
|
52
|
+
on_namespace_loaded(autovivified_module)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Invoked when a class or module is created or reopened, either from the
|
58
|
+
# tracer or from module autovivification. If the namespace has matching
|
59
|
+
# subdirectories, we descend into them now.
|
60
|
+
#
|
61
|
+
# @private
|
62
|
+
# @param namespace [Module]
|
63
|
+
# @return [void]
|
64
|
+
def on_namespace_loaded(namespace)
|
65
|
+
if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
|
66
|
+
subdirs.each do |subdir|
|
67
|
+
set_autoloads_in_dir(subdir, namespace)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Zeitwerk::RealModName
|
2
|
+
UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
|
3
|
+
private_constant :UNBOUND_METHOD_MODULE_NAME
|
4
|
+
|
5
|
+
# Returns the real name of the class or module, as set after the first
|
6
|
+
# constant to which it was assigned (or nil).
|
7
|
+
#
|
8
|
+
# The name method can be overridden, hence the indirection in this method.
|
9
|
+
#
|
10
|
+
# @param mod [Class, Module]
|
11
|
+
# @return [String, nil]
|
12
|
+
if UnboundMethod.method_defined?(:bind_call)
|
13
|
+
def real_mod_name(mod)
|
14
|
+
UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
|
15
|
+
end
|
16
|
+
else
|
17
|
+
def real_mod_name(mod)
|
18
|
+
UNBOUND_METHOD_MODULE_NAME.bind(mod).call
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zeitwerk
|
4
|
+
module Registry # :nodoc: all
|
5
|
+
class << self
|
6
|
+
# Keeps track of all loaders. Useful to broadcast messages and to prevent
|
7
|
+
# them from being garbage collected.
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
# @return [<Zeitwerk::Loader>]
|
11
|
+
attr_reader :loaders
|
12
|
+
|
13
|
+
# Registers loaders created with `for_gem` to make the method idempotent
|
14
|
+
# in case of reload.
|
15
|
+
#
|
16
|
+
# @private
|
17
|
+
# @return [{String => Zeitwerk::Loader}]
|
18
|
+
attr_reader :loaders_managing_gems
|
19
|
+
|
20
|
+
# Maps real paths to the loaders responsible for them.
|
21
|
+
#
|
22
|
+
# This information is used by our decorated `Kernel#require` to be able to
|
23
|
+
# invoke callbacks and autovivify modules.
|
24
|
+
#
|
25
|
+
# @private
|
26
|
+
# @return [{String => Zeitwerk::Loader}]
|
27
|
+
attr_reader :autoloads
|
28
|
+
|
29
|
+
# This hash table addresses an edge case in which an autoload is ignored.
|
30
|
+
#
|
31
|
+
# For example, let's suppose we want to autoload in a gem like this:
|
32
|
+
#
|
33
|
+
# # lib/my_gem.rb
|
34
|
+
# loader = Zeitwerk::Loader.new
|
35
|
+
# loader.push_dir(__dir__)
|
36
|
+
# loader.setup
|
37
|
+
#
|
38
|
+
# module MyGem
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# if you require "my_gem", as Bundler would do, this happens while setting
|
42
|
+
# up autoloads:
|
43
|
+
#
|
44
|
+
# 1. Object.autoload?(:MyGem) returns `nil` because the autoload for
|
45
|
+
# the constant is issued by Zeitwerk while the same file is being
|
46
|
+
# required.
|
47
|
+
# 2. The constant `MyGem` is undefined while setup runs.
|
48
|
+
#
|
49
|
+
# Therefore, a directory `lib/my_gem` would autovivify a module according to
|
50
|
+
# the existing information. But that would be wrong.
|
51
|
+
#
|
52
|
+
# To overcome this fundamental limitation, we keep track of the constant
|
53
|
+
# paths that are in this situation ---in the example above, "MyGem"--- and
|
54
|
+
# take this collection into account for the autovivification logic.
|
55
|
+
#
|
56
|
+
# Note that you cannot generally address this by moving the setup code
|
57
|
+
# below the constant definition, because we want libraries to be able to
|
58
|
+
# use managed constants in the module body:
|
59
|
+
#
|
60
|
+
# module MyGem
|
61
|
+
# include MyConcern
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# @private
|
65
|
+
# @return [{String => (String, Zeitwerk::Loader)}]
|
66
|
+
attr_reader :inceptions
|
67
|
+
|
68
|
+
# Registers a loader.
|
69
|
+
#
|
70
|
+
# @private
|
71
|
+
# @param loader [Zeitwerk::Loader]
|
72
|
+
# @return [void]
|
73
|
+
def register_loader(loader)
|
74
|
+
loaders << loader
|
75
|
+
end
|
76
|
+
|
77
|
+
# This method returns always a loader, the same instance for the same root
|
78
|
+
# file. That is how Zeitwerk::Loader.for_gem is idempotent.
|
79
|
+
#
|
80
|
+
# @private
|
81
|
+
# @param root_file [String]
|
82
|
+
# @return [Zeitwerk::Loader]
|
83
|
+
def loader_for_gem(root_file)
|
84
|
+
loaders_managing_gems[root_file] ||= begin
|
85
|
+
Loader.new.tap do |loader|
|
86
|
+
loader.tag = File.basename(root_file, ".rb")
|
87
|
+
loader.inflector = GemInflector.new(root_file)
|
88
|
+
loader.push_dir(File.dirname(root_file))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @private
|
94
|
+
# @param loader [Zeitwerk::Loader]
|
95
|
+
# @param realpath [String]
|
96
|
+
# @return [void]
|
97
|
+
def register_autoload(loader, realpath)
|
98
|
+
autoloads[realpath] = loader
|
99
|
+
end
|
100
|
+
|
101
|
+
# @private
|
102
|
+
# @param realpath [String]
|
103
|
+
# @return [void]
|
104
|
+
def unregister_autoload(realpath)
|
105
|
+
autoloads.delete(realpath)
|
106
|
+
end
|
107
|
+
|
108
|
+
# @private
|
109
|
+
# @param cpath [String]
|
110
|
+
# @param realpath [String]
|
111
|
+
# @param loader [Zeitwerk::Loader]
|
112
|
+
# @return [void]
|
113
|
+
def register_inception(cpath, realpath, loader)
|
114
|
+
inceptions[cpath] = [realpath, loader]
|
115
|
+
end
|
116
|
+
|
117
|
+
# @private
|
118
|
+
# @param cpath [String]
|
119
|
+
# @return [String, nil]
|
120
|
+
def inception?(cpath)
|
121
|
+
if pair = inceptions[cpath]
|
122
|
+
pair.first
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# @private
|
127
|
+
# @param path [String]
|
128
|
+
# @return [Zeitwerk::Loader, nil]
|
129
|
+
def loader_for(path)
|
130
|
+
autoloads[path]
|
131
|
+
end
|
132
|
+
|
133
|
+
# @private
|
134
|
+
# @param loader [Zeitwerk::Loader]
|
135
|
+
# @return [void]
|
136
|
+
def on_unload(loader)
|
137
|
+
autoloads.delete_if { |_path, object| object == loader }
|
138
|
+
inceptions.delete_if { |_cpath, (_path, object)| object == loader }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
@loaders = []
|
143
|
+
@loaders_managing_gems = {}
|
144
|
+
@autoloads = {}
|
145
|
+
@inceptions = {}
|
146
|
+
end
|
147
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zeitwerk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Xavier Noria
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-11-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |2
|
14
|
+
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
15
|
+
and application may have their own independent autoloader, with its own
|
16
|
+
configuration, inflector, and logger. Supports autoloading, preloading,
|
17
|
+
reloading, and eager loading.
|
18
|
+
email: fxn@hashref.com
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- MIT-LICENSE
|
24
|
+
- README.md
|
25
|
+
- lib/zeitwerk.rb
|
26
|
+
- lib/zeitwerk/error.rb
|
27
|
+
- lib/zeitwerk/explicit_namespace.rb
|
28
|
+
- lib/zeitwerk/gem_inflector.rb
|
29
|
+
- lib/zeitwerk/inflector.rb
|
30
|
+
- lib/zeitwerk/kernel.rb
|
31
|
+
- lib/zeitwerk/loader.rb
|
32
|
+
- lib/zeitwerk/loader/callbacks.rb
|
33
|
+
- lib/zeitwerk/real_mod_name.rb
|
34
|
+
- lib/zeitwerk/registry.rb
|
35
|
+
- lib/zeitwerk/version.rb
|
36
|
+
homepage: https://github.com/fxn/zeitwerk
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata: {}
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 2.4.4
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubygems_version: 3.0.3
|
56
|
+
signing_key:
|
57
|
+
specification_version: 4
|
58
|
+
summary: Efficient and thread-safe constant autoloader
|
59
|
+
test_files: []
|