zeitwerk 2.4.2 → 2.5.0.beta

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.
@@ -18,7 +18,7 @@ module Zeitwerk::Loader::Callbacks
18
18
  raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
19
19
  end
20
20
 
21
- run_on_load_callbacks(cpath)
21
+ run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
22
22
  end
23
23
 
24
24
  # Invoked from our decorated Kernel#require when a managed directory is
@@ -54,7 +54,7 @@ module Zeitwerk::Loader::Callbacks
54
54
 
55
55
  on_namespace_loaded(autovivified_module)
56
56
 
57
- run_on_load_callbacks(cpath)
57
+ run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
58
58
  end
59
59
  end
60
60
  end
@@ -75,12 +75,13 @@ module Zeitwerk::Loader::Callbacks
75
75
 
76
76
  private
77
77
 
78
- # @sig (String) -> void
79
- def run_on_load_callbacks(cpath)
80
- # Very common, do not even compute a hash code.
81
- return if on_load_callbacks.empty?
82
-
78
+ # @sig (String, Object) -> void
79
+ def run_on_load_callbacks(cpath, value, abspath)
80
+ # Order matters. If present, run the most specific one.
83
81
  callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
84
- callbacks.each(&:call) if callbacks
82
+ callbacks&.each { |c| c.call(value, abspath) }
83
+
84
+ callbacks = on_load_callbacks[:ANY]
85
+ callbacks&.each { |c| c.call(cpath, value, abspath) }
85
86
  end
86
87
  end
@@ -0,0 +1,308 @@
1
+ require "set"
2
+ require "securerandom"
3
+
4
+ module Zeitwerk::Loader::Config
5
+ # Absolute paths of the root directories. Stored in a hash to preserve
6
+ # order, easily handle duplicates, and also be able to have a fast lookup,
7
+ # needed for detecting nested paths.
8
+ #
9
+ # "/Users/fxn/blog/app/assets" => true,
10
+ # "/Users/fxn/blog/app/channels" => true,
11
+ # ...
12
+ #
13
+ # This is a private collection maintained by the loader. The public
14
+ # interface for it is `push_dir` and `dirs`.
15
+ #
16
+ # @private
17
+ # @sig Hash[String, true]
18
+ attr_reader :root_dirs
19
+
20
+ # @sig #camelize
21
+ attr_accessor :inflector
22
+
23
+ # Absolute paths of files, directories, or glob patterns to be totally
24
+ # ignored.
25
+ #
26
+ # @private
27
+ # @sig Set[String]
28
+ attr_reader :ignored_glob_patterns
29
+
30
+ # The actual collection of absolute file and directory names at the time the
31
+ # ignored glob patterns were expanded. Computed on setup, and recomputed on
32
+ # reload.
33
+ #
34
+ # @private
35
+ # @sig Set[String]
36
+ attr_reader :ignored_paths
37
+
38
+ # Absolute paths of directories or glob patterns to be collapsed.
39
+ #
40
+ # @private
41
+ # @sig Set[String]
42
+ attr_reader :collapse_glob_patterns
43
+
44
+ # The actual collection of absolute directory names at the time the collapse
45
+ # glob patterns were expanded. Computed on setup, and recomputed on reload.
46
+ #
47
+ # @private
48
+ # @sig Set[String]
49
+ attr_reader :collapse_dirs
50
+
51
+ # Absolute paths of files or directories not to be eager loaded.
52
+ #
53
+ # @private
54
+ # @sig Set[String]
55
+ attr_reader :eager_load_exclusions
56
+
57
+ # User-oriented callbacks to be fired when a constant is loaded.
58
+ #
59
+ # @private
60
+ # @sig Hash[String, Array[{ (Object, String) -> void }]]
61
+ # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
62
+ attr_reader :on_load_callbacks
63
+
64
+ # User-oriented callbacks to be fired before constants are removed.
65
+ #
66
+ # @private
67
+ # @sig Hash[String, Array[{ (Object, String) -> void }]]
68
+ # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
69
+ attr_reader :on_unload_callbacks
70
+
71
+ # @sig #call | #debug | nil
72
+ attr_accessor :logger
73
+
74
+ def initialize
75
+ @initialized_at = Time.now
76
+ @root_dirs = {}
77
+ @inflector = Zeitwerk::Inflector.new
78
+ @ignored_glob_patterns = Set.new
79
+ @ignored_paths = Set.new
80
+ @collapse_glob_patterns = Set.new
81
+ @collapse_dirs = Set.new
82
+ @eager_load_exclusions = Set.new
83
+ @reloading_enabled = false
84
+ @on_load_callbacks = {}
85
+ @on_unload_callbacks = {}
86
+ @logger = self.class.default_logger
87
+ @tag = SecureRandom.hex(3)
88
+ end
89
+
90
+ # Pushes `path` to the list of root directories.
91
+ #
92
+ # Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
93
+ # the same process already manages that directory or one of its ascendants or
94
+ # descendants.
95
+ #
96
+ # @raise [Zeitwerk::Error]
97
+ # @sig (String | Pathname, Module) -> void
98
+ def push_dir(path, namespace: Object)
99
+ # Note that Class < Module.
100
+ unless namespace.is_a?(Module)
101
+ raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
102
+ end
103
+
104
+ abspath = File.expand_path(path)
105
+ if dir?(abspath)
106
+ raise_if_conflicting_directory(abspath)
107
+ root_dirs[abspath] = namespace
108
+ else
109
+ raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
110
+ end
111
+ end
112
+
113
+ # Returns the loader's tag.
114
+ #
115
+ # Implemented as a method instead of via attr_reader for symmetry with the
116
+ # writer below.
117
+ #
118
+ # @sig () -> String
119
+ def tag
120
+ @tag
121
+ end
122
+
123
+ # Sets a tag for the loader, useful for logging.
124
+ #
125
+ # @param tag [#to_s]
126
+ # @sig (#to_s) -> void
127
+ def tag=(tag)
128
+ @tag = tag.to_s
129
+ end
130
+
131
+ # Absolute paths of the root directories. This is a read-only collection,
132
+ # please push here via `push_dir`.
133
+ #
134
+ # @sig () -> Array[String]
135
+ def dirs
136
+ root_dirs.keys.freeze
137
+ end
138
+
139
+ # You need to call this method before setup in order to be able to reload.
140
+ # There is no way to undo this, either you want to reload or you don't.
141
+ #
142
+ # @raise [Zeitwerk::Error]
143
+ # @sig () -> void
144
+ def enable_reloading
145
+ mutex.synchronize do
146
+ break if @reloading_enabled
147
+
148
+ if @setup
149
+ raise Zeitwerk::Error, "cannot enable reloading after setup"
150
+ else
151
+ @reloading_enabled = true
152
+ end
153
+ end
154
+ end
155
+
156
+ # @sig () -> bool
157
+ def reloading_enabled?
158
+ @reloading_enabled
159
+ end
160
+
161
+ # Let eager load ignore the given files or directories. The constants defined
162
+ # in those files are still autoloadable.
163
+ #
164
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
165
+ def do_not_eager_load(*paths)
166
+ mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
167
+ end
168
+
169
+ # Configure files, directories, or glob patterns to be totally ignored.
170
+ #
171
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
172
+ def ignore(*glob_patterns)
173
+ glob_patterns = expand_paths(glob_patterns)
174
+ mutex.synchronize do
175
+ ignored_glob_patterns.merge(glob_patterns)
176
+ ignored_paths.merge(expand_glob_patterns(glob_patterns))
177
+ end
178
+ end
179
+
180
+ # Configure directories or glob patterns to be collapsed.
181
+ #
182
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
183
+ def collapse(*glob_patterns)
184
+ glob_patterns = expand_paths(glob_patterns)
185
+ mutex.synchronize do
186
+ collapse_glob_patterns.merge(glob_patterns)
187
+ collapse_dirs.merge(expand_glob_patterns(glob_patterns))
188
+ end
189
+ end
190
+
191
+ # Configure a block to be invoked once a certain constant path is loaded.
192
+ # Supports multiple callbacks, and if there are many, they are executed in
193
+ # the order in which they were defined.
194
+ #
195
+ # loader.on_load("SomeApiClient") do |klass, _abspath|
196
+ # klass.endpoint = "https://api.dev"
197
+ # end
198
+ #
199
+ # Can also be configured for any constant loaded:
200
+ #
201
+ # loader.on_load do |cpath, value, abspath|
202
+ # # ...
203
+ # end
204
+ #
205
+ # @raise [TypeError]
206
+ # @sig (String) { (Object, String) -> void } -> void
207
+ # (:ANY) { (String, Object, String) -> void } -> void
208
+ def on_load(cpath = :ANY, &block)
209
+ raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
210
+
211
+ mutex.synchronize do
212
+ (on_load_callbacks[cpath] ||= []) << block
213
+ end
214
+ end
215
+
216
+ # Configure a block to be invoked right before a certain constant is removed.
217
+ # Supports multiple callbacks, and if there are many, they are executed in the
218
+ # order in which they were defined.
219
+ #
220
+ # loader.on_unload("Country") do |klass, _abspath|
221
+ # klass.clear_cache
222
+ # end
223
+ #
224
+ # Can also be configured for any removed constant:
225
+ #
226
+ # loader.on_unload do |cpath, value, abspath|
227
+ # # ...
228
+ # end
229
+ #
230
+ # @raise [TypeError]
231
+ # @sig (String) { (Object) -> void } -> void
232
+ # (:ANY) { (String, Object) -> void } -> void
233
+ def on_unload(cpath = :ANY, &block)
234
+ raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
235
+
236
+ mutex.synchronize do
237
+ (on_unload_callbacks[cpath] ||= []) << block
238
+ end
239
+ end
240
+
241
+ # Logs to `$stdout`, handy shortcut for debugging.
242
+ #
243
+ # @sig () -> void
244
+ def log!
245
+ @logger = ->(msg) { puts msg }
246
+ end
247
+
248
+ # @private
249
+ # @sig (String) -> bool
250
+ def manages?(dir)
251
+ dir = dir + "/"
252
+ ignored_paths.each do |ignored_path|
253
+ return false if dir.start_with?(ignored_path + "/")
254
+ end
255
+
256
+ root_dirs.each_key do |root_dir|
257
+ return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
258
+ end
259
+
260
+ false
261
+ end
262
+
263
+ private
264
+
265
+ # @sig () -> Array[String]
266
+ def actual_root_dirs
267
+ root_dirs.reject do |root_dir, _namespace|
268
+ !dir?(root_dir) || ignored_paths.member?(root_dir)
269
+ end
270
+ end
271
+
272
+ # @sig (String) -> bool
273
+ def root_dir?(dir)
274
+ root_dirs.key?(dir)
275
+ end
276
+
277
+ # @sig (String) -> bool
278
+ def excluded_from_eager_load?(abspath)
279
+ eager_load_exclusions.member?(abspath)
280
+ end
281
+
282
+ # @sig (String) -> bool
283
+ def collapse?(dir)
284
+ collapse_dirs.member?(dir)
285
+ end
286
+
287
+ # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
288
+ def expand_paths(paths)
289
+ paths.flatten.map! { |path| File.expand_path(path) }
290
+ end
291
+
292
+ # @sig (Array[String]) -> Array[String]
293
+ def expand_glob_patterns(glob_patterns)
294
+ # Note that Dir.glob works with regular file names just fine. That is,
295
+ # glob patterns technically need no wildcards.
296
+ glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
297
+ end
298
+
299
+ # @sig () -> void
300
+ def recompute_ignored_paths
301
+ ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
302
+ end
303
+
304
+ # @sig () -> void
305
+ def recompute_collapse_dirs
306
+ collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
307
+ end
308
+ end
@@ -0,0 +1,95 @@
1
+ module Zeitwerk::Loader::Helpers
2
+ private
3
+
4
+ # --- Logging -----------------------------------------------------------------------------------
5
+
6
+ # @sig (String) -> void
7
+ def log(message)
8
+ method_name = logger.respond_to?(:debug) ? :debug : :call
9
+ logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
10
+ end
11
+
12
+ # --- Files and directories ---------------------------------------------------------------------
13
+
14
+ # @sig (String) { (String, String) -> void } -> void
15
+ def ls(dir)
16
+ Dir.each_child(dir) do |basename|
17
+ next if hidden?(basename)
18
+
19
+ abspath = File.join(dir, basename)
20
+ next if ignored_paths.member?(abspath)
21
+
22
+ # We freeze abspath because that saves allocations when passed later to
23
+ # File methods. See #125.
24
+ yield basename, abspath.freeze
25
+ end
26
+ end
27
+
28
+ # @sig (String) -> bool
29
+ def ruby?(path)
30
+ path.end_with?(".rb")
31
+ end
32
+
33
+ # @sig (String) -> bool
34
+ def dir?(path)
35
+ File.directory?(path)
36
+ end
37
+
38
+ # @sig String -> bool
39
+ def hidden?(basename)
40
+ basename.start_with?(".")
41
+ end
42
+
43
+ # --- Constants ---------------------------------------------------------------------------------
44
+
45
+ # The autoload? predicate takes into account the ancestor chain of the
46
+ # receiver, like const_defined? and other methods in the constants API do.
47
+ #
48
+ # For example, given
49
+ #
50
+ # class A
51
+ # autoload :X, "x.rb"
52
+ # end
53
+ #
54
+ # class B < A
55
+ # end
56
+ #
57
+ # B.autoload?(:X) returns "x.rb".
58
+ #
59
+ # We need a way to strictly check in parent ignoring ancestors.
60
+ #
61
+ # @sig (Module, Symbol) -> String?
62
+ if method(:autoload?).arity == 1
63
+ def strict_autoload_path(parent, cname)
64
+ parent.autoload?(cname) if cdef?(parent, cname)
65
+ end
66
+ else
67
+ def strict_autoload_path(parent, cname)
68
+ parent.autoload?(cname, false)
69
+ end
70
+ end
71
+
72
+ # @sig (Module, Symbol) -> String
73
+ if Symbol.method_defined?(:name)
74
+ # Symbol#name was introduced in Ruby 3.0. It returns always the same
75
+ # frozen object, so we may save a few string allocations.
76
+ def cpath(parent, cname)
77
+ Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
78
+ end
79
+ else
80
+ def cpath(parent, cname)
81
+ Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
82
+ end
83
+ end
84
+
85
+ # @sig (Module, Symbol) -> bool
86
+ def cdef?(parent, cname)
87
+ parent.const_defined?(cname, false)
88
+ end
89
+
90
+ # @raise [NameError]
91
+ # @sig (Module, Symbol) -> Object
92
+ def cget(parent, cname)
93
+ parent.const_get(cname, false)
94
+ end
95
+ end
@@ -17,7 +17,7 @@ module Zeitwerk
17
17
  # @sig Hash[String, Zeitwerk::Loader]
18
18
  attr_reader :loaders_managing_gems
19
19
 
20
- # Maps real paths to the loaders responsible for them.
20
+ # Maps absolute paths to the loaders responsible for them.
21
21
  #
22
22
  # This information is used by our decorated `Kernel#require` to be able to
23
23
  # invoke callbacks and autovivify modules.
@@ -90,20 +90,20 @@ module Zeitwerk
90
90
 
91
91
  # @private
92
92
  # @sig (Zeitwerk::Loader, String) -> String
93
- def register_autoload(loader, realpath)
94
- autoloads[realpath] = loader
93
+ def register_autoload(loader, abspath)
94
+ autoloads[abspath] = loader
95
95
  end
96
96
 
97
97
  # @private
98
98
  # @sig (String) -> void
99
- def unregister_autoload(realpath)
100
- autoloads.delete(realpath)
99
+ def unregister_autoload(abspath)
100
+ autoloads.delete(abspath)
101
101
  end
102
102
 
103
103
  # @private
104
104
  # @sig (String, String, Zeitwerk::Loader) -> void
105
- def register_inception(cpath, realpath, loader)
106
- inceptions[cpath] = [realpath, loader]
105
+ def register_inception(cpath, abspath, loader)
106
+ inceptions[cpath] = [abspath, loader]
107
107
  end
108
108
 
109
109
  # @private