zeitwerk 2.6.1 → 2.6.7

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,7 +250,15 @@ 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 ---------------------------------------------------------------------------
@@ -311,11 +287,32 @@ module Zeitwerk
311
287
  Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
312
288
  end
313
289
 
314
- # Broadcasts `eager_load` to all loaders.
290
+ # Broadcasts `eager_load` to all loaders. Those that have not been setup
291
+ # are skipped.
315
292
  #
316
293
  # @sig () -> void
317
294
  def eager_load_all
318
- Registry.loaders.each(&:eager_load)
295
+ Registry.loaders.each do |loader|
296
+ begin
297
+ loader.eager_load
298
+ rescue SetupRequired
299
+ # This is fine, we eager load what can be eager loaded.
300
+ end
301
+ end
302
+ end
303
+
304
+ # Broadcasts `eager_load_namespace` to all loaders. Those that have not
305
+ # been setup are skipped.
306
+ #
307
+ # @sig (Module) -> void
308
+ def eager_load_namespace(mod)
309
+ Registry.loaders.each do |loader|
310
+ begin
311
+ loader.eager_load_namespace(mod)
312
+ rescue SetupRequired
313
+ # This is fine, we eager load what can be eager loaded.
314
+ end
315
+ end
319
316
  end
320
317
 
321
318
  # Returns an array with the absolute paths of the root directories of all
@@ -327,10 +324,8 @@ module Zeitwerk
327
324
  end
328
325
  end
329
326
 
330
- private # -------------------------------------------------------------------------------------
331
-
332
327
  # @sig (String, Module) -> void
333
- def set_autoloads_in_dir(dir, parent)
328
+ private def set_autoloads_in_dir(dir, parent)
334
329
  ls(dir) do |basename, abspath|
335
330
  begin
336
331
  if ruby?(basename)
@@ -338,19 +333,11 @@ module Zeitwerk
338
333
  cname = inflector.camelize(basename, abspath).to_sym
339
334
  autoload_file(parent, cname, abspath)
340
335
  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)
336
+ if collapse?(abspath)
337
+ set_autoloads_in_dir(abspath, parent)
338
+ else
348
339
  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
340
+ autoload_subdir(parent, cname, abspath)
354
341
  end
355
342
  end
356
343
  rescue ::NameError => error
@@ -373,17 +360,26 @@ module Zeitwerk
373
360
  end
374
361
 
375
362
  # @sig (Module, Symbol, String) -> void
376
- def autoload_subdir(parent, cname, subdir)
363
+ private def autoload_subdir(parent, cname, subdir)
377
364
  if autoload_path = autoload_path_set_by_me_for?(parent, cname)
378
365
  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
366
+ if ruby?(autoload_path)
367
+ # Scanning visited a Ruby file first, and now a directory for the same
368
+ # constant has been found. This means we are dealing with an explicit
369
+ # namespace whose definition was seen first.
370
+ #
371
+ # Registering is idempotent, and we have to keep the autoload pointing
372
+ # to the file. This may run again if more directories are found later
373
+ # on, no big deal.
374
+ register_explicit_namespace(cpath)
375
+ end
376
+ # If the existing autoload points to a file, it has to be preserved, if
377
+ # not, it is fine as it is. In either case, we do not need to override.
378
+ # Just remember the subdirectory conforms this namespace.
379
+ namespace_dirs[cpath] << subdir
384
380
  elsif !cdef?(parent, cname)
385
381
  # First time we find this namespace, set an autoload for it.
386
- lazy_subdirs[cpath(parent, cname)] << subdir
382
+ namespace_dirs[cpath(parent, cname)] << subdir
387
383
  set_autoload(parent, cname, subdir)
388
384
  else
389
385
  # For whatever reason the constant that corresponds to this namespace has
@@ -394,10 +390,11 @@ module Zeitwerk
394
390
  end
395
391
 
396
392
  # @sig (Module, Symbol, String) -> void
397
- def autoload_file(parent, cname, file)
393
+ private def autoload_file(parent, cname, file)
398
394
  if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
399
395
  # First autoload for a Ruby file wins, just ignore subsequent ones.
400
396
  if ruby?(autoload_path)
397
+ shadowed_files << file
401
398
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
402
399
  else
403
400
  promote_namespace_from_implicit_to_explicit(
@@ -408,6 +405,7 @@ module Zeitwerk
408
405
  )
409
406
  end
410
407
  elsif cdef?(parent, cname)
408
+ shadowed_files << file
411
409
  log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
412
410
  else
413
411
  set_autoload(parent, cname, file)
@@ -418,7 +416,7 @@ module Zeitwerk
418
416
  # the file where we've found the namespace is explicitly defined.
419
417
  #
420
418
  # @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
421
- def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
419
+ private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
422
420
  autoloads.delete(dir)
423
421
  Registry.unregister_autoload(dir)
424
422
 
@@ -429,7 +427,7 @@ module Zeitwerk
429
427
  end
430
428
 
431
429
  # @sig (Module, Symbol, String) -> void
432
- def set_autoload(parent, cname, abspath)
430
+ private def set_autoload(parent, cname, abspath)
433
431
  parent.autoload(cname, abspath)
434
432
 
435
433
  if logger
@@ -450,7 +448,7 @@ module Zeitwerk
450
448
  end
451
449
 
452
450
  # @sig (Module, Symbol) -> String?
453
- def autoload_path_set_by_me_for?(parent, cname)
451
+ private def autoload_path_set_by_me_for?(parent, cname)
454
452
  if autoload_path = strict_autoload_path(parent, cname)
455
453
  autoload_path if autoloads.key?(autoload_path)
456
454
  else
@@ -459,26 +457,27 @@ module Zeitwerk
459
457
  end
460
458
 
461
459
  # @sig (String) -> void
462
- def register_explicit_namespace(cpath)
463
- ExplicitNamespace.register(cpath, self)
460
+ private def register_explicit_namespace(cpath)
461
+ ExplicitNamespace.__register(cpath, self)
464
462
  end
465
463
 
466
464
  # @sig (String) -> void
467
- def raise_if_conflicting_directory(dir)
465
+ private def raise_if_conflicting_directory(dir)
468
466
  MUTEX.synchronize do
467
+ dir_slash = dir + "/"
468
+
469
469
  Registry.loaders.each do |loader|
470
470
  next if loader == self
471
- next if loader.ignores?(dir)
471
+ next if loader.__ignores?(dir)
472
472
 
473
- dir = dir + "/"
474
- loader.root_dirs.each do |root_dir, _namespace|
473
+ loader.__roots.each_key do |root_dir|
475
474
  next if ignores?(root_dir)
476
475
 
477
- root_dir = root_dir + "/"
478
- if dir.start_with?(root_dir) || root_dir.start_with?(dir)
476
+ root_dir_slash = root_dir + "/"
477
+ if dir_slash.start_with?(root_dir_slash) || root_dir_slash.start_with?(dir_slash)
479
478
  require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
480
479
  raise Error,
481
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
480
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
482
481
  " which is already managed by\n\n#{loader.pretty_inspect}\n"
483
482
  EOS
484
483
  end
@@ -488,23 +487,23 @@ module Zeitwerk
488
487
  end
489
488
 
490
489
  # @sig (String, Object, String) -> void
491
- def run_on_unload_callbacks(cpath, value, abspath)
490
+ private def run_on_unload_callbacks(cpath, value, abspath)
492
491
  # Order matters. If present, run the most specific one.
493
492
  on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
494
493
  on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
495
494
  end
496
495
 
497
496
  # @sig (Module, Symbol) -> void
498
- def unload_autoload(parent, cname)
499
- parent.__send__(:remove_const, cname)
497
+ private def unload_autoload(parent, cname)
498
+ crem(parent, cname)
500
499
  log("autoload for #{cpath(parent, cname)} removed") if logger
501
500
  end
502
501
 
503
502
  # @sig (Module, Symbol) -> void
504
- def unload_cref(parent, cname)
503
+ private def unload_cref(parent, cname)
505
504
  # Let's optimistically remove_const. The way we use it, this is going to
506
505
  # succeed always if all is good.
507
- parent.__send__(:remove_const, cname)
506
+ crem(parent, cname)
508
507
  rescue ::NameError
509
508
  # There are a few edge scenarios in which this may happen. If the constant
510
509
  # is gone, that is OK, anyway.
@@ -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.7"
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.7
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-02-10 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.3
63
65
  signing_key:
64
66
  specification_version: 4
65
67
  summary: Efficient and thread-safe constant autoloader