zeitwerk 2.5.4 → 2.6.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,12 +12,28 @@ module Zeitwerk::Loader::Helpers
14
12
  # --- Files and directories ---------------------------------------------------------------------
15
13
 
16
14
  # @sig (String) { (String, String) -> void } -> void
17
- def ls(dir)
18
- Dir.each_child(dir) do |basename|
15
+ private def ls(dir)
16
+ children = Dir.children(dir)
17
+
18
+ # The order in which a directory is listed depends on the file system.
19
+ #
20
+ # Since client code may run in different platforms, it seems convenient to
21
+ # order directory entries. This provides consistent eager loading across
22
+ # platforms, for example.
23
+ children.sort!
24
+
25
+ children.each do |basename|
19
26
  next if hidden?(basename)
20
27
 
21
28
  abspath = File.join(dir, basename)
22
- next if ignored_paths.member?(abspath)
29
+ next if ignored_path?(abspath)
30
+
31
+ if dir?(abspath)
32
+ next if roots.key?(abspath)
33
+ next if !has_at_least_one_ruby_file?(abspath)
34
+ else
35
+ next unless ruby?(abspath)
36
+ end
23
37
 
24
38
  # We freeze abspath because that saves allocations when passed later to
25
39
  # File methods. See #125.
@@ -28,20 +42,46 @@ module Zeitwerk::Loader::Helpers
28
42
  end
29
43
 
30
44
  # @sig (String) -> bool
31
- def ruby?(path)
45
+ private def has_at_least_one_ruby_file?(dir)
46
+ to_visit = [dir]
47
+
48
+ while dir = to_visit.shift
49
+ ls(dir) do |_basename, abspath|
50
+ if dir?(abspath)
51
+ to_visit << abspath
52
+ else
53
+ return true
54
+ end
55
+ end
56
+ end
57
+
58
+ false
59
+ end
60
+
61
+ # @sig (String) -> bool
62
+ private def ruby?(path)
32
63
  path.end_with?(".rb")
33
64
  end
34
65
 
35
66
  # @sig (String) -> bool
36
- def dir?(path)
67
+ private def dir?(path)
37
68
  File.directory?(path)
38
69
  end
39
70
 
40
- # @sig String -> bool
41
- def hidden?(basename)
71
+ # @sig (String) -> bool
72
+ private def hidden?(basename)
42
73
  basename.start_with?(".")
43
74
  end
44
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
+
45
85
  # --- Constants ---------------------------------------------------------------------------------
46
86
 
47
87
  # The autoload? predicate takes into account the ancestor chain of the
@@ -62,11 +102,11 @@ module Zeitwerk::Loader::Helpers
62
102
  #
63
103
  # @sig (Module, Symbol) -> String?
64
104
  if method(:autoload?).arity == 1
65
- def strict_autoload_path(parent, cname)
105
+ private def strict_autoload_path(parent, cname)
66
106
  parent.autoload?(cname) if cdef?(parent, cname)
67
107
  end
68
108
  else
69
- def strict_autoload_path(parent, cname)
109
+ private def strict_autoload_path(parent, cname)
70
110
  parent.autoload?(cname, false)
71
111
  end
72
112
  end
@@ -75,23 +115,73 @@ module Zeitwerk::Loader::Helpers
75
115
  if Symbol.method_defined?(:name)
76
116
  # Symbol#name was introduced in Ruby 3.0. It returns always the same
77
117
  # frozen object, so we may save a few string allocations.
78
- def cpath(parent, cname)
118
+ private def cpath(parent, cname)
79
119
  Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
80
120
  end
81
121
  else
82
- def cpath(parent, cname)
122
+ private def cpath(parent, cname)
83
123
  Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
84
124
  end
85
125
  end
86
126
 
87
127
  # @sig (Module, Symbol) -> bool
88
- def cdef?(parent, cname)
128
+ private def cdef?(parent, cname)
89
129
  parent.const_defined?(cname, false)
90
130
  end
91
131
 
92
132
  # @raise [NameError]
93
133
  # @sig (Module, Symbol) -> Object
94
- def cget(parent, cname)
134
+ private def cget(parent, cname)
95
135
  parent.const_get(cname, false)
96
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
97
187
  end