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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +21 -0
- data/README.md +245 -32
- data/lib/im/const_path.rb +48 -0
- data/lib/im/error.rb +24 -0
- data/lib/im/explicit_namespace.rb +96 -0
- data/lib/im/gem_inflector.rb +17 -0
- data/lib/im/gem_loader.rb +65 -0
- data/lib/im/inflector.rb +46 -0
- data/lib/im/internal.rb +12 -0
- data/lib/im/kernel.rb +34 -9
- data/lib/im/loader/callbacks.rb +93 -0
- data/lib/im/loader/config.rb +346 -0
- data/lib/im/loader/eager_load.rb +214 -0
- data/lib/im/loader/helpers.rb +123 -0
- data/lib/im/loader.rb +586 -0
- data/lib/im/module_const_added.rb +63 -0
- data/lib/im/registry.rb +166 -0
- data/lib/im/version.rb +1 -1
- data/lib/im.rb +18 -172
- metadata +24 -60
- data/CHANGELOG.md +0 -28
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -182
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -8
- data/lib/im/module.rb +0 -9
- data/lib/im/ruby_version_check.rb +0 -22
@@ -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
|