zeitwerk 2.6.0 → 2.6.12

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.
@@ -0,0 +1,234 @@
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
+ basename = File.basename(dir)
49
+ return if hidden?(basename)
50
+
51
+ unless collapse?(dir)
52
+ cnames << inflector.camelize(basename, dir).to_sym
53
+ end
54
+ end
55
+
56
+ raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
57
+
58
+ return if @eager_loaded
59
+
60
+ namespace = root_namespace
61
+ cnames.reverse_each do |cname|
62
+ # Can happen if there are no Ruby files. This is not an error condition,
63
+ # the directory is actually managed. Could have Ruby files later.
64
+ return unless cdef?(namespace, cname)
65
+ namespace = cget(namespace, cname)
66
+ end
67
+
68
+ # A shortcircuiting test depends on the invocation of this method. Please
69
+ # keep them in sync if refactored.
70
+ actual_eager_load_dir(abspath, namespace)
71
+ end
72
+
73
+ # @sig (Module) -> void
74
+ def eager_load_namespace(mod)
75
+ raise Zeitwerk::SetupRequired unless @setup
76
+
77
+ unless mod.is_a?(Module)
78
+ raise Zeitwerk::Error, "#{mod.inspect} is not a class or module object"
79
+ end
80
+
81
+ return if @eager_loaded
82
+
83
+ mod_name = real_mod_name(mod)
84
+ return unless mod_name
85
+
86
+ actual_roots.each do |root_dir, root_namespace|
87
+ if mod.equal?(Object)
88
+ # A shortcircuiting test depends on the invocation of this method.
89
+ # Please keep them in sync if refactored.
90
+ actual_eager_load_dir(root_dir, root_namespace)
91
+ elsif root_namespace.equal?(Object)
92
+ eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
93
+ else
94
+ root_namespace_name = real_mod_name(root_namespace)
95
+ if root_namespace_name.start_with?(mod_name + "::")
96
+ actual_eager_load_dir(root_dir, root_namespace)
97
+ elsif mod_name == root_namespace_name
98
+ actual_eager_load_dir(root_dir, root_namespace)
99
+ elsif mod_name.start_with?(root_namespace_name + "::")
100
+ eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
101
+ else
102
+ # Unrelated constant hierarchies, do nothing.
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ # Loads the given Ruby file.
109
+ #
110
+ # Raises if the argument is ignored, shadowed, or not managed by the receiver.
111
+ #
112
+ # The method is implemented as `constantize` for files, in a sense, to be able
113
+ # to descend orderly and make sure the file is loadable.
114
+ #
115
+ # @sig (String | Pathname) -> void
116
+ def load_file(path)
117
+ abspath = File.expand_path(path)
118
+
119
+ raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
120
+ raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
121
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
122
+
123
+ basename = File.basename(abspath, ".rb")
124
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
125
+
126
+ base_cname = inflector.camelize(basename, abspath).to_sym
127
+
128
+ root_namespace = nil
129
+ cnames = []
130
+
131
+ walk_up(File.dirname(abspath)) do |dir|
132
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(dir)
133
+
134
+ break if root_namespace = roots[dir]
135
+
136
+ basename = File.basename(dir)
137
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
138
+
139
+ unless collapse?(dir)
140
+ cnames << inflector.camelize(basename, dir).to_sym
141
+ end
142
+ end
143
+
144
+ raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
145
+
146
+ namespace = root_namespace
147
+ cnames.reverse_each do |cname|
148
+ namespace = cget(namespace, cname)
149
+ end
150
+
151
+ raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
152
+
153
+ cget(namespace, base_cname)
154
+ end
155
+
156
+ # The caller is responsible for making sure `namespace` is the namespace that
157
+ # corresponds to `dir`.
158
+ #
159
+ # @sig (String, Module, Boolean) -> void
160
+ private def actual_eager_load_dir(dir, namespace, force: false)
161
+ honour_exclusions = !force
162
+ return if honour_exclusions && excluded_from_eager_load?(dir)
163
+
164
+ log("eager load directory #{dir} start") if logger
165
+
166
+ queue = [[dir, namespace]]
167
+ while to_eager_load = queue.shift
168
+ dir, namespace = to_eager_load
169
+
170
+ ls(dir) do |basename, abspath|
171
+ next if honour_exclusions && eager_load_exclusions.member?(abspath)
172
+
173
+ if ruby?(abspath)
174
+ if (cref = autoloads[abspath])
175
+ cget(*cref)
176
+ end
177
+ else
178
+ if collapse?(abspath)
179
+ queue << [abspath, namespace]
180
+ else
181
+ cname = inflector.camelize(basename, abspath).to_sym
182
+ queue << [abspath, cget(namespace, cname)]
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ log("eager load directory #{dir} end") if logger
189
+ end
190
+
191
+ # In order to invoke this method, the caller has to ensure `child` is a
192
+ # strict namespace descendant of `root_namespace`.
193
+ #
194
+ # @sig (Module, String, Module, Boolean) -> void
195
+ private def eager_load_child_namespace(child, child_name, root_dir, root_namespace)
196
+ suffix = child_name
197
+ unless root_namespace.equal?(Object)
198
+ suffix = suffix.delete_prefix(real_mod_name(root_namespace) + "::")
199
+ end
200
+
201
+ # These directories are at the same namespace level, there may be more if
202
+ # we find collapsed ones. As we scan, we look for matches for the first
203
+ # segment, and store them in `next_dirs`. If there are any, we look for
204
+ # the next segments in those matches. Repeat.
205
+ #
206
+ # If we exhaust the search locating directories that match all segments,
207
+ # we just need to eager load those ones.
208
+ dirs = [root_dir]
209
+ next_dirs = []
210
+
211
+ suffix.split("::").each do |segment|
212
+ while dir = dirs.shift
213
+ ls(dir) do |basename, abspath|
214
+ next unless dir?(abspath)
215
+
216
+ if collapse?(abspath)
217
+ dirs << abspath
218
+ elsif segment == inflector.camelize(basename, abspath)
219
+ next_dirs << abspath
220
+ end
221
+ end
222
+ end
223
+
224
+ return if next_dirs.empty?
225
+
226
+ dirs.replace(next_dirs)
227
+ next_dirs.clear
228
+ end
229
+
230
+ dirs.each do |dir|
231
+ actual_eager_load_dir(dir, child)
232
+ end
233
+ end
234
+ 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,73 @@ 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
137
+
138
+ # @raise [NameError]
139
+ # @sig (Module, Symbol) -> Object
140
+ private def crem(parent, cname)
141
+ parent.__send__(:remove_const, cname)
142
+ end
143
+
144
+ CNAME_VALIDATOR = Module.new
145
+ private_constant :CNAME_VALIDATOR
146
+
147
+ # @raise [Zeitwerk::NameError]
148
+ # @sig (String, String) -> Symbol
149
+ private def cname_for(basename, abspath)
150
+ cname = inflector.camelize(basename, abspath)
151
+
152
+ unless cname.is_a?(String)
153
+ raise TypeError, "#{inflector.class}#camelize must return a String, received #{cname.inspect}"
154
+ end
155
+
156
+ if cname.include?("::")
157
+ raise Zeitwerk::NameError.new(<<~MESSAGE, cname)
158
+ wrong constant name #{cname} inferred by #{inflector.class} from
159
+
160
+ #{abspath}
161
+
162
+ #{inflector.class}#camelize should return a simple constant name without "::"
163
+ MESSAGE
164
+ end
165
+
166
+ begin
167
+ CNAME_VALIDATOR.const_defined?(cname, false)
168
+ rescue ::NameError => error
169
+ path_type = ruby?(abspath) ? "file" : "directory"
170
+
171
+ raise Zeitwerk::NameError.new(<<~MESSAGE, error.name)
172
+ #{error.message} inferred by #{inflector.class} from #{path_type}
173
+
174
+ #{abspath}
175
+
176
+ Possible ways to address this:
177
+
178
+ * Tell Zeitwerk to ignore this particular #{path_type}.
179
+ * Tell Zeitwerk to ignore one of its parent directories.
180
+ * Rename the #{path_type} to comply with the naming conventions.
181
+ * Modify the inflector to handle this case.
182
+ MESSAGE
183
+ end
184
+
185
+ cname.to_sym
186
+ end
129
187
  end