zeitwerk 2.7.4 → 2.8.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/README.md +150 -71
- data/lib/zeitwerk/core_ext/kernel.rb +1 -1
- data/lib/zeitwerk/cref/map.rb +1 -1
- data/lib/zeitwerk/cref.rb +8 -1
- data/lib/zeitwerk/error.rb +12 -1
- data/lib/zeitwerk/gem_inflector.rb +3 -3
- data/lib/zeitwerk/gem_loader.rb +6 -6
- data/lib/zeitwerk/inflector.rb +8 -8
- data/lib/zeitwerk/loader/callbacks.rb +4 -4
- data/lib/zeitwerk/loader/config.rb +73 -27
- data/lib/zeitwerk/loader/eager_load.rb +42 -48
- data/lib/zeitwerk/loader/file_system.rb +212 -0
- data/lib/zeitwerk/loader/helpers.rb +2 -101
- data/lib/zeitwerk/loader.rb +195 -162
- data/lib/zeitwerk/real_mod_name.rb +1 -1
- data/lib/zeitwerk/registry/loaders.rb +2 -2
- data/lib/zeitwerk/registry.rb +30 -4
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -13
- metadata +4 -6
data/lib/zeitwerk/loader.rb
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'monitor'
|
|
4
|
+
require 'set'
|
|
5
5
|
|
|
6
6
|
module Zeitwerk
|
|
7
7
|
class Loader
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
8
|
+
require_relative 'loader/helpers'
|
|
9
|
+
require_relative 'loader/callbacks'
|
|
10
|
+
require_relative 'loader/config'
|
|
11
|
+
require_relative 'loader/eager_load'
|
|
12
|
+
require_relative 'loader/file_system'
|
|
12
13
|
|
|
13
14
|
extend Internal
|
|
14
15
|
|
|
@@ -18,14 +19,11 @@ module Zeitwerk
|
|
|
18
19
|
include Config
|
|
19
20
|
include EagerLoad
|
|
20
21
|
|
|
21
|
-
MUTEX = Mutex.new #: Mutex
|
|
22
|
-
private_constant :MUTEX
|
|
23
|
-
|
|
24
22
|
# Maps absolute paths for which an autoload has been set ---and not
|
|
25
23
|
# executed--- to their corresponding Zeitwerk::Cref object.
|
|
26
24
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
25
|
+
# '/Users/fxn/blog/app/models/user.rb' => #<Zeitwerk::Cref:... @mod=Object, @cname=:User, ...>,
|
|
26
|
+
# '/Users/fxn/blog/app/models/hotel/pricing.rb' => #<Zeitwerk::Cref:... @mod=Hotel, @cname=:Pricing, ...>,
|
|
29
27
|
# ...
|
|
30
28
|
#
|
|
31
29
|
#: Hash[String, Zeitwerk::Cref]
|
|
@@ -104,6 +102,7 @@ module Zeitwerk
|
|
|
104
102
|
attr_reader :dirs_autoload_monitor
|
|
105
103
|
private :dirs_autoload_monitor
|
|
106
104
|
|
|
105
|
+
#: () -> void
|
|
107
106
|
def initialize
|
|
108
107
|
super
|
|
109
108
|
|
|
@@ -115,6 +114,7 @@ module Zeitwerk
|
|
|
115
114
|
@shadowed_files = Set.new
|
|
116
115
|
@setup = false
|
|
117
116
|
@eager_loaded = false
|
|
117
|
+
@fs = FileSystem.new(self)
|
|
118
118
|
|
|
119
119
|
@mutex = Mutex.new
|
|
120
120
|
@dirs_autoload_monitor = Monitor.new
|
|
@@ -130,7 +130,7 @@ module Zeitwerk
|
|
|
130
130
|
break if @setup
|
|
131
131
|
|
|
132
132
|
actual_roots.each do |root_dir, root_namespace|
|
|
133
|
-
define_autoloads_for_dir(root_dir, root_namespace)
|
|
133
|
+
define_autoloads_for_dir(root_dir, root_namespace, external: true)
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
on_setup_callbacks.each(&:call)
|
|
@@ -154,73 +154,79 @@ module Zeitwerk
|
|
|
154
154
|
def unload
|
|
155
155
|
mutex.synchronize do
|
|
156
156
|
raise SetupRequired unless @setup
|
|
157
|
+
__unload
|
|
158
|
+
end
|
|
159
|
+
end
|
|
157
160
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
# This is an internal method.
|
|
162
|
+
#
|
|
163
|
+
#: () -> void
|
|
164
|
+
def __unload
|
|
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
|
|
165
172
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
end
|
|
173
|
+
autoloads.each do |abspath, cref|
|
|
174
|
+
if cref.autoload?
|
|
175
|
+
unload_autoload(cref)
|
|
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(cref)
|
|
181
|
+
unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
|
|
176
182
|
end
|
|
183
|
+
end
|
|
177
184
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
end
|
|
185
|
+
to_unload.each do |abspath, cref|
|
|
186
|
+
unless on_unload_callbacks.empty?
|
|
187
|
+
begin
|
|
188
|
+
value = cref.get
|
|
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(cref, value, abspath)
|
|
189
195
|
end
|
|
190
|
-
|
|
191
|
-
unload_cref(cref)
|
|
192
|
-
unloaded_files.add(abspath) if ruby?(abspath)
|
|
193
196
|
end
|
|
194
197
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
#
|
|
199
|
-
# To make it aware of changes, the gem defines singleton methods in
|
|
200
|
-
# $LOADED_FEATURES:
|
|
201
|
-
#
|
|
202
|
-
# https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
|
|
203
|
-
#
|
|
204
|
-
# Rails applications may depend on bootsnap, so for unloading to work
|
|
205
|
-
# in that setting it is preferable that we restrict our API choice to
|
|
206
|
-
# one of those methods.
|
|
207
|
-
$LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
|
|
208
|
-
end
|
|
198
|
+
unload_cref(cref)
|
|
199
|
+
unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
|
|
200
|
+
end
|
|
209
201
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
unless unloaded_files.empty?
|
|
203
|
+
# Bootsnap decorates Kernel#require to speed it up using a cache and
|
|
204
|
+
# this optimization does not check if $LOADED_FEATURES has the file.
|
|
205
|
+
#
|
|
206
|
+
# To make it aware of changes, the gem defines singleton methods in
|
|
207
|
+
# $LOADED_FEATURES:
|
|
208
|
+
#
|
|
209
|
+
# https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
|
|
210
|
+
#
|
|
211
|
+
# Rails applications may depend on bootsnap, so for unloading to work
|
|
212
|
+
# in that setting it is preferable that we restrict our API choice to
|
|
213
|
+
# one of those methods.
|
|
214
|
+
$LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
|
|
215
|
+
end
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
autoloads.clear
|
|
218
|
+
autoloaded_dirs.clear
|
|
219
|
+
to_unload.clear
|
|
220
|
+
namespace_dirs.clear
|
|
221
|
+
shadowed_files.clear
|
|
218
222
|
|
|
219
|
-
|
|
223
|
+
unregister_inceptions
|
|
224
|
+
unregister_explicit_namespaces
|
|
220
225
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
Registry.autoloads.unregister_loader(self)
|
|
227
|
+
|
|
228
|
+
@setup = false
|
|
229
|
+
@eager_loaded = false
|
|
224
230
|
end
|
|
225
231
|
|
|
226
232
|
# Unloads all loaded code, and calls setup again so that the loader is able
|
|
@@ -235,8 +241,11 @@ module Zeitwerk
|
|
|
235
241
|
raise SetupRequired unless @setup
|
|
236
242
|
|
|
237
243
|
unload
|
|
244
|
+
|
|
238
245
|
recompute_ignored_paths
|
|
239
246
|
recompute_collapse_dirs
|
|
247
|
+
recompute_collapse_parents
|
|
248
|
+
|
|
240
249
|
setup
|
|
241
250
|
end
|
|
242
251
|
|
|
@@ -253,18 +262,20 @@ module Zeitwerk
|
|
|
253
262
|
while (dir, cpath = queue.shift)
|
|
254
263
|
result[dir] = cpath
|
|
255
264
|
|
|
256
|
-
prefix = cpath ==
|
|
265
|
+
prefix = cpath == 'Object' ? '' : cpath + '::'
|
|
257
266
|
|
|
258
|
-
ls(dir) do |basename, abspath, ftype|
|
|
267
|
+
@fs.ls(dir, collapse: false) do |basename, abspath, ftype|
|
|
259
268
|
if ftype == :file
|
|
260
|
-
basename
|
|
261
|
-
|
|
262
|
-
else
|
|
263
|
-
if collapse?(abspath)
|
|
264
|
-
queue << [abspath, cpath]
|
|
269
|
+
if basename == @nsfile
|
|
270
|
+
result[abspath] = cpath
|
|
265
271
|
else
|
|
266
|
-
|
|
272
|
+
basename.delete_suffix!('.rb')
|
|
273
|
+
result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
|
|
267
274
|
end
|
|
275
|
+
elsif collapse?(abspath)
|
|
276
|
+
queue.unshift([abspath, cpath])
|
|
277
|
+
else
|
|
278
|
+
queue.push([abspath, "#{prefix}#{cname_for(basename, abspath)}"])
|
|
268
279
|
end
|
|
269
280
|
end
|
|
270
281
|
end
|
|
@@ -279,16 +290,18 @@ module Zeitwerk
|
|
|
279
290
|
|
|
280
291
|
raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
|
281
292
|
|
|
282
|
-
|
|
293
|
+
ftype = @fs.supported_ftype?(abspath)
|
|
294
|
+
return unless ftype
|
|
295
|
+
|
|
283
296
|
return if ignored_path?(abspath)
|
|
284
297
|
|
|
285
298
|
paths = []
|
|
286
299
|
|
|
287
|
-
if
|
|
288
|
-
basename = File.basename(abspath
|
|
289
|
-
return if hidden?(basename)
|
|
300
|
+
if ftype == :file
|
|
301
|
+
basename = File.basename(abspath)
|
|
302
|
+
return if @fs.hidden?(basename)
|
|
290
303
|
|
|
291
|
-
paths << [basename, abspath]
|
|
304
|
+
paths << [basename.delete_suffix('.rb'), abspath] unless basename == @nsfile
|
|
292
305
|
walk_up_from = File.dirname(abspath)
|
|
293
306
|
else
|
|
294
307
|
walk_up_from = abspath
|
|
@@ -296,12 +309,12 @@ module Zeitwerk
|
|
|
296
309
|
|
|
297
310
|
root_namespace = nil
|
|
298
311
|
|
|
299
|
-
walk_up(walk_up_from) do |dir|
|
|
312
|
+
@fs.walk_up(walk_up_from) do |dir|
|
|
300
313
|
break if root_namespace = roots[dir]
|
|
301
314
|
return if ignored_path?(dir)
|
|
302
315
|
|
|
303
316
|
basename = File.basename(dir)
|
|
304
|
-
return if hidden?(basename)
|
|
317
|
+
return if @fs.hidden?(basename)
|
|
305
318
|
|
|
306
319
|
paths << [basename, dir] unless collapse?(dir)
|
|
307
320
|
end
|
|
@@ -314,9 +327,9 @@ module Zeitwerk
|
|
|
314
327
|
cnames = paths.reverse_each.map { cname_for(_1, _2) }
|
|
315
328
|
|
|
316
329
|
if root_namespace == Object
|
|
317
|
-
cnames.join(
|
|
330
|
+
cnames.join('::')
|
|
318
331
|
else
|
|
319
|
-
"#{real_mod_name(root_namespace)}::#{cnames.join(
|
|
332
|
+
"#{real_mod_name(root_namespace)}::#{cnames.join('::')}"
|
|
320
333
|
end
|
|
321
334
|
end
|
|
322
335
|
end
|
|
@@ -363,6 +376,16 @@ module Zeitwerk
|
|
|
363
376
|
shadowed_files.member?(file)
|
|
364
377
|
end
|
|
365
378
|
|
|
379
|
+
#: { () -> String } -> void
|
|
380
|
+
internal def log
|
|
381
|
+
return unless logger
|
|
382
|
+
|
|
383
|
+
message = yield
|
|
384
|
+
method_name = logger.respond_to?(:debug) ? :debug : :call
|
|
385
|
+
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
|
|
366
389
|
# --- Class methods ---------------------------------------------------------------------------
|
|
367
390
|
|
|
368
391
|
class << self
|
|
@@ -373,10 +396,10 @@ module Zeitwerk
|
|
|
373
396
|
|
|
374
397
|
# This is a shortcut for
|
|
375
398
|
#
|
|
376
|
-
# require
|
|
399
|
+
# require 'zeitwerk'
|
|
377
400
|
#
|
|
378
401
|
# loader = Zeitwerk::Loader.new
|
|
379
|
-
# loader.tag = File.basename(__FILE__,
|
|
402
|
+
# loader.tag = File.basename(__FILE__, '.rb')
|
|
380
403
|
# loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
381
404
|
# loader.push_dir(__dir__)
|
|
382
405
|
#
|
|
@@ -394,10 +417,10 @@ module Zeitwerk
|
|
|
394
417
|
|
|
395
418
|
# This is a shortcut for
|
|
396
419
|
#
|
|
397
|
-
# require
|
|
420
|
+
# require 'zeitwerk'
|
|
398
421
|
#
|
|
399
422
|
# loader = Zeitwerk::Loader.new
|
|
400
|
-
# loader.tag = namespace.name +
|
|
423
|
+
# loader.tag = namespace.name + '-' + File.basename(__FILE__, '.rb')
|
|
401
424
|
# loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
402
425
|
# loader.push_dir(__dir__, namespace: namespace)
|
|
403
426
|
#
|
|
@@ -414,7 +437,7 @@ module Zeitwerk
|
|
|
414
437
|
end
|
|
415
438
|
|
|
416
439
|
unless real_mod_name(namespace)
|
|
417
|
-
raise Zeitwerk::Error,
|
|
440
|
+
raise Zeitwerk::Error, 'extending anonymous namespaces is unsupported'
|
|
418
441
|
end
|
|
419
442
|
|
|
420
443
|
called_from = caller_locations(1, 1).first.path
|
|
@@ -462,68 +485,94 @@ module Zeitwerk
|
|
|
462
485
|
end
|
|
463
486
|
end
|
|
464
487
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
488
|
+
# Scans `dir` and sets autoloads in `mod` for the constants its contents are
|
|
489
|
+
# expected to define.
|
|
490
|
+
#
|
|
491
|
+
# The `external` flag indicates whether `mod` has been externally defined,
|
|
492
|
+
# as is the case with root namespaces or reopened third-party namespaces.
|
|
493
|
+
#
|
|
494
|
+
#: (String, Module, external: boolish) -> void
|
|
495
|
+
private def define_autoloads_for_dir(dir, mod, external:)
|
|
496
|
+
@fs.ls(dir) do |basename, abspath, ftype|
|
|
468
497
|
if ftype == :file
|
|
469
|
-
basename
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
autoload_subdir(cref, abspath)
|
|
498
|
+
if basename == @nsfile
|
|
499
|
+
if external
|
|
500
|
+
cpath = real_mod_name(mod)
|
|
501
|
+
location = Object.const_source_location(cpath)&.join(':')
|
|
502
|
+
location = nil if location&.empty?
|
|
503
|
+
raise Zeitwerk::ConflictingNamespaceDefinitionError.new(cpath, location: location, conflicting_file: abspath)
|
|
504
|
+
end
|
|
505
|
+
next # Pass if this is a managed namespace, the nsfile was already probed when visiting the parent directory.
|
|
478
506
|
end
|
|
507
|
+
|
|
508
|
+
basename.delete_suffix!('.rb')
|
|
509
|
+
cref = Cref.new(mod, cname_for(basename, abspath))
|
|
510
|
+
visit_file(cref, abspath)
|
|
511
|
+
else
|
|
512
|
+
cref = Cref.new(mod, cname_for(basename, abspath))
|
|
513
|
+
visit_subdir(cref, abspath, external:)
|
|
479
514
|
end
|
|
480
515
|
end
|
|
481
516
|
end
|
|
482
517
|
|
|
483
518
|
#: (Zeitwerk::Cref, String) -> void
|
|
484
|
-
private def
|
|
519
|
+
private def visit_file(cref, file)
|
|
520
|
+
if autoload_path = cref.autoload? || Registry.inceptions.registered?(cref)
|
|
521
|
+
if @fs.rb_extension?(autoload_path)
|
|
522
|
+
if File.basename(autoload_path) == @nsfile && autoload_path_set_by_me_for?(cref)
|
|
523
|
+
raise Zeitwerk::ConflictingNamespaceDefinitionError.new(cref.path, location: autoload_path, conflicting_file: file)
|
|
524
|
+
end
|
|
525
|
+
shadowed_files << file
|
|
526
|
+
log { "file #{file} is ignored because #{autoload_path} has precedence" }
|
|
527
|
+
else
|
|
528
|
+
promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
|
|
529
|
+
end
|
|
530
|
+
elsif cref.defined?
|
|
531
|
+
shadowed_files << file
|
|
532
|
+
if location = cref.location
|
|
533
|
+
log { "file #{file} is ignored because #{cref} is already defined in #{location}" }
|
|
534
|
+
else
|
|
535
|
+
log { "file #{file} is ignored because #{cref} is already defined (unknown location)" }
|
|
536
|
+
end
|
|
537
|
+
else
|
|
538
|
+
define_autoload(cref, file)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
#: (Zeitwerk::Cref, String, external: boolish) -> void
|
|
543
|
+
private def visit_subdir(cref, subdir, external:)
|
|
485
544
|
if autoload_path = autoload_path_set_by_me_for?(cref)
|
|
486
|
-
if
|
|
545
|
+
if @fs.rb_extension?(autoload_path)
|
|
546
|
+
# The namespace that corresponds to this subdirectory is defined in a
|
|
547
|
+
# file, either regular or nsfile. Therefore, a nsfile would be a
|
|
548
|
+
# duplication.
|
|
549
|
+
if nsfile_abspath = @fs.has_exactly_one_nsfile?(cref, subdir)
|
|
550
|
+
raise Zeitwerk::ConflictingNamespaceDefinitionError.new(cref.path, location: autoload_path, conflicting_file: nsfile_abspath)
|
|
551
|
+
end
|
|
487
552
|
# Scanning visited a Ruby file first, and now a directory for the same
|
|
488
|
-
# constant has been found. This
|
|
489
|
-
# namespace whose definition was seen first.
|
|
553
|
+
# constant has been found. This is an explicit namespace.
|
|
490
554
|
#
|
|
491
|
-
#
|
|
492
|
-
#
|
|
493
|
-
# on, no big deal.
|
|
555
|
+
# The namespace may be spread over multiple directories and perhaps it
|
|
556
|
+
# was already registered, but registering is idempotent, just do it.
|
|
494
557
|
register_explicit_namespace(cref)
|
|
558
|
+
elsif nsfile_abspath = @fs.has_exactly_one_nsfile?(cref, subdir)
|
|
559
|
+
# Scanning found a matching directory first, and now we saw a nsfile.
|
|
560
|
+
promote_namespace_from_implicit_to_explicit(dir: subdir, file: nsfile_abspath, cref: cref)
|
|
495
561
|
end
|
|
496
|
-
# If the existing autoload points to a file, it has to be preserved, if
|
|
497
|
-
# not, it is fine as it is. In either case, we do not need to override.
|
|
498
|
-
# Just remember the subdirectory conforms this namespace.
|
|
499
562
|
namespace_dirs.get_or_set(cref) { [] } << subdir
|
|
500
563
|
elsif !cref.defined?
|
|
501
|
-
|
|
564
|
+
if nsfile_abspath = @fs.has_exactly_one_nsfile?(cref, subdir)
|
|
565
|
+
define_autoload(cref, nsfile_abspath)
|
|
566
|
+
register_explicit_namespace(cref)
|
|
567
|
+
else
|
|
568
|
+
define_autoload(cref, subdir)
|
|
569
|
+
end
|
|
502
570
|
namespace_dirs.get_or_set(cref) { [] } << subdir
|
|
503
|
-
define_autoload(cref, subdir)
|
|
504
571
|
else
|
|
505
572
|
# For whatever reason the constant that corresponds to this namespace has
|
|
506
573
|
# already been defined, we have to recurse.
|
|
507
|
-
log
|
|
508
|
-
define_autoloads_for_dir(subdir, cref.get)
|
|
509
|
-
end
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
#: (Zeitwerk::Cref, String) -> void
|
|
513
|
-
private def autoload_file(cref, file)
|
|
514
|
-
if autoload_path = cref.autoload? || Registry.inceptions.registered?(cref)
|
|
515
|
-
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
|
516
|
-
if ruby?(autoload_path)
|
|
517
|
-
shadowed_files << file
|
|
518
|
-
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
|
519
|
-
else
|
|
520
|
-
promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
|
|
521
|
-
end
|
|
522
|
-
elsif cref.defined?
|
|
523
|
-
shadowed_files << file
|
|
524
|
-
log("file #{file} is ignored because #{cref} is already defined") if logger
|
|
525
|
-
else
|
|
526
|
-
define_autoload(cref, file)
|
|
574
|
+
log { "the namespace #{cref} already exists, descending into #{subdir}" }
|
|
575
|
+
define_autoloads_for_dir(subdir, cref.get, external:)
|
|
527
576
|
end
|
|
528
577
|
end
|
|
529
578
|
|
|
@@ -535,7 +584,7 @@ module Zeitwerk
|
|
|
535
584
|
autoloads.delete(dir)
|
|
536
585
|
Registry.autoloads.unregister(dir)
|
|
537
586
|
|
|
538
|
-
log
|
|
587
|
+
log { "earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}" }
|
|
539
588
|
|
|
540
589
|
# Order matters: When Module#const_added is triggered by the autoload, we
|
|
541
590
|
# don't want the namespace to be registered yet.
|
|
@@ -548,10 +597,10 @@ module Zeitwerk
|
|
|
548
597
|
cref.autoload(abspath)
|
|
549
598
|
|
|
550
599
|
if logger
|
|
551
|
-
if
|
|
552
|
-
log
|
|
600
|
+
if @fs.rb_extension?(abspath)
|
|
601
|
+
log { "autoload set for #{cref}, to be loaded from #{abspath}" }
|
|
553
602
|
else
|
|
554
|
-
log
|
|
603
|
+
log { "autoload set for #{cref}, to be autovivified from #{abspath}" }
|
|
555
604
|
end
|
|
556
605
|
end
|
|
557
606
|
|
|
@@ -595,28 +644,12 @@ module Zeitwerk
|
|
|
595
644
|
end
|
|
596
645
|
|
|
597
646
|
#: (String) -> void
|
|
598
|
-
private def
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
loader.
|
|
604
|
-
# Conflicting directories are rare, optimize for the common case.
|
|
605
|
-
next if !dir.start_with?(root_dir) && !root_dir.start_with?(dir)
|
|
606
|
-
|
|
607
|
-
dir_slash = dir + "/"
|
|
608
|
-
root_dir_slash = root_dir + "/"
|
|
609
|
-
next if !dir_slash.start_with?(root_dir_slash) && !root_dir_slash.start_with?(dir_slash)
|
|
610
|
-
|
|
611
|
-
next if ignores?(root_dir)
|
|
612
|
-
break if loader.__ignores?(dir)
|
|
613
|
-
|
|
614
|
-
require "pp" # Needed to have pretty_inspect available.
|
|
615
|
-
raise Error,
|
|
616
|
-
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
|
617
|
-
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
|
618
|
-
end
|
|
619
|
-
end
|
|
647
|
+
private def raise_if_conflicting_root_dir(root_dir)
|
|
648
|
+
if loader = Registry.conflicting_root_dir?(self, root_dir)
|
|
649
|
+
require 'pp' # Needed to have pretty_inspect available.
|
|
650
|
+
raise Error,
|
|
651
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{root_dir}," \
|
|
652
|
+
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
|
620
653
|
end
|
|
621
654
|
end
|
|
622
655
|
|
|
@@ -630,7 +663,7 @@ module Zeitwerk
|
|
|
630
663
|
#: (Zeitwerk::Cref) -> void
|
|
631
664
|
private def unload_autoload(cref)
|
|
632
665
|
cref.remove
|
|
633
|
-
log
|
|
666
|
+
log { "autoload for #{cref} removed" }
|
|
634
667
|
end
|
|
635
668
|
|
|
636
669
|
#: (Zeitwerk::Cref) -> void
|
|
@@ -642,7 +675,7 @@ module Zeitwerk
|
|
|
642
675
|
# There are a few edge scenarios in which this may happen. If the constant
|
|
643
676
|
# is gone, that is OK, anyway.
|
|
644
677
|
else
|
|
645
|
-
log
|
|
678
|
+
log { "#{cref} unloaded" }
|
|
646
679
|
end
|
|
647
680
|
end
|
|
648
681
|
end
|
|
@@ -7,7 +7,7 @@ module Zeitwerk::RealModName
|
|
|
7
7
|
|
|
8
8
|
# Returns the real name of the class or module.
|
|
9
9
|
#
|
|
10
|
-
# We need this indirection
|
|
10
|
+
# We need this indirection because the `name` method can be overridden, and
|
|
11
11
|
# because in practice what we really need is the constant paths of modules
|
|
12
12
|
# with a permanent name, not so much what the user considers to be the name of
|
|
13
13
|
# a certain class or module of theirs.
|
data/lib/zeitwerk/registry.rb
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Zeitwerk
|
|
4
4
|
module Registry # :nodoc: all
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
5
|
+
require_relative 'registry/autoloads'
|
|
6
|
+
require_relative 'registry/explicit_namespaces'
|
|
7
|
+
require_relative 'registry/inceptions'
|
|
8
|
+
require_relative 'registry/loaders'
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
11
|
# Keeps track of all loaders. Useful to broadcast messages and to prevent
|
|
@@ -44,6 +44,31 @@ module Zeitwerk
|
|
|
44
44
|
gem_loaders_by_root_file.delete_if { |_, l| l == loader }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
#: (Zeitwerk::Loader, String) -> Zeitwerk::Loader?
|
|
48
|
+
def conflicting_root_dir?(loader, new_root_dir)
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
loaders.each do |existing_loader|
|
|
51
|
+
next if existing_loader == loader
|
|
52
|
+
|
|
53
|
+
existing_loader.__roots.each_key do |existing_root_dir|
|
|
54
|
+
# Conflicting directories are rare, optimize for the common case.
|
|
55
|
+
next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
|
|
56
|
+
|
|
57
|
+
new_root_dir_slash = new_root_dir + '/'
|
|
58
|
+
existing_root_dir_slash = existing_root_dir + '/'
|
|
59
|
+
next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
|
|
60
|
+
|
|
61
|
+
next if loader.__ignores?(existing_root_dir)
|
|
62
|
+
break if existing_loader.__ignores?(new_root_dir)
|
|
63
|
+
|
|
64
|
+
return existing_loader
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
47
72
|
# This method returns always a loader, the same instance for the same root
|
|
48
73
|
# file. That is how Zeitwerk::Loader.for_gem is idempotent.
|
|
49
74
|
#
|
|
@@ -59,5 +84,6 @@ module Zeitwerk
|
|
|
59
84
|
@autoloads = Autoloads.new
|
|
60
85
|
@explicit_namespaces = ExplicitNamespaces.new
|
|
61
86
|
@inceptions = Inceptions.new
|
|
87
|
+
@mutex = Mutex.new
|
|
62
88
|
end
|
|
63
89
|
end
|
data/lib/zeitwerk/version.rb
CHANGED
data/lib/zeitwerk.rb
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zeitwerk
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative
|
|
14
|
-
require_relative
|
|
4
|
+
require_relative 'zeitwerk/real_mod_name'
|
|
5
|
+
require_relative 'zeitwerk/internal'
|
|
6
|
+
require_relative 'zeitwerk/cref'
|
|
7
|
+
require_relative 'zeitwerk/loader'
|
|
8
|
+
require_relative 'zeitwerk/gem_loader'
|
|
9
|
+
require_relative 'zeitwerk/registry'
|
|
10
|
+
require_relative 'zeitwerk/inflector'
|
|
11
|
+
require_relative 'zeitwerk/gem_inflector'
|
|
12
|
+
require_relative 'zeitwerk/null_inflector'
|
|
13
|
+
require_relative 'zeitwerk/error'
|
|
14
|
+
require_relative 'zeitwerk/version'
|
|
15
15
|
|
|
16
|
-
require_relative
|
|
17
|
-
require_relative
|
|
16
|
+
require_relative 'zeitwerk/core_ext/kernel'
|
|
17
|
+
require_relative 'zeitwerk/core_ext/module'
|
|
18
18
|
|
|
19
19
|
# This is a dangerous method.
|
|
20
20
|
#
|