im 0.1.5 → 0.2.0

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