zeitwerk 2.7.5 → 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,15 +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"
12
- require_relative "loader/file_system"
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'
13
13
 
14
14
  extend Internal
15
15
 
@@ -22,8 +22,8 @@ module Zeitwerk
22
22
  # Maps absolute paths for which an autoload has been set ---and not
23
23
  # executed--- to their corresponding Zeitwerk::Cref object.
24
24
  #
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, ...>,
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, ...>,
27
27
  # ...
28
28
  #
29
29
  #: Hash[String, Zeitwerk::Cref]
@@ -102,6 +102,7 @@ module Zeitwerk
102
102
  attr_reader :dirs_autoload_monitor
103
103
  private :dirs_autoload_monitor
104
104
 
105
+ #: () -> void
105
106
  def initialize
106
107
  super
107
108
 
@@ -129,7 +130,7 @@ module Zeitwerk
129
130
  break if @setup
130
131
 
131
132
  actual_roots.each do |root_dir, root_namespace|
132
- define_autoloads_for_dir(root_dir, root_namespace)
133
+ define_autoloads_for_dir(root_dir, root_namespace, external: true)
133
134
  end
134
135
 
135
136
  on_setup_callbacks.each(&:call)
@@ -153,73 +154,79 @@ module Zeitwerk
153
154
  def unload
154
155
  mutex.synchronize do
155
156
  raise SetupRequired unless @setup
157
+ __unload
158
+ end
159
+ end
156
160
 
157
- # We are going to keep track of the files that were required by our
158
- # autoloads to later remove them from $LOADED_FEATURES, thus making them
159
- # loadable by Kernel#require again.
160
- #
161
- # Directories are not stored in $LOADED_FEATURES, keeping track of files
162
- # is enough.
163
- unloaded_files = Set.new
164
-
165
- autoloads.each do |abspath, cref|
166
- if cref.autoload?
167
- unload_autoload(cref)
168
- else
169
- # Could happen if loaded with require_relative. That is unsupported,
170
- # and the constant path would escape unloadable_cpath? This is just
171
- # defensive code to clean things up as much as we are able to.
172
- unload_cref(cref)
173
- unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
174
- end
175
- end
176
-
177
- to_unload.each do |abspath, cref|
178
- unless on_unload_callbacks.empty?
179
- begin
180
- value = cref.get
181
- rescue ::NameError
182
- # Perhaps the user deleted the constant by hand, or perhaps an
183
- # autoload failed to define the expected constant but the user
184
- # rescued the exception.
185
- else
186
- run_on_unload_callbacks(cref, value, abspath)
187
- end
188
- end
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
189
172
 
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.
190
180
  unload_cref(cref)
191
181
  unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
192
182
  end
183
+ end
193
184
 
194
- unless unloaded_files.empty?
195
- # Bootsnap decorates Kernel#require to speed it up using a cache and
196
- # this optimization does not check if $LOADED_FEATURES has the file.
197
- #
198
- # To make it aware of changes, the gem defines singleton methods in
199
- # $LOADED_FEATURES:
200
- #
201
- # https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
202
- #
203
- # Rails applications may depend on bootsnap, so for unloading to work
204
- # in that setting it is preferable that we restrict our API choice to
205
- # one of those methods.
206
- $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
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)
195
+ end
207
196
  end
208
197
 
209
- autoloads.clear
210
- autoloaded_dirs.clear
211
- to_unload.clear
212
- namespace_dirs.clear
213
- shadowed_files.clear
198
+ unload_cref(cref)
199
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
200
+ end
214
201
 
215
- unregister_inceptions
216
- unregister_explicit_namespaces
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
217
216
 
218
- Registry.autoloads.unregister_loader(self)
217
+ autoloads.clear
218
+ autoloaded_dirs.clear
219
+ to_unload.clear
220
+ namespace_dirs.clear
221
+ shadowed_files.clear
219
222
 
220
- @setup = false
221
- @eager_loaded = false
222
- end
223
+ unregister_inceptions
224
+ unregister_explicit_namespaces
225
+
226
+ Registry.autoloads.unregister_loader(self)
227
+
228
+ @setup = false
229
+ @eager_loaded = false
223
230
  end
224
231
 
225
232
  # Unloads all loaded code, and calls setup again so that the loader is able
@@ -234,8 +241,11 @@ module Zeitwerk
234
241
  raise SetupRequired unless @setup
235
242
 
236
243
  unload
244
+
237
245
  recompute_ignored_paths
238
246
  recompute_collapse_dirs
247
+ recompute_collapse_parents
248
+
239
249
  setup
240
250
  end
241
251
 
@@ -252,18 +262,20 @@ module Zeitwerk
252
262
  while (dir, cpath = queue.shift)
253
263
  result[dir] = cpath
254
264
 
255
- prefix = cpath == "Object" ? "" : cpath + "::"
265
+ prefix = cpath == 'Object' ? '' : cpath + '::'
256
266
 
257
- @fs.ls(dir) do |basename, abspath, ftype|
267
+ @fs.ls(dir, collapse: false) do |basename, abspath, ftype|
258
268
  if ftype == :file
259
- basename.delete_suffix!(".rb")
260
- result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
261
- else
262
- if collapse?(abspath)
263
- queue << [abspath, cpath]
269
+ if basename == @nsfile
270
+ result[abspath] = cpath
264
271
  else
265
- queue << [abspath, "#{prefix}#{cname_for(basename, abspath)}"]
272
+ basename.delete_suffix!('.rb')
273
+ result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
266
274
  end
275
+ elsif collapse?(abspath)
276
+ queue.unshift([abspath, cpath])
277
+ else
278
+ queue.push([abspath, "#{prefix}#{cname_for(basename, abspath)}"])
267
279
  end
268
280
  end
269
281
  end
@@ -285,11 +297,11 @@ module Zeitwerk
285
297
 
286
298
  paths = []
287
299
 
288
- if :file == ftype
289
- basename = File.basename(abspath, ".rb")
300
+ if ftype == :file
301
+ basename = File.basename(abspath)
290
302
  return if @fs.hidden?(basename)
291
303
 
292
- paths << [basename, abspath]
304
+ paths << [basename.delete_suffix('.rb'), abspath] unless basename == @nsfile
293
305
  walk_up_from = File.dirname(abspath)
294
306
  else
295
307
  walk_up_from = abspath
@@ -315,9 +327,9 @@ module Zeitwerk
315
327
  cnames = paths.reverse_each.map { cname_for(_1, _2) }
316
328
 
317
329
  if root_namespace == Object
318
- cnames.join("::")
330
+ cnames.join('::')
319
331
  else
320
- "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
332
+ "#{real_mod_name(root_namespace)}::#{cnames.join('::')}"
321
333
  end
322
334
  end
323
335
  end
@@ -384,10 +396,10 @@ module Zeitwerk
384
396
 
385
397
  # This is a shortcut for
386
398
  #
387
- # require "zeitwerk"
399
+ # require 'zeitwerk'
388
400
  #
389
401
  # loader = Zeitwerk::Loader.new
390
- # loader.tag = File.basename(__FILE__, ".rb")
402
+ # loader.tag = File.basename(__FILE__, '.rb')
391
403
  # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
392
404
  # loader.push_dir(__dir__)
393
405
  #
@@ -405,10 +417,10 @@ module Zeitwerk
405
417
 
406
418
  # This is a shortcut for
407
419
  #
408
- # require "zeitwerk"
420
+ # require 'zeitwerk'
409
421
  #
410
422
  # loader = Zeitwerk::Loader.new
411
- # loader.tag = namespace.name + "-" + File.basename(__FILE__, ".rb")
423
+ # loader.tag = namespace.name + '-' + File.basename(__FILE__, '.rb')
412
424
  # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
413
425
  # loader.push_dir(__dir__, namespace: namespace)
414
426
  #
@@ -425,7 +437,7 @@ module Zeitwerk
425
437
  end
426
438
 
427
439
  unless real_mod_name(namespace)
428
- raise Zeitwerk::Error, "extending anonymous namespaces is unsupported"
440
+ raise Zeitwerk::Error, 'extending anonymous namespaces is unsupported'
429
441
  end
430
442
 
431
443
  called_from = caller_locations(1, 1).first.path
@@ -473,68 +485,94 @@ module Zeitwerk
473
485
  end
474
486
  end
475
487
 
476
- #: (String, Module) -> void
477
- private def define_autoloads_for_dir(dir, parent)
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:)
478
496
  @fs.ls(dir) do |basename, abspath, ftype|
479
497
  if ftype == :file
480
- basename.delete_suffix!(".rb")
481
- cref = Cref.new(parent, cname_for(basename, abspath))
482
- autoload_file(cref, abspath)
483
- else
484
- if collapse?(abspath)
485
- define_autoloads_for_dir(abspath, parent)
486
- else
487
- cref = Cref.new(parent, cname_for(basename, abspath))
488
- 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.
489
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:)
490
514
  end
491
515
  end
492
516
  end
493
517
 
494
518
  #: (Zeitwerk::Cref, String) -> void
495
- 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:)
496
544
  if autoload_path = autoload_path_set_by_me_for?(cref)
497
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
498
552
  # Scanning visited a Ruby file first, and now a directory for the same
499
- # constant has been found. This means we are dealing with an explicit
500
- # namespace whose definition was seen first.
553
+ # constant has been found. This is an explicit namespace.
501
554
  #
502
- # Registering is idempotent, and we have to keep the autoload pointing
503
- # to the file. This may run again if more directories are found later
504
- # 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.
505
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)
506
561
  end
507
- # If the existing autoload points to a file, it has to be preserved, if
508
- # not, it is fine as it is. In either case, we do not need to override.
509
- # Just remember the subdirectory conforms this namespace.
510
562
  namespace_dirs.get_or_set(cref) { [] } << subdir
511
563
  elsif !cref.defined?
512
- # 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
513
570
  namespace_dirs.get_or_set(cref) { [] } << subdir
514
- define_autoload(cref, subdir)
515
571
  else
516
572
  # For whatever reason the constant that corresponds to this namespace has
517
573
  # already been defined, we have to recurse.
518
574
  log { "the namespace #{cref} already exists, descending into #{subdir}" }
519
- define_autoloads_for_dir(subdir, cref.get)
520
- end
521
- end
522
-
523
- #: (Zeitwerk::Cref, String) -> void
524
- private def autoload_file(cref, file)
525
- if autoload_path = cref.autoload? || Registry.inceptions.registered?(cref)
526
- # First autoload for a Ruby file wins, just ignore subsequent ones.
527
- if @fs.rb_extension?(autoload_path)
528
- shadowed_files << file
529
- log { "file #{file} is ignored because #{autoload_path} has precedence" }
530
- else
531
- promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
532
- end
533
- elsif cref.defined?
534
- shadowed_files << file
535
- log { "file #{file} is ignored because #{cref} is already defined" }
536
- else
537
- define_autoload(cref, file)
575
+ define_autoloads_for_dir(subdir, cref.get, external:)
538
576
  end
539
577
  end
540
578
 
@@ -608,7 +646,7 @@ module Zeitwerk
608
646
  #: (String) -> void
609
647
  private def raise_if_conflicting_root_dir(root_dir)
610
648
  if loader = Registry.conflicting_root_dir?(self, root_dir)
611
- require "pp" # Needed to have pretty_inspect available.
649
+ require 'pp' # Needed to have pretty_inspect available.
612
650
  raise Error,
613
651
  "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{root_dir}," \
614
652
  " which is already managed by\n\n#{loader.pretty_inspect}\n"
@@ -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
@@ -54,8 +54,8 @@ module Zeitwerk
54
54
  # Conflicting directories are rare, optimize for the common case.
55
55
  next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
56
56
 
57
- new_root_dir_slash = new_root_dir + "/"
58
- existing_root_dir_slash = existing_root_dir + "/"
57
+ new_root_dir_slash = new_root_dir + '/'
58
+ existing_root_dir_slash = existing_root_dir + '/'
59
59
  next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
60
60
 
61
61
  next if loader.__ignores?(existing_root_dir)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Zeitwerk
4
4
  #: String
5
- VERSION = "2.7.5"
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
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.5
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria