zeitwerk 2.6.2 → 2.6.10

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.
@@ -4,85 +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 order,
8
- # easily handle duplicates, have a fast lookup needed for detecting nested
9
- # paths, and store 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
18
  # "/Users/fxn/blog/app/channels" => Object,
12
19
  # "/Users/fxn/blog/app/adapters" => ActiveJob::QueueAdapters,
13
20
  # ...
14
21
  #
22
+ # Stored in a hash to preserve order, easily handle duplicates, and have a
23
+ # fast lookup by directory.
24
+ #
15
25
  # This is a private collection maintained by the loader. The public
16
26
  # interface for it is `push_dir` and `dirs`.
17
27
  #
18
- # @private
19
28
  # @sig Hash[String, Module]
20
- attr_reader :root_dirs
21
-
22
- # @sig #camelize
23
- attr_accessor :inflector
29
+ attr_reader :roots
30
+ internal :roots
24
31
 
25
32
  # Absolute paths of files, directories, or glob patterns to be totally
26
33
  # ignored.
27
34
  #
28
- # @private
29
35
  # @sig Set[String]
30
36
  attr_reader :ignored_glob_patterns
37
+ private :ignored_glob_patterns
31
38
 
32
39
  # The actual collection of absolute file and directory names at the time the
33
40
  # ignored glob patterns were expanded. Computed on setup, and recomputed on
34
41
  # reload.
35
42
  #
36
- # @private
37
43
  # @sig Set[String]
38
44
  attr_reader :ignored_paths
45
+ private :ignored_paths
39
46
 
40
47
  # Absolute paths of directories or glob patterns to be collapsed.
41
48
  #
42
- # @private
43
49
  # @sig Set[String]
44
50
  attr_reader :collapse_glob_patterns
51
+ private :collapse_glob_patterns
45
52
 
46
53
  # The actual collection of absolute directory names at the time the collapse
47
54
  # glob patterns were expanded. Computed on setup, and recomputed on reload.
48
55
  #
49
- # @private
50
56
  # @sig Set[String]
51
57
  attr_reader :collapse_dirs
58
+ private :collapse_dirs
52
59
 
53
60
  # Absolute paths of files or directories not to be eager loaded.
54
61
  #
55
- # @private
56
62
  # @sig Set[String]
57
63
  attr_reader :eager_load_exclusions
64
+ private :eager_load_exclusions
58
65
 
59
66
  # User-oriented callbacks to be fired on setup and on reload.
60
67
  #
61
- # @private
62
68
  # @sig Array[{ () -> void }]
63
69
  attr_reader :on_setup_callbacks
70
+ private :on_setup_callbacks
64
71
 
65
72
  # User-oriented callbacks to be fired when a constant is loaded.
66
73
  #
67
- # @private
68
74
  # @sig Hash[String, Array[{ (Object, String) -> void }]]
69
75
  # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
70
76
  attr_reader :on_load_callbacks
77
+ private :on_load_callbacks
71
78
 
72
79
  # User-oriented callbacks to be fired before constants are removed.
73
80
  #
74
- # @private
75
81
  # @sig Hash[String, Array[{ (Object, String) -> void }]]
76
82
  # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
77
83
  attr_reader :on_unload_callbacks
78
-
79
- # @sig #call | #debug | nil
80
- attr_accessor :logger
84
+ private :on_unload_callbacks
81
85
 
82
86
  def initialize
83
- @initialized_at = Time.now
84
- @root_dirs = {}
85
87
  @inflector = Zeitwerk::Inflector.new
88
+ @logger = self.class.default_logger
89
+ @tag = SecureRandom.hex(3)
90
+ @initialized_at = Time.now
91
+ @roots = {}
86
92
  @ignored_glob_patterns = Set.new
87
93
  @ignored_paths = Set.new
88
94
  @collapse_glob_patterns = Set.new
@@ -92,8 +98,6 @@ module Zeitwerk::Loader::Config
92
98
  @on_setup_callbacks = []
93
99
  @on_load_callbacks = {}
94
100
  @on_unload_callbacks = {}
95
- @logger = self.class.default_logger
96
- @tag = SecureRandom.hex(3)
97
101
  end
98
102
 
99
103
  # Pushes `path` to the list of root directories.
@@ -105,15 +109,18 @@ module Zeitwerk::Loader::Config
105
109
  # @raise [Zeitwerk::Error]
106
110
  # @sig (String | Pathname, Module) -> void
107
111
  def push_dir(path, namespace: Object)
108
- # Note that Class < Module.
109
- unless namespace.is_a?(Module)
112
+ unless namespace.is_a?(Module) # Note that Class < Module.
110
113
  raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
111
114
  end
112
115
 
116
+ unless real_mod_name(namespace)
117
+ raise Zeitwerk::Error, "root namespaces cannot be anonymous"
118
+ end
119
+
113
120
  abspath = File.expand_path(path)
114
121
  if dir?(abspath)
115
122
  raise_if_conflicting_directory(abspath)
116
- root_dirs[abspath] = namespace
123
+ roots[abspath] = namespace
117
124
  else
118
125
  raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
119
126
  end
@@ -141,14 +148,24 @@ module Zeitwerk::Loader::Config
141
148
  # instead. Keys are the absolute paths of the root directories as strings,
142
149
  # values are their corresponding namespaces, class or module objects.
143
150
  #
151
+ # If `ignored` is falsey (default), ignored root directories are filtered out.
152
+ #
144
153
  # These are read-only collections, please add to them with `push_dir`.
145
154
  #
146
155
  # @sig () -> Array[String] | Hash[String, Module]
147
- def dirs(namespaces: false)
156
+ def dirs(namespaces: false, ignored: false)
148
157
  if namespaces
149
- root_dirs.clone
158
+ if ignored || ignored_paths.empty?
159
+ roots.clone
160
+ else
161
+ roots.reject { |root_dir, _namespace| ignored_path?(root_dir) }
162
+ end
150
163
  else
151
- root_dirs.keys
164
+ if ignored || ignored_paths.empty?
165
+ roots.keys
166
+ else
167
+ roots.keys.reject { |root_dir| ignored_path?(root_dir) }
168
+ end
152
169
  end.freeze
153
170
  end
154
171
 
@@ -275,71 +292,73 @@ module Zeitwerk::Loader::Config
275
292
  # Returns true if the argument has been configured to be ignored, or is a
276
293
  # descendant of an ignored directory.
277
294
  #
278
- # @private
279
295
  # @sig (String) -> bool
280
- def ignores?(abspath)
296
+ internal def ignores?(abspath)
281
297
  # Common use case.
282
298
  return false if ignored_paths.empty?
283
299
 
284
- walk_up(abspath) do |abspath|
285
- return true if ignored_paths.member?(abspath)
286
- return false if root_dirs.key?(abspath)
300
+ walk_up(abspath) do |path|
301
+ return true if ignored_path?(path)
302
+ return false if roots.key?(path)
287
303
  end
288
304
 
289
305
  false
290
306
  end
291
307
 
292
- private
308
+ # @sig (String) -> bool
309
+ private def ignored_path?(abspath)
310
+ ignored_paths.member?(abspath)
311
+ end
293
312
 
294
313
  # @sig () -> Array[String]
295
- def actual_root_dirs
296
- root_dirs.reject do |root_dir, _namespace|
297
- !dir?(root_dir) || ignored_paths.member?(root_dir)
314
+ private def actual_roots
315
+ roots.reject do |root_dir, _root_namespace|
316
+ !dir?(root_dir) || ignored_path?(root_dir)
298
317
  end
299
318
  end
300
319
 
301
320
  # @sig (String) -> bool
302
- def root_dir?(dir)
303
- root_dirs.key?(dir)
321
+ private def root_dir?(dir)
322
+ roots.key?(dir)
304
323
  end
305
324
 
306
325
  # @sig (String) -> bool
307
- def excluded_from_eager_load?(abspath)
326
+ private def excluded_from_eager_load?(abspath)
308
327
  # Optimize this common use case.
309
328
  return false if eager_load_exclusions.empty?
310
329
 
311
- walk_up(abspath) do |abspath|
312
- return true if eager_load_exclusions.member?(abspath)
313
- return false if root_dirs.key?(abspath)
330
+ walk_up(abspath) do |path|
331
+ return true if eager_load_exclusions.member?(path)
332
+ return false if roots.key?(path)
314
333
  end
315
334
 
316
335
  false
317
336
  end
318
337
 
319
338
  # @sig (String) -> bool
320
- def collapse?(dir)
339
+ private def collapse?(dir)
321
340
  collapse_dirs.member?(dir)
322
341
  end
323
342
 
324
343
  # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
325
- def expand_paths(paths)
344
+ private def expand_paths(paths)
326
345
  paths.flatten.map! { |path| File.expand_path(path) }
327
346
  end
328
347
 
329
348
  # @sig (Array[String]) -> Array[String]
330
- def expand_glob_patterns(glob_patterns)
349
+ private def expand_glob_patterns(glob_patterns)
331
350
  # Note that Dir.glob works with regular file names just fine. That is,
332
351
  # glob patterns technically need no wildcards.
333
352
  glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
334
353
  end
335
354
 
336
355
  # @sig () -> void
337
- def recompute_ignored_paths
356
+ private def recompute_ignored_paths
338
357
  ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
339
358
  end
340
359
 
341
360
  # @sig () -> void
342
- def recompute_collapse_dirs
361
+ private def recompute_collapse_dirs
343
362
  collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
344
363
  end
345
364
  end
@@ -9,11 +9,12 @@ module Zeitwerk::Loader::EagerLoad
9
9
  def eager_load(force: false)
10
10
  mutex.synchronize do
11
11
  break if @eager_loaded
12
+ raise Zeitwerk::SetupRequired unless @setup
12
13
 
13
14
  log("eager load start") if logger
14
15
 
15
- actual_root_dirs.each do |root_dir, namespace|
16
- actual_eager_load_dir(root_dir, namespace, force: force)
16
+ actual_roots.each do |root_dir, root_namespace|
17
+ actual_eager_load_dir(root_dir, root_namespace, force: force)
17
18
  end
18
19
 
19
20
  autoloaded_dirs.each do |autoloaded_dir|
@@ -29,6 +30,8 @@ module Zeitwerk::Loader::EagerLoad
29
30
 
30
31
  # @sig (String | Pathname) -> void
31
32
  def eager_load_dir(path)
33
+ raise Zeitwerk::SetupRequired unless @setup
34
+
32
35
  abspath = File.expand_path(path)
33
36
 
34
37
  raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
@@ -37,13 +40,15 @@ module Zeitwerk::Loader::EagerLoad
37
40
 
38
41
  root_namespace = nil
39
42
  walk_up(abspath) do |dir|
40
- return if ignored_paths.member?(dir)
43
+ return if ignored_path?(dir)
41
44
  return if eager_load_exclusions.member?(dir)
42
45
 
43
- break if root_namespace = root_dirs[dir]
46
+ break if root_namespace = roots[dir]
47
+
48
+ basename = File.basename(dir)
49
+ return if hidden?(basename)
44
50
 
45
51
  unless collapse?(dir)
46
- basename = File.basename(dir)
47
52
  cnames << inflector.camelize(basename, dir).to_sym
48
53
  end
49
54
  end
@@ -67,29 +72,32 @@ module Zeitwerk::Loader::EagerLoad
67
72
 
68
73
  # @sig (Module) -> void
69
74
  def eager_load_namespace(mod)
75
+ raise Zeitwerk::SetupRequired unless @setup
76
+
70
77
  unless mod.is_a?(Module)
71
78
  raise Zeitwerk::Error, "#{mod.inspect} is not a class or module object"
72
79
  end
73
80
 
74
81
  return if @eager_loaded
75
82
 
76
- actual_root_dirs.each do |root_dir, root_namespace|
83
+ mod_name = real_mod_name(mod)
84
+ return unless mod_name
85
+
86
+ actual_roots.each do |root_dir, root_namespace|
77
87
  if mod.equal?(Object)
78
88
  # A shortcircuiting test depends on the invocation of this method.
79
89
  # Please keep them in sync if refactored.
80
90
  actual_eager_load_dir(root_dir, root_namespace)
81
91
  elsif root_namespace.equal?(Object)
82
- eager_load_child_namespace(mod, root_dir, root_namespace)
92
+ eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
83
93
  else
84
- mod_name = real_mod_name(mod)
85
94
  root_namespace_name = real_mod_name(root_namespace)
86
-
87
95
  if root_namespace_name.start_with?(mod_name + "::")
88
96
  actual_eager_load_dir(root_dir, root_namespace)
89
97
  elsif mod_name == root_namespace_name
90
98
  actual_eager_load_dir(root_dir, root_namespace)
91
99
  elsif mod_name.start_with?(root_namespace_name + "::")
92
- eager_load_child_namespace(mod, root_dir, root_namespace)
100
+ eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
93
101
  else
94
102
  # Unrelated constant hierarchies, do nothing.
95
103
  end
@@ -110,21 +118,25 @@ module Zeitwerk::Loader::EagerLoad
110
118
 
111
119
  raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
112
120
  raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
113
- raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_paths.member?(abspath)
121
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
114
122
 
115
123
  basename = File.basename(abspath, ".rb")
124
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
125
+
116
126
  base_cname = inflector.camelize(basename, abspath).to_sym
117
127
 
118
128
  root_namespace = nil
119
129
  cnames = []
120
130
 
121
131
  walk_up(File.dirname(abspath)) do |dir|
122
- raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_paths.member?(dir)
132
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(dir)
133
+
134
+ break if root_namespace = roots[dir]
123
135
 
124
- break if root_namespace = root_dirs[dir]
136
+ basename = File.basename(dir)
137
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
125
138
 
126
139
  unless collapse?(dir)
127
- basename = File.basename(dir)
128
140
  cnames << inflector.camelize(basename, dir).to_sym
129
141
  end
130
142
  end
@@ -177,11 +189,11 @@ module Zeitwerk::Loader::EagerLoad
177
189
  end
178
190
 
179
191
  # In order to invoke this method, the caller has to ensure `child` is a
180
- # strict namespace descendendant of `root_namespace`.
192
+ # strict namespace descendant of `root_namespace`.
181
193
  #
182
194
  # @sig (Module, String, Module, Boolean) -> void
183
- private def eager_load_child_namespace(child, root_dir, root_namespace)
184
- suffix = real_mod_name(child)
195
+ private def eager_load_child_namespace(child, child_name, root_dir, root_namespace)
196
+ suffix = child_name
185
197
  unless root_namespace.equal?(Object)
186
198
  suffix = suffix.delete_prefix(real_mod_name(root_namespace) + "::")
187
199
  end
@@ -202,7 +214,7 @@ module Zeitwerk::Loader::EagerLoad
202
214
  next unless dir?(abspath)
203
215
 
204
216
  if collapse?(abspath)
205
- current_dirs << abspath
217
+ dirs << abspath
206
218
  elsif segment == inflector.camelize(basename, abspath)
207
219
  next_dirs << abspath
208
220
  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,10 @@ 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 if root_dirs.key?(abspath)
32
+ next if roots.key?(abspath)
35
33
  next if !has_at_least_one_ruby_file?(abspath)
36
34
  else
37
35
  next unless ruby?(abspath)
@@ -44,7 +42,7 @@ module Zeitwerk::Loader::Helpers
44
42
  end
45
43
 
46
44
  # @sig (String) -> bool
47
- def has_at_least_one_ruby_file?(dir)
45
+ private def has_at_least_one_ruby_file?(dir)
48
46
  to_visit = [dir]
49
47
 
50
48
  while dir = to_visit.shift
@@ -61,22 +59,22 @@ module Zeitwerk::Loader::Helpers
61
59
  end
62
60
 
63
61
  # @sig (String) -> bool
64
- def ruby?(path)
62
+ private def ruby?(path)
65
63
  path.end_with?(".rb")
66
64
  end
67
65
 
68
66
  # @sig (String) -> bool
69
- def dir?(path)
67
+ private def dir?(path)
70
68
  File.directory?(path)
71
69
  end
72
70
 
73
71
  # @sig (String) -> bool
74
- def hidden?(basename)
72
+ private def hidden?(basename)
75
73
  basename.start_with?(".")
76
74
  end
77
75
 
78
76
  # @sig (String) { (String) -> void } -> void
79
- def walk_up(abspath)
77
+ private def walk_up(abspath)
80
78
  loop do
81
79
  yield abspath
82
80
  abspath, basename = File.split(abspath)
@@ -104,11 +102,11 @@ module Zeitwerk::Loader::Helpers
104
102
  #
105
103
  # @sig (Module, Symbol) -> String?
106
104
  if method(:autoload?).arity == 1
107
- def strict_autoload_path(parent, cname)
105
+ private def strict_autoload_path(parent, cname)
108
106
  parent.autoload?(cname) if cdef?(parent, cname)
109
107
  end
110
108
  else
111
- def strict_autoload_path(parent, cname)
109
+ private def strict_autoload_path(parent, cname)
112
110
  parent.autoload?(cname, false)
113
111
  end
114
112
  end
@@ -117,23 +115,73 @@ module Zeitwerk::Loader::Helpers
117
115
  if Symbol.method_defined?(:name)
118
116
  # Symbol#name was introduced in Ruby 3.0. It returns always the same
119
117
  # frozen object, so we may save a few string allocations.
120
- def cpath(parent, cname)
118
+ private def cpath(parent, cname)
121
119
  Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
122
120
  end
123
121
  else
124
- def cpath(parent, cname)
122
+ private def cpath(parent, cname)
125
123
  Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
126
124
  end
127
125
  end
128
126
 
129
127
  # @sig (Module, Symbol) -> bool
130
- def cdef?(parent, cname)
128
+ private def cdef?(parent, cname)
131
129
  parent.const_defined?(cname, false)
132
130
  end
133
131
 
134
132
  # @raise [NameError]
135
133
  # @sig (Module, Symbol) -> Object
136
- def cget(parent, cname)
134
+ private def cget(parent, cname)
137
135
  parent.const_get(cname, false)
138
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
139
187
  end