im 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 -185
- metadata +25 -61
- data/CHANGELOG.md +0 -31
- data/Gemfile +0 -8
- data/Gemfile.lock +0 -37
- 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
|