zeitwerk 2.6.1 → 2.6.8

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.
@@ -7,11 +7,15 @@ module Zeitwerk
7
7
  require_relative "loader/helpers"
8
8
  require_relative "loader/callbacks"
9
9
  require_relative "loader/config"
10
+ require_relative "loader/eager_load"
11
+
12
+ extend Internal
10
13
 
11
14
  include RealModName
12
15
  include Callbacks
13
16
  include Helpers
14
17
  include Config
18
+ include EagerLoad
15
19
 
16
20
  MUTEX = Mutex.new
17
21
  private_constant :MUTEX
@@ -24,9 +28,9 @@ module Zeitwerk
24
28
  # "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
25
29
  # ...
26
30
  #
27
- # @private
28
31
  # @sig Hash[String, [Module, Symbol]]
29
32
  attr_reader :autoloads
33
+ internal :autoloads
30
34
 
31
35
  # We keep track of autoloaded directories to remove them from the registry
32
36
  # at the end of eager loading.
@@ -34,9 +38,9 @@ module Zeitwerk
34
38
  # Files are removed as they are autoloaded, but directories need to wait due
35
39
  # to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
36
40
  #
37
- # @private
38
41
  # @sig Array[String]
39
42
  attr_reader :autoloaded_dirs
43
+ internal :autoloaded_dirs
40
44
 
41
45
  # Stores metadata needed for unloading. Its entries look like this:
42
46
  #
@@ -50,11 +54,11 @@ module Zeitwerk
50
54
  # If reloading is enabled, this hash is filled as constants are autoloaded
51
55
  # or eager loaded. Otherwise, the collection remains empty.
52
56
  #
53
- # @private
54
57
  # @sig Hash[String, [String, [Module, Symbol]]]
55
58
  attr_reader :to_unload
59
+ internal :to_unload
56
60
 
57
- # Maps constant paths of namespaces to arrays of corresponding directories.
61
+ # Maps namespace constant paths to their respective directories.
58
62
  #
59
63
  # For example, given this mapping:
60
64
  #
@@ -64,21 +68,32 @@ module Zeitwerk
64
68
  # ...
65
69
  # ]
66
70
  #
67
- # when `Admin` gets defined we know that it plays the role of a namespace and
68
- # that its children are spread over those directories. We'll visit them to set
69
- # up the corresponding autoloads.
71
+ # when `Admin` gets defined we know that it plays the role of a namespace
72
+ # and that its children are spread over those directories. We'll visit them
73
+ # to set up the corresponding autoloads.
70
74
  #
71
- # @private
72
75
  # @sig Hash[String, Array[String]]
73
- attr_reader :lazy_subdirs
76
+ attr_reader :namespace_dirs
77
+ internal :namespace_dirs
78
+
79
+ # A shadowed file is a file managed by this loader that is ignored when
80
+ # setting autoloads because its matching constant is already taken.
81
+ #
82
+ # This private set is populated as we descend. For example, if the loader
83
+ # has only scanned the top-level, `shadowed_files` does not have shadowed
84
+ # files that may exist deep in the project tree yet.
85
+ #
86
+ # @sig Set[String]
87
+ attr_reader :shadowed_files
88
+ internal :shadowed_files
74
89
 
75
- # @private
76
90
  # @sig Mutex
77
91
  attr_reader :mutex
92
+ private :mutex
78
93
 
79
- # @private
80
94
  # @sig Mutex
81
95
  attr_reader :mutex2
96
+ private :mutex2
82
97
 
83
98
  def initialize
84
99
  super
@@ -86,7 +101,8 @@ module Zeitwerk
86
101
  @autoloads = {}
87
102
  @autoloaded_dirs = []
88
103
  @to_unload = {}
89
- @lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
104
+ @namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
105
+ @shadowed_files = Set.new
90
106
  @mutex = Mutex.new
91
107
  @mutex2 = Mutex.new
92
108
  @setup = false
@@ -95,15 +111,15 @@ module Zeitwerk
95
111
  Registry.register_loader(self)
96
112
  end
97
113
 
98
- # Sets autoloads in the root namespace.
114
+ # Sets autoloads in the root namespaces.
99
115
  #
100
116
  # @sig () -> void
101
117
  def setup
102
118
  mutex.synchronize do
103
119
  break if @setup
104
120
 
105
- actual_root_dirs.each do |root_dir, namespace|
106
- 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)
107
123
  end
108
124
 
109
125
  on_setup_callbacks.each(&:call)
@@ -120,12 +136,14 @@ module Zeitwerk
120
136
  # unload them.
121
137
  #
122
138
  # This method is public but undocumented. Main interface is `reload`, which
123
- # 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
124
140
  # `unregister`, which is undocumented too.
125
141
  #
126
142
  # @sig () -> void
127
143
  def unload
128
144
  mutex.synchronize do
145
+ raise SetupRequired unless @setup
146
+
129
147
  # We are going to keep track of the files that were required by our
130
148
  # autoloads to later remove them from $LOADED_FEATURES, thus making them
131
149
  # loadable by Kernel#require again.
@@ -181,10 +199,11 @@ module Zeitwerk
181
199
  autoloads.clear
182
200
  autoloaded_dirs.clear
183
201
  to_unload.clear
184
- lazy_subdirs.clear
202
+ namespace_dirs.clear
203
+ shadowed_files.clear
185
204
 
186
205
  Registry.on_unload(self)
187
- ExplicitNamespace.unregister_loader(self)
206
+ ExplicitNamespace.__unregister_loader(self)
188
207
 
189
208
  @setup = false
190
209
  @eager_loaded = false
@@ -201,6 +220,7 @@ module Zeitwerk
201
220
  # @sig () -> void
202
221
  def reload
203
222
  raise ReloadingDisabledError unless reloading_enabled?
223
+ raise SetupRequired unless @setup
204
224
 
205
225
  unload
206
226
  recompute_ignored_paths
@@ -208,58 +228,6 @@ module Zeitwerk
208
228
  setup
209
229
  end
210
230
 
211
- # Eager loads all files in the root directories, recursively. Files do not
212
- # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
213
- # are not eager loaded. You can opt-out specifically in specific files and
214
- # directories with `do_not_eager_load`, and that can be overridden passing
215
- # `force: true`.
216
- #
217
- # @sig (true | false) -> void
218
- def eager_load(force: false)
219
- mutex.synchronize do
220
- break if @eager_loaded
221
-
222
- log("eager load start") if logger
223
-
224
- honour_exclusions = !force
225
-
226
- queue = []
227
- actual_root_dirs.each do |root_dir, namespace|
228
- queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
229
- end
230
-
231
- while to_eager_load = queue.shift
232
- namespace, dir = to_eager_load
233
-
234
- ls(dir) do |basename, abspath|
235
- next if honour_exclusions && excluded_from_eager_load?(abspath)
236
-
237
- if ruby?(abspath)
238
- if cref = autoloads[abspath]
239
- cget(*cref)
240
- end
241
- elsif !root_dirs.key?(abspath)
242
- if collapse?(abspath)
243
- queue << [namespace, abspath]
244
- else
245
- cname = inflector.camelize(basename, abspath)
246
- queue << [cget(namespace, cname), abspath]
247
- end
248
- end
249
- end
250
- end
251
-
252
- autoloaded_dirs.each do |autoloaded_dir|
253
- Registry.unregister_autoload(autoloaded_dir)
254
- end
255
- autoloaded_dirs.clear
256
-
257
- @eager_loaded = true
258
-
259
- log("eager load end") if logger
260
- end
261
- end
262
-
263
231
  # Says if the given constant path would be unloaded on reload. This
264
232
  # predicate returns `false` if reloading is disabled.
265
233
  #
@@ -282,18 +250,29 @@ module Zeitwerk
282
250
  # @sig () -> void
283
251
  def unregister
284
252
  Registry.unregister_loader(self)
285
- ExplicitNamespace.unregister_loader(self)
253
+ ExplicitNamespace.__unregister_loader(self)
254
+ end
255
+
256
+ # The return value of this predicate is only meaningful if the loader has
257
+ # scanned the file. This is the case in the spots where we use it.
258
+ #
259
+ # @sig (String) -> Boolean
260
+ internal def shadowed_file?(file)
261
+ shadowed_files.member?(file)
286
262
  end
287
263
 
288
264
  # --- Class methods ---------------------------------------------------------------------------
289
265
 
290
266
  class << self
267
+ include RealModName
268
+
291
269
  # @sig #call | #debug | nil
292
270
  attr_accessor :default_logger
293
271
 
294
272
  # This is a shortcut for
295
273
  #
296
274
  # require "zeitwerk"
275
+ #
297
276
  # loader = Zeitwerk::Loader.new
298
277
  # loader.tag = File.basename(__FILE__, ".rb")
299
278
  # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
@@ -308,14 +287,64 @@ module Zeitwerk
308
287
  # @sig (bool) -> Zeitwerk::GemLoader
309
288
  def for_gem(warn_on_extra_files: true)
310
289
  called_from = caller_locations(1, 1).first.path
311
- Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
290
+ Registry.loader_for_gem(called_from, namespace: Object, warn_on_extra_files: warn_on_extra_files)
291
+ end
292
+
293
+ # This is a shortcut for
294
+ #
295
+ # require "zeitwerk"
296
+ #
297
+ # loader = Zeitwerk::Loader.new
298
+ # loader.tag = namespace.name + "-" + File.basename(__FILE__, ".rb")
299
+ # loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
300
+ # loader.push_dir(__dir__, namespace: namespace)
301
+ #
302
+ # except that this method returns the same object in subsequent calls from
303
+ # the same file, in the unlikely case the gem wants to be able to reload.
304
+ #
305
+ # This method returns a subclass of Zeitwerk::Loader, but the exact type
306
+ # is private, client code can only rely on the interface.
307
+ #
308
+ # @sig (bool) -> Zeitwerk::GemLoader
309
+ def for_gem_extension(namespace)
310
+ unless namespace.is_a?(Module) # Note that Class < Module.
311
+ raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
312
+ end
313
+
314
+ unless real_mod_name(namespace)
315
+ raise Zeitwerk::Error, "extending anonymous namespaces is unsupported"
316
+ end
317
+
318
+ called_from = caller_locations(1, 1).first.path
319
+ Registry.loader_for_gem(called_from, namespace: namespace, warn_on_extra_files: false)
312
320
  end
313
321
 
314
- # Broadcasts `eager_load` to all loaders.
322
+ # Broadcasts `eager_load` to all loaders. Those that have not been setup
323
+ # are skipped.
315
324
  #
316
325
  # @sig () -> void
317
326
  def eager_load_all
318
- Registry.loaders.each(&:eager_load)
327
+ Registry.loaders.each do |loader|
328
+ begin
329
+ loader.eager_load
330
+ rescue SetupRequired
331
+ # This is fine, we eager load what can be eager loaded.
332
+ end
333
+ end
334
+ end
335
+
336
+ # Broadcasts `eager_load_namespace` to all loaders. Those that have not
337
+ # been setup are skipped.
338
+ #
339
+ # @sig (Module) -> void
340
+ def eager_load_namespace(mod)
341
+ Registry.loaders.each do |loader|
342
+ begin
343
+ loader.eager_load_namespace(mod)
344
+ rescue SetupRequired
345
+ # This is fine, we eager load what can be eager loaded.
346
+ end
347
+ end
319
348
  end
320
349
 
321
350
  # Returns an array with the absolute paths of the root directories of all
@@ -327,10 +356,8 @@ module Zeitwerk
327
356
  end
328
357
  end
329
358
 
330
- private # -------------------------------------------------------------------------------------
331
-
332
359
  # @sig (String, Module) -> void
333
- def set_autoloads_in_dir(dir, parent)
360
+ private def set_autoloads_in_dir(dir, parent)
334
361
  ls(dir) do |basename, abspath|
335
362
  begin
336
363
  if ruby?(basename)
@@ -338,19 +365,11 @@ module Zeitwerk
338
365
  cname = inflector.camelize(basename, abspath).to_sym
339
366
  autoload_file(parent, cname, abspath)
340
367
  else
341
- # In a Rails application, `app/models/concerns` is a subdirectory of
342
- # `app/models`, but both of them are root directories.
343
- #
344
- # To resolve the ambiguity file name -> constant path this introduces,
345
- # the `app/models/concerns` directory is totally ignored as a namespace,
346
- # it counts only as root. The guard checks that.
347
- unless root_dir?(abspath)
368
+ if collapse?(abspath)
369
+ set_autoloads_in_dir(abspath, parent)
370
+ else
348
371
  cname = inflector.camelize(basename, abspath).to_sym
349
- if collapse?(abspath)
350
- set_autoloads_in_dir(abspath, parent)
351
- else
352
- autoload_subdir(parent, cname, abspath)
353
- end
372
+ autoload_subdir(parent, cname, abspath)
354
373
  end
355
374
  end
356
375
  rescue ::NameError => error
@@ -373,17 +392,26 @@ module Zeitwerk
373
392
  end
374
393
 
375
394
  # @sig (Module, Symbol, String) -> void
376
- def autoload_subdir(parent, cname, subdir)
395
+ private def autoload_subdir(parent, cname, subdir)
377
396
  if autoload_path = autoload_path_set_by_me_for?(parent, cname)
378
397
  cpath = cpath(parent, cname)
379
- register_explicit_namespace(cpath) if ruby?(autoload_path)
380
- # We do not need to issue another autoload, the existing one is enough
381
- # no matter if it is for a file or a directory. Just remember the
382
- # subdirectory has to be visited if the namespace is used.
383
- lazy_subdirs[cpath] << subdir
398
+ if ruby?(autoload_path)
399
+ # Scanning visited a Ruby file first, and now a directory for the same
400
+ # constant has been found. This means we are dealing with an explicit
401
+ # namespace whose definition was seen first.
402
+ #
403
+ # Registering is idempotent, and we have to keep the autoload pointing
404
+ # to the file. This may run again if more directories are found later
405
+ # on, no big deal.
406
+ register_explicit_namespace(cpath)
407
+ end
408
+ # If the existing autoload points to a file, it has to be preserved, if
409
+ # not, it is fine as it is. In either case, we do not need to override.
410
+ # Just remember the subdirectory conforms this namespace.
411
+ namespace_dirs[cpath] << subdir
384
412
  elsif !cdef?(parent, cname)
385
413
  # First time we find this namespace, set an autoload for it.
386
- lazy_subdirs[cpath(parent, cname)] << subdir
414
+ namespace_dirs[cpath(parent, cname)] << subdir
387
415
  set_autoload(parent, cname, subdir)
388
416
  else
389
417
  # For whatever reason the constant that corresponds to this namespace has
@@ -394,10 +422,11 @@ module Zeitwerk
394
422
  end
395
423
 
396
424
  # @sig (Module, Symbol, String) -> void
397
- def autoload_file(parent, cname, file)
425
+ private def autoload_file(parent, cname, file)
398
426
  if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
399
427
  # First autoload for a Ruby file wins, just ignore subsequent ones.
400
428
  if ruby?(autoload_path)
429
+ shadowed_files << file
401
430
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
402
431
  else
403
432
  promote_namespace_from_implicit_to_explicit(
@@ -408,6 +437,7 @@ module Zeitwerk
408
437
  )
409
438
  end
410
439
  elsif cdef?(parent, cname)
440
+ shadowed_files << file
411
441
  log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
412
442
  else
413
443
  set_autoload(parent, cname, file)
@@ -418,7 +448,7 @@ module Zeitwerk
418
448
  # the file where we've found the namespace is explicitly defined.
419
449
  #
420
450
  # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
421
- def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
451
+ private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
422
452
  autoloads.delete(dir)
423
453
  Registry.unregister_autoload(dir)
424
454
 
@@ -429,7 +459,7 @@ module Zeitwerk
429
459
  end
430
460
 
431
461
  # @sig (Module, Symbol, String) -> void
432
- def set_autoload(parent, cname, abspath)
462
+ private def set_autoload(parent, cname, abspath)
433
463
  parent.autoload(cname, abspath)
434
464
 
435
465
  if logger
@@ -450,7 +480,7 @@ module Zeitwerk
450
480
  end
451
481
 
452
482
  # @sig (Module, Symbol) -> String?
453
- def autoload_path_set_by_me_for?(parent, cname)
483
+ private def autoload_path_set_by_me_for?(parent, cname)
454
484
  if autoload_path = strict_autoload_path(parent, cname)
455
485
  autoload_path if autoloads.key?(autoload_path)
456
486
  else
@@ -459,26 +489,27 @@ module Zeitwerk
459
489
  end
460
490
 
461
491
  # @sig (String) -> void
462
- def register_explicit_namespace(cpath)
463
- ExplicitNamespace.register(cpath, self)
492
+ private def register_explicit_namespace(cpath)
493
+ ExplicitNamespace.__register(cpath, self)
464
494
  end
465
495
 
466
496
  # @sig (String) -> void
467
- def raise_if_conflicting_directory(dir)
497
+ private def raise_if_conflicting_directory(dir)
468
498
  MUTEX.synchronize do
499
+ dir_slash = dir + "/"
500
+
469
501
  Registry.loaders.each do |loader|
470
502
  next if loader == self
471
- next if loader.ignores?(dir)
503
+ next if loader.__ignores?(dir)
472
504
 
473
- dir = dir + "/"
474
- loader.root_dirs.each do |root_dir, _namespace|
505
+ loader.__roots.each_key do |root_dir|
475
506
  next if ignores?(root_dir)
476
507
 
477
- root_dir = root_dir + "/"
478
- if dir.start_with?(root_dir) || root_dir.start_with?(dir)
508
+ root_dir_slash = root_dir + "/"
509
+ if dir_slash.start_with?(root_dir_slash) || root_dir_slash.start_with?(dir_slash)
479
510
  require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
480
511
  raise Error,
481
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
512
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
482
513
  " which is already managed by\n\n#{loader.pretty_inspect}\n"
483
514
  EOS
484
515
  end
@@ -488,23 +519,23 @@ module Zeitwerk
488
519
  end
489
520
 
490
521
  # @sig (String, Object, String) -> void
491
- def run_on_unload_callbacks(cpath, value, abspath)
522
+ private def run_on_unload_callbacks(cpath, value, abspath)
492
523
  # Order matters. If present, run the most specific one.
493
524
  on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
494
525
  on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
495
526
  end
496
527
 
497
528
  # @sig (Module, Symbol) -> void
498
- def unload_autoload(parent, cname)
499
- parent.__send__(:remove_const, cname)
529
+ private def unload_autoload(parent, cname)
530
+ crem(parent, cname)
500
531
  log("autoload for #{cpath(parent, cname)} removed") if logger
501
532
  end
502
533
 
503
534
  # @sig (Module, Symbol) -> void
504
- def unload_cref(parent, cname)
535
+ private def unload_cref(parent, cname)
505
536
  # Let's optimistically remove_const. The way we use it, this is going to
506
537
  # succeed always if all is good.
507
- parent.__send__(:remove_const, cname)
538
+ crem(parent, cname)
508
539
  rescue ::NameError
509
540
  # There are a few edge scenarios in which this may happen. If the constant
510
541
  # 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.1"
4
+ VERSION = "2.6.8"
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.1
4
+ version: 2.6.8
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-09-30 00:00:00.000000000 Z
11
+ date: 2023-04-28 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -28,10 +28,12 @@ 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
34
35
  - lib/zeitwerk/loader/config.rb
36
+ - lib/zeitwerk/loader/eager_load.rb
35
37
  - lib/zeitwerk/loader/helpers.rb
36
38
  - lib/zeitwerk/real_mod_name.rb
37
39
  - lib/zeitwerk/registry.rb
@@ -59,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
61
  - !ruby/object:Gem::Version
60
62
  version: '0'
61
63
  requirements: []
62
- rubygems_version: 3.3.3
64
+ rubygems_version: 3.4.11
63
65
  signing_key:
64
66
  specification_version: 4
65
67
  summary: Efficient and thread-safe constant autoloader