zeitwerk 2.6.7 → 2.6.18

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.
@@ -2,38 +2,45 @@
2
2
 
3
3
  module Zeitwerk::Loader::Callbacks
4
4
  include Zeitwerk::RealModName
5
+ extend Zeitwerk::Internal
5
6
 
6
7
  # Invoked from our decorated Kernel#require when a managed file is autoloaded.
7
8
  #
8
- # @private
9
9
  # @sig (String) -> void
10
- def on_file_autoloaded(file)
11
- cref = autoloads.delete(file)
12
- cpath = cpath(*cref)
10
+ internal def on_file_autoloaded(file)
11
+ cref = autoloads.delete(file)
13
12
 
14
13
  Zeitwerk::Registry.unregister_autoload(file)
15
14
 
16
- if cdef?(*cref)
17
- log("constant #{cpath} loaded from file #{file}") if logger
18
- to_unload[cpath] = [file, cref] if reloading_enabled?
19
- run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
15
+ if cref.defined?
16
+ log("constant #{cref.path} loaded from file #{file}") if logger
17
+ to_unload[cref.path] = [file, cref] if reloading_enabled?
18
+ run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
20
19
  else
21
- msg = "expected file #{file} to define constant #{cpath}, but didn't"
20
+ msg = "expected file #{file} to define constant #{cref.path}, but didn't"
22
21
  log(msg) if logger
23
- crem(*cref)
24
- to_unload[cpath] = [file, cref] if reloading_enabled?
25
- raise Zeitwerk::NameError.new(msg, cref.last)
22
+
23
+ # Ruby still keeps the autoload defined, but we remove it because the
24
+ # contract in Zeitwerk is more strict.
25
+ cref.remove
26
+
27
+ # Since the expected constant was not defined, there is nothing to unload.
28
+ # However, if the exception is rescued and reloading is enabled, we still
29
+ # need to deleted the file from $LOADED_FEATURES.
30
+ to_unload[cref.path] = [file, cref] if reloading_enabled?
31
+
32
+ raise Zeitwerk::NameError.new(msg, cref.cname)
26
33
  end
27
34
  end
28
35
 
29
36
  # Invoked from our decorated Kernel#require when a managed directory is
30
37
  # autoloaded.
31
38
  #
32
- # @private
33
39
  # @sig (String) -> void
34
- def on_dir_autoloaded(dir)
35
- # Module#autoload does not serialize concurrent requires, and we handle
36
- # directories ourselves, so the callback needs to account for concurrency.
40
+ internal def on_dir_autoloaded(dir)
41
+ # Module#autoload does not serialize concurrent requires in CRuby < 3.2, and
42
+ # we handle directories ourselves without going through Kernel#require, so
43
+ # the callback needs to account for concurrency.
37
44
  #
38
45
  # Multi-threading would introduce a race condition here in which thread t1
39
46
  # autovivifies the module, and while autoloads for its children are being
@@ -43,10 +50,10 @@ module Zeitwerk::Loader::Callbacks
43
50
  # That not only would reassign the constant (undesirable per se) but, worse,
44
51
  # the module object created by t2 wouldn't have any of the autoloads for its
45
52
  # children, since t1 would have correctly deleted its namespace_dirs entry.
46
- mutex2.synchronize do
53
+ dirs_autoload_monitor.synchronize do
47
54
  if cref = autoloads.delete(dir)
48
- autovivified_module = cref[0].const_set(cref[1], Module.new)
49
- cpath = autovivified_module.name
55
+ implicit_namespace = cref.set(Module.new)
56
+ cpath = implicit_namespace.name
50
57
  log("module #{cpath} autovivified from directory #{dir}") if logger
51
58
 
52
59
  to_unload[cpath] = [dir, cref] if reloading_enabled?
@@ -57,9 +64,9 @@ module Zeitwerk::Loader::Callbacks
57
64
  # these to be able to unregister later if eager loading.
58
65
  autoloaded_dirs << dir
59
66
 
60
- on_namespace_loaded(autovivified_module)
67
+ on_namespace_loaded(implicit_namespace)
61
68
 
62
- run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
69
+ run_on_load_callbacks(cpath, implicit_namespace, dir) unless on_load_callbacks.empty?
63
70
  end
64
71
  end
65
72
  end
@@ -73,7 +80,7 @@ module Zeitwerk::Loader::Callbacks
73
80
  def on_namespace_loaded(namespace)
74
81
  if dirs = namespace_dirs.delete(real_mod_name(namespace))
75
82
  dirs.each do |dir|
76
- set_autoloads_in_dir(dir, namespace)
83
+ define_autoloads_for_dir(dir, namespace)
77
84
  end
78
85
  end
79
86
  end
@@ -109,8 +109,7 @@ module Zeitwerk::Loader::Config
109
109
  # @raise [Zeitwerk::Error]
110
110
  # @sig (String | Pathname, Module) -> void
111
111
  def push_dir(path, namespace: Object)
112
- # Note that Class < Module.
113
- unless namespace.is_a?(Module)
112
+ unless namespace.is_a?(Module) # Note that Class < Module.
114
113
  raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
115
114
  end
116
115
 
@@ -298,9 +297,9 @@ module Zeitwerk::Loader::Config
298
297
  # Common use case.
299
298
  return false if ignored_paths.empty?
300
299
 
301
- walk_up(abspath) do |abspath|
302
- return true if ignored_path?(abspath)
303
- return false if roots.key?(abspath)
300
+ walk_up(abspath) do |path|
301
+ return true if ignored_path?(path)
302
+ return false if roots.key?(path)
304
303
  end
305
304
 
306
305
  false
@@ -328,9 +327,9 @@ module Zeitwerk::Loader::Config
328
327
  # Optimize this common use case.
329
328
  return false if eager_load_exclusions.empty?
330
329
 
331
- walk_up(abspath) do |abspath|
332
- return true if eager_load_exclusions.member?(abspath)
333
- return false if roots.key?(abspath)
330
+ walk_up(abspath) do |path|
331
+ return true if eager_load_exclusions.member?(path)
332
+ return false if roots.key?(path)
334
333
  end
335
334
 
336
335
  false
@@ -45,8 +45,10 @@ module Zeitwerk::Loader::EagerLoad
45
45
 
46
46
  break if root_namespace = roots[dir]
47
47
 
48
+ basename = File.basename(dir)
49
+ return if hidden?(basename)
50
+
48
51
  unless collapse?(dir)
49
- basename = File.basename(dir)
50
52
  cnames << inflector.camelize(basename, dir).to_sym
51
53
  end
52
54
  end
@@ -59,8 +61,8 @@ module Zeitwerk::Loader::EagerLoad
59
61
  cnames.reverse_each do |cname|
60
62
  # Can happen if there are no Ruby files. This is not an error condition,
61
63
  # the directory is actually managed. Could have Ruby files later.
62
- return unless cdef?(namespace, cname)
63
- namespace = cget(namespace, cname)
64
+ return unless namespace.const_defined?(cname, false)
65
+ namespace = namespace.const_get(cname, false)
64
66
  end
65
67
 
66
68
  # A shortcircuiting test depends on the invocation of this method. Please
@@ -119,6 +121,8 @@ module Zeitwerk::Loader::EagerLoad
119
121
  raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
120
122
 
121
123
  basename = File.basename(abspath, ".rb")
124
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
125
+
122
126
  base_cname = inflector.camelize(basename, abspath).to_sym
123
127
 
124
128
  root_namespace = nil
@@ -129,8 +133,10 @@ module Zeitwerk::Loader::EagerLoad
129
133
 
130
134
  break if root_namespace = roots[dir]
131
135
 
136
+ basename = File.basename(dir)
137
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
138
+
132
139
  unless collapse?(dir)
133
- basename = File.basename(dir)
134
140
  cnames << inflector.camelize(basename, dir).to_sym
135
141
  end
136
142
  end
@@ -139,12 +145,12 @@ module Zeitwerk::Loader::EagerLoad
139
145
 
140
146
  namespace = root_namespace
141
147
  cnames.reverse_each do |cname|
142
- namespace = cget(namespace, cname)
148
+ namespace = namespace.const_get(cname, false)
143
149
  end
144
150
 
145
151
  raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
146
152
 
147
- cget(namespace, base_cname)
153
+ namespace.const_get(base_cname, false)
148
154
  end
149
155
 
150
156
  # The caller is responsible for making sure `namespace` is the namespace that
@@ -158,22 +164,20 @@ module Zeitwerk::Loader::EagerLoad
158
164
  log("eager load directory #{dir} start") if logger
159
165
 
160
166
  queue = [[dir, namespace]]
161
- while to_eager_load = queue.shift
162
- dir, namespace = to_eager_load
163
-
164
- ls(dir) do |basename, abspath|
167
+ while (current_dir, namespace = queue.shift)
168
+ ls(current_dir) do |basename, abspath, ftype|
165
169
  next if honour_exclusions && eager_load_exclusions.member?(abspath)
166
170
 
167
- if ruby?(abspath)
168
- if (cref = autoloads[abspath]) && !shadowed_file?(abspath)
169
- cget(*cref)
171
+ if ftype == :file
172
+ if (cref = autoloads[abspath])
173
+ cref.get
170
174
  end
171
175
  else
172
176
  if collapse?(abspath)
173
177
  queue << [abspath, namespace]
174
178
  else
175
179
  cname = inflector.camelize(basename, abspath).to_sym
176
- queue << [abspath, cget(namespace, cname)]
180
+ queue << [abspath, namespace.const_get(cname, false)]
177
181
  end
178
182
  end
179
183
  end
@@ -203,9 +207,9 @@ module Zeitwerk::Loader::EagerLoad
203
207
  next_dirs = []
204
208
 
205
209
  suffix.split("::").each do |segment|
206
- while dir = dirs.shift
207
- ls(dir) do |basename, abspath|
208
- next unless dir?(abspath)
210
+ while (dir = dirs.shift)
211
+ ls(dir) do |basename, abspath, ftype|
212
+ next unless ftype == :directory
209
213
 
210
214
  if collapse?(abspath)
211
215
  dirs << abspath
@@ -30,27 +30,45 @@ module Zeitwerk::Loader::Helpers
30
30
 
31
31
  if dir?(abspath)
32
32
  next if roots.key?(abspath)
33
- next if !has_at_least_one_ruby_file?(abspath)
33
+
34
+ if !has_at_least_one_ruby_file?(abspath)
35
+ log("directory #{abspath} is ignored because it has no Ruby files") if logger
36
+ next
37
+ end
38
+
39
+ ftype = :directory
34
40
  else
35
41
  next unless ruby?(abspath)
42
+ ftype = :file
36
43
  end
37
44
 
38
45
  # We freeze abspath because that saves allocations when passed later to
39
46
  # File methods. See #125.
40
- yield basename, abspath.freeze
47
+ yield basename, abspath.freeze, ftype
41
48
  end
42
49
  end
43
50
 
51
+ # Looks for a Ruby file using breadth-first search. This type of search is
52
+ # important to list as less directories as possible and return fast in the
53
+ # common case in which there are Ruby files.
54
+ #
44
55
  # @sig (String) -> bool
45
56
  private def has_at_least_one_ruby_file?(dir)
46
57
  to_visit = [dir]
47
58
 
48
- while dir = to_visit.shift
49
- ls(dir) do |_basename, abspath|
59
+ while (dir = to_visit.shift)
60
+ children = Dir.children(dir)
61
+
62
+ children.each do |basename|
63
+ next if hidden?(basename)
64
+
65
+ abspath = File.join(dir, basename)
66
+ next if ignored_path?(abspath)
67
+
50
68
  if dir?(abspath)
51
- to_visit << abspath
69
+ to_visit << abspath unless roots.key?(abspath)
52
70
  else
53
- return true
71
+ return true if ruby?(abspath)
54
72
  end
55
73
  end
56
74
  end
@@ -82,62 +100,49 @@ module Zeitwerk::Loader::Helpers
82
100
  end
83
101
  end
84
102
 
85
- # --- Constants ---------------------------------------------------------------------------------
103
+ # --- Inflection --------------------------------------------------------------------------------
86
104
 
87
- # The autoload? predicate takes into account the ancestor chain of the
88
- # receiver, like const_defined? and other methods in the constants API do.
89
- #
90
- # For example, given
91
- #
92
- # class A
93
- # autoload :X, "x.rb"
94
- # end
95
- #
96
- # class B < A
97
- # end
98
- #
99
- # B.autoload?(:X) returns "x.rb".
100
- #
101
- # We need a way to strictly check in parent ignoring ancestors.
102
- #
103
- # @sig (Module, Symbol) -> String?
104
- if method(:autoload?).arity == 1
105
- private def strict_autoload_path(parent, cname)
106
- parent.autoload?(cname) if cdef?(parent, cname)
107
- end
108
- else
109
- private def strict_autoload_path(parent, cname)
110
- parent.autoload?(cname, false)
111
- end
112
- end
105
+ CNAME_VALIDATOR = Module.new
106
+ private_constant :CNAME_VALIDATOR
113
107
 
114
- # @sig (Module, Symbol) -> String
115
- if Symbol.method_defined?(:name)
116
- # Symbol#name was introduced in Ruby 3.0. It returns always the same
117
- # frozen object, so we may save a few string allocations.
118
- private def cpath(parent, cname)
119
- Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
108
+ # @raise [Zeitwerk::NameError]
109
+ # @sig (String, String) -> Symbol
110
+ private def cname_for(basename, abspath)
111
+ cname = inflector.camelize(basename, abspath)
112
+
113
+ unless cname.is_a?(String)
114
+ raise TypeError, "#{inflector.class}#camelize must return a String, received #{cname.inspect}"
120
115
  end
121
- else
122
- private def cpath(parent, cname)
123
- Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
116
+
117
+ if cname.include?("::")
118
+ raise Zeitwerk::NameError.new(<<~MESSAGE, cname)
119
+ wrong constant name #{cname} inferred by #{inflector.class} from
120
+
121
+ #{abspath}
122
+
123
+ #{inflector.class}#camelize should return a simple constant name without "::"
124
+ MESSAGE
124
125
  end
125
- end
126
126
 
127
- # @sig (Module, Symbol) -> bool
128
- private def cdef?(parent, cname)
129
- parent.const_defined?(cname, false)
130
- end
127
+ begin
128
+ CNAME_VALIDATOR.const_defined?(cname, false)
129
+ rescue ::NameError => error
130
+ path_type = ruby?(abspath) ? "file" : "directory"
131
131
 
132
- # @raise [NameError]
133
- # @sig (Module, Symbol) -> Object
134
- private def cget(parent, cname)
135
- parent.const_get(cname, false)
136
- end
132
+ raise Zeitwerk::NameError.new(<<~MESSAGE, error.name)
133
+ #{error.message} inferred by #{inflector.class} from #{path_type}
134
+
135
+ #{abspath}
136
+
137
+ Possible ways to address this:
138
+
139
+ * Tell Zeitwerk to ignore this particular #{path_type}.
140
+ * Tell Zeitwerk to ignore one of its parent directories.
141
+ * Rename the #{path_type} to comply with the naming conventions.
142
+ * Modify the inflector to handle this case.
143
+ MESSAGE
144
+ end
137
145
 
138
- # @raise [NameError]
139
- # @sig (Module, Symbol) -> Object
140
- private def crem(parent, cname)
141
- parent.__send__(:remove_const, cname)
146
+ cname.to_sym
142
147
  end
143
148
  end