im 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/im/kernel.rb CHANGED
@@ -1,19 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kernel
4
- alias_method :im_original_require, :require
4
+ module_function
5
5
 
6
- def require(path)
7
- Im.handle_require(path, caller_locations(1, 1).first.path) do
8
- im_original_require(path)
9
- end
6
+ alias_method :im_original_require, :require
7
+ class << self
8
+ alias_method :im_original_require, :require
10
9
  end
11
10
 
12
- alias_method :im_original_autoload, :autoload
11
+ # @sig (String) -> true | false
12
+ def require(path)
13
+ filetype, feature_path = $:.resolve_feature_path(path)
13
14
 
14
- def autoload(name, path)
15
- Im.handle_autoload(path) do
16
- im_original_autoload(name, path)
15
+ if (loader = Im::Registry.loader_for(path)) ||
16
+ ((loader = Im::Registry.loader_for(feature_path)) && (path = feature_path))
17
+ if :rb == filetype
18
+ if loaded = !$LOADED_FEATURES.include?(feature_path)
19
+ $LOADED_FEATURES << feature_path
20
+ begin
21
+ load path, loader
22
+ rescue => e
23
+ $LOADED_FEATURES.delete(feature_path)
24
+ raise e
25
+ end
26
+ loader.on_file_autoloaded(path)
27
+ end
28
+ loaded
29
+ else
30
+ loader.on_dir_autoloaded(path)
31
+ true
32
+ end
33
+ else
34
+ required = im_original_require(path)
35
+ if required
36
+ abspath = $LOADED_FEATURES.last
37
+ if loader = Im::Registry.loader_for(abspath)
38
+ loader.on_file_autoloaded(abspath)
39
+ end
40
+ end
41
+ required
17
42
  end
18
43
  end
19
44
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im::Loader::Callbacks
4
+ # Invoked from our decorated Kernel#require when a managed file is autoloaded.
5
+ #
6
+ # @private
7
+ # @sig (String) -> void
8
+ def on_file_autoloaded(file)
9
+ cref = autoloads.delete(file)
10
+
11
+ relative_cpath = relative_cpath(*cref)
12
+ to_unload[relative_cpath] = [file, cref] if reloading_enabled?
13
+ Im::Registry.unregister_autoload(file)
14
+
15
+ if cdef?(*cref)
16
+ obj = cget(*cref)
17
+ if obj.is_a?(Module)
18
+ register_module_name(obj, relative_cpath)
19
+ Im::Registry.register_autoloaded_module(obj, relative_cpath, self)
20
+ end
21
+ log("constant #{relative_cpath} loaded from file #{file}") if logger
22
+ run_on_load_callbacks(relative_cpath, obj, file) unless on_load_callbacks.empty?
23
+ else
24
+ raise Im::NameError.new("expected file #{file} to define constant #{cpath(*cref)}, but didn't", cref.last)
25
+ end
26
+ end
27
+
28
+ # Invoked from our decorated Kernel#require when a managed directory is
29
+ # autoloaded.
30
+ #
31
+ # @private
32
+ # @sig (String) -> void
33
+ def on_dir_autoloaded(dir)
34
+ # Module#autoload does not serialize concurrent requires, and we handle
35
+ # directories ourselves, so the callback needs to account for concurrency.
36
+ #
37
+ # Multi-threading would introduce a race condition here in which thread t1
38
+ # autovivifies the module, and while autoloads for its children are being
39
+ # set, thread t2 autoloads the same namespace.
40
+ #
41
+ # Without the mutex and subsequent delete call, t2 would reset the module.
42
+ # That not only would reassign the constant (undesirable per se) but, worse,
43
+ # the module object created by t2 wouldn't have any of the autoloads for its
44
+ # children, since t1 would have correctly deleted its namespace_dirs entry.
45
+ mutex2.synchronize do
46
+ if cref = autoloads.delete(dir)
47
+ autovivified_module = cref[0].const_set(cref[1], Module.new)
48
+ relative_cpath = relative_cpath(*cref)
49
+ register_module_name(autovivified_module, relative_cpath)
50
+ Im::Registry.register_autoloaded_module(autovivified_module, relative_cpath, self)
51
+ log("module #{relative_cpath} autovivified from directory #{dir}") if logger
52
+
53
+ to_unload[relative_cpath] = [dir, cref] if reloading_enabled?
54
+
55
+ # We don't unregister `dir` in the registry because concurrent threads
56
+ # wouldn't find a loader associated to it in Kernel#require and would
57
+ # try to require the directory. Instead, we are going to keep track of
58
+ # these to be able to unregister later if eager loading.
59
+ autoloaded_dirs << dir
60
+
61
+ on_namespace_loaded(relative_cpath)
62
+
63
+ run_on_load_callbacks(relative_cpath, autovivified_module, dir) unless on_load_callbacks.empty?
64
+ end
65
+ end
66
+ end
67
+
68
+ # Invoked when a class or module is created or reopened, either from the
69
+ # tracer or from module autovivification. If the namespace has matching
70
+ # subdirectories, we descend into them now.
71
+ #
72
+ # @private
73
+ # @sig (Module) -> void
74
+ def on_namespace_loaded(module_name)
75
+ if dirs = namespace_dirs.delete(module_name)
76
+ dirs.each do |dir|
77
+ set_autoloads_in_dir(dir, cget(self, module_name))
78
+ end
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # @sig (String, Object) -> void
85
+ def run_on_load_callbacks(cpath, value, abspath)
86
+ # Order matters. If present, run the most specific one.
87
+ callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
88
+ callbacks&.each { |c| c.call(value, abspath) }
89
+
90
+ callbacks = on_load_callbacks[:ANY]
91
+ callbacks&.each { |c| c.call(cpath, value, abspath) }
92
+ end
93
+ end
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "securerandom"
5
+
6
+ module Im::Loader::Config
7
+ extend Im::Internal
8
+
9
+ # @sig #camelize
10
+ attr_accessor :inflector
11
+
12
+ # @sig #call | #debug | nil
13
+ attr_accessor :logger
14
+
15
+ # Absolute paths of the root directories, as a set:
16
+ #
17
+ # #<Set: {
18
+ # "/Users/fxn/blog/app/channels",
19
+ # "/Users/fxn/blog/app/adapters",
20
+ # ...
21
+ # }>
22
+ #
23
+ # This is a private collection maintained by the loader. The public
24
+ # interface for it is `push_dir` and `dirs`.
25
+ #
26
+ # @sig Array[String]
27
+ attr_reader :root_dirs
28
+ internal :root_dirs
29
+
30
+ # Absolute paths of files, directories, or glob patterns to be totally
31
+ # ignored.
32
+ #
33
+ # @sig Set[String]
34
+ attr_reader :ignored_glob_patterns
35
+ private :ignored_glob_patterns
36
+
37
+ # The actual collection of absolute file and directory names at the time the
38
+ # ignored glob patterns were expanded. Computed on setup, and recomputed on
39
+ # reload.
40
+ #
41
+ # @sig Set[String]
42
+ attr_reader :ignored_paths
43
+ private :ignored_paths
44
+
45
+ # Absolute paths of directories or glob patterns to be collapsed.
46
+ #
47
+ # @sig Set[String]
48
+ attr_reader :collapse_glob_patterns
49
+ private :collapse_glob_patterns
50
+
51
+ # The actual collection of absolute directory names at the time the collapse
52
+ # glob patterns were expanded. Computed on setup, and recomputed on reload.
53
+ #
54
+ # @sig Set[String]
55
+ attr_reader :collapse_dirs
56
+ private :collapse_dirs
57
+
58
+ # Absolute paths of files or directories not to be eager loaded.
59
+ #
60
+ # @sig Set[String]
61
+ attr_reader :eager_load_exclusions
62
+ private :eager_load_exclusions
63
+
64
+ # User-oriented callbacks to be fired on setup and on reload.
65
+ #
66
+ # @sig Array[{ () -> void }]
67
+ attr_reader :on_setup_callbacks
68
+ private :on_setup_callbacks
69
+
70
+ # User-oriented callbacks to be fired when a constant is loaded.
71
+ #
72
+ # @sig Hash[String, Array[{ (Object, String) -> void }]]
73
+ # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
74
+ attr_reader :on_load_callbacks
75
+ private :on_load_callbacks
76
+
77
+ # User-oriented callbacks to be fired before constants are removed.
78
+ #
79
+ # @sig Hash[String, Array[{ (Object, String) -> void }]]
80
+ # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
81
+ attr_reader :on_unload_callbacks
82
+ private :on_unload_callbacks
83
+
84
+ def initialize
85
+ @inflector = Im::Inflector.new
86
+ @logger = self.class.default_logger
87
+ @tag = SecureRandom.hex(3)
88
+ @initialized_at = Time.now
89
+ @root_dirs = Set.new
90
+ @ignored_glob_patterns = Set.new
91
+ @ignored_paths = Set.new
92
+ @collapse_glob_patterns = Set.new
93
+ @collapse_dirs = Set.new
94
+ @eager_load_exclusions = Set.new
95
+ @reloading_enabled = false
96
+ @on_setup_callbacks = []
97
+ @on_load_callbacks = {}
98
+ @on_unload_callbacks = {}
99
+ end
100
+
101
+ # Pushes `path` to the list of root directories.
102
+ #
103
+ # Raises `Im::Error` if `path` does not exist, or if another loader in
104
+ # the same process already manages that directory or one of its ascendants or
105
+ # descendants.
106
+ #
107
+ # @raise [Im::Error]
108
+ # @sig (String | Pathname, Module) -> void
109
+ def push_dir(path)
110
+ abspath = File.expand_path(path)
111
+ if dir?(abspath)
112
+ raise_if_conflicting_directory(abspath)
113
+ root_dirs << abspath
114
+ else
115
+ raise Im::Error, "the root directory #{abspath} does not exist"
116
+ end
117
+ end
118
+
119
+ # Returns the loader's tag.
120
+ #
121
+ # Implemented as a method instead of via attr_reader for symmetry with the
122
+ # writer below.
123
+ #
124
+ # @sig () -> String
125
+ def tag
126
+ @tag
127
+ end
128
+
129
+ # Sets a tag for the loader, useful for logging.
130
+ #
131
+ # @sig (#to_s) -> void
132
+ def tag=(tag)
133
+ @tag = tag.to_s
134
+ end
135
+
136
+ # Rturns an array with the absolute paths of the root directories as strings.
137
+ # If `ignored` is falsey (default), ignored root directories are filtered out.
138
+ #
139
+ # These are read-only collections, please add to them with `push_dir`.
140
+ #
141
+ # @sig () -> Array[String] | Hash[String, Module]
142
+ def dirs(ignored: false)
143
+ if ignored || ignored_paths.empty?
144
+ root_dirs
145
+ else
146
+ root_dirs.reject { |root_dir| ignored_path?(root_dir) }
147
+ end.freeze
148
+ end
149
+
150
+ # You need to call this method before setup in order to be able to reload.
151
+ # There is no way to undo this, either you want to reload or you don't.
152
+ #
153
+ # @raise [Im::Error]
154
+ # @sig () -> void
155
+ def enable_reloading
156
+ mutex.synchronize do
157
+ break if @reloading_enabled
158
+
159
+ if @setup
160
+ raise Im::Error, "cannot enable reloading after setup"
161
+ else
162
+ @reloading_enabled = true
163
+ end
164
+ end
165
+ end
166
+
167
+ # @sig () -> bool
168
+ def reloading_enabled?
169
+ @reloading_enabled
170
+ end
171
+
172
+ # Let eager load ignore the given files or directories. The constants defined
173
+ # in those files are still autoloadable.
174
+ #
175
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
176
+ def do_not_eager_load(*paths)
177
+ mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
178
+ end
179
+
180
+ # Configure files, directories, or glob patterns to be totally ignored.
181
+ #
182
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
183
+ def ignore(*glob_patterns)
184
+ glob_patterns = expand_paths(glob_patterns)
185
+ mutex.synchronize do
186
+ ignored_glob_patterns.merge(glob_patterns)
187
+ ignored_paths.merge(expand_glob_patterns(glob_patterns))
188
+ end
189
+ end
190
+
191
+ # Configure directories or glob patterns to be collapsed.
192
+ #
193
+ # @sig (*(String | Pathname | Array[String | Pathname])) -> void
194
+ def collapse(*glob_patterns)
195
+ glob_patterns = expand_paths(glob_patterns)
196
+ mutex.synchronize do
197
+ collapse_glob_patterns.merge(glob_patterns)
198
+ collapse_dirs.merge(expand_glob_patterns(glob_patterns))
199
+ end
200
+ end
201
+
202
+ # Configure a block to be called after setup and on each reload.
203
+ # If setup was already done, the block runs immediately.
204
+ #
205
+ # @sig () { () -> void } -> void
206
+ def on_setup(&block)
207
+ mutex.synchronize do
208
+ on_setup_callbacks << block
209
+ block.call if @setup
210
+ end
211
+ end
212
+
213
+ # Configure a block to be invoked once a certain constant path is loaded.
214
+ # Supports multiple callbacks, and if there are many, they are executed in
215
+ # the order in which they were defined.
216
+ #
217
+ # loader.on_load("SomeApiClient") do |klass, _abspath|
218
+ # klass.endpoint = "https://api.dev"
219
+ # end
220
+ #
221
+ # Can also be configured for any constant loaded:
222
+ #
223
+ # loader.on_load do |cpath, value, abspath|
224
+ # # ...
225
+ # end
226
+ #
227
+ # @raise [TypeError]
228
+ # @sig (String) { (Object, String) -> void } -> void
229
+ # (:ANY) { (String, Object, String) -> void } -> void
230
+ def on_load(cpath = :ANY, &block)
231
+ raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
232
+
233
+ mutex.synchronize { _on_load(cpath, &block) }
234
+ end
235
+
236
+ # @sig (String) { (Object, String) -> void } -> void
237
+ # (:ANY) { (String, Object, String) -> void } -> void
238
+ private def _on_load(cpath, &block)
239
+ (on_load_callbacks[cpath] ||= []) << block
240
+ end
241
+
242
+ # Configure a block to be invoked right before a certain constant is removed.
243
+ # Supports multiple callbacks, and if there are many, they are executed in the
244
+ # order in which they were defined.
245
+ #
246
+ # loader.on_unload("Country") do |klass, _abspath|
247
+ # klass.clear_cache
248
+ # end
249
+ #
250
+ # Can also be configured for any removed constant:
251
+ #
252
+ # loader.on_unload do |cpath, value, abspath|
253
+ # # ...
254
+ # end
255
+ #
256
+ # @raise [TypeError]
257
+ # @sig (String) { (Object) -> void } -> void
258
+ # (:ANY) { (String, Object) -> void } -> void
259
+ def on_unload(cpath = :ANY, &block)
260
+ raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
261
+
262
+ mutex.synchronize do
263
+ (on_unload_callbacks[cpath] ||= []) << block
264
+ end
265
+ end
266
+
267
+ # Logs to `$stdout`, handy shortcut for debugging.
268
+ #
269
+ # @sig () -> void
270
+ def log!
271
+ @logger = ->(msg) { puts msg }
272
+ end
273
+
274
+ # Returns true if the argument has been configured to be ignored, or is a
275
+ # descendant of an ignored directory.
276
+ #
277
+ # @sig (String) -> bool
278
+ internal def ignores?(abspath)
279
+ # Common use case.
280
+ return false if ignored_paths.empty?
281
+
282
+ walk_up(abspath) do |abspath|
283
+ return true if ignored_path?(abspath)
284
+ return false if root_dir?(abspath)
285
+ end
286
+
287
+ false
288
+ end
289
+
290
+ # @sig (String) -> bool
291
+ private def ignored_path?(abspath)
292
+ ignored_paths.member?(abspath)
293
+ end
294
+
295
+ # @sig () -> Array[String]
296
+ private def actual_roots
297
+ root_dirs.reject do |root_dir|
298
+ !dir?(root_dir) || ignored_path?(root_dir)
299
+ end
300
+ end
301
+
302
+ # @sig (String) -> bool
303
+ private def root_dir?(dir)
304
+ root_dirs.include?(dir)
305
+ end
306
+
307
+ # @sig (String) -> bool
308
+ private def excluded_from_eager_load?(abspath)
309
+ # Optimize this common use case.
310
+ return false if eager_load_exclusions.empty?
311
+
312
+ walk_up(abspath) do |abspath|
313
+ return true if eager_load_exclusions.member?(abspath)
314
+ return false if root_dir?(abspath)
315
+ end
316
+
317
+ false
318
+ end
319
+
320
+ # @sig (String) -> bool
321
+ private def collapse?(dir)
322
+ collapse_dirs.member?(dir)
323
+ end
324
+
325
+ # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
326
+ private def expand_paths(paths)
327
+ paths.flatten.map! { |path| File.expand_path(path) }
328
+ end
329
+
330
+ # @sig (Array[String]) -> Array[String]
331
+ private def expand_glob_patterns(glob_patterns)
332
+ # Note that Dir.glob works with regular file names just fine. That is,
333
+ # glob patterns technically need no wildcards.
334
+ glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
335
+ end
336
+
337
+ # @sig () -> void
338
+ private def recompute_ignored_paths
339
+ ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
340
+ end
341
+
342
+ # @sig () -> void
343
+ private def recompute_collapse_dirs
344
+ collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
345
+ end
346
+ end
@@ -0,0 +1,214 @@
1
+ module Im::Loader::EagerLoad
2
+ # Eager loads all files in the root directories, recursively. Files do not
3
+ # need to be in `$LOAD_PATH`, absolute file names are used. Ignored and
4
+ # shadowed files are not eager loaded. You can opt-out specifically in
5
+ # specific files and directories with `do_not_eager_load`, and that can be
6
+ # overridden passing `force: true`.
7
+ #
8
+ # @sig (true | false) -> void
9
+ def eager_load(force: false)
10
+ mutex.synchronize do
11
+ break if @eager_loaded
12
+ raise Im::SetupRequired unless @setup
13
+
14
+ log("eager load start") if logger
15
+
16
+ actual_roots.each do |root_dir|
17
+ actual_eager_load_dir(root_dir, self, force: force)
18
+ end
19
+
20
+ autoloaded_dirs.each do |autoloaded_dir|
21
+ Im::Registry.unregister_autoload(autoloaded_dir)
22
+ end
23
+ autoloaded_dirs.clear
24
+
25
+ @eager_loaded = true
26
+
27
+ log("eager load end") if logger
28
+ end
29
+ end
30
+
31
+ # @sig (String | Pathname) -> void
32
+ def eager_load_dir(path)
33
+ raise Im::SetupRequired unless @setup
34
+
35
+ abspath = File.expand_path(path)
36
+
37
+ raise Im::Error.new("#{abspath} is not a directory") unless dir?(abspath)
38
+
39
+ cnames = []
40
+
41
+ found_root = false
42
+ walk_up(abspath) do |dir|
43
+ return if ignored_path?(dir)
44
+ return if eager_load_exclusions.member?(dir)
45
+
46
+ break if found_root = root_dirs.include?(dir)
47
+
48
+ unless collapse?(dir)
49
+ basename = File.basename(dir)
50
+ cnames << inflector.camelize(basename, dir).to_sym
51
+ end
52
+ end
53
+
54
+ raise Im::Error.new("I do not manage #{abspath}") unless found_root
55
+
56
+ return if @eager_loaded
57
+
58
+ namespace = self
59
+ cnames.reverse_each do |cname|
60
+ # Can happen if there are no Ruby files. This is not an error condition,
61
+ # the directory is actually managed. Could have Ruby files later.
62
+ return unless cdef?(namespace, cname)
63
+ namespace = cget(namespace, cname)
64
+ end
65
+
66
+ # A shortcircuiting test depends on the invocation of this method. Please
67
+ # keep them in sync if refactored.
68
+ actual_eager_load_dir(abspath, namespace)
69
+ end
70
+
71
+ # @sig (Module) -> void
72
+ def eager_load_namespace(mod)
73
+ raise Im::SetupRequired unless @setup
74
+
75
+ unless mod.is_a?(Module)
76
+ raise Im::Error, "#{mod.inspect} is not a class or module object"
77
+ end
78
+
79
+ return if @eager_loaded
80
+
81
+ mod_name = Im.cpath(mod)
82
+
83
+ actual_roots.each do |root_dir|
84
+ if mod.equal?(self)
85
+ # A shortcircuiting test depends on the invocation of this method.
86
+ # Please keep them in sync if refactored.
87
+ actual_eager_load_dir(root_dir, self)
88
+ else
89
+ eager_load_child_namespace(mod, mod_name, root_dir)
90
+ end
91
+ end
92
+ end
93
+
94
+ # Loads the given Ruby file.
95
+ #
96
+ # Raises if the argument is ignored, shadowed, or not managed by the receiver.
97
+ #
98
+ # The method is implemented as `constantize` for files, in a sense, to be able
99
+ # to descend orderly and make sure the file is loadable.
100
+ #
101
+ # @sig (String | Pathname) -> void
102
+ def load_file(path)
103
+ abspath = File.expand_path(path)
104
+
105
+ raise Im::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
106
+ raise Im::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
107
+ raise Im::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
108
+
109
+ basename = File.basename(abspath, ".rb")
110
+ base_cname = inflector.camelize(basename, abspath).to_sym
111
+
112
+ root_included = false
113
+ cnames = []
114
+
115
+ walk_up(File.dirname(abspath)) do |dir|
116
+ raise Im::Error.new("#{abspath} is ignored") if ignored_path?(dir)
117
+
118
+ break if root_included = root_dirs.include?(dir)
119
+
120
+ unless collapse?(dir)
121
+ basename = File.basename(dir)
122
+ cnames << inflector.camelize(basename, dir).to_sym
123
+ end
124
+ end
125
+
126
+ raise Im::Error.new("I do not manage #{abspath}") unless root_included
127
+
128
+ namespace = self
129
+ cnames.reverse_each do |cname|
130
+ namespace = cget(namespace, cname)
131
+ end
132
+
133
+ raise Im::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
134
+
135
+ cget(namespace, base_cname)
136
+ end
137
+
138
+ # The caller is responsible for making sure `namespace` is the namespace that
139
+ # corresponds to `dir`.
140
+ #
141
+ # @sig (String, Module, Boolean) -> void
142
+ private def actual_eager_load_dir(dir, namespace, force: false)
143
+ honour_exclusions = !force
144
+ return if honour_exclusions && excluded_from_eager_load?(dir)
145
+
146
+ log("eager load directory #{dir} start") if logger
147
+
148
+ queue = [[dir, namespace]]
149
+ while to_eager_load = queue.shift
150
+ dir, namespace = to_eager_load
151
+
152
+ ls(dir) do |basename, abspath|
153
+ next if honour_exclusions && eager_load_exclusions.member?(abspath)
154
+
155
+ if ruby?(abspath)
156
+ if (cref = autoloads[abspath]) && !shadowed_file?(abspath)
157
+ cget(*cref)
158
+ end
159
+ else
160
+ if collapse?(abspath)
161
+ queue << [abspath, namespace]
162
+ else
163
+ cname = inflector.camelize(basename, abspath).to_sym
164
+ queue << [abspath, cget(namespace, cname)]
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ log("eager load directory #{dir} end") if logger
171
+ end
172
+
173
+ # In order to invoke this method, the caller has to ensure `child` is a
174
+ # strict namespace descendendant of `root_namespace`.
175
+ #
176
+ # @sig (Module, String, Module, Boolean) -> void
177
+ private def eager_load_child_namespace(child, child_name, root_dir)
178
+ suffix = child_name
179
+ suffix = suffix.delete_prefix("#{self}::")
180
+
181
+ # These directories are at the same namespace level, there may be more if
182
+ # we find collapsed ones. As we scan, we look for matches for the first
183
+ # segment, and store them in `next_dirs`. If there are any, we look for
184
+ # the next segments in those matches. Repeat.
185
+ #
186
+ # If we exhaust the search locating directories that match all segments,
187
+ # we just need to eager load those ones.
188
+ dirs = [root_dir]
189
+ next_dirs = []
190
+
191
+ suffix.split("::").each do |segment|
192
+ while dir = dirs.shift
193
+ ls(dir) do |basename, abspath|
194
+ next unless dir?(abspath)
195
+
196
+ if collapse?(abspath)
197
+ dirs << abspath
198
+ elsif segment == inflector.camelize(basename, abspath)
199
+ next_dirs << abspath
200
+ end
201
+ end
202
+ end
203
+
204
+ return if next_dirs.empty?
205
+
206
+ dirs.replace(next_dirs)
207
+ next_dirs.clear
208
+ end
209
+
210
+ dirs.each do |dir|
211
+ actual_eager_load_dir(dir, child)
212
+ end
213
+ end
214
+ end