zeitwerk 2.6.1 → 2.6.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf4acabb8b402560402158aced0f30ae6bae5120dc65d719f4fb15c1385a2454
4
- data.tar.gz: 0a297687455cf8c3924a64c5e36223c35b0f69712c807ee9bc70748f8c42ff37
3
+ metadata.gz: 220865ab50f0336d05c8b907a08a94a8bf92843ba688c1a9686163a190b0754e
4
+ data.tar.gz: a9d62662351c60a3b264a68726a84c41fc4b6f92eaae855fb4887e3eb4cf14a1
5
5
  SHA512:
6
- metadata.gz: 1d6666a098ae430f452edcd8b5e458541f20b20c4ee5903c4d18fe19cdce5afe81d6311a1bff5f9412b18ae57274e9b13132f8d59d9189b0ccd6cf7711337d6e
7
- data.tar.gz: a131561faa463e489f7e9c4898bc8e212b2f6814ba3ce634b7ef62e25467f38b88e4c63106e8d53ee933681e1f0dc196244d16f13c299d2ca0771ee4031655a1
6
+ metadata.gz: b802eaabb27e6268eafa8d35d4402819551203c5fd2a1692d9a9bae65075bf668f5aecfaf3fa6c763987796b60fd5bde4704aebda4a8bfa1c74a526c27264ab5
7
+ data.tar.gz: c4a53a60b49e5cb83aee148271b26fee8de15fd072327e52363438b0531d9953961305375ec727cc3a0ff2c021254eb081056350165922bd9f6f8b1e1b3e96f3
data/README.md CHANGED
@@ -27,8 +27,14 @@
27
27
  - [Autoloading](#autoloading)
28
28
  - [Eager loading](#eager-loading)
29
29
  - [Eager load exclusions](#eager-load-exclusions)
30
+ - [Eager load directories](#eager-load-directories)
31
+ - [Eager load namespaces](#eager-load-namespaces)
32
+ - [Eager load namespaces shared by several loaders](#eager-load-namespaces-shared-by-several-loaders)
30
33
  - [Global eager load](#global-eager-load)
34
+ - [Loading individual files](#loading-individual-files)
31
35
  - [Reloading](#reloading)
36
+ - [Configuration and usage](#configuration-and-usage)
37
+ - [Thread-safety](#thread-safety)
32
38
  - [Inflection](#inflection)
33
39
  - [Zeitwerk::Inflector](#zeitwerkinflector)
34
40
  - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
@@ -44,6 +50,7 @@
44
50
  - [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
45
51
  - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
46
52
  - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
53
+ - [Shadowed files](#shadowed-files)
47
54
  - [Edge cases](#edge-cases)
48
55
  - [Beware of circular dependencies](#beware-of-circular-dependencies)
49
56
  - [Reopening third-party namespaces](#reopening-third-party-namespaces)
@@ -206,7 +213,7 @@ serializers/user_serializer.rb -> UserSerializer
206
213
  <a id="markdown-custom-root-namespaces" name="custom-root-namespaces"></a>
207
214
  #### Custom root namespaces
208
215
 
209
- While `Object` is by far the most common root namespace, you can associate a different one to a particular root directory. The method `push_dir` accepts a class or module object in the optional `namespace` keyword argument.
216
+ While `Object` is by far the most common root namespace, you can associate a different one to a particular root directory. The method `push_dir` accepts a non-anonymous class or module object in the optional `namespace` keyword argument.
210
217
 
211
218
  For example, given:
212
219
 
@@ -442,6 +449,8 @@ In gems, the method needs to be invoked after the main namespace has been define
442
449
 
443
450
  Eager loading is synchronized and idempotent.
444
451
 
452
+ Attempting to eager load without previously calling `setup` raises `Zeitwerk::SetupRequired`.
453
+
445
454
  <a id="markdown-eager-load-exclusions" name="eager-load-exclusions"></a>
446
455
  #### Eager load exclusions
447
456
 
@@ -464,6 +473,81 @@ Which may be handy if the project eager loads in the test suite to [ensure proje
464
473
 
465
474
  The `force` flag does not affect ignored files and directories, those are still ignored.
466
475
 
476
+ <a id="markdown-eager-load-directories" name="eager-load-directories"></a>
477
+ #### Eager load directories
478
+
479
+ The method `Zeitwerk::Loader#eager_load_dir` eager loads a given directory, recursively:
480
+
481
+ ```ruby
482
+ loader.eager_load_dir("#{__dir__}/custom_web_app/routes")
483
+ ```
484
+
485
+ This is useful when the loader is not eager loading the entire project, but you still need some subtree to be loaded for things to function properly.
486
+
487
+ Both strings and `Pathname` objects are supported as arguments. If the argument is not a directory managed by the receiver, the method raises `Zeitwerk::Error`.
488
+
489
+ [Eager load exclusions](#eager-load-exclusions), [ignored files and directories](#ignoring-parts-of-the-project), and [shadowed files](https://github.com/fxn/zeitwerk#shadowed-files) are not eager loaded.
490
+
491
+ `Zeitwerk::Loader#eager_load_dir` is idempotent, but compatible with reloading. If you eager load a directory and then reload, eager loading that directory will load its (current) contents again.
492
+
493
+ The method checks if a regular eager load was already executed, in which case it returns fast.
494
+
495
+ Nested root directories which are descendants of the argument are skipped. Those subtrees are considered to be conceptually apart.
496
+
497
+ Attempting to eager load a directory without previously calling `setup` raises `Zeitwerk::SetupRequired`.
498
+
499
+ <a id="markdown-eager-load-namespaces" name="eager-load-namespaces"></a>
500
+ #### Eager load namespaces
501
+
502
+ The method `Zeitwerk::Loader#eager_load_namespace` eager loads a given namespace, recursively:
503
+
504
+ ```ruby
505
+ loader.eager_load_namespace(MyApp::Routes)
506
+ ```
507
+
508
+ This is useful when the loader is not eager loading the entire project, but you still need some namespace to be loaded for things to function properly.
509
+
510
+ The argument has to be a class or module object and the method raises `Zeitwerk::Error` otherwise.
511
+
512
+ If the namespace is spread over multiple directories in the receiver's source tree, they are all eager loaded. For example, if you have a structure like
513
+
514
+ ```
515
+ root_dir1/my_app/routes
516
+ root_dir2/my_app/routes
517
+ root_dir3/my_app/routes
518
+ ```
519
+
520
+ where `root_directory{1,2,3}` are root directories, eager loading `MyApp::Routes` will eager load the contents of the three corresponding directories.
521
+
522
+ There might exist external source trees implementing part of the namespace. This happens routinely, because top-level constants are stored in the globally shared `Object`. It happens also when deliberately [reopening third-party namespaces](reopening-third-party-namespaces). Such external code is not eager loaded, the implementation is carefully scoped to what the receiver manages to avoid side-effects elsewhere.
523
+
524
+ This method is flexible about what it accepts. Its semantics have to be interpreted as: "_If_ you manage this namespace, or part of this namespace, please eager load what you got". In particular, if the receiver does not manage the namespace, it will simply do nothing, this is not an error condition.
525
+
526
+ [Eager load exclusions](#eager-load-exclusions), [ignored files and directories](#ignoring-parts-of-the-project), and [shadowed files](https://github.com/fxn/zeitwerk#shadowed-files) are not eager loaded.
527
+
528
+ `Zeitwerk::Loader#eager_load_namespace` is idempotent, but compatible with reloading. If you eager load a namespace and then reload, eager loading that namespace will load its (current) descendants again.
529
+
530
+ The method checks if a regular eager load was already executed, in which case it returns fast.
531
+
532
+ If root directories are assigned to custom namespaces, the method behaves as you'd expect, according to the namespacing relationship between the custom namespace and the argument.
533
+
534
+ Attempting to eager load a namespace without previously calling `setup` raises `Zeitwerk::SetupRequired`.
535
+
536
+ <a id="markdown-eager-load-namespaces-shared-by-several-loaders" name="eager-load-namespaces-shared-by-several-loaders"></a>
537
+ #### Eager load namespaces shared by several loaders
538
+
539
+ The method `Zeitwerk::Loader.eager_load_namespace` broadcasts `eager_load_namespace` to all loaders.
540
+
541
+ ```ruby
542
+ Zeitwerk::Loader.eager_load_namespace(MyFramework::Routes)
543
+ ```
544
+
545
+ This may be handy, for example, if a framework supports plugins and a shared namespace needs to be eager loaded for the framework to function properly.
546
+
547
+ Please, note that loaders only eager load namespaces they manage, as documented above. Therefore, this method does not allow you to eager load namespaces not managed by Zeitwerk loaders.
548
+
549
+ This method does not require that all registered loaders have `setup` already invoked, since that is out of your control. If there's any in that state, it is simply skipped.
550
+
467
551
  <a id="markdown-global-eager-load" name="global-eager-load"></a>
468
552
  #### Global eager load
469
553
 
@@ -479,9 +563,31 @@ Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager lo
479
563
 
480
564
  This method does not accept the `force` flag, since in general it wouldn't be a good idea to force eager loading in 3rd party code.
481
565
 
566
+ This method does not require that all registered loaders have `setup` already invoked, since that is out of your control. If there's any in that state, it is simply skipped.
567
+
568
+ <a id="markdown-loading-individual-files" name="loading-individual-files"></a>
569
+ ### Loading individual files
570
+
571
+ The method `Zeitwerk::Loader#load_file` loads an individual Ruby file:
572
+
573
+ ```ruby
574
+ loader.load_file("#{__dir__}/custom_web_app/routes.rb")
575
+ ```
576
+
577
+ This is useful when the loader is not eager loading the entire project, but you still need an individual file to be loaded for things to function properly.
578
+
579
+ Both strings and `Pathname` objects are supported as arguments. The method raises `Zeitwerk::Error` if the argument is not a Ruby file, is [ignored](#ignoring-parts-of-the-project), is [shadowed](https://github.com/fxn/zeitwerk#shadowed-files), or is not managed by the receiver.
580
+
581
+ `Zeitwerk::Loader#load_file` is idempotent, but compatible with reloading. If you load a file and then reload, a new call will load its (current) contents again.
582
+
583
+ If you want to eager load a directory, `Zeitwerk::Loader#eager_load_dir` is more efficient than invoking `Zeitwerk::Loader#load_file` on its files.
584
+
482
585
  <a id="markdown-reloading" name="reloading"></a>
483
586
  ### Reloading
484
587
 
588
+ <a id="markdown-configuration-and-usage" name="configuration-and-usage"></a>
589
+ #### Configuration and usage
590
+
485
591
  Zeitwerk is able to reload code, but you need to enable this feature:
486
592
 
487
593
  ```ruby
@@ -495,7 +601,7 @@ loader.reload
495
601
 
496
602
  There is no way to undo this, either you want to reload or you don't.
497
603
 
498
- Enabling reloading after setup raises `Zeitwerk::Error`. Attempting to reload without having it enabled raises `Zeitwerk::ReloadingDisabledError`.
604
+ Enabling reloading after setup raises `Zeitwerk::Error`. Attempting to reload without having it enabled raises `Zeitwerk::ReloadingDisabledError`. Attempting to reload without previously calling `setup` raises `Zeitwerk::SetupRequired`.
499
605
 
500
606
  Generally speaking, reloading is useful while developing running services like web applications. Gems that implement regular libraries, so to speak, or services running in testing or production environments, won't normally have a use case for reloading. If reloading is not enabled, Zeitwerk is able to use less memory.
501
607
 
@@ -503,12 +609,34 @@ Reloading removes the currently loaded classes and modules and resets the loader
503
609
 
504
610
  It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent.
505
611
 
506
- Reloading is not thread-safe:
612
+ <a id="markdown-thread-safety" name="thread-safety"></a>
613
+ #### Thread-safety
507
614
 
508
- * You should not reload while another thread is reloading.
509
- * You should not autoload while another thread is reloading.
615
+ In order to reload safely, no other thread can be autoloading or reloading concurrently. Client code is responsible for this coordination.
510
616
 
511
- In order to reload in a thread-safe manner, frameworks need to implement some coordination. For example, a web framework that serves each request with its own thread may have a globally accessible read/write lock: When a request comes in, the framework acquires the lock for reading at the beginning, and releases it at the end. On the other hand, the code in the framework responsible for the call to `Zeitwerk::Loader#reload` needs to acquire the lock for writing.
617
+ For example, a web framework that serves each request in its own thread and has reloading enabled could create a read-write lock on boot like this:
618
+
619
+ ```ruby
620
+ require "concurrent/atomic/read_write_lock"
621
+
622
+ MyFramework::RELOAD_RW_LOCK = Concurrent::ReadWriteLock.new
623
+ ```
624
+
625
+ You acquire the lock for reading for serving each individual request:
626
+
627
+ ```ruby
628
+ MyFramework::RELOAD_RW_LOCK.with_read_lock do
629
+ serve(request)
630
+ end
631
+ ```
632
+
633
+ Then, when a reload is triggered, just acquire the lock for writing in order to execute the method call safely:
634
+
635
+ ```ruby
636
+ MyFramework::RELOAD_RW_LOCK.with_write_lock do
637
+ loader.reload
638
+ end
639
+ ```
512
640
 
513
641
  On reloading, client code has to update anything that would otherwise be storing a stale object. For example, if the routing layer of a web framework stores reloadable controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes.
514
642
 
@@ -905,10 +1033,39 @@ loader.ignore(tests)
905
1033
  loader.setup
906
1034
  ```
907
1035
 
1036
+ <a id="markdown-shadowed-files" name="shadowed-files"></a>
1037
+ ### Shadowed files
1038
+
1039
+ In Ruby, if you have several files called `foo.rb` in different directories of `$LOAD_PATH` and execute
1040
+
1041
+ ```ruby
1042
+ require "foo"
1043
+ ```
1044
+
1045
+ the first one found gets loaded, and the rest are ignored.
1046
+
1047
+ Zeitwerk behaves in a similar way. If `foo.rb` is present in several root directories (at the same namespace level), the constant `Foo` is autoloaded from the first one, and the rest of the files are not evaluated. If logging is enabled, you'll see something like
1048
+
1049
+ ```
1050
+ file #{file} is ignored because #{previous_occurrence} has precedence
1051
+ ```
1052
+
1053
+ (This message is not public interface and may change, you cannot rely on that exact wording.)
1054
+
1055
+ Even if there's only one `foo.rb`, if the constant `Foo` is already defined when Zeitwerk finds `foo.rb`, then the file is ignored too. This could happen if `Foo` was defined by a dependency, for example. If logging is enabled, you'll see something like
1056
+
1057
+ ```
1058
+ file #{file} is ignored because #{constant_path} is already defined
1059
+ ```
1060
+
1061
+ (This message is not public interface and may change, you cannot rely on that exact wording.)
1062
+
1063
+ Shadowing only applies to Ruby files, namespace definition can be spread over multiple directories. And you can also reopen third-party namespaces if done [orderly](#reopening-third-party-namespaces).
1064
+
908
1065
  <a id="markdown-edge-cases" name="edge-cases"></a>
909
1066
  ### Edge cases
910
1067
 
911
- A class or module that acts as a namespace:
1068
+ [Explicit namespaces](#explicit-namespaces) like `Trip` here:
912
1069
 
913
1070
  ```ruby
914
1071
  # trip.rb
@@ -922,7 +1079,7 @@ module Trip::Geolocation
922
1079
  end
923
1080
  ```
924
1081
 
925
- has to be defined with the `class` or `module` keywords, as in the example above.
1082
+ have to be defined with the `class`/`module` keywords, as in the example above.
926
1083
 
927
1084
  For technical reasons, raw constant assignment is not supported:
928
1085
 
@@ -984,7 +1141,7 @@ require "active_job"
984
1141
  require "active_job/queue_adapters"
985
1142
 
986
1143
  require "zeitwerk"
987
- # By passign the flag, we acknowledge the extra directory lib/active_job
1144
+ # By passing the flag, we acknowledge the extra directory lib/active_job
988
1145
  # has to be managed by the loader and no warning has to be issued for it.
989
1146
  loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
990
1147
  loader.setup
@@ -12,4 +12,10 @@ module Zeitwerk
12
12
 
13
13
  class NameError < ::NameError
14
14
  end
15
+
16
+ class SetupRequired < Error
17
+ def initialize
18
+ super("please, finish your configuration and call Zeitwerk::Loader#setup once all is ready")
19
+ end
20
+ end
15
21
  end
@@ -11,28 +11,28 @@ module Zeitwerk
11
11
  module ExplicitNamespace # :nodoc: all
12
12
  class << self
13
13
  include RealModName
14
+ extend Internal
14
15
 
15
16
  # Maps constant paths that correspond to explicit namespaces according to
16
17
  # the file system, to the loader responsible for them.
17
18
  #
18
- # @private
19
19
  # @sig Hash[String, Zeitwerk::Loader]
20
20
  attr_reader :cpaths
21
+ private :cpaths
21
22
 
22
- # @private
23
23
  # @sig Mutex
24
24
  attr_reader :mutex
25
+ private :mutex
25
26
 
26
- # @private
27
27
  # @sig TracePoint
28
28
  attr_reader :tracer
29
+ private :tracer
29
30
 
30
31
  # Asserts `cpath` corresponds to an explicit namespace for which `loader`
31
32
  # is responsible.
32
33
  #
33
- # @private
34
34
  # @sig (String, Zeitwerk::Loader) -> void
35
- def register(cpath, loader)
35
+ internal def register(cpath, loader)
36
36
  mutex.synchronize do
37
37
  cpaths[cpath] = loader
38
38
  # We check enabled? because, looking at the C source code, enabling an
@@ -41,24 +41,21 @@ module Zeitwerk
41
41
  end
42
42
  end
43
43
 
44
- # @private
45
44
  # @sig (Zeitwerk::Loader) -> void
46
- def unregister_loader(loader)
45
+ internal def unregister_loader(loader)
47
46
  cpaths.delete_if { |_cpath, l| l == loader }
48
47
  disable_tracer_if_unneeded
49
48
  end
50
49
 
51
- private
52
-
53
50
  # @sig () -> void
54
- def disable_tracer_if_unneeded
51
+ private def disable_tracer_if_unneeded
55
52
  mutex.synchronize do
56
53
  tracer.disable if cpaths.empty?
57
54
  end
58
55
  end
59
56
 
60
57
  # @sig (TracePoint) -> void
61
- def tracepoint_class_callback(event)
58
+ private def tracepoint_class_callback(event)
62
59
  # If the class is a singleton class, we won't do anything with it so we
63
60
  # can bail out immediately. This is several orders of magnitude faster
64
61
  # than accessing its name.
@@ -43,7 +43,7 @@ module Zeitwerk
43
43
  next if abspath == expected_namespace_dir
44
44
 
45
45
  basename_without_ext = basename.delete_suffix(".rb")
46
- cname = inflector.camelize(basename_without_ext, abspath)
46
+ cname = inflector.camelize(basename_without_ext, abspath).to_sym
47
47
  ftype = dir?(abspath) ? "directory" : "file"
48
48
 
49
49
  warn(<<~EOS)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is a private module.
4
+ module Zeitwerk::Internal
5
+ def internal(method_name)
6
+ private method_name
7
+
8
+ mangled = "__#{method_name}"
9
+ alias_method mangled, method_name
10
+ public mangled
11
+ end
12
+ end
@@ -19,6 +19,9 @@ module Kernel
19
19
  # included in Object, and changes in ancestors don't get propagated into
20
20
  # already existing ancestor chains on Ruby < 3.0.
21
21
  alias_method :zeitwerk_original_require, :require
22
+ class << self
23
+ alias_method :zeitwerk_original_require, :require
24
+ end
22
25
 
23
26
  # @sig (String) -> true | false
24
27
  def require(path)
@@ -42,7 +42,7 @@ module Zeitwerk::Loader::Callbacks
42
42
  # Without the mutex and subsequent delete call, t2 would reset the module.
43
43
  # That not only would reassign the constant (undesirable per se) but, worse,
44
44
  # the module object created by t2 wouldn't have any of the autoloads for its
45
- # children, since t1 would have correctly deleted its lazy_subdirs entry.
45
+ # children, since t1 would have correctly deleted its namespace_dirs entry.
46
46
  mutex2.synchronize do
47
47
  if cref = autoloads.delete(dir)
48
48
  autovivified_module = cref[0].const_set(cref[1], Module.new)
@@ -71,9 +71,9 @@ module Zeitwerk::Loader::Callbacks
71
71
  # @private
72
72
  # @sig (Module) -> void
73
73
  def on_namespace_loaded(namespace)
74
- if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
75
- subdirs.each do |subdir|
76
- set_autoloads_in_dir(subdir, namespace)
74
+ if dirs = namespace_dirs.delete(real_mod_name(namespace))
75
+ dirs.each do |dir|
76
+ set_autoloads_in_dir(dir, namespace)
77
77
  end
78
78
  end
79
79
  end
@@ -4,86 +4,91 @@ require "set"
4
4
  require "securerandom"
5
5
 
6
6
  module Zeitwerk::Loader::Config
7
- # Absolute paths of the root directories. Stored in a hash to preserve
8
- # order, easily handle duplicates, have a fast lookup needed for detecting
9
- # nested paths, and store custom namespaces as values.
7
+ extend Zeitwerk::Internal
8
+ include Zeitwerk::RealModName
9
+
10
+ # @sig #camelize
11
+ attr_accessor :inflector
12
+
13
+ # @sig #call | #debug | nil
14
+ attr_accessor :logger
15
+
16
+ # Absolute paths of the root directories, mapped to their respective root namespaces:
10
17
  #
11
- # "/Users/fxn/blog/app/assets" => Object,
12
18
  # "/Users/fxn/blog/app/channels" => Object,
13
- # "/Users/fxn/blog/adapters" => ActiveJob::QueueAdapters,
19
+ # "/Users/fxn/blog/app/adapters" => ActiveJob::QueueAdapters,
14
20
  # ...
15
21
  #
22
+ # Stored in a hash to preserve order, easily handle duplicates, and have a
23
+ # fast lookup by directory.
24
+ #
16
25
  # This is a private collection maintained by the loader. The public
17
26
  # interface for it is `push_dir` and `dirs`.
18
27
  #
19
- # @private
20
28
  # @sig Hash[String, Module]
21
- attr_reader :root_dirs
22
-
23
- # @sig #camelize
24
- attr_accessor :inflector
29
+ attr_reader :roots
30
+ internal :roots
25
31
 
26
32
  # Absolute paths of files, directories, or glob patterns to be totally
27
33
  # ignored.
28
34
  #
29
- # @private
30
35
  # @sig Set[String]
31
36
  attr_reader :ignored_glob_patterns
37
+ private :ignored_glob_patterns
32
38
 
33
39
  # The actual collection of absolute file and directory names at the time the
34
40
  # ignored glob patterns were expanded. Computed on setup, and recomputed on
35
41
  # reload.
36
42
  #
37
- # @private
38
43
  # @sig Set[String]
39
44
  attr_reader :ignored_paths
45
+ private :ignored_paths
40
46
 
41
47
  # Absolute paths of directories or glob patterns to be collapsed.
42
48
  #
43
- # @private
44
49
  # @sig Set[String]
45
50
  attr_reader :collapse_glob_patterns
51
+ private :collapse_glob_patterns
46
52
 
47
53
  # The actual collection of absolute directory names at the time the collapse
48
54
  # glob patterns were expanded. Computed on setup, and recomputed on reload.
49
55
  #
50
- # @private
51
56
  # @sig Set[String]
52
57
  attr_reader :collapse_dirs
58
+ private :collapse_dirs
53
59
 
54
60
  # Absolute paths of files or directories not to be eager loaded.
55
61
  #
56
- # @private
57
62
  # @sig Set[String]
58
63
  attr_reader :eager_load_exclusions
64
+ private :eager_load_exclusions
59
65
 
60
66
  # User-oriented callbacks to be fired on setup and on reload.
61
67
  #
62
- # @private
63
68
  # @sig Array[{ () -> void }]
64
69
  attr_reader :on_setup_callbacks
70
+ private :on_setup_callbacks
65
71
 
66
72
  # User-oriented callbacks to be fired when a constant is loaded.
67
73
  #
68
- # @private
69
74
  # @sig Hash[String, Array[{ (Object, String) -> void }]]
70
75
  # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
71
76
  attr_reader :on_load_callbacks
77
+ private :on_load_callbacks
72
78
 
73
79
  # User-oriented callbacks to be fired before constants are removed.
74
80
  #
75
- # @private
76
81
  # @sig Hash[String, Array[{ (Object, String) -> void }]]
77
82
  # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
78
83
  attr_reader :on_unload_callbacks
79
-
80
- # @sig #call | #debug | nil
81
- attr_accessor :logger
84
+ private :on_unload_callbacks
82
85
 
83
86
  def initialize
84
- @initialized_at = Time.now
85
- @root_dirs = {}
86
87
  @inflector = Zeitwerk::Inflector.new
88
+ @logger = self.class.default_logger
89
+ @tag = SecureRandom.hex(3)
90
+ @initialized_at = Time.now
91
+ @roots = {}
87
92
  @ignored_glob_patterns = Set.new
88
93
  @ignored_paths = Set.new
89
94
  @collapse_glob_patterns = Set.new
@@ -93,8 +98,6 @@ module Zeitwerk::Loader::Config
93
98
  @on_setup_callbacks = []
94
99
  @on_load_callbacks = {}
95
100
  @on_unload_callbacks = {}
96
- @logger = self.class.default_logger
97
- @tag = SecureRandom.hex(3)
98
101
  end
99
102
 
100
103
  # Pushes `path` to the list of root directories.
@@ -111,10 +114,14 @@ module Zeitwerk::Loader::Config
111
114
  raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
112
115
  end
113
116
 
117
+ unless real_mod_name(namespace)
118
+ raise Zeitwerk::Error, "root namespaces cannot be anonymous"
119
+ end
120
+
114
121
  abspath = File.expand_path(path)
115
122
  if dir?(abspath)
116
123
  raise_if_conflicting_directory(abspath)
117
- root_dirs[abspath] = namespace
124
+ roots[abspath] = namespace
118
125
  else
119
126
  raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
120
127
  end
@@ -147,9 +154,9 @@ module Zeitwerk::Loader::Config
147
154
  # @sig () -> Array[String] | Hash[String, Module]
148
155
  def dirs(namespaces: false)
149
156
  if namespaces
150
- root_dirs.clone
157
+ roots.clone
151
158
  else
152
- root_dirs.keys
159
+ roots.keys
153
160
  end.freeze
154
161
  end
155
162
 
@@ -273,57 +280,76 @@ module Zeitwerk::Loader::Config
273
280
  @logger = ->(msg) { puts msg }
274
281
  end
275
282
 
276
- # @private
283
+ # Returns true if the argument has been configured to be ignored, or is a
284
+ # descendant of an ignored directory.
285
+ #
277
286
  # @sig (String) -> bool
278
- def ignores?(abspath)
279
- ignored_paths.any? do |ignored_path|
280
- ignored_path == abspath || (dir?(ignored_path) && abspath.start_with?(ignored_path + "/"))
287
+ internal def ignores?(abspath)
288
+ # Common use case.
289
+ return false if ignored_paths.empty?
290
+
291
+ walk_up(abspath) do |abspath|
292
+ return true if ignored_path?(abspath)
293
+ return false if roots.key?(abspath)
281
294
  end
295
+
296
+ false
282
297
  end
283
298
 
284
- private
299
+ # @sig (String) -> bool
300
+ private def ignored_path?(abspath)
301
+ ignored_paths.member?(abspath)
302
+ end
285
303
 
286
304
  # @sig () -> Array[String]
287
- def actual_root_dirs
288
- root_dirs.reject do |root_dir, _namespace|
289
- !dir?(root_dir) || ignored_paths.member?(root_dir)
305
+ private def actual_roots
306
+ roots.reject do |root_dir, _root_namespace|
307
+ !dir?(root_dir) || ignored_path?(root_dir)
290
308
  end
291
309
  end
292
310
 
293
311
  # @sig (String) -> bool
294
- def root_dir?(dir)
295
- root_dirs.key?(dir)
312
+ private def root_dir?(dir)
313
+ roots.key?(dir)
296
314
  end
297
315
 
298
316
  # @sig (String) -> bool
299
- def excluded_from_eager_load?(abspath)
300
- eager_load_exclusions.member?(abspath)
317
+ private def excluded_from_eager_load?(abspath)
318
+ # Optimize this common use case.
319
+ return false if eager_load_exclusions.empty?
320
+
321
+ walk_up(abspath) do |abspath|
322
+ return true if eager_load_exclusions.member?(abspath)
323
+ return false if roots.key?(abspath)
324
+ end
325
+
326
+ false
301
327
  end
302
328
 
303
329
  # @sig (String) -> bool
304
- def collapse?(dir)
330
+ private def collapse?(dir)
305
331
  collapse_dirs.member?(dir)
306
332
  end
307
333
 
308
334
  # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
309
- def expand_paths(paths)
335
+ private def expand_paths(paths)
310
336
  paths.flatten.map! { |path| File.expand_path(path) }
311
337
  end
312
338
 
313
339
  # @sig (Array[String]) -> Array[String]
314
- def expand_glob_patterns(glob_patterns)
340
+ private def expand_glob_patterns(glob_patterns)
315
341
  # Note that Dir.glob works with regular file names just fine. That is,
316
342
  # glob patterns technically need no wildcards.
317
343
  glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
318
344
  end
319
345
 
320
346
  # @sig () -> void
321
- def recompute_ignored_paths
347
+ private def recompute_ignored_paths
322
348
  ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
323
349
  end
324
350
 
325
351
  # @sig () -> void
326
- def recompute_collapse_dirs
352
+ private def recompute_collapse_dirs
327
353
  collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
328
354
  end
329
355
  end
@@ -0,0 +1,228 @@
1
+ module Zeitwerk::Loader::EagerLoad
2
+ # Eager loads all files in the root directories, recursively. Files do not
3
+ # need to be in `$LOAD_PATH`, absolute file names are used. Ignored and
4
+ # shadowed files are not eager loaded. You can opt-out specifically in
5
+ # specific files and directories with `do_not_eager_load`, and that can be
6
+ # overridden passing `force: true`.
7
+ #
8
+ # @sig (true | false) -> void
9
+ def eager_load(force: false)
10
+ mutex.synchronize do
11
+ break if @eager_loaded
12
+ raise Zeitwerk::SetupRequired unless @setup
13
+
14
+ log("eager load start") if logger
15
+
16
+ actual_roots.each do |root_dir, root_namespace|
17
+ actual_eager_load_dir(root_dir, root_namespace, force: force)
18
+ end
19
+
20
+ autoloaded_dirs.each do |autoloaded_dir|
21
+ Zeitwerk::Registry.unregister_autoload(autoloaded_dir)
22
+ end
23
+ autoloaded_dirs.clear
24
+
25
+ @eager_loaded = true
26
+
27
+ log("eager load end") if logger
28
+ end
29
+ end
30
+
31
+ # @sig (String | Pathname) -> void
32
+ def eager_load_dir(path)
33
+ raise Zeitwerk::SetupRequired unless @setup
34
+
35
+ abspath = File.expand_path(path)
36
+
37
+ raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
38
+
39
+ cnames = []
40
+
41
+ root_namespace = nil
42
+ walk_up(abspath) do |dir|
43
+ return if ignored_path?(dir)
44
+ return if eager_load_exclusions.member?(dir)
45
+
46
+ break if root_namespace = roots[dir]
47
+
48
+ unless collapse?(dir)
49
+ basename = File.basename(dir)
50
+ cnames << inflector.camelize(basename, dir).to_sym
51
+ end
52
+ end
53
+
54
+ raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
55
+
56
+ return if @eager_loaded
57
+
58
+ namespace = root_namespace
59
+ cnames.reverse_each do |cname|
60
+ # Can happen if there are no Ruby files. This is not an error condition,
61
+ # the directory is actually managed. Could have Ruby files later.
62
+ return unless cdef?(namespace, cname)
63
+ namespace = cget(namespace, cname)
64
+ end
65
+
66
+ # A shortcircuiting test depends on the invocation of this method. Please
67
+ # keep them in sync if refactored.
68
+ actual_eager_load_dir(abspath, namespace)
69
+ end
70
+
71
+ # @sig (Module) -> void
72
+ def eager_load_namespace(mod)
73
+ raise Zeitwerk::SetupRequired unless @setup
74
+
75
+ unless mod.is_a?(Module)
76
+ raise Zeitwerk::Error, "#{mod.inspect} is not a class or module object"
77
+ end
78
+
79
+ return if @eager_loaded
80
+
81
+ mod_name = real_mod_name(mod)
82
+ return unless mod_name
83
+
84
+ actual_roots.each do |root_dir, root_namespace|
85
+ if mod.equal?(Object)
86
+ # A shortcircuiting test depends on the invocation of this method.
87
+ # Please keep them in sync if refactored.
88
+ actual_eager_load_dir(root_dir, root_namespace)
89
+ elsif root_namespace.equal?(Object)
90
+ eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
91
+ else
92
+ root_namespace_name = real_mod_name(root_namespace)
93
+ if root_namespace_name.start_with?(mod_name + "::")
94
+ actual_eager_load_dir(root_dir, root_namespace)
95
+ elsif mod_name == root_namespace_name
96
+ actual_eager_load_dir(root_dir, root_namespace)
97
+ elsif mod_name.start_with?(root_namespace_name + "::")
98
+ eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
99
+ else
100
+ # Unrelated constant hierarchies, do nothing.
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ # Loads the given Ruby file.
107
+ #
108
+ # Raises if the argument is ignored, shadowed, or not managed by the receiver.
109
+ #
110
+ # The method is implemented as `constantize` for files, in a sense, to be able
111
+ # to descend orderly and make sure the file is loadable.
112
+ #
113
+ # @sig (String | Pathname) -> void
114
+ def load_file(path)
115
+ abspath = File.expand_path(path)
116
+
117
+ raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
118
+ raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
119
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
120
+
121
+ basename = File.basename(abspath, ".rb")
122
+ base_cname = inflector.camelize(basename, abspath).to_sym
123
+
124
+ root_namespace = nil
125
+ cnames = []
126
+
127
+ walk_up(File.dirname(abspath)) do |dir|
128
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(dir)
129
+
130
+ break if root_namespace = roots[dir]
131
+
132
+ unless collapse?(dir)
133
+ basename = File.basename(dir)
134
+ cnames << inflector.camelize(basename, dir).to_sym
135
+ end
136
+ end
137
+
138
+ raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
139
+
140
+ namespace = root_namespace
141
+ cnames.reverse_each do |cname|
142
+ namespace = cget(namespace, cname)
143
+ end
144
+
145
+ raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
146
+
147
+ cget(namespace, base_cname)
148
+ end
149
+
150
+ # The caller is responsible for making sure `namespace` is the namespace that
151
+ # corresponds to `dir`.
152
+ #
153
+ # @sig (String, Module, Boolean) -> void
154
+ private def actual_eager_load_dir(dir, namespace, force: false)
155
+ honour_exclusions = !force
156
+ return if honour_exclusions && excluded_from_eager_load?(dir)
157
+
158
+ log("eager load directory #{dir} start") if logger
159
+
160
+ queue = [[dir, namespace]]
161
+ while to_eager_load = queue.shift
162
+ dir, namespace = to_eager_load
163
+
164
+ ls(dir) do |basename, abspath|
165
+ next if honour_exclusions && eager_load_exclusions.member?(abspath)
166
+
167
+ if ruby?(abspath)
168
+ if (cref = autoloads[abspath]) && !shadowed_file?(abspath)
169
+ cget(*cref)
170
+ end
171
+ else
172
+ if collapse?(abspath)
173
+ queue << [abspath, namespace]
174
+ else
175
+ cname = inflector.camelize(basename, abspath).to_sym
176
+ queue << [abspath, cget(namespace, cname)]
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ log("eager load directory #{dir} end") if logger
183
+ end
184
+
185
+ # In order to invoke this method, the caller has to ensure `child` is a
186
+ # strict namespace descendendant of `root_namespace`.
187
+ #
188
+ # @sig (Module, String, Module, Boolean) -> void
189
+ private def eager_load_child_namespace(child, child_name, root_dir, root_namespace)
190
+ suffix = child_name
191
+ unless root_namespace.equal?(Object)
192
+ suffix = suffix.delete_prefix(real_mod_name(root_namespace) + "::")
193
+ end
194
+
195
+ # These directories are at the same namespace level, there may be more if
196
+ # we find collapsed ones. As we scan, we look for matches for the first
197
+ # segment, and store them in `next_dirs`. If there are any, we look for
198
+ # the next segments in those matches. Repeat.
199
+ #
200
+ # If we exhaust the search locating directories that match all segments,
201
+ # we just need to eager load those ones.
202
+ dirs = [root_dir]
203
+ next_dirs = []
204
+
205
+ suffix.split("::").each do |segment|
206
+ while dir = dirs.shift
207
+ ls(dir) do |basename, abspath|
208
+ next unless dir?(abspath)
209
+
210
+ if collapse?(abspath)
211
+ dirs << abspath
212
+ elsif segment == inflector.camelize(basename, abspath)
213
+ next_dirs << abspath
214
+ end
215
+ end
216
+ end
217
+
218
+ return if next_dirs.empty?
219
+
220
+ dirs.replace(next_dirs)
221
+ next_dirs.clear
222
+ end
223
+
224
+ dirs.each do |dir|
225
+ actual_eager_load_dir(dir, child)
226
+ end
227
+ end
228
+ end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk::Loader::Helpers
4
- private
5
-
6
4
  # --- Logging -----------------------------------------------------------------------------------
7
5
 
8
6
  # @sig (String) -> void
9
- def log(message)
7
+ private def log(message)
10
8
  method_name = logger.respond_to?(:debug) ? :debug : :call
11
9
  logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
12
10
  end
@@ -14,7 +12,7 @@ module Zeitwerk::Loader::Helpers
14
12
  # --- Files and directories ---------------------------------------------------------------------
15
13
 
16
14
  # @sig (String) { (String, String) -> void } -> void
17
- def ls(dir)
15
+ private def ls(dir)
18
16
  children = Dir.children(dir)
19
17
 
20
18
  # The order in which a directory is listed depends on the file system.
@@ -28,10 +26,11 @@ module Zeitwerk::Loader::Helpers
28
26
  next if hidden?(basename)
29
27
 
30
28
  abspath = File.join(dir, basename)
31
- next if ignored_paths.member?(abspath)
29
+ next if ignored_path?(abspath)
32
30
 
33
31
  if dir?(abspath)
34
- next unless has_at_least_one_ruby_file?(abspath)
32
+ next if roots.key?(abspath)
33
+ next if !has_at_least_one_ruby_file?(abspath)
35
34
  else
36
35
  next unless ruby?(abspath)
37
36
  end
@@ -43,7 +42,7 @@ module Zeitwerk::Loader::Helpers
43
42
  end
44
43
 
45
44
  # @sig (String) -> bool
46
- def has_at_least_one_ruby_file?(dir)
45
+ private def has_at_least_one_ruby_file?(dir)
47
46
  to_visit = [dir]
48
47
 
49
48
  while dir = to_visit.shift
@@ -60,20 +59,29 @@ module Zeitwerk::Loader::Helpers
60
59
  end
61
60
 
62
61
  # @sig (String) -> bool
63
- def ruby?(path)
62
+ private def ruby?(path)
64
63
  path.end_with?(".rb")
65
64
  end
66
65
 
67
66
  # @sig (String) -> bool
68
- def dir?(path)
67
+ private def dir?(path)
69
68
  File.directory?(path)
70
69
  end
71
70
 
72
71
  # @sig (String) -> bool
73
- def hidden?(basename)
72
+ private def hidden?(basename)
74
73
  basename.start_with?(".")
75
74
  end
76
75
 
76
+ # @sig (String) { (String) -> void } -> void
77
+ private def walk_up(abspath)
78
+ loop do
79
+ yield abspath
80
+ abspath, basename = File.split(abspath)
81
+ break if basename == "/"
82
+ end
83
+ end
84
+
77
85
  # --- Constants ---------------------------------------------------------------------------------
78
86
 
79
87
  # The autoload? predicate takes into account the ancestor chain of the
@@ -94,11 +102,11 @@ module Zeitwerk::Loader::Helpers
94
102
  #
95
103
  # @sig (Module, Symbol) -> String?
96
104
  if method(:autoload?).arity == 1
97
- def strict_autoload_path(parent, cname)
105
+ private def strict_autoload_path(parent, cname)
98
106
  parent.autoload?(cname) if cdef?(parent, cname)
99
107
  end
100
108
  else
101
- def strict_autoload_path(parent, cname)
109
+ private def strict_autoload_path(parent, cname)
102
110
  parent.autoload?(cname, false)
103
111
  end
104
112
  end
@@ -107,23 +115,23 @@ module Zeitwerk::Loader::Helpers
107
115
  if Symbol.method_defined?(:name)
108
116
  # Symbol#name was introduced in Ruby 3.0. It returns always the same
109
117
  # frozen object, so we may save a few string allocations.
110
- def cpath(parent, cname)
118
+ private def cpath(parent, cname)
111
119
  Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
112
120
  end
113
121
  else
114
- def cpath(parent, cname)
122
+ private def cpath(parent, cname)
115
123
  Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
116
124
  end
117
125
  end
118
126
 
119
127
  # @sig (Module, Symbol) -> bool
120
- def cdef?(parent, cname)
128
+ private def cdef?(parent, cname)
121
129
  parent.const_defined?(cname, false)
122
130
  end
123
131
 
124
132
  # @raise [NameError]
125
133
  # @sig (Module, Symbol) -> Object
126
- def cget(parent, cname)
134
+ private def cget(parent, cname)
127
135
  parent.const_get(cname, false)
128
136
  end
129
137
  end
@@ -7,11 +7,13 @@ 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"
10
11
 
11
12
  include RealModName
12
13
  include Callbacks
13
14
  include Helpers
14
15
  include Config
16
+ include EagerLoad
15
17
 
16
18
  MUTEX = Mutex.new
17
19
  private_constant :MUTEX
@@ -54,7 +56,7 @@ module Zeitwerk
54
56
  # @sig Hash[String, [String, [Module, Symbol]]]
55
57
  attr_reader :to_unload
56
58
 
57
- # Maps constant paths of namespaces to arrays of corresponding directories.
59
+ # Maps namespace constant paths to their respective directories.
58
60
  #
59
61
  # For example, given this mapping:
60
62
  #
@@ -64,13 +66,24 @@ module Zeitwerk
64
66
  # ...
65
67
  # ]
66
68
  #
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.
69
+ # when `Admin` gets defined we know that it plays the role of a namespace
70
+ # and that its children are spread over those directories. We'll visit them
71
+ # to set up the corresponding autoloads.
70
72
  #
71
73
  # @private
72
74
  # @sig Hash[String, Array[String]]
73
- attr_reader :lazy_subdirs
75
+ attr_reader :namespace_dirs
76
+
77
+ # A shadowed file is a file managed by this loader that is ignored when
78
+ # setting autoloads because its matching constant is already taken.
79
+ #
80
+ # This private set is populated as we descend. For example, if the loader
81
+ # has only scanned the top-level, `shadowed_files` does not have shadowed
82
+ # files that may exist deep in the project tree yet.
83
+ #
84
+ # @private
85
+ # @sig Set[String]
86
+ attr_reader :shadowed_files
74
87
 
75
88
  # @private
76
89
  # @sig Mutex
@@ -86,7 +99,8 @@ module Zeitwerk
86
99
  @autoloads = {}
87
100
  @autoloaded_dirs = []
88
101
  @to_unload = {}
89
- @lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
102
+ @namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
103
+ @shadowed_files = Set.new
90
104
  @mutex = Mutex.new
91
105
  @mutex2 = Mutex.new
92
106
  @setup = false
@@ -95,15 +109,15 @@ module Zeitwerk
95
109
  Registry.register_loader(self)
96
110
  end
97
111
 
98
- # Sets autoloads in the root namespace.
112
+ # Sets autoloads in the root namespaces.
99
113
  #
100
114
  # @sig () -> void
101
115
  def setup
102
116
  mutex.synchronize do
103
117
  break if @setup
104
118
 
105
- actual_root_dirs.each do |root_dir, namespace|
106
- set_autoloads_in_dir(root_dir, namespace)
119
+ actual_roots.each do |root_dir, root_namespace|
120
+ set_autoloads_in_dir(root_dir, root_namespace)
107
121
  end
108
122
 
109
123
  on_setup_callbacks.each(&:call)
@@ -126,6 +140,8 @@ module Zeitwerk
126
140
  # @sig () -> void
127
141
  def unload
128
142
  mutex.synchronize do
143
+ raise SetupRequired unless @setup
144
+
129
145
  # We are going to keep track of the files that were required by our
130
146
  # autoloads to later remove them from $LOADED_FEATURES, thus making them
131
147
  # loadable by Kernel#require again.
@@ -181,10 +197,11 @@ module Zeitwerk
181
197
  autoloads.clear
182
198
  autoloaded_dirs.clear
183
199
  to_unload.clear
184
- lazy_subdirs.clear
200
+ namespace_dirs.clear
201
+ shadowed_files.clear
185
202
 
186
203
  Registry.on_unload(self)
187
- ExplicitNamespace.unregister_loader(self)
204
+ ExplicitNamespace.__unregister_loader(self)
188
205
 
189
206
  @setup = false
190
207
  @eager_loaded = false
@@ -201,6 +218,7 @@ module Zeitwerk
201
218
  # @sig () -> void
202
219
  def reload
203
220
  raise ReloadingDisabledError unless reloading_enabled?
221
+ raise SetupRequired unless @setup
204
222
 
205
223
  unload
206
224
  recompute_ignored_paths
@@ -208,58 +226,6 @@ module Zeitwerk
208
226
  setup
209
227
  end
210
228
 
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
229
  # Says if the given constant path would be unloaded on reload. This
264
230
  # predicate returns `false` if reloading is disabled.
265
231
  #
@@ -282,7 +248,16 @@ module Zeitwerk
282
248
  # @sig () -> void
283
249
  def unregister
284
250
  Registry.unregister_loader(self)
285
- ExplicitNamespace.unregister_loader(self)
251
+ ExplicitNamespace.__unregister_loader(self)
252
+ end
253
+
254
+ # The return value of this predicate is only meaningful if the loader has
255
+ # scanned the file. This is the case in the spots where we use it.
256
+ #
257
+ # @private
258
+ # @sig (String) -> Boolean
259
+ def shadowed_file?(file)
260
+ shadowed_files.member?(file)
286
261
  end
287
262
 
288
263
  # --- Class methods ---------------------------------------------------------------------------
@@ -311,11 +286,32 @@ module Zeitwerk
311
286
  Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
312
287
  end
313
288
 
314
- # Broadcasts `eager_load` to all loaders.
289
+ # Broadcasts `eager_load` to all loaders. Those that have not been setup
290
+ # are skipped.
315
291
  #
316
292
  # @sig () -> void
317
293
  def eager_load_all
318
- Registry.loaders.each(&:eager_load)
294
+ Registry.loaders.each do |loader|
295
+ begin
296
+ loader.eager_load
297
+ rescue SetupRequired
298
+ # This is fine, we eager load what can be eager loaded.
299
+ end
300
+ end
301
+ end
302
+
303
+ # Broadcasts `eager_load_namespace` to all loaders. Those that have not
304
+ # been setup are skipped.
305
+ #
306
+ # @sig (Module) -> void
307
+ def eager_load_namespace(mod)
308
+ Registry.loaders.each do |loader|
309
+ begin
310
+ loader.eager_load_namespace(mod)
311
+ rescue SetupRequired
312
+ # This is fine, we eager load what can be eager loaded.
313
+ end
314
+ end
319
315
  end
320
316
 
321
317
  # Returns an array with the absolute paths of the root directories of all
@@ -338,19 +334,11 @@ module Zeitwerk
338
334
  cname = inflector.camelize(basename, abspath).to_sym
339
335
  autoload_file(parent, cname, abspath)
340
336
  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)
337
+ if collapse?(abspath)
338
+ set_autoloads_in_dir(abspath, parent)
339
+ else
348
340
  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
341
+ autoload_subdir(parent, cname, abspath)
354
342
  end
355
343
  end
356
344
  rescue ::NameError => error
@@ -380,10 +368,10 @@ module Zeitwerk
380
368
  # We do not need to issue another autoload, the existing one is enough
381
369
  # no matter if it is for a file or a directory. Just remember the
382
370
  # subdirectory has to be visited if the namespace is used.
383
- lazy_subdirs[cpath] << subdir
371
+ namespace_dirs[cpath] << subdir
384
372
  elsif !cdef?(parent, cname)
385
373
  # First time we find this namespace, set an autoload for it.
386
- lazy_subdirs[cpath(parent, cname)] << subdir
374
+ namespace_dirs[cpath(parent, cname)] << subdir
387
375
  set_autoload(parent, cname, subdir)
388
376
  else
389
377
  # For whatever reason the constant that corresponds to this namespace has
@@ -398,6 +386,7 @@ module Zeitwerk
398
386
  if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
399
387
  # First autoload for a Ruby file wins, just ignore subsequent ones.
400
388
  if ruby?(autoload_path)
389
+ shadowed_files << file
401
390
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
402
391
  else
403
392
  promote_namespace_from_implicit_to_explicit(
@@ -408,6 +397,7 @@ module Zeitwerk
408
397
  )
409
398
  end
410
399
  elsif cdef?(parent, cname)
400
+ shadowed_files << file
411
401
  log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
412
402
  else
413
403
  set_autoload(parent, cname, file)
@@ -460,25 +450,26 @@ module Zeitwerk
460
450
 
461
451
  # @sig (String) -> void
462
452
  def register_explicit_namespace(cpath)
463
- ExplicitNamespace.register(cpath, self)
453
+ ExplicitNamespace.__register(cpath, self)
464
454
  end
465
455
 
466
456
  # @sig (String) -> void
467
457
  def raise_if_conflicting_directory(dir)
468
458
  MUTEX.synchronize do
459
+ dir_slash = dir + "/"
460
+
469
461
  Registry.loaders.each do |loader|
470
462
  next if loader == self
471
- next if loader.ignores?(dir)
463
+ next if loader.__ignores?(dir)
472
464
 
473
- dir = dir + "/"
474
- loader.root_dirs.each do |root_dir, _namespace|
465
+ loader.__roots.each_key do |root_dir|
475
466
  next if ignores?(root_dir)
476
467
 
477
- root_dir = root_dir + "/"
478
- if dir.start_with?(root_dir) || root_dir.start_with?(dir)
468
+ root_dir_slash = root_dir + "/"
469
+ if dir_slash.start_with?(root_dir_slash) || root_dir_slash.start_with?(dir_slash)
479
470
  require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
480
471
  raise Error,
481
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
472
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
482
473
  " which is already managed by\n\n#{loader.pretty_inspect}\n"
483
474
  EOS
484
475
  end
@@ -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.6"
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.6
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: 2022-11-08 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