im 0.1.6 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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(absolute_cpath, 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