zeitwerk 2.6.2 → 2.6.10

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.
@@ -9,6 +9,8 @@ module Zeitwerk
9
9
  require_relative "loader/config"
10
10
  require_relative "loader/eager_load"
11
11
 
12
+ extend Internal
13
+
12
14
  include RealModName
13
15
  include Callbacks
14
16
  include Helpers
@@ -26,9 +28,9 @@ module Zeitwerk
26
28
  # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
27
29
  # ...
28
30
  #
29
- # @private
30
31
  # @sig Hash[String, [Module, Symbol]]
31
32
  attr_reader :autoloads
33
+ internal :autoloads
32
34
 
33
35
  # We keep track of autoloaded directories to remove them from the registry
34
36
  # at the end of eager loading.
@@ -36,9 +38,9 @@ module Zeitwerk
36
38
  # Files are removed as they are autoloaded, but directories need to wait due
37
39
  # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
38
40
  #
39
- # @private
40
41
  # @sig Array[String]
41
42
  attr_reader :autoloaded_dirs
43
+ internal :autoloaded_dirs
42
44
 
43
45
  # Stores metadata needed for unloading. Its entries look like this:
44
46
  #
@@ -52,9 +54,9 @@ module Zeitwerk
52
54
  # If reloading is enabled, this hash is filled as constants are autoloaded
53
55
  # or eager loaded. Otherwise, the collection remains empty.
54
56
  #
55
- # @private
56
57
  # @sig Hash[String, [String, [Module, Symbol]]]
57
58
  attr_reader :to_unload
59
+ internal :to_unload
58
60
 
59
61
  # Maps namespace constant paths to their respective directories.
60
62
  #
@@ -70,9 +72,9 @@ module Zeitwerk
70
72
  # and that its children are spread over those directories. We'll visit them
71
73
  # to set up the corresponding autoloads.
72
74
  #
73
- # @private
74
75
  # @sig Hash[String, Array[String]]
75
76
  attr_reader :namespace_dirs
77
+ internal :namespace_dirs
76
78
 
77
79
  # A shadowed file is a file managed by this loader that is ignored when
78
80
  # setting autoloads because its matching constant is already taken.
@@ -81,17 +83,17 @@ module Zeitwerk
81
83
  # has only scanned the top-level, `shadowed_files` does not have shadowed
82
84
  # files that may exist deep in the project tree yet.
83
85
  #
84
- # @private
85
86
  # @sig Set[String]
86
87
  attr_reader :shadowed_files
88
+ internal :shadowed_files
87
89
 
88
- # @private
89
90
  # @sig Mutex
90
91
  attr_reader :mutex
92
+ private :mutex
91
93
 
92
- # @private
93
94
  # @sig Mutex
94
95
  attr_reader :mutex2
96
+ private :mutex2
95
97
 
96
98
  def initialize
97
99
  super
@@ -109,15 +111,15 @@ module Zeitwerk
109
111
  Registry.register_loader(self)
110
112
  end
111
113
 
112
- # Sets autoloads in the root namespace.
114
+ # Sets autoloads in the root namespaces.
113
115
  #
114
116
  # @sig () -> void
115
117
  def setup
116
118
  mutex.synchronize do
117
119
  break if @setup
118
120
 
119
- actual_root_dirs.each do |root_dir, namespace|
120
- set_autoloads_in_dir(root_dir, namespace)
121
+ actual_roots.each do |root_dir, root_namespace|
122
+ set_autoloads_in_dir(root_dir, root_namespace)
121
123
  end
122
124
 
123
125
  on_setup_callbacks.each(&:call)
@@ -134,12 +136,14 @@ module Zeitwerk
134
136
  # unload them.
135
137
  #
136
138
  # This method is public but undocumented. Main interface is `reload`, which
137
- # means `unload` + `setup`. This one is avaiable to be used together with
139
+ # means `unload` + `setup`. This one is available to be used together with
138
140
  # `unregister`, which is undocumented too.
139
141
  #
140
142
  # @sig () -> void
141
143
  def unload
142
144
  mutex.synchronize do
145
+ raise SetupRequired unless @setup
146
+
143
147
  # We are going to keep track of the files that were required by our
144
148
  # autoloads to later remove them from $LOADED_FEATURES, thus making them
145
149
  # loadable by Kernel#require again.
@@ -199,7 +203,7 @@ module Zeitwerk
199
203
  shadowed_files.clear
200
204
 
201
205
  Registry.on_unload(self)
202
- ExplicitNamespace.unregister_loader(self)
206
+ ExplicitNamespace.__unregister_loader(self)
203
207
 
204
208
  @setup = false
205
209
  @eager_loaded = false
@@ -216,6 +220,7 @@ module Zeitwerk
216
220
  # @sig () -> void
217
221
  def reload
218
222
  raise ReloadingDisabledError unless reloading_enabled?
223
+ raise SetupRequired unless @setup
219
224
 
220
225
  unload
221
226
  recompute_ignored_paths
@@ -223,6 +228,54 @@ module Zeitwerk
223
228
  setup
224
229
  end
225
230
 
231
+ # @sig (String | Pathname) -> String?
232
+ def cpath_expected_at(path)
233
+ abspath = File.expand_path(path)
234
+
235
+ raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
236
+
237
+ return unless dir?(abspath) || ruby?(abspath)
238
+ return if ignored_path?(abspath)
239
+
240
+ paths = []
241
+
242
+ if ruby?(abspath)
243
+ basename = File.basename(abspath, ".rb")
244
+ return if hidden?(basename)
245
+
246
+ paths << [basename, abspath]
247
+ walk_up_from = File.dirname(abspath)
248
+ else
249
+ walk_up_from = abspath
250
+ end
251
+
252
+ root_namespace = nil
253
+
254
+ walk_up(walk_up_from) do |dir|
255
+ break if root_namespace = roots[dir]
256
+ return if ignored_path?(dir)
257
+
258
+ basename = File.basename(dir)
259
+ return if hidden?(basename)
260
+
261
+ paths << [basename, abspath] unless collapse?(dir)
262
+ end
263
+
264
+ return unless root_namespace
265
+
266
+ if paths.empty?
267
+ real_mod_name(root_namespace)
268
+ else
269
+ cnames = paths.reverse_each.map { |b, a| cname_for(b, a) }
270
+
271
+ if root_namespace == Object
272
+ cnames.join("::")
273
+ else
274
+ "#{real_mod_name(root_namespace)}::#{cnames.join("::")}"
275
+ end
276
+ end
277
+ end
278
+
226
279
  # Says if the given constant path would be unloaded on reload. This
227
280
  # predicate returns `false` if reloading is disabled.
228
281
  #
@@ -245,27 +298,29 @@ module Zeitwerk
245
298
  # @sig () -> void
246
299
  def unregister
247
300
  Registry.unregister_loader(self)
248
- ExplicitNamespace.unregister_loader(self)
301
+ ExplicitNamespace.__unregister_loader(self)
249
302
  end
250
303
 
251
304
  # The return value of this predicate is only meaningful if the loader has
252
305
  # scanned the file. This is the case in the spots where we use it.
253
306
  #
254
- # @private
255
307
  # @sig (String) -> Boolean
256
- def shadowed_file?(file)
308
+ internal def shadowed_file?(file)
257
309
  shadowed_files.member?(file)
258
310
  end
259
311
 
260
312
  # --- Class methods ---------------------------------------------------------------------------
261
313
 
262
314
  class << self
315
+ include RealModName
316
+
263
317
  # @sig #call | #debug | nil
264
318
  attr_accessor :default_logger
265
319
 
266
320
  # This is a shortcut for
267
321
  #
268
322
  # require "zeitwerk"
323
+ #
269
324
  # loader = Zeitwerk::Loader.new
270
325
  # loader.tag = File.basename(__FILE__, ".rb")
271
326
  # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
@@ -280,22 +335,63 @@ module Zeitwerk
280
335
  # @sig (bool) -> Zeitwerk::GemLoader
281
336
  def for_gem(warn_on_extra_files: true)
282
337
  called_from = caller_locations(1, 1).first.path
283
- Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
338
+ Registry.loader_for_gem(called_from, namespace: Object, warn_on_extra_files: warn_on_extra_files)
339
+ end
340
+
341
+ # This is a shortcut for
342
+ #
343
+ # require "zeitwerk"
344
+ #
345
+ # loader = Zeitwerk::Loader.new
346
+ # loader.tag = namespace.name + "-" + File.basename(__FILE__, ".rb")
347
+ # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
348
+ # loader.push_dir(__dir__, namespace: namespace)
349
+ #
350
+ # except that this method returns the same object in subsequent calls from
351
+ # the same file, in the unlikely case the gem wants to be able to reload.
352
+ #
353
+ # This method returns a subclass of Zeitwerk::Loader, but the exact type
354
+ # is private, client code can only rely on the interface.
355
+ #
356
+ # @sig (bool) -> Zeitwerk::GemLoader
357
+ def for_gem_extension(namespace)
358
+ unless namespace.is_a?(Module) # Note that Class < Module.
359
+ raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
360
+ end
361
+
362
+ unless real_mod_name(namespace)
363
+ raise Zeitwerk::Error, "extending anonymous namespaces is unsupported"
364
+ end
365
+
366
+ called_from = caller_locations(1, 1).first.path
367
+ Registry.loader_for_gem(called_from, namespace: namespace, warn_on_extra_files: false)
284
368
  end
285
369
 
286
- # Broadcasts `eager_load` to all loaders.
370
+ # Broadcasts `eager_load` to all loaders. Those that have not been setup
371
+ # are skipped.
287
372
  #
288
373
  # @sig () -> void
289
374
  def eager_load_all
290
- Registry.loaders.each(&:eager_load)
375
+ Registry.loaders.each do |loader|
376
+ begin
377
+ loader.eager_load
378
+ rescue SetupRequired
379
+ # This is fine, we eager load what can be eager loaded.
380
+ end
381
+ end
291
382
  end
292
383
 
293
- # Broadcasts `eager_load_namespace` to all loaders.
384
+ # Broadcasts `eager_load_namespace` to all loaders. Those that have not
385
+ # been setup are skipped.
294
386
  #
295
387
  # @sig (Module) -> void
296
388
  def eager_load_namespace(mod)
297
389
  Registry.loaders.each do |loader|
298
- loader.eager_load_namespace(mod)
390
+ begin
391
+ loader.eager_load_namespace(mod)
392
+ rescue SetupRequired
393
+ # This is fine, we eager load what can be eager loaded.
394
+ end
299
395
  end
300
396
  end
301
397
 
@@ -308,51 +404,39 @@ module Zeitwerk
308
404
  end
309
405
  end
310
406
 
311
- private # -------------------------------------------------------------------------------------
312
-
313
407
  # @sig (String, Module) -> void
314
- def set_autoloads_in_dir(dir, parent)
408
+ private def set_autoloads_in_dir(dir, parent)
315
409
  ls(dir) do |basename, abspath|
316
- begin
317
- if ruby?(basename)
318
- basename.delete_suffix!(".rb")
319
- cname = inflector.camelize(basename, abspath).to_sym
320
- autoload_file(parent, cname, abspath)
410
+ if ruby?(basename)
411
+ basename.delete_suffix!(".rb")
412
+ autoload_file(parent, cname_for(basename, abspath), abspath)
413
+ else
414
+ if collapse?(abspath)
415
+ set_autoloads_in_dir(abspath, parent)
321
416
  else
322
- if collapse?(abspath)
323
- set_autoloads_in_dir(abspath, parent)
324
- else
325
- cname = inflector.camelize(basename, abspath).to_sym
326
- autoload_subdir(parent, cname, abspath)
327
- end
417
+ autoload_subdir(parent, cname_for(basename, abspath), abspath)
328
418
  end
329
- rescue ::NameError => error
330
- path_type = ruby?(abspath) ? "file" : "directory"
331
-
332
- raise NameError.new(<<~MESSAGE, error.name)
333
- #{error.message} inferred by #{inflector.class} from #{path_type}
334
-
335
- #{abspath}
336
-
337
- Possible ways to address this:
338
-
339
- * Tell Zeitwerk to ignore this particular #{path_type}.
340
- * Tell Zeitwerk to ignore one of its parent directories.
341
- * Rename the #{path_type} to comply with the naming conventions.
342
- * Modify the inflector to handle this case.
343
- MESSAGE
344
419
  end
345
420
  end
346
421
  end
347
422
 
348
423
  # @sig (Module, Symbol, String) -> void
349
- def autoload_subdir(parent, cname, subdir)
424
+ private def autoload_subdir(parent, cname, subdir)
350
425
  if autoload_path = autoload_path_set_by_me_for?(parent, cname)
351
426
  cpath = cpath(parent, cname)
352
- register_explicit_namespace(cpath) if ruby?(autoload_path)
353
- # We do not need to issue another autoload, the existing one is enough
354
- # no matter if it is for a file or a directory. Just remember the
355
- # subdirectory has to be visited if the namespace is used.
427
+ if ruby?(autoload_path)
428
+ # Scanning visited a Ruby file first, and now a directory for the same
429
+ # constant has been found. This means we are dealing with an explicit
430
+ # namespace whose definition was seen first.
431
+ #
432
+ # Registering is idempotent, and we have to keep the autoload pointing
433
+ # to the file. This may run again if more directories are found later
434
+ # on, no big deal.
435
+ register_explicit_namespace(cpath)
436
+ end
437
+ # If the existing autoload points to a file, it has to be preserved, if
438
+ # not, it is fine as it is. In either case, we do not need to override.
439
+ # Just remember the subdirectory conforms this namespace.
356
440
  namespace_dirs[cpath] << subdir
357
441
  elsif !cdef?(parent, cname)
358
442
  # First time we find this namespace, set an autoload for it.
@@ -367,7 +451,7 @@ module Zeitwerk
367
451
  end
368
452
 
369
453
  # @sig (Module, Symbol, String) -> void
370
- def autoload_file(parent, cname, file)
454
+ private def autoload_file(parent, cname, file)
371
455
  if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
372
456
  # First autoload for a Ruby file wins, just ignore subsequent ones.
373
457
  if ruby?(autoload_path)
@@ -393,7 +477,7 @@ module Zeitwerk
393
477
  # the file where we've found the namespace is explicitly defined.
394
478
  #
395
479
  # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
396
- def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
480
+ private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
397
481
  autoloads.delete(dir)
398
482
  Registry.unregister_autoload(dir)
399
483
 
@@ -404,7 +488,7 @@ module Zeitwerk
404
488
  end
405
489
 
406
490
  # @sig (Module, Symbol, String) -> void
407
- def set_autoload(parent, cname, abspath)
491
+ private def set_autoload(parent, cname, abspath)
408
492
  parent.autoload(cname, abspath)
409
493
 
410
494
  if logger
@@ -425,7 +509,7 @@ module Zeitwerk
425
509
  end
426
510
 
427
511
  # @sig (Module, Symbol) -> String?
428
- def autoload_path_set_by_me_for?(parent, cname)
512
+ private def autoload_path_set_by_me_for?(parent, cname)
429
513
  if autoload_path = strict_autoload_path(parent, cname)
430
514
  autoload_path if autoloads.key?(autoload_path)
431
515
  else
@@ -434,28 +518,28 @@ module Zeitwerk
434
518
  end
435
519
 
436
520
  # @sig (String) -> void
437
- def register_explicit_namespace(cpath)
438
- ExplicitNamespace.register(cpath, self)
521
+ private def register_explicit_namespace(cpath)
522
+ ExplicitNamespace.__register(cpath, self)
439
523
  end
440
524
 
441
525
  # @sig (String) -> void
442
- def raise_if_conflicting_directory(dir)
526
+ private def raise_if_conflicting_directory(dir)
443
527
  MUTEX.synchronize do
528
+ dir_slash = dir + "/"
529
+
444
530
  Registry.loaders.each do |loader|
445
531
  next if loader == self
446
- next if loader.ignores?(dir)
532
+ next if loader.__ignores?(dir)
447
533
 
448
- dir = dir + "/"
449
- loader.root_dirs.each do |root_dir, _namespace|
534
+ loader.__roots.each_key do |root_dir|
450
535
  next if ignores?(root_dir)
451
536
 
452
- root_dir = root_dir + "/"
453
- if dir.start_with?(root_dir) || root_dir.start_with?(dir)
537
+ root_dir_slash = root_dir + "/"
538
+ if dir_slash.start_with?(root_dir_slash) || root_dir_slash.start_with?(dir_slash)
454
539
  require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
455
540
  raise Error,
456
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
541
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
457
542
  " which is already managed by\n\n#{loader.pretty_inspect}\n"
458
- EOS
459
543
  end
460
544
  end
461
545
  end
@@ -463,23 +547,23 @@ module Zeitwerk
463
547
  end
464
548
 
465
549
  # @sig (String, Object, String) -> void
466
- def run_on_unload_callbacks(cpath, value, abspath)
550
+ private def run_on_unload_callbacks(cpath, value, abspath)
467
551
  # Order matters. If present, run the most specific one.
468
552
  on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
469
553
  on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
470
554
  end
471
555
 
472
556
  # @sig (Module, Symbol) -> void
473
- def unload_autoload(parent, cname)
474
- parent.__send__(:remove_const, cname)
557
+ private def unload_autoload(parent, cname)
558
+ crem(parent, cname)
475
559
  log("autoload for #{cpath(parent, cname)} removed") if logger
476
560
  end
477
561
 
478
562
  # @sig (Module, Symbol) -> void
479
- def unload_cref(parent, cname)
563
+ private def unload_cref(parent, cname)
480
564
  # Let's optimistically remove_const. The way we use it, this is going to
481
565
  # succeed always if all is good.
482
- parent.__send__(:remove_const, cname)
566
+ crem(parent, cname)
483
567
  rescue ::NameError
484
568
  # There are a few edge scenarios in which this may happen. If the constant
485
569
  # is gone, that is OK, anyway.
@@ -86,8 +86,8 @@ module Zeitwerk
86
86
  #
87
87
  # @private
88
88
  # @sig (String) -> Zeitwerk::Loader
89
- def loader_for_gem(root_file, warn_on_extra_files:)
90
- gem_loaders_by_root_file[root_file] ||= GemLoader._new(root_file, warn_on_extra_files: warn_on_extra_files)
89
+ def loader_for_gem(root_file, namespace:, warn_on_extra_files:)
90
+ gem_loaders_by_root_file[root_file] ||= GemLoader.__new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
91
91
  end
92
92
 
93
93
  # @private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.6.2"
4
+ VERSION = "2.6.10"
5
5
  end
data/lib/zeitwerk.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Zeitwerk
4
4
  require_relative "zeitwerk/real_mod_name"
5
+ require_relative "zeitwerk/internal"
5
6
  require_relative "zeitwerk/loader"
6
7
  require_relative "zeitwerk/gem_loader"
7
8
  require_relative "zeitwerk/registry"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.2
4
+ version: 2.6.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-31 00:00:00.000000000 Z
11
+ date: 2023-07-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -28,6 +28,7 @@ files:
28
28
  - lib/zeitwerk/gem_inflector.rb
29
29
  - lib/zeitwerk/gem_loader.rb
30
30
  - lib/zeitwerk/inflector.rb
31
+ - lib/zeitwerk/internal.rb
31
32
  - lib/zeitwerk/kernel.rb
32
33
  - lib/zeitwerk/loader.rb
33
34
  - lib/zeitwerk/loader/callbacks.rb
@@ -60,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
60
61
  - !ruby/object:Gem::Version
61
62
  version: '0'
62
63
  requirements: []
63
- rubygems_version: 3.3.3
64
+ rubygems_version: 3.4.16
64
65
  signing_key:
65
66
  specification_version: 4
66
67
  summary: Efficient and thread-safe constant autoloader