zeitwerk 2.7.5 → 2.8.2

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.
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
- require "securerandom"
3
+ require 'set'
4
+ require 'securerandom'
5
5
 
6
6
  module Zeitwerk::Loader::Config
7
7
  extend Zeitwerk::Internal
8
8
  include Zeitwerk::RealModName
9
9
 
10
+ UNDEFINED = Object.new.freeze
11
+ private_constant :UNDEFINED
12
+
10
13
  #: camelize(String, String) -> String
11
14
  attr_accessor :inflector
12
15
 
@@ -15,8 +18,8 @@ module Zeitwerk::Loader::Config
15
18
 
16
19
  # Absolute paths of the root directories, mapped to their respective root namespaces:
17
20
  #
18
- # "/Users/fxn/blog/app/channels" => Object,
19
- # "/Users/fxn/blog/app/adapters" => ActiveJob::QueueAdapters,
21
+ # '/Users/fxn/blog/app/channels' => Object,
22
+ # '/Users/fxn/blog/app/adapters' => ActiveJob::QueueAdapters,
20
23
  # ...
21
24
  #
22
25
  # Stored in a hash to preserve order, easily handle duplicates, and have a
@@ -29,6 +32,12 @@ module Zeitwerk::Loader::Config
29
32
  attr_reader :roots
30
33
  internal :roots
31
34
 
35
+ # Basename of files that define namespaces. For example, if `nsfile` is
36
+ # 'ns.rb', then `foo/ns.rb` defines the `Foo` namespace.
37
+ #
38
+ #: String?
39
+ attr_reader :nsfile
40
+
32
41
  # Absolute paths of files, directories, or glob patterns to be ignored.
33
42
  #
34
43
  #: Set[String]
@@ -50,12 +59,20 @@ module Zeitwerk::Loader::Config
50
59
  private :collapse_glob_patterns
51
60
 
52
61
  # The actual collection of absolute directory names at the time the collapse
53
- # glob patterns were expanded. Computed on setup, and recomputed on reload.
62
+ # glob patterns were expanded. Computed on setup and recomputed on reload.
54
63
  #
55
64
  #: Set[String]
56
65
  attr_reader :collapse_dirs
57
66
  private :collapse_dirs
58
67
 
68
+ # Absolute paths of directories that are parents of collapsed directories.
69
+ # This is a cache to optimize some tree walks. Computed on setup and
70
+ # recomputed on reload.
71
+ #
72
+ #: Set[String]
73
+ attr_reader :collapse_parents
74
+ private :collapse_parents
75
+
59
76
  # Absolute paths of files or directories not to be eager loaded.
60
77
  #
61
78
  #: Set[String]
@@ -82,16 +99,19 @@ module Zeitwerk::Loader::Config
82
99
  attr_reader :on_unload_callbacks
83
100
  private :on_unload_callbacks
84
101
 
102
+ #: () -> void
85
103
  def initialize
86
104
  @inflector = Zeitwerk::Inflector.new
87
105
  @logger = self.class.default_logger
88
106
  @tag = SecureRandom.hex(3)
89
107
  @initialized_at = Time.now
90
108
  @roots = {}
109
+ @nsfile = nil
91
110
  @ignored_glob_patterns = Set.new
92
111
  @ignored_paths = Set.new
93
112
  @collapse_glob_patterns = Set.new
94
113
  @collapse_dirs = Set.new
114
+ @collapse_parents = Set.new
95
115
  @eager_load_exclusions = Set.new
96
116
  @reloading_enabled = false
97
117
  @on_setup_callbacks = []
@@ -112,7 +132,7 @@ module Zeitwerk::Loader::Config
112
132
  end
113
133
 
114
134
  unless real_mod_name(namespace)
115
- raise Zeitwerk::Error, "root namespaces cannot be anonymous"
135
+ raise Zeitwerk::Error, 'root namespaces cannot be anonymous'
116
136
  end
117
137
 
118
138
  abspath = File.expand_path(path)
@@ -141,6 +161,18 @@ module Zeitwerk::Loader::Config
141
161
  @tag = tag.to_s
142
162
  end
143
163
 
164
+ #: (String?) -> void ! TypeError, ArgumentError
165
+ def nsfile=(nsfile)
166
+ unless nsfile.nil?
167
+ raise TypeError, 'nsfiles must be strings' unless nsfile.is_a?(String)
168
+ raise ArgumentError, 'nsfiles must have .rb extension' unless @fs.rb_extension?(nsfile)
169
+ raise ArgumentError, 'nsfiles must be basenames, not paths' unless File.basename(nsfile) == nsfile
170
+ raise ArgumentError, 'nsfiles cannot be hidden' if @fs.hidden?(nsfile)
171
+ end
172
+
173
+ @nsfile = nsfile
174
+ end
175
+
144
176
  # If `namespaces` is falsey (default), returns an array with the absolute
145
177
  # paths of the root directories as strings. If truthy, returns a hash table
146
178
  # instead. Keys are the absolute paths of the root directories as strings,
@@ -176,7 +208,7 @@ module Zeitwerk::Loader::Config
176
208
  break if @reloading_enabled
177
209
 
178
210
  if @setup
179
- raise Zeitwerk::Error, "cannot enable reloading after setup"
211
+ raise Zeitwerk::Error, 'cannot enable reloading after setup'
180
212
  else
181
213
  @reloading_enabled = true
182
214
  end
@@ -214,7 +246,11 @@ module Zeitwerk::Loader::Config
214
246
  glob_patterns = expand_paths(glob_patterns)
215
247
  mutex.synchronize do
216
248
  collapse_glob_patterns.merge(glob_patterns)
217
- collapse_dirs.merge(expand_glob_patterns(glob_patterns))
249
+ new_collapse_dirs = expand_glob_patterns(glob_patterns)
250
+ collapse_dirs.merge(new_collapse_dirs)
251
+ new_collapse_dirs.each do |dir|
252
+ collapse_parents << File.dirname(dir)
253
+ end
218
254
  end
219
255
  end
220
256
 
@@ -233,8 +269,8 @@ module Zeitwerk::Loader::Config
233
269
  # Supports multiple callbacks, and if there are many, they are executed in
234
270
  # the order in which they were defined.
235
271
  #
236
- # loader.on_load("SomeApiClient") do |klass, _abspath|
237
- # klass.endpoint = "https://api.dev"
272
+ # loader.on_load('SomeApiClient') do |klass, _abspath|
273
+ # klass.endpoint = 'https://api.dev'
238
274
  # end
239
275
  #
240
276
  # Can also be configured for any constant loaded:
@@ -243,12 +279,19 @@ module Zeitwerk::Loader::Config
243
279
  # # ...
244
280
  # end
245
281
  #
246
- #: (String?) { (top, String) -> void } -> void ! TypeError
247
- def on_load(cpath = :ANY, &block)
248
- raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
282
+ #: (String) { (top, String) -> void } -> void ! TypeError | NameError
283
+ #| { (String, top, String) -> void } -> void
284
+ def on_load(cpath = UNDEFINED, &block)
285
+ key = if cpath.equal?(UNDEFINED)
286
+ :ANY
287
+ elsif !cpath.is_a?(String)
288
+ raise TypeError, 'on_load only accepts strings'
289
+ else
290
+ @cpv.validate!(cpath)
291
+ end
249
292
 
250
293
  mutex.synchronize do
251
- (on_load_callbacks[cpath] ||= []) << block
294
+ (on_load_callbacks[key] ||= []) << block
252
295
  end
253
296
  end
254
297
 
@@ -256,7 +299,7 @@ module Zeitwerk::Loader::Config
256
299
  # Supports multiple callbacks, and if there are many, they are executed in the
257
300
  # order in which they were defined.
258
301
  #
259
- # loader.on_unload("Country") do |klass, _abspath|
302
+ # loader.on_unload('Country') do |klass, _abspath|
260
303
  # klass.clear_cache
261
304
  # end
262
305
  #
@@ -266,12 +309,19 @@ module Zeitwerk::Loader::Config
266
309
  # # ...
267
310
  # end
268
311
  #
269
- #: (String?) { (top, String) -> void } -> void ! TypeError
270
- def on_unload(cpath = :ANY, &block)
271
- raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
312
+ #: (String) { (top, String) -> void } -> void ! TypeError | NameError
313
+ #| { (String, top, String) -> void } -> void
314
+ def on_unload(cpath = UNDEFINED, &block)
315
+ key = if cpath.equal?(UNDEFINED)
316
+ :ANY
317
+ elsif !cpath.is_a?(String)
318
+ raise TypeError, 'on_unload only accepts strings'
319
+ else
320
+ @cpv.validate!(cpath)
321
+ end
272
322
 
273
323
  mutex.synchronize do
274
- (on_unload_callbacks[cpath] ||= []) << block
324
+ (on_unload_callbacks[key] ||= []) << block
275
325
  end
276
326
  end
277
327
 
@@ -315,6 +365,16 @@ module Zeitwerk::Loader::Config
315
365
  roots.key?(dir)
316
366
  end
317
367
 
368
+ #: (String) -> bool
369
+ internal def collapse?(dir)
370
+ collapse_dirs.member?(dir)
371
+ end
372
+
373
+ #: (String) -> bool
374
+ internal def collapse_parent?(dir)
375
+ collapse_parents.member?(dir)
376
+ end
377
+
318
378
  #: (String) -> bool
319
379
  private def excluded_from_eager_load?(abspath)
320
380
  # Optimize this common use case.
@@ -328,11 +388,6 @@ module Zeitwerk::Loader::Config
328
388
  false
329
389
  end
330
390
 
331
- #: (String) -> bool
332
- private def collapse?(dir)
333
- collapse_dirs.member?(dir)
334
- end
335
-
336
391
  #: (String | Pathname | Array[String | Pathname]) -> Array[String]
337
392
  private def expand_paths(paths)
338
393
  paths.flatten.map! { |path| File.expand_path(path) }
@@ -354,4 +409,12 @@ module Zeitwerk::Loader::Config
354
409
  private def recompute_collapse_dirs
355
410
  collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
356
411
  end
412
+
413
+ #: () -> void
414
+ private def recompute_collapse_parents
415
+ collapse_parents.clear
416
+ collapse_dirs.each do |dir|
417
+ collapse_parents << File.dirname(dir)
418
+ end
419
+ end
357
420
  end
@@ -0,0 +1,17 @@
1
+ # @private
2
+ class Zeitwerk::Loader::ConstantPathValidator # :nodoc
3
+ CNAME_VALIDATOR = Module.new.freeze #: Module
4
+ private_constant :CNAME_VALIDATOR
5
+
6
+ # Technically, this validation works with symbols, but API boundary restricts
7
+ # input to strings, so we assume strings, and we test strings.
8
+ #
9
+ #: (String) -> String ! NameError
10
+ def validate!(possible_cpath)
11
+ # We do this before validating because as of this writing, TruffleRuby
12
+ # raises TypeError if the argument has leading colons.
13
+ possible_cpath = possible_cpath.delete_prefix('::')
14
+ CNAME_VALIDATOR.const_defined?(possible_cpath, false)
15
+ possible_cpath
16
+ end
17
+ end
@@ -1,9 +1,10 @@
1
1
  module Zeitwerk::Loader::EagerLoad
2
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`.
3
+ # need to be in `$LOAD_PATH`, absolute file names are used.
4
+ #
5
+ # Ignored files are not eager loaded. You can opt-out specifically in specific
6
+ # files and directories with `do_not_eager_load`, and that can be overridden
7
+ # passing `force: true`.
7
8
  #
8
9
  #: (?force: boolish) -> void
9
10
  def eager_load(force: false)
@@ -11,7 +12,7 @@ module Zeitwerk::Loader::EagerLoad
11
12
  break if @eager_loaded
12
13
  raise Zeitwerk::SetupRequired unless @setup
13
14
 
14
- log { "eager load start" }
15
+ log { 'eager load start' }
15
16
 
16
17
  actual_roots.each do |root_dir, root_namespace|
17
18
  actual_eager_load_dir(root_dir, root_namespace, force: force)
@@ -24,7 +25,7 @@ module Zeitwerk::Loader::EagerLoad
24
25
 
25
26
  @eager_loaded = true
26
27
 
27
- log { "eager load end" }
28
+ log { 'eager load end' }
28
29
  end
29
30
  end
30
31
 
@@ -91,11 +92,11 @@ module Zeitwerk::Loader::EagerLoad
91
92
  eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
92
93
  else
93
94
  root_namespace_name = real_mod_name(root_namespace)
94
- if root_namespace_name.start_with?(mod_name + "::")
95
+ if root_namespace_name.start_with?(mod_name + '::')
95
96
  actual_eager_load_dir(root_dir, root_namespace)
96
97
  elsif mod_name == root_namespace_name
97
98
  actual_eager_load_dir(root_dir, root_namespace)
98
- elsif mod_name.start_with?(root_namespace_name + "::")
99
+ elsif mod_name.start_with?(root_namespace_name + '::')
99
100
  eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
100
101
  else
101
102
  # Unrelated constant hierarchies, do nothing.
@@ -119,7 +120,7 @@ module Zeitwerk::Loader::EagerLoad
119
120
  raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
120
121
  raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
121
122
 
122
- file_basename = File.basename(abspath, ".rb")
123
+ file_basename = File.basename(abspath)
123
124
  raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
124
125
 
125
126
  root_namespace = nil
@@ -138,17 +139,20 @@ module Zeitwerk::Loader::EagerLoad
138
139
 
139
140
  raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
140
141
 
141
- base_cname = cname_for(file_basename, abspath)
142
-
143
142
  namespace = root_namespace
144
143
  paths.reverse_each do |basename, dir|
145
144
  cname = cname_for(basename, dir)
146
145
  namespace = namespace.const_get(cname, false)
147
146
  end
148
147
 
149
- raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
150
-
151
- namespace.const_get(base_cname, false)
148
+ if file_basename == @nsfile
149
+ namespace
150
+ elsif shadowed_file?(abspath)
151
+ raise Zeitwerk::Error.new("#{abspath} is shadowed")
152
+ else
153
+ cname = cname_for(file_basename.delete_suffix('.rb'), abspath)
154
+ namespace.const_get(cname, false)
155
+ end
152
156
  end
153
157
 
154
158
  # The caller is responsible for making sure `namespace` is the namespace that
@@ -171,12 +175,8 @@ module Zeitwerk::Loader::EagerLoad
171
175
  cref.get
172
176
  end
173
177
  else
174
- if collapse?(abspath)
175
- queue << [abspath, namespace]
176
- else
177
- cname = cname_for(basename, abspath)
178
- queue << [abspath, namespace.const_get(cname, false)]
179
- end
178
+ cname = cname_for(basename, abspath)
179
+ queue << [abspath, namespace.const_get(cname, false)]
180
180
  end
181
181
  end
182
182
  end
@@ -191,7 +191,7 @@ module Zeitwerk::Loader::EagerLoad
191
191
  private def eager_load_child_namespace(child, child_name, root_dir, root_namespace)
192
192
  suffix = child_name
193
193
  unless root_namespace.equal?(Object)
194
- suffix = suffix.delete_prefix(real_mod_name(root_namespace) + "::")
194
+ suffix = suffix.delete_prefix(real_mod_name(root_namespace) + '::')
195
195
  end
196
196
 
197
197
  # These directories are at the same namespace level, there may be more if
@@ -204,14 +204,10 @@ module Zeitwerk::Loader::EagerLoad
204
204
  dirs = [root_dir]
205
205
  next_dirs = []
206
206
 
207
- suffix.split("::").each do |segment|
207
+ suffix.split('::').each do |segment|
208
208
  while (dir = dirs.shift)
209
209
  @fs.ls(dir) do |basename, abspath, ftype|
210
- next unless ftype == :directory
211
-
212
- if collapse?(abspath)
213
- dirs << abspath
214
- elsif segment == cname_for(basename, abspath).to_s
210
+ if ftype == :directory && segment == cname_for(basename, abspath).to_s
215
211
  next_dirs << abspath
216
212
  end
217
213
  end
@@ -12,8 +12,22 @@ class Zeitwerk::Loader::FileSystem # :nodoc:
12
12
  @loader = loader
13
13
  end
14
14
 
15
+ # This method lists directories, filtering out the following:
16
+ #
17
+ # - Hidden entries.
18
+ # - Ignored entries.
19
+ # - Files whose extension is not `.rb`.
20
+ # - Nested root directories, since they represent separate trees.
21
+ # - Subdirectories that (recursively) contain no Ruby files.
22
+ #
23
+ # If `collapse` is true, collapsed directories are not yielded, instead, the
24
+ # method recurses so that the caller gets a conceptually flat listing.
25
+ #
26
+ # For every entry that is not excluded, `ls` yields its basename, absolute
27
+ # path, and file type, which can only be :file or :directory.
28
+ #
15
29
  #: (String) { (String, String, Symbol) -> void } -> void
16
- def ls(dir)
30
+ def ls(dir, collapse: true, &block)
17
31
  children = relevant_dir_entries(dir)
18
32
 
19
33
  # The order in which a directory is listed depends on the file system.
@@ -24,9 +38,14 @@ class Zeitwerk::Loader::FileSystem # :nodoc:
24
38
  children.sort_by!(&:first)
25
39
 
26
40
  children.each do |basename, abspath, ftype|
27
- if :directory == ftype && !has_at_least_one_ruby_file?(abspath)
28
- @loader.__log { "directory #{abspath} is ignored because it has no Ruby files" }
29
- next
41
+ if ftype == :directory
42
+ if !has_at_least_one_ruby_file?(abspath)
43
+ @loader.__log { "directory #{abspath} is ignored because it has no Ruby files" }
44
+ next
45
+ elsif collapse && @loader.__collapse?(abspath)
46
+ ls(abspath, collapse: collapse, &block)
47
+ next
48
+ end
30
49
  end
31
50
 
32
51
  yield basename, abspath, ftype
@@ -38,8 +57,47 @@ class Zeitwerk::Loader::FileSystem # :nodoc:
38
57
  loop do
39
58
  yield abspath
40
59
  abspath, basename = File.split(abspath)
41
- break if basename == "/"
60
+ break if basename == '/'
61
+ end
62
+ end
63
+
64
+ # Returns the absolute path to an nsfile in `dir`, if there is exactly one. If
65
+ # there is none, it returns `nil`.
66
+ #
67
+ # This method accounts for collapsed directories, which conceptually allow for
68
+ # multiple nsfiles. If two are found, Zeitwerk::ConflictingNamespaceDefinitionError is raised.
69
+ #
70
+ #: (Zeitwerk::Cref, String) -> String? ! Zeitwerk::ConflictingNamespaceDefinitionError
71
+ def has_exactly_one_nsfile?(cref, dir)
72
+ return unless @loader.nsfile
73
+
74
+ # When `dir` does not have any collapsed directories a simple lookup
75
+ # suffices. This is a common case worth optimizing.
76
+ unless @loader.__collapse_parent?(dir)
77
+ nsfile_abspath = File.join(dir, @loader.nsfile)
78
+ if File.exist?(nsfile_abspath) && !@loader.__ignored_path?(nsfile_abspath)
79
+ return nsfile_abspath
80
+ end
81
+ return
82
+ end
83
+
84
+ nsfile = nil
85
+
86
+ to_visit = [dir]
87
+ while (dir = to_visit.shift)
88
+ relevant_dir_entries(dir) do |basename, abspath, ftype|
89
+ if ftype == :file && basename == @loader.nsfile
90
+ if nsfile
91
+ raise Zeitwerk::ConflictingNamespaceDefinitionError.new(cref.path, location: nsfile, conflicting_file: abspath)
92
+ end
93
+ nsfile = abspath
94
+ elsif ftype == :directory && @loader.__collapse?(abspath)
95
+ to_visit << abspath
96
+ end
97
+ end
42
98
  end
99
+
100
+ nsfile
43
101
  end
44
102
 
45
103
  # Encodes the documented conventions.
@@ -55,7 +113,7 @@ class Zeitwerk::Loader::FileSystem # :nodoc:
55
113
 
56
114
  #: (String) -> bool
57
115
  def rb_extension?(path)
58
- path.end_with?(".rb")
116
+ path.end_with?('.rb')
59
117
  end
60
118
 
61
119
  #: (String) -> bool
@@ -65,7 +123,7 @@ class Zeitwerk::Loader::FileSystem # :nodoc:
65
123
 
66
124
  #: (String) -> bool
67
125
  def hidden?(basename)
68
- basename.start_with?(".")
126
+ basename.start_with?('.')
69
127
  end
70
128
 
71
129
  private
@@ -80,7 +138,7 @@ class Zeitwerk::Loader::FileSystem # :nodoc:
80
138
 
81
139
  while (dir = to_visit.shift)
82
140
  relevant_dir_entries(dir) do |_, abspath, ftype|
83
- return true if :file == ftype
141
+ return true if ftype == :file
84
142
  to_visit << abspath
85
143
  end
86
144
  end
@@ -96,18 +154,9 @@ class Zeitwerk::Loader::FileSystem # :nodoc:
96
154
  each_ruby_file_or_directory(dir) do |basename, abspath, ftype|
97
155
  next if @loader.__ignored_path?(abspath)
98
156
 
99
- if :link == ftype
100
- begin
101
- ftype = File.stat(abspath).ftype.to_sym
102
- rescue Errno::ENOENT
103
- warn "ignoring broken symlink #{abspath}"
104
- next
105
- end
106
- end
107
-
108
- if :file == ftype
109
- yield basename, abspath, ftype if rb_extension?(basename)
110
- elsif :directory == ftype
157
+ if ftype == :file
158
+ yield basename, abspath, ftype
159
+ else
111
160
  # Conceptually, root directories represent a separate project tree.
112
161
  yield basename, abspath, ftype unless @loader.__root_dir?(abspath)
113
162
  end
@@ -135,10 +184,10 @@ class Zeitwerk::Loader::FileSystem # :nodoc:
135
184
  if rb_extension?(basename)
136
185
  abspath = File.join(dir, basename).freeze
137
186
  yield basename, abspath, :file # By convention.
138
- elsif :directory == ftype
187
+ elsif ftype == :directory
139
188
  abspath = File.join(dir, basename).freeze
140
189
  yield basename, abspath, :directory
141
- elsif :link == ftype
190
+ elsif ftype == :link
142
191
  abspath = File.join(dir, basename).freeze
143
192
  yield basename, abspath, :directory if dir?(abspath)
144
193
  end
@@ -155,9 +204,7 @@ class Zeitwerk::Loader::FileSystem # :nodoc:
155
204
  yield basename, abspath, :file # By convention.
156
205
  else
157
206
  abspath = File.join(dir, basename).freeze
158
- if dir?(abspath)
159
- yield basename, abspath, :directory
160
- end
207
+ yield basename, abspath, :directory if dir?(abspath)
161
208
  end
162
209
  end
163
210
  end
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk::Loader::Helpers
4
- CNAME_VALIDATOR = Module.new #: Module
5
- private_constant :CNAME_VALIDATOR
6
-
7
4
  #: (String, String) -> Symbol ! Zeitwerk::NameError
8
5
  private def cname_for(basename, abspath)
9
6
  cname = inflector.camelize(basename, abspath)
@@ -12,7 +9,7 @@ module Zeitwerk::Loader::Helpers
12
9
  raise TypeError, "#{inflector.class}#camelize must return a String, received #{cname.inspect}"
13
10
  end
14
11
 
15
- if cname.include?("::")
12
+ if cname.include?('::')
16
13
  raise Zeitwerk::NameError.new(<<~MESSAGE, cname)
17
14
  wrong constant name #{cname} inferred by #{inflector.class} from
18
15
 
@@ -23,9 +20,9 @@ module Zeitwerk::Loader::Helpers
23
20
  end
24
21
 
25
22
  begin
26
- CNAME_VALIDATOR.const_defined?(cname, false)
23
+ @cpv.validate!(cname)
27
24
  rescue ::NameError => error
28
- path_type = @fs.rb_extension?(abspath) ? "file" : "directory"
25
+ path_type = @fs.rb_extension?(abspath) ? 'file' : 'directory'
29
26
 
30
27
  raise Zeitwerk::NameError.new(<<~MESSAGE, error.name)
31
28
  #{error.message} inferred by #{inflector.class} from #{path_type}