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.
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "monitor"
4
- require "set"
3
+ require 'monitor'
4
+ require 'set'
5
5
 
6
6
  module Zeitwerk
7
7
  class Loader
8
- require_relative "loader/helpers"
9
- require_relative "loader/callbacks"
10
- require_relative "loader/config"
11
- require_relative "loader/eager_load"
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
- # "/Users/fxn/blog/app/models/user.rb" => #<Zeitwerk::Cref:... @mod=Object, @cname=:User, ...>,
28
- # "/Users/fxn/blog/app/models/hotel/pricing.rb" => #<Zeitwerk::Cref:... @mod=Hotel, @cname=:Pricing, ...>,
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
- # We are going to keep track of the files that were required by our
159
- # autoloads to later remove them from $LOADED_FEATURES, thus making them
160
- # loadable by Kernel#require again.
161
- #
162
- # Directories are not stored in $LOADED_FEATURES, keeping track of files
163
- # is enough.
164
- unloaded_files = Set.new
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
- autoloads.each do |abspath, cref|
167
- if cref.autoload?
168
- unload_autoload(cref)
169
- else
170
- # Could happen if loaded with require_relative. That is unsupported,
171
- # and the constant path would escape unloadable_cpath? This is just
172
- # defensive code to clean things up as much as we are able to.
173
- unload_cref(cref)
174
- unloaded_files.add(abspath) if ruby?(abspath)
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
- to_unload.each do |abspath, cref|
179
- unless on_unload_callbacks.empty?
180
- begin
181
- value = cref.get
182
- rescue ::NameError
183
- # Perhaps the user deleted the constant by hand, or perhaps an
184
- # autoload failed to define the expected constant but the user
185
- # rescued the exception.
186
- else
187
- run_on_unload_callbacks(cref, value, abspath)
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
- unless unloaded_files.empty?
196
- # Bootsnap decorates Kernel#require to speed it up using a cache and
197
- # this optimization does not check if $LOADED_FEATURES has the file.
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
- autoloads.clear
211
- autoloaded_dirs.clear
212
- to_unload.clear
213
- namespace_dirs.clear
214
- shadowed_files.clear
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
- unregister_inceptions
217
- unregister_explicit_namespaces
217
+ autoloads.clear
218
+ autoloaded_dirs.clear
219
+ to_unload.clear
220
+ namespace_dirs.clear
221
+ shadowed_files.clear
218
222
 
219
- Registry.autoloads.unregister_loader(self)
223
+ unregister_inceptions
224
+ unregister_explicit_namespaces
220
225
 
221
- @setup = false
222
- @eager_loaded = false
223
- end
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 == "Object" ? "" : 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.delete_suffix!(".rb")
261
- result[abspath] = prefix + inflector.camelize(basename, abspath)
262
- else
263
- if collapse?(abspath)
264
- queue << [abspath, cpath]
269
+ if basename == @nsfile
270
+ result[abspath] = cpath
265
271
  else
266
- queue << [abspath, prefix + inflector.camelize(basename, abspath)]
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
- return unless dir?(abspath) || ruby?(abspath)
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 ruby?(abspath)
288
- basename = File.basename(abspath, ".rb")
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 "zeitwerk"
399
+ # require 'zeitwerk'
377
400
  #
378
401
  # loader = Zeitwerk::Loader.new
379
- # loader.tag = File.basename(__FILE__, ".rb")
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 "zeitwerk"
420
+ # require 'zeitwerk'
398
421
  #
399
422
  # loader = Zeitwerk::Loader.new
400
- # loader.tag = namespace.name + "-" + File.basename(__FILE__, ".rb")
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, "extending anonymous namespaces is unsupported"
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
- #: (String, Module) -> void
466
- private def define_autoloads_for_dir(dir, parent)
467
- ls(dir) do |basename, abspath, ftype|
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.delete_suffix!(".rb")
470
- cref = Cref.new(parent, cname_for(basename, abspath))
471
- autoload_file(cref, abspath)
472
- else
473
- if collapse?(abspath)
474
- define_autoloads_for_dir(abspath, parent)
475
- else
476
- cref = Cref.new(parent, cname_for(basename, abspath))
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 autoload_subdir(cref, subdir)
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 ruby?(autoload_path)
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 means we are dealing with an explicit
489
- # namespace whose definition was seen first.
553
+ # constant has been found. This is an explicit namespace.
490
554
  #
491
- # Registering is idempotent, and we have to keep the autoload pointing
492
- # to the file. This may run again if more directories are found later
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
- # First time we find this namespace, set an autoload for it.
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("the namespace #{cref} already exists, descending into #{subdir}") if logger
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("earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}") if logger
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 ruby?(abspath)
552
- log("autoload set for #{cref}, to be loaded from #{abspath}")
600
+ if @fs.rb_extension?(abspath)
601
+ log { "autoload set for #{cref}, to be loaded from #{abspath}" }
553
602
  else
554
- log("autoload set for #{cref}, to be autovivified from #{abspath}")
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 raise_if_conflicting_directory(dir)
599
- MUTEX.synchronize do
600
- Registry.loaders.each do |loader|
601
- next if loader == self
602
-
603
- loader.__roots.each_key do |root_dir|
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("autoload for #{cref} removed") if logger
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("#{cref} unloaded") if logger
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 becasue the `name` method can be overridden, and
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.
@@ -6,8 +6,8 @@ module Zeitwerk::Registry
6
6
  end
7
7
 
8
8
  #: ({ (Zeitwerk::Loader) -> void }) -> void
9
- def each(&block)
10
- @loaders.each(&block)
9
+ def each(&)
10
+ @loaders.each(&)
11
11
  end
12
12
 
13
13
  #: (Zeitwerk::Loader) -> void
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Zeitwerk
4
4
  module Registry # :nodoc: all
5
- require_relative "registry/autoloads"
6
- require_relative "registry/explicit_namespaces"
7
- require_relative "registry/inceptions"
8
- require_relative "registry/loaders"
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Zeitwerk
4
4
  #: String
5
- VERSION = "2.7.4"
5
+ VERSION = '2.8.0'
6
6
  end
data/lib/zeitwerk.rb CHANGED
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
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"
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 "zeitwerk/core_ext/kernel"
17
- require_relative "zeitwerk/core_ext/module"
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
  #