zeitwerk 2.6.1 → 2.6.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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