zeitwerk 2.7.5 → 2.8.2

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,16 @@
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
+ require_relative 'loader/constant_path_validator'
13
14
 
14
15
  extend Internal
15
16
 
@@ -22,8 +23,8 @@ module Zeitwerk
22
23
  # Maps absolute paths for which an autoload has been set ---and not
23
24
  # executed--- to their corresponding Zeitwerk::Cref object.
24
25
  #
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, ...>,
26
+ # '/Users/fxn/blog/app/models/user.rb' => #<Zeitwerk::Cref:... @mod=Object, @cname=:User, ...>,
27
+ # '/Users/fxn/blog/app/models/hotel/pricing.rb' => #<Zeitwerk::Cref:... @mod=Hotel, @cname=:Pricing, ...>,
27
28
  # ...
28
29
  #
29
30
  #: Hash[String, Zeitwerk::Cref]
@@ -102,6 +103,7 @@ module Zeitwerk
102
103
  attr_reader :dirs_autoload_monitor
103
104
  private :dirs_autoload_monitor
104
105
 
106
+ #: () -> void
105
107
  def initialize
106
108
  super
107
109
 
@@ -114,6 +116,7 @@ module Zeitwerk
114
116
  @setup = false
115
117
  @eager_loaded = false
116
118
  @fs = FileSystem.new(self)
119
+ @cpv = ConstantPathValidator.new
117
120
 
118
121
  @mutex = Mutex.new
119
122
  @dirs_autoload_monitor = Monitor.new
@@ -129,7 +132,7 @@ module Zeitwerk
129
132
  break if @setup
130
133
 
131
134
  actual_roots.each do |root_dir, root_namespace|
132
- define_autoloads_for_dir(root_dir, root_namespace)
135
+ define_autoloads_for_dir(root_dir, root_namespace, external: true)
133
136
  end
134
137
 
135
138
  on_setup_callbacks.each(&:call)
@@ -153,73 +156,79 @@ module Zeitwerk
153
156
  def unload
154
157
  mutex.synchronize do
155
158
  raise SetupRequired unless @setup
159
+ __unload
160
+ end
161
+ end
156
162
 
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
163
+ # This is an internal method.
164
+ #
165
+ #: () -> void
166
+ def __unload
167
+ # We are going to keep track of the files that were required by our
168
+ # autoloads to later remove them from $LOADED_FEATURES, thus making them
169
+ # loadable by Kernel#require again.
170
+ #
171
+ # Directories are not stored in $LOADED_FEATURES, keeping track of files
172
+ # is enough.
173
+ unloaded_files = Set.new
189
174
 
175
+ autoloads.each do |abspath, cref|
176
+ if cref.autoload?
177
+ unload_autoload(cref)
178
+ else
179
+ # Could happen if loaded with require_relative. That is unsupported,
180
+ # and the constant path would escape unloadable_cpath? This is just
181
+ # defensive code to clean things up as much as we are able to.
190
182
  unload_cref(cref)
191
183
  unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
192
184
  end
185
+ end
193
186
 
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) }
187
+ to_unload.each do |abspath, cref|
188
+ unless on_unload_callbacks.empty?
189
+ begin
190
+ value = cref.get
191
+ rescue ::NameError
192
+ # Perhaps the user deleted the constant by hand, or perhaps an
193
+ # autoload failed to define the expected constant but the user
194
+ # rescued the exception.
195
+ else
196
+ run_on_unload_callbacks(cref, value, abspath)
197
+ end
207
198
  end
208
199
 
209
- autoloads.clear
210
- autoloaded_dirs.clear
211
- to_unload.clear
212
- namespace_dirs.clear
213
- shadowed_files.clear
200
+ unload_cref(cref)
201
+ unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
202
+ end
214
203
 
215
- unregister_inceptions
216
- unregister_explicit_namespaces
204
+ unless unloaded_files.empty?
205
+ # Bootsnap decorates Kernel#require to speed it up using a cache and
206
+ # this optimization does not check if $LOADED_FEATURES has the file.
207
+ #
208
+ # To make it aware of changes, the gem defines singleton methods in
209
+ # $LOADED_FEATURES:
210
+ #
211
+ # https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
212
+ #
213
+ # Rails applications may depend on bootsnap, so for unloading to work
214
+ # in that setting it is preferable that we restrict our API choice to
215
+ # one of those methods.
216
+ $LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
217
+ end
217
218
 
218
- Registry.autoloads.unregister_loader(self)
219
+ autoloads.clear
220
+ autoloaded_dirs.clear
221
+ to_unload.clear
222
+ namespace_dirs.clear
223
+ shadowed_files.clear
219
224
 
220
- @setup = false
221
- @eager_loaded = false
222
- end
225
+ unregister_inceptions
226
+ unregister_explicit_namespaces
227
+
228
+ Registry.autoloads.unregister_loader(self)
229
+
230
+ @setup = false
231
+ @eager_loaded = false
223
232
  end
224
233
 
225
234
  # Unloads all loaded code, and calls setup again so that the loader is able
@@ -234,8 +243,11 @@ module Zeitwerk
234
243
  raise SetupRequired unless @setup
235
244
 
236
245
  unload
246
+
237
247
  recompute_ignored_paths
238
248
  recompute_collapse_dirs
249
+ recompute_collapse_parents
250
+
239
251
  setup
240
252
  end
241
253
 
@@ -252,18 +264,20 @@ module Zeitwerk
252
264
  while (dir, cpath = queue.shift)
253
265
  result[dir] = cpath
254
266
 
255
- prefix = cpath == "Object" ? "" : cpath + "::"
267
+ prefix = cpath == 'Object' ? '' : cpath + '::'
256
268
 
257
- @fs.ls(dir) do |basename, abspath, ftype|
269
+ @fs.ls(dir, collapse: false) do |basename, abspath, ftype|
258
270
  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]
271
+ if basename == @nsfile
272
+ result[abspath] = cpath
264
273
  else
265
- queue << [abspath, "#{prefix}#{cname_for(basename, abspath)}"]
274
+ basename.delete_suffix!('.rb')
275
+ result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
266
276
  end
277
+ elsif collapse?(abspath)
278
+ queue.unshift([abspath, cpath])
279
+ else
280
+ queue.push([abspath, "#{prefix}#{cname_for(basename, abspath)}"])
267
281
  end
268
282
  end
269
283
  end
@@ -285,11 +299,11 @@ module Zeitwerk
285
299
 
286
300
  paths = []
287
301
 
288
- if :file == ftype
289
- basename = File.basename(abspath, ".rb")
302
+ if ftype == :file
303
+ basename = File.basename(abspath)
290
304
  return if @fs.hidden?(basename)
291
305
 
292
- paths << [basename, abspath]
306
+ paths << [basename.delete_suffix('.rb'), abspath] unless basename == @nsfile
293
307
  walk_up_from = File.dirname(abspath)
294
308
  else
295
309
  walk_up_from = abspath
@@ -315,9 +329,9 @@ module Zeitwerk
315
329
  cnames = paths.reverse_each.map { cname_for(_1, _2) }
316
330
 
317
331
  if root_namespace == Object
318
- cnames.join("::")
332
+ cnames.join('::')
319
333
  else
320
- "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
334
+ "#{real_mod_name(root_namespace)}::#{cnames.join('::')}"
321
335
  end
322
336
  end
323
337
  end
@@ -384,10 +398,10 @@ module Zeitwerk
384
398
 
385
399
  # This is a shortcut for
386
400
  #
387
- # require "zeitwerk"
401
+ # require 'zeitwerk'
388
402
  #
389
403
  # loader = Zeitwerk::Loader.new
390
- # loader.tag = File.basename(__FILE__, ".rb")
404
+ # loader.tag = File.basename(__FILE__, '.rb')
391
405
  # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
392
406
  # loader.push_dir(__dir__)
393
407
  #
@@ -405,10 +419,10 @@ module Zeitwerk
405
419
 
406
420
  # This is a shortcut for
407
421
  #
408
- # require "zeitwerk"
422
+ # require 'zeitwerk'
409
423
  #
410
424
  # loader = Zeitwerk::Loader.new
411
- # loader.tag = namespace.name + "-" + File.basename(__FILE__, ".rb")
425
+ # loader.tag = namespace.name + '-' + File.basename(__FILE__, '.rb')
412
426
  # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
413
427
  # loader.push_dir(__dir__, namespace: namespace)
414
428
  #
@@ -425,7 +439,7 @@ module Zeitwerk
425
439
  end
426
440
 
427
441
  unless real_mod_name(namespace)
428
- raise Zeitwerk::Error, "extending anonymous namespaces is unsupported"
442
+ raise Zeitwerk::Error, 'extending anonymous namespaces is unsupported'
429
443
  end
430
444
 
431
445
  called_from = caller_locations(1, 1).first.path
@@ -473,68 +487,94 @@ module Zeitwerk
473
487
  end
474
488
  end
475
489
 
476
- #: (String, Module) -> void
477
- private def define_autoloads_for_dir(dir, parent)
490
+ # Scans `dir` and sets autoloads in `mod` for the constants its contents are
491
+ # expected to define.
492
+ #
493
+ # The `external` flag indicates whether `mod` has been externally defined,
494
+ # as is the case with root namespaces or reopened third-party namespaces.
495
+ #
496
+ #: (String, Module, external: boolish) -> void
497
+ private def define_autoloads_for_dir(dir, mod, external:)
478
498
  @fs.ls(dir) do |basename, abspath, ftype|
479
499
  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)
500
+ if basename == @nsfile
501
+ if external
502
+ cpath = real_mod_name(mod)
503
+ location = Object.const_source_location(cpath)&.join(':')
504
+ location = nil if location&.empty?
505
+ raise Zeitwerk::ConflictingNamespaceDefinitionError.new(cpath, location: location, conflicting_file: abspath)
506
+ end
507
+ next # Pass if this is a managed namespace, the nsfile was already probed when visiting the parent directory.
489
508
  end
509
+
510
+ basename.delete_suffix!('.rb')
511
+ cref = Cref.new(mod, cname_for(basename, abspath))
512
+ visit_file(cref, abspath)
513
+ else
514
+ cref = Cref.new(mod, cname_for(basename, abspath))
515
+ visit_subdir(cref, abspath, external:)
490
516
  end
491
517
  end
492
518
  end
493
519
 
494
520
  #: (Zeitwerk::Cref, String) -> void
495
- private def autoload_subdir(cref, subdir)
521
+ private def visit_file(cref, file)
522
+ if autoload_path = cref.autoload? || Registry.inceptions.registered?(cref)
523
+ if @fs.rb_extension?(autoload_path)
524
+ if File.basename(autoload_path) == @nsfile && autoload_path_set_by_me_for?(cref)
525
+ raise Zeitwerk::ConflictingNamespaceDefinitionError.new(cref.path, location: autoload_path, conflicting_file: file)
526
+ end
527
+ shadowed_files << file
528
+ log { "file #{file} is ignored because #{autoload_path} has precedence" }
529
+ else
530
+ promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
531
+ end
532
+ elsif cref.defined?
533
+ shadowed_files << file
534
+ if location = cref.location
535
+ log { "file #{file} is ignored because #{cref} is already defined in #{location}" }
536
+ else
537
+ log { "file #{file} is ignored because #{cref} is already defined (unknown location)" }
538
+ end
539
+ else
540
+ define_autoload(cref, file)
541
+ end
542
+ end
543
+
544
+ #: (Zeitwerk::Cref, String, external: boolish) -> void
545
+ private def visit_subdir(cref, subdir, external:)
496
546
  if autoload_path = autoload_path_set_by_me_for?(cref)
497
547
  if @fs.rb_extension?(autoload_path)
548
+ # The namespace that corresponds to this subdirectory is defined in a
549
+ # file, either regular or nsfile. Therefore, a nsfile would be a
550
+ # duplication.
551
+ if nsfile_abspath = @fs.has_exactly_one_nsfile?(cref, subdir)
552
+ raise Zeitwerk::ConflictingNamespaceDefinitionError.new(cref.path, location: autoload_path, conflicting_file: nsfile_abspath)
553
+ end
498
554
  # 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.
555
+ # constant has been found. This is an explicit namespace.
501
556
  #
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.
557
+ # The namespace may be spread over multiple directories and perhaps it
558
+ # was already registered, but registering is idempotent, just do it.
505
559
  register_explicit_namespace(cref)
560
+ elsif nsfile_abspath = @fs.has_exactly_one_nsfile?(cref, subdir)
561
+ # Scanning found a matching directory first, and now we saw a nsfile.
562
+ promote_namespace_from_implicit_to_explicit(dir: subdir, file: nsfile_abspath, cref: cref)
506
563
  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
564
  namespace_dirs.get_or_set(cref) { [] } << subdir
511
565
  elsif !cref.defined?
512
- # First time we find this namespace, set an autoload for it.
566
+ if nsfile_abspath = @fs.has_exactly_one_nsfile?(cref, subdir)
567
+ define_autoload(cref, nsfile_abspath)
568
+ register_explicit_namespace(cref)
569
+ else
570
+ define_autoload(cref, subdir)
571
+ end
513
572
  namespace_dirs.get_or_set(cref) { [] } << subdir
514
- define_autoload(cref, subdir)
515
573
  else
516
574
  # For whatever reason the constant that corresponds to this namespace has
517
575
  # already been defined, we have to recurse.
518
576
  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)
577
+ define_autoloads_for_dir(subdir, cref.get, external:)
538
578
  end
539
579
  end
540
580
 
@@ -608,7 +648,7 @@ module Zeitwerk
608
648
  #: (String) -> void
609
649
  private def raise_if_conflicting_root_dir(root_dir)
610
650
  if loader = Registry.conflicting_root_dir?(self, root_dir)
611
- require "pp" # Needed to have pretty_inspect available.
651
+ require 'pp' # Needed to have pretty_inspect available.
612
652
  raise Error,
613
653
  "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{root_dir}," \
614
654
  " which is already managed by\n\n#{loader.pretty_inspect}\n"
@@ -6,8 +6,8 @@ module Zeitwerk::Registry
6
6
  end
7
7
 
8
8
  #: ({ (Zeitwerk::Loader) -> void }) -> void
9
- def each(&)
10
- @loaders.each(&)
9
+ def each(&block)
10
+ @loaders.each(&block)
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
@@ -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.2'
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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
@@ -34,6 +34,7 @@ files:
34
34
  - lib/zeitwerk/loader.rb
35
35
  - lib/zeitwerk/loader/callbacks.rb
36
36
  - lib/zeitwerk/loader/config.rb
37
+ - lib/zeitwerk/loader/constant_path_validator.rb
37
38
  - lib/zeitwerk/loader/eager_load.rb
38
39
  - lib/zeitwerk/loader/file_system.rb
39
40
  - lib/zeitwerk/loader/helpers.rb