zeitwerk 2.4.2 → 2.5.0.beta4

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,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