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.
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im::Loader::Helpers
4
+ # --- Logging -----------------------------------------------------------------------------------
5
+
6
+ # @sig (String) -> void
7
+ private def log(message)
8
+ method_name = logger.respond_to?(:debug) ? :debug : :call
9
+ logger.send(method_name, "Im@#{tag}: #{message}")
10
+ end
11
+
12
+ # --- Files and directories ---------------------------------------------------------------------
13
+
14
+ # @sig (String) { (String, String) -> void } -> void
15
+ private def ls(dir)
16
+ children = Dir.children(dir)
17
+
18
+ # The order in which a directory is listed depends on the file system.
19
+ #
20
+ # Since client code may run in different platforms, it seems convenient to
21
+ # order directory entries. This provides consistent eager loading across
22
+ # platforms, for example.
23
+ children.sort!
24
+
25
+ children.each do |basename|
26
+ next if hidden?(basename)
27
+
28
+ abspath = File.join(dir, basename)
29
+ next if ignored_path?(abspath)
30
+
31
+ if dir?(abspath)
32
+ next if root_dirs.include?(abspath)
33
+ next if !has_at_least_one_ruby_file?(abspath)
34
+ else
35
+ next unless ruby?(abspath)
36
+ end
37
+
38
+ # We freeze abspath because that saves allocations when passed later to
39
+ # File methods. See #125.
40
+ yield basename, abspath.freeze
41
+ end
42
+ end
43
+
44
+ # @sig (String) -> bool
45
+ private def has_at_least_one_ruby_file?(dir)
46
+ to_visit = [dir]
47
+
48
+ while dir = to_visit.shift
49
+ ls(dir) do |_basename, abspath|
50
+ if dir?(abspath)
51
+ to_visit << abspath
52
+ else
53
+ return true
54
+ end
55
+ end
56
+ end
57
+
58
+ false
59
+ end
60
+
61
+ # @sig (String) -> bool
62
+ private def ruby?(path)
63
+ path.end_with?(".rb")
64
+ end
65
+
66
+ # @sig (String) -> bool
67
+ private def dir?(path)
68
+ File.directory?(path)
69
+ end
70
+
71
+ # @sig (String) -> bool
72
+ private def hidden?(basename)
73
+ basename.start_with?(".")
74
+ end
75
+
76
+ # @sig (String) { (String) -> void } -> void
77
+ private def walk_up(abspath)
78
+ loop do
79
+ yield abspath
80
+ abspath, basename = File.split(abspath)
81
+ break if basename == "/"
82
+ end
83
+ end
84
+
85
+ # --- Constants ---------------------------------------------------------------------------------
86
+
87
+ # The autoload? predicate takes into account the ancestor chain of the
88
+ # receiver, like const_defined? and other methods in the constants API do.
89
+ #
90
+ # For example, given
91
+ #
92
+ # class A
93
+ # autoload :X, "x.rb"
94
+ # end
95
+ #
96
+ # class B < A
97
+ # end
98
+ #
99
+ # B.autoload?(:X) returns "x.rb".
100
+ #
101
+ # We need a way to strictly check in parent ignoring ancestors.
102
+ #
103
+ # @sig (Module, Symbol) -> String?
104
+ private def strict_autoload_path(parent, cname)
105
+ parent.autoload?(cname, false)
106
+ end
107
+
108
+ # @sig (Module, Symbol) -> String
109
+ private def cpath(parent, cname)
110
+ Object == parent ? cname.name : "#{Im.cpath(parent)}::#{cname.name}"
111
+ end
112
+
113
+ # @sig (Module, Symbol) -> bool
114
+ private def cdef?(parent, cname)
115
+ parent.const_defined?(cname, false)
116
+ end
117
+
118
+ # @raise [NameError]
119
+ # @sig (Module, Symbol) -> Object
120
+ private def cget(parent, cname)
121
+ parent.const_get(cname, false)
122
+ end
123
+ end
data/lib/im/loader.rb ADDED
@@ -0,0 +1,586 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Im
6
+ class Loader < Module
7
+ UNBOUND_METHOD_MODULE_TO_S = Module.instance_method(:to_s)
8
+ UNBOUND_METHOD_REMOVE_CONST = Module.instance_method(:remove_const)
9
+
10
+ require_relative "loader/helpers"
11
+ require_relative "loader/callbacks"
12
+ require_relative "loader/config"
13
+ require_relative "loader/eager_load"
14
+
15
+ include Callbacks
16
+ include Helpers
17
+ include Config
18
+ include EagerLoad
19
+
20
+ MUTEX = Mutex.new
21
+ private_constant :MUTEX
22
+
23
+ # Make debugging easier
24
+ def inspect
25
+ Object.instance_method(:inspect).bind_call(self)
26
+ end
27
+
28
+ def pretty_print(q)
29
+ q.pp_object(self)
30
+ end
31
+
32
+ # Maps absolute paths for which an autoload has been set ---and not
33
+ # executed--- to their corresponding parent class or module and constant
34
+ # name.
35
+ #
36
+ # "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
37
+ # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
38
+ # ...
39
+ #
40
+ # @private
41
+ # @sig Hash[String, [Module, Symbol]]
42
+ attr_reader :autoloads
43
+
44
+ # We keep track of autoloaded directories to remove them from the registry
45
+ # at the end of eager loading.
46
+ #
47
+ # Files are removed as they are autoloaded, but directories need to wait due
48
+ # to concurrency (see why in Im::Loader::Callbacks#on_dir_autoloaded).
49
+ #
50
+ # @private
51
+ # @sig Array[String]
52
+ attr_reader :autoloaded_dirs
53
+
54
+ # Stores metadata needed for unloading. Its entries look like this:
55
+ #
56
+ # "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
57
+ #
58
+ # The cpath as key helps implementing unloadable_cpath? The file name is
59
+ # stored in order to be able to delete it from $LOADED_FEATURES, and the
60
+ # pair [Module, Symbol] is used to remove_const the constant from the class
61
+ # or module object.
62
+ #
63
+ # If reloading is enabled, this hash is filled as constants are autoloaded
64
+ # or eager loaded. Otherwise, the collection remains empty.
65
+ #
66
+ # @private
67
+ # @sig Hash[String, [String, [Module, Symbol]]]
68
+ attr_reader :to_unload
69
+
70
+ # Maps namespace constant paths to their respective directories.
71
+ #
72
+ # For example, given this mapping:
73
+ #
74
+ # "Admin" => [
75
+ # "/Users/fxn/blog/app/controllers/admin",
76
+ # "/Users/fxn/blog/app/models/admin",
77
+ # ...
78
+ # ]
79
+ #
80
+ # when `Admin` gets defined we know that it plays the role of a namespace
81
+ # and that its children are spread over those directories. We'll visit them
82
+ # to set up the corresponding autoloads.
83
+ #
84
+ # @private
85
+ # @sig Hash[String, Array[String]]
86
+ attr_reader :namespace_dirs
87
+
88
+ # A shadowed file is a file managed by this loader that is ignored when
89
+ # setting autoloads because its matching constant is already taken.
90
+ #
91
+ # This private set is populated as we descend. For example, if the loader
92
+ # has only scanned the top-level, `shadowed_files` does not have shadowed
93
+ # files that may exist deep in the project tree yet.
94
+ #
95
+ # @private
96
+ # @sig Set[String]
97
+ attr_reader :shadowed_files
98
+
99
+ # @private
100
+ # @sig Hash[Integer, String]
101
+ attr_reader :module_cpaths
102
+
103
+ # @private
104
+ # @sig String
105
+ attr_reader :module_prefix
106
+
107
+ # @private
108
+ # @sig Mutex
109
+ attr_reader :mutex
110
+
111
+ # @private
112
+ # @sig Mutex
113
+ attr_reader :mutex2
114
+
115
+ def initialize
116
+ super
117
+
118
+ @module_prefix = "#{UNBOUND_METHOD_MODULE_TO_S.bind_call(self)}::"
119
+ @autoloads = {}
120
+ @autoloaded_dirs = []
121
+ @to_unload = {}
122
+ @namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
123
+ @shadowed_files = Set.new
124
+ @module_cpaths = {}
125
+ @mutex = Mutex.new
126
+ @mutex2 = Mutex.new
127
+ @setup = false
128
+ @eager_loaded = false
129
+
130
+ Registry.register_loader(self)
131
+ Registry.register_autoloaded_module(self, nil, self)
132
+ end
133
+
134
+ # Sets autoloads in the root namespaces.
135
+ #
136
+ # @sig () -> void
137
+ def setup
138
+ mutex.synchronize do
139
+ break if @setup
140
+
141
+ actual_roots.each { |root_dir| set_autoloads_in_dir(root_dir) }
142
+
143
+ on_setup_callbacks.each(&:call)
144
+
145
+ @setup = true
146
+ end
147
+ end
148
+
149
+ # Removes loaded constants and configured autoloads.
150
+ #
151
+ # The objects the constants stored are no longer reachable through them. In
152
+ # addition, since said objects are normally not referenced from anywhere
153
+ # else, they are eligible for garbage collection, which would effectively
154
+ # unload them.
155
+ #
156
+ # This method is public but undocumented. Main interface is `reload`, which
157
+ # means `unload` + `setup`. This one is avaiable to be used together with
158
+ # `unregister`, which is undocumented too.
159
+ #
160
+ # @sig () -> void
161
+ def unload
162
+ mutex.synchronize do
163
+ raise SetupRequired unless @setup
164
+
165
+ # We are going to keep track of the files that were required by our
166
+ # autoloads to later remove them from $LOADED_FEATURES, thus making them
167
+ # loadable by Kernel#require again.
168
+ #
169
+ # Directories are not stored in $LOADED_FEATURES, keeping track of files
170
+ # is enough.
171
+ unloaded_files = Set.new
172
+
173
+ autoloads.each do |abspath, (parent, cname)|
174
+ if parent.autoload?(cname)
175
+ unload_autoload(parent, cname)
176
+ else
177
+ # Could happen if loaded with require_relative. That is unsupported,
178
+ # and the constant path would escape unloadable_cpath? This is just
179
+ # defensive code to clean things up as much as we are able to.
180
+ unload_cref(parent, cname)
181
+ unloaded_files.add(abspath) if ruby?(abspath)
182
+ end
183
+ end
184
+
185
+ to_unload.each do |cpath, (abspath, (parent, cname))|
186
+ unless on_unload_callbacks.empty?
187
+ begin
188
+ value = cget(parent, cname)
189
+ rescue ::NameError
190
+ # Perhaps the user deleted the constant by hand, or perhaps an
191
+ # autoload failed to define the expected constant but the user
192
+ # rescued the exception.
193
+ else
194
+ run_on_unload_callbacks(cpath, value, abspath)
195
+ end
196
+ end
197
+
198
+ # Replace all inbound references to constant by autoloads.
199
+ reset_inbound_references(parent, cname)
200
+
201
+ unload_cref(parent, cname)
202
+ unloaded_files.add(abspath) if ruby?(abspath)
203
+ end
204
+
205
+ unless unloaded_files.empty?
206
+ # Bootsnap decorates Kernel#require to speed it up using a cache and
207
+ # this optimization does not check if $LOADED_FEATURES has the file.
208
+ #
209
+ # To make it aware of changes, the gem defines singleton methods in
210
+ # $LOADED_FEATURES:
211
+ #
212
+ # https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
213
+ #
214
+ # Rails applications may depend on bootsnap, so for unloading to work
215
+ # in that setting it is preferable that we restrict our API choice to
216
+ # one of those methods.
217
+ $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
218
+ end
219
+
220
+ autoloads.clear
221
+ autoloaded_dirs.clear
222
+ to_unload.clear
223
+ namespace_dirs.clear
224
+ shadowed_files.clear
225
+ module_cpaths.clear
226
+
227
+ Registry.on_unload(self)
228
+ ExplicitNamespace.__unregister_loader(self)
229
+
230
+ @setup = false
231
+ @eager_loaded = false
232
+ end
233
+ end
234
+
235
+ # Unloads all loaded code, and calls setup again so that the loader is able
236
+ # to pick any changes in the file system.
237
+ #
238
+ # This method is not thread-safe, please see how this can be achieved by
239
+ # client code in the README of the project.
240
+ #
241
+ # @raise [Im::Error]
242
+ # @sig () -> void
243
+ def reload
244
+ raise ReloadingDisabledError unless reloading_enabled?
245
+ raise SetupRequired unless @setup
246
+
247
+ unload
248
+ recompute_ignored_paths
249
+ recompute_collapse_dirs
250
+ setup
251
+ end
252
+
253
+ # Says if the given constant path would be unloaded on reload. This
254
+ # predicate returns `false` if reloading is disabled.
255
+ #
256
+ # @sig (String) -> bool
257
+ def unloadable_cpath?(cpath)
258
+ to_unload.key?(cpath)
259
+ end
260
+
261
+ # Returns an array with the constant paths that would be unloaded on reload.
262
+ # This predicate returns an empty array if reloading is disabled.
263
+ #
264
+ # @sig () -> Array[String]
265
+ def unloadable_cpaths
266
+ to_unload.keys.freeze
267
+ end
268
+
269
+ # This is a dangerous method.
270
+ #
271
+ # @experimental
272
+ # @sig () -> void
273
+ def unregister
274
+ Registry.unregister_loader(self)
275
+ ExplicitNamespace.__unregister_loader(self)
276
+ end
277
+
278
+ # The return value of this predicate is only meaningful if the loader has
279
+ # scanned the file. This is the case in the spots where we use it.
280
+ #
281
+ # @private
282
+ # @sig (String) -> Boolean
283
+ def shadowed_file?(file)
284
+ shadowed_files.member?(file)
285
+ end
286
+
287
+ # --- Class methods ---------------------------------------------------------------------------
288
+
289
+ class << self
290
+ # @sig #call | #debug | nil
291
+ attr_accessor :default_logger
292
+
293
+ # This is a shortcut for
294
+ #
295
+ # require "im"
296
+ # loader = Im::Loader.new
297
+ # loader.tag = File.basename(__FILE__, ".rb")
298
+ # loader.inflector = Im::GemInflector.new(__FILE__)
299
+ # loader.push_dir(__dir__)
300
+ #
301
+ # except that this method returns the same object in subsequent calls from
302
+ # the same file, in the unlikely case the gem wants to be able to reload.
303
+ #
304
+ # This method returns a subclass of Im::Loader, but the exact type
305
+ # is private, client code can only rely on the interface.
306
+ #
307
+ # @sig (bool) -> Im::GemLoader
308
+ def for_gem(warn_on_extra_files: true)
309
+ called_from = caller_locations(1, 1).first.path
310
+ Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
311
+ end
312
+
313
+ # Broadcasts `eager_load` to all loaders. Those that have not been setup
314
+ # are skipped.
315
+ #
316
+ # @sig () -> void
317
+ def eager_load_all
318
+ Registry.loaders.each do |loader|
319
+ begin
320
+ loader.eager_load
321
+ rescue SetupRequired
322
+ # This is fine, we eager load what can be eager loaded.
323
+ end
324
+ end
325
+ end
326
+
327
+ # Broadcasts `eager_load_namespace` to all loaders. Those that have not
328
+ # been setup are skipped.
329
+ #
330
+ # @sig (Module) -> void
331
+ def eager_load_namespace(mod)
332
+ Registry.loaders.each do |loader|
333
+ begin
334
+ loader.eager_load_namespace(mod)
335
+ rescue SetupRequired
336
+ # This is fine, we eager load what can be eager loaded.
337
+ end
338
+ end
339
+ end
340
+
341
+ # Returns an array with the absolute paths of the root directories of all
342
+ # registered loaders. This is a read-only collection.
343
+ #
344
+ # @sig () -> Array[String]
345
+ def all_dirs
346
+ Registry.loaders.map(&:dirs).inject(&:+).freeze
347
+ end
348
+ end
349
+
350
+ private # -------------------------------------------------------------------------------------
351
+
352
+ # @sig (String, Module) -> void
353
+ def set_autoloads_in_dir(dir, parent = self)
354
+ ls(dir) do |basename, abspath|
355
+ begin
356
+ if ruby?(basename)
357
+ basename.delete_suffix!(".rb")
358
+ cname = inflector.camelize(basename, abspath).to_sym
359
+ autoload_file(parent, cname, abspath) if parent
360
+ Registry.register_path(self, abspath)
361
+ else
362
+ if collapse?(abspath)
363
+ set_autoloads_in_dir(abspath, parent)
364
+ else
365
+ cname = inflector.camelize(basename, abspath).to_sym
366
+ autoload_subdir(parent, cname, abspath) if parent
367
+ set_autoloads_in_dir(abspath, nil)
368
+ end
369
+ end
370
+ rescue ::NameError => error
371
+ path_type = ruby?(abspath) ? "file" : "directory"
372
+
373
+ raise NameError.new(<<~MESSAGE, error.name)
374
+ #{error.message} inferred by #{inflector.class} from #{path_type}
375
+
376
+ #{abspath}
377
+
378
+ Possible ways to address this:
379
+
380
+ * Tell Im to ignore this particular #{path_type}.
381
+ * Tell Im to ignore one of its parent directories.
382
+ * Rename the #{path_type} to comply with the naming conventions.
383
+ * Modify the inflector to handle this case.
384
+ MESSAGE
385
+ end
386
+ end
387
+ end
388
+
389
+ # @sig (Module, Symbol, String) -> void
390
+ def autoload_subdir(parent, cname, subdir)
391
+ if autoload_path = autoload_path_set_by_me_for?(parent, cname)
392
+ absolute_cpath = cpath(parent, cname)
393
+ relative_cpath = relative_cpath(parent, cname)
394
+ register_explicit_namespace(cpath, relative_cpath) if ruby?(autoload_path)
395
+
396
+ # We do not need to issue another autoload, the existing one is enough
397
+ # no matter if it is for a file or a directory. Just remember the
398
+ # subdirectory has to be visited if the namespace is used.
399
+ namespace_dirs[relative_cpath] << subdir
400
+ elsif !cdef?(parent, cname)
401
+ # First time we find this namespace, set an autoload for it.
402
+ namespace_dirs[relative_cpath(parent, cname)] << subdir
403
+ set_autoload(parent, cname, subdir)
404
+ else
405
+ # For whatever reason the constant that corresponds to this namespace has
406
+ # already been defined, we have to recurse.
407
+ log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
408
+ set_autoloads_in_dir(subdir, cget(parent, cname))
409
+ end
410
+ end
411
+
412
+ # @sig (Module, Symbol, String) -> void
413
+ def autoload_file(parent, cname, file)
414
+ if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
415
+ # First autoload for a Ruby file wins, just ignore subsequent ones.
416
+ if ruby?(autoload_path)
417
+ shadowed_files << file
418
+ log("file #{file} is ignored because #{autoload_path} has precedence") if logger
419
+ else
420
+ promote_namespace_from_implicit_to_explicit(
421
+ dir: autoload_path,
422
+ file: file,
423
+ parent: parent,
424
+ cname: cname
425
+ )
426
+ end
427
+ elsif cdef?(parent, cname)
428
+ shadowed_files << file
429
+ log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
430
+ else
431
+ set_autoload(parent, cname, file)
432
+ end
433
+ end
434
+
435
+ # `dir` is the directory that would have autovivified a namespace. `file` is
436
+ # the file where we've found the namespace is explicitly defined.
437
+ #
438
+ # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
439
+ def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
440
+ autoloads.delete(dir)
441
+ Registry.unregister_autoload(dir)
442
+ Registry.unregister_path(dir)
443
+
444
+ log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
445
+
446
+ set_autoload(parent, cname, file)
447
+ register_explicit_namespace(cpath(parent, cname), relative_cpath(parent, cname))
448
+ end
449
+
450
+ # @sig (Module, Symbol, String) -> void
451
+ def set_autoload(parent, cname, abspath)
452
+ parent.autoload(cname, abspath)
453
+
454
+ if logger
455
+ if ruby?(abspath)
456
+ log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
457
+ else
458
+ log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
459
+ end
460
+ end
461
+
462
+ autoloads[abspath] = [parent, cname]
463
+ Registry.register_autoload(self, abspath)
464
+
465
+ # See why in the documentation of Im::Registry.inceptions.
466
+ unless parent.autoload?(cname)
467
+ Registry.register_inception(cpath(parent, cname), abspath, self)
468
+ end
469
+ end
470
+
471
+ # @sig (Module, Symbol) -> String?
472
+ def autoload_path_set_by_me_for?(parent, cname)
473
+ if autoload_path = strict_autoload_path(parent, cname)
474
+ autoload_path if autoloads.key?(autoload_path)
475
+ else
476
+ Registry.inception?(cpath(parent, cname))
477
+ end
478
+ end
479
+
480
+ # @sig (String) -> void
481
+ def register_explicit_namespace(cpath, module_name)
482
+ ExplicitNamespace.__register(cpath, module_name, self)
483
+ end
484
+
485
+ # @sig (String) -> void
486
+ def raise_if_conflicting_directory(dir)
487
+ MUTEX.synchronize do
488
+ dir_slash = dir + "/"
489
+
490
+ Registry.loaders.each do |loader|
491
+ next if loader == self
492
+ next if loader.__ignores?(dir)
493
+
494
+ loader.__root_dirs.each do |root_dir|
495
+ next if ignores?(root_dir)
496
+
497
+ root_dir_slash = root_dir + "/"
498
+ if dir_slash.start_with?(root_dir_slash) || root_dir_slash.start_with?(dir_slash)
499
+ require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
500
+ raise Error,
501
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
502
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
503
+ EOS
504
+ end
505
+ end
506
+ end
507
+ end
508
+ end
509
+
510
+ # @sig (Module, Symbol) -> String
511
+ def relative_cpath(parent, cname)
512
+ if self == parent
513
+ cname.to_s
514
+ elsif module_cpaths.key?(parent.object_id)
515
+ "#{module_cpaths[parent.object_id]}::#{cname}"
516
+ else
517
+ # If an autoloaded file loads an autoloaded constant from another file, we need to deduce the module name
518
+ # before we can add the parent to module_cpaths. In this case, we have no choice but to work from to_s.
519
+ mod_name = Im.cpath(parent)
520
+ current_module_prefix = "#{Im.cpath(self)}::"
521
+ raise InvalidModuleName, "invalid module name for #{parent}" unless mod_name.start_with?(current_module_prefix)
522
+ "#{mod_name}::#{cname}".delete_prefix!(current_module_prefix)
523
+ end
524
+ end
525
+
526
+ # @sig (Module, String)
527
+ def register_module_name(mod, module_name)
528
+ module_cpaths[mod.object_id] = module_name
529
+ end
530
+
531
+ # @sig (String, Object, String) -> void
532
+ def run_on_unload_callbacks(cpath, value, abspath)
533
+ # Order matters. If present, run the most specific one.
534
+ on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
535
+ on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
536
+ end
537
+
538
+ # @sig (Module, Symbol) -> void
539
+ def unload_autoload(parent, cname)
540
+ parent.__send__(:remove_const, cname)
541
+ log("autoload for #{cpath(parent, cname)} removed") if logger
542
+ end
543
+
544
+ # @sig (Module, Symbol) -> void
545
+ def unload_cref(parent, cname)
546
+ # Let's optimistically remove_const. The way we use it, this is going to
547
+ # succeed always if all is good.
548
+ parent.__send__(:remove_const, cname)
549
+ rescue ::NameError
550
+ # There are a few edge scenarios in which this may happen. If the constant
551
+ # is gone, that is OK, anyway.
552
+ else
553
+ log("#{cpath(parent, cname)} unloaded") if logger
554
+ end
555
+
556
+ # When a named constant that points to an Im-autoloaded module is removed,
557
+ # any inbound (named) references to the module must be removed and replaced
558
+ # by autoloads with an on_load callback to reset the alias.
559
+ def reset_inbound_references(parent, cname)
560
+ return unless (mod = parent.const_get(cname)).is_a?(Module)
561
+
562
+ mod_name, loader, references = Im::Registry.autoloaded_modules[mod.object_id]
563
+ return unless mod_name
564
+
565
+ begin
566
+ references.each do |reference|
567
+ reset_inbound_reference(*reference, mod_name)
568
+ log("inbound reference from #{cpath(*reference)} to #{loader}::#{mod_name} replaced by autoload") if logger
569
+ end
570
+ ensure
571
+ Im::Registry.autoloaded_modules.delete(mod.object_id)
572
+ end
573
+ rescue ::NameError
574
+ end
575
+
576
+ def reset_inbound_reference(parent, cname, mod_name)
577
+ UNBOUND_METHOD_REMOVE_CONST.bind_call(parent, cname)
578
+ abspath, _ = to_unload[mod_name]
579
+
580
+ # Bypass public on_load to avoid mutex deadlock.
581
+ _on_load(mod_name) { parent.const_set(cname, const_get(mod_name, false)) }
582
+
583
+ parent.autoload(cname, abspath)
584
+ end
585
+ end
586
+ end