zeitwerk 2.6.18 → 2.8.1

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,22 +1,22 @@
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
- # @sig #camelize
10
+ #: camelize(String, String) -> String
11
11
  attr_accessor :inflector
12
12
 
13
- # @sig #call | #debug | nil
13
+ #: call(String) -> void | debug(String) -> void | nil
14
14
  attr_accessor :logger
15
15
 
16
16
  # Absolute paths of the root directories, mapped to their respective root namespaces:
17
17
  #
18
- # "/Users/fxn/blog/app/channels" => Object,
19
- # "/Users/fxn/blog/app/adapters" => ActiveJob::QueueAdapters,
18
+ # '/Users/fxn/blog/app/channels' => Object,
19
+ # '/Users/fxn/blog/app/adapters' => ActiveJob::QueueAdapters,
20
20
  # ...
21
21
  #
22
22
  # Stored in a hash to preserve order, easily handle duplicates, and have a
@@ -25,14 +25,19 @@ module Zeitwerk::Loader::Config
25
25
  # This is a private collection maintained by the loader. The public
26
26
  # interface for it is `push_dir` and `dirs`.
27
27
  #
28
- # @sig Hash[String, Module]
28
+ #: Hash[String, Module]
29
29
  attr_reader :roots
30
30
  internal :roots
31
31
 
32
- # Absolute paths of files, directories, or glob patterns to be totally
33
- # ignored.
32
+ # Basename of files that define namespaces. For example, if `nsfile` is
33
+ # 'ns.rb', then `foo/ns.rb` defines the `Foo` namespace.
34
34
  #
35
- # @sig Set[String]
35
+ #: String?
36
+ attr_reader :nsfile
37
+
38
+ # Absolute paths of files, directories, or glob patterns to be ignored.
39
+ #
40
+ #: Set[String]
36
41
  attr_reader :ignored_glob_patterns
37
42
  private :ignored_glob_patterns
38
43
 
@@ -40,59 +45,70 @@ module Zeitwerk::Loader::Config
40
45
  # ignored glob patterns were expanded. Computed on setup, and recomputed on
41
46
  # reload.
42
47
  #
43
- # @sig Set[String]
48
+ #: Set[String]
44
49
  attr_reader :ignored_paths
45
50
  private :ignored_paths
46
51
 
47
52
  # Absolute paths of directories or glob patterns to be collapsed.
48
53
  #
49
- # @sig Set[String]
54
+ #: Set[String]
50
55
  attr_reader :collapse_glob_patterns
51
56
  private :collapse_glob_patterns
52
57
 
53
58
  # The actual collection of absolute directory names at the time the collapse
54
- # glob patterns were expanded. Computed on setup, and recomputed on reload.
59
+ # glob patterns were expanded. Computed on setup and recomputed on reload.
55
60
  #
56
- # @sig Set[String]
61
+ #: Set[String]
57
62
  attr_reader :collapse_dirs
58
63
  private :collapse_dirs
59
64
 
65
+ # Absolute paths of directories that are parents of collapsed directories.
66
+ # This is a cache to optimize some tree walks. Computed on setup and
67
+ # recomputed on reload.
68
+ #
69
+ #: Set[String]
70
+ attr_reader :collapse_parents
71
+ private :collapse_parents
72
+
60
73
  # Absolute paths of files or directories not to be eager loaded.
61
74
  #
62
- # @sig Set[String]
75
+ #: Set[String]
63
76
  attr_reader :eager_load_exclusions
64
77
  private :eager_load_exclusions
65
78
 
66
79
  # User-oriented callbacks to be fired on setup and on reload.
67
80
  #
68
- # @sig Array[{ () -> void }]
81
+ #: Array[{ () -> void }]
69
82
  attr_reader :on_setup_callbacks
70
83
  private :on_setup_callbacks
71
84
 
72
85
  # User-oriented callbacks to be fired when a constant is loaded.
73
86
  #
74
- # @sig Hash[String, Array[{ (Object, String) -> void }]]
75
- # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
87
+ #: Hash[String, Array[{ (top, String) -> void }]]
88
+ #| Hash[Symbol, Array[{ (String, top, String) -> void }]]
76
89
  attr_reader :on_load_callbacks
77
90
  private :on_load_callbacks
78
91
 
79
92
  # User-oriented callbacks to be fired before constants are removed.
80
93
  #
81
- # @sig Hash[String, Array[{ (Object, String) -> void }]]
82
- # Hash[Symbol, Array[{ (String, Object, String) -> void }]]
94
+ #: Hash[String, Array[{ (top, String) -> void }]]
95
+ #| Hash[Symbol, Array[{ (String, top, String) -> void }]]
83
96
  attr_reader :on_unload_callbacks
84
97
  private :on_unload_callbacks
85
98
 
99
+ #: () -> void
86
100
  def initialize
87
101
  @inflector = Zeitwerk::Inflector.new
88
102
  @logger = self.class.default_logger
89
103
  @tag = SecureRandom.hex(3)
90
104
  @initialized_at = Time.now
91
105
  @roots = {}
106
+ @nsfile = nil
92
107
  @ignored_glob_patterns = Set.new
93
108
  @ignored_paths = Set.new
94
109
  @collapse_glob_patterns = Set.new
95
110
  @collapse_dirs = Set.new
111
+ @collapse_parents = Set.new
96
112
  @eager_load_exclusions = Set.new
97
113
  @reloading_enabled = false
98
114
  @on_setup_callbacks = []
@@ -106,20 +122,19 @@ module Zeitwerk::Loader::Config
106
122
  # the same process already manages that directory or one of its ascendants or
107
123
  # descendants.
108
124
  #
109
- # @raise [Zeitwerk::Error]
110
- # @sig (String | Pathname, Module) -> void
125
+ #: (String | Pathname, namespace: Module) -> void ! Zeitwerk::Error
111
126
  def push_dir(path, namespace: Object)
112
127
  unless namespace.is_a?(Module) # Note that Class < Module.
113
128
  raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
114
129
  end
115
130
 
116
131
  unless real_mod_name(namespace)
117
- raise Zeitwerk::Error, "root namespaces cannot be anonymous"
132
+ raise Zeitwerk::Error, 'root namespaces cannot be anonymous'
118
133
  end
119
134
 
120
135
  abspath = File.expand_path(path)
121
- if dir?(abspath)
122
- raise_if_conflicting_directory(abspath)
136
+ if @fs.dir?(abspath)
137
+ raise_if_conflicting_root_dir(abspath)
123
138
  roots[abspath] = namespace
124
139
  else
125
140
  raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
@@ -131,18 +146,30 @@ module Zeitwerk::Loader::Config
131
146
  # Implemented as a method instead of via attr_reader for symmetry with the
132
147
  # writer below.
133
148
  #
134
- # @sig () -> String
149
+ #: () -> String
135
150
  def tag
136
151
  @tag
137
152
  end
138
153
 
139
154
  # Sets a tag for the loader, useful for logging.
140
155
  #
141
- # @sig (#to_s) -> void
156
+ #: (to_s() -> String) -> void
142
157
  def tag=(tag)
143
158
  @tag = tag.to_s
144
159
  end
145
160
 
161
+ #: (String?) -> void ! TypeError, ArgumentError
162
+ def nsfile=(nsfile)
163
+ unless nsfile.nil?
164
+ raise TypeError, 'nsfiles must be strings' unless nsfile.is_a?(String)
165
+ raise ArgumentError, 'nsfiles must have .rb extension' unless @fs.rb_extension?(nsfile)
166
+ raise ArgumentError, 'nsfiles must be basenames, not paths' unless File.basename(nsfile) == nsfile
167
+ raise ArgumentError, 'nsfiles cannot be hidden' if @fs.hidden?(nsfile)
168
+ end
169
+
170
+ @nsfile = nsfile
171
+ end
172
+
146
173
  # If `namespaces` is falsey (default), returns an array with the absolute
147
174
  # paths of the root directories as strings. If truthy, returns a hash table
148
175
  # instead. Keys are the absolute paths of the root directories as strings,
@@ -152,7 +179,7 @@ module Zeitwerk::Loader::Config
152
179
  #
153
180
  # These are read-only collections, please add to them with `push_dir`.
154
181
  #
155
- # @sig () -> Array[String] | Hash[String, Module]
182
+ #: (?namespaces: boolish, ?ignored: boolish) -> Array[String] | Hash[String, Module]
156
183
  def dirs(namespaces: false, ignored: false)
157
184
  if namespaces
158
185
  if ignored || ignored_paths.empty?
@@ -172,21 +199,20 @@ module Zeitwerk::Loader::Config
172
199
  # You need to call this method before setup in order to be able to reload.
173
200
  # There is no way to undo this, either you want to reload or you don't.
174
201
  #
175
- # @raise [Zeitwerk::Error]
176
- # @sig () -> void
202
+ #: () -> void ! Zeitwerk::Error
177
203
  def enable_reloading
178
204
  mutex.synchronize do
179
205
  break if @reloading_enabled
180
206
 
181
207
  if @setup
182
- raise Zeitwerk::Error, "cannot enable reloading after setup"
208
+ raise Zeitwerk::Error, 'cannot enable reloading after setup'
183
209
  else
184
210
  @reloading_enabled = true
185
211
  end
186
212
  end
187
213
  end
188
214
 
189
- # @sig () -> bool
215
+ #: () -> bool
190
216
  def reloading_enabled?
191
217
  @reloading_enabled
192
218
  end
@@ -194,14 +220,14 @@ module Zeitwerk::Loader::Config
194
220
  # Let eager load ignore the given files or directories. The constants defined
195
221
  # in those files are still autoloadable.
196
222
  #
197
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
223
+ #: (*(String | Pathname | Array[String | Pathname])) -> void
198
224
  def do_not_eager_load(*paths)
199
225
  mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
200
226
  end
201
227
 
202
228
  # Configure files, directories, or glob patterns to be totally ignored.
203
229
  #
204
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
230
+ #: (*(String | Pathname | Array[String | Pathname])) -> void
205
231
  def ignore(*glob_patterns)
206
232
  glob_patterns = expand_paths(glob_patterns)
207
233
  mutex.synchronize do
@@ -212,19 +238,23 @@ module Zeitwerk::Loader::Config
212
238
 
213
239
  # Configure directories or glob patterns to be collapsed.
214
240
  #
215
- # @sig (*(String | Pathname | Array[String | Pathname])) -> void
241
+ #: (*(String | Pathname | Array[String | Pathname])) -> void
216
242
  def collapse(*glob_patterns)
217
243
  glob_patterns = expand_paths(glob_patterns)
218
244
  mutex.synchronize do
219
245
  collapse_glob_patterns.merge(glob_patterns)
220
- collapse_dirs.merge(expand_glob_patterns(glob_patterns))
246
+ new_collapse_dirs = expand_glob_patterns(glob_patterns)
247
+ collapse_dirs.merge(new_collapse_dirs)
248
+ new_collapse_dirs.each do |dir|
249
+ collapse_parents << File.dirname(dir)
250
+ end
221
251
  end
222
252
  end
223
253
 
224
254
  # Configure a block to be called after setup and on each reload.
225
255
  # If setup was already done, the block runs immediately.
226
256
  #
227
- # @sig () { () -> void } -> void
257
+ #: () { () -> void } -> void
228
258
  def on_setup(&block)
229
259
  mutex.synchronize do
230
260
  on_setup_callbacks << block
@@ -236,8 +266,8 @@ module Zeitwerk::Loader::Config
236
266
  # Supports multiple callbacks, and if there are many, they are executed in
237
267
  # the order in which they were defined.
238
268
  #
239
- # loader.on_load("SomeApiClient") do |klass, _abspath|
240
- # klass.endpoint = "https://api.dev"
269
+ # loader.on_load('SomeApiClient') do |klass, _abspath|
270
+ # klass.endpoint = 'https://api.dev'
241
271
  # end
242
272
  #
243
273
  # Can also be configured for any constant loaded:
@@ -246,11 +276,9 @@ module Zeitwerk::Loader::Config
246
276
  # # ...
247
277
  # end
248
278
  #
249
- # @raise [TypeError]
250
- # @sig (String) { (Object, String) -> void } -> void
251
- # (:ANY) { (String, Object, String) -> void } -> void
279
+ #: (String?) { (top, String) -> void } -> void ! TypeError
252
280
  def on_load(cpath = :ANY, &block)
253
- raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
281
+ raise TypeError, 'on_load only accepts strings' unless cpath.is_a?(String) || cpath == :ANY
254
282
 
255
283
  mutex.synchronize do
256
284
  (on_load_callbacks[cpath] ||= []) << block
@@ -261,7 +289,7 @@ module Zeitwerk::Loader::Config
261
289
  # Supports multiple callbacks, and if there are many, they are executed in the
262
290
  # order in which they were defined.
263
291
  #
264
- # loader.on_unload("Country") do |klass, _abspath|
292
+ # loader.on_unload('Country') do |klass, _abspath|
265
293
  # klass.clear_cache
266
294
  # end
267
295
  #
@@ -271,11 +299,9 @@ module Zeitwerk::Loader::Config
271
299
  # # ...
272
300
  # end
273
301
  #
274
- # @raise [TypeError]
275
- # @sig (String) { (Object) -> void } -> void
276
- # (:ANY) { (String, Object) -> void } -> void
302
+ #: (String?) { (top, String) -> void } -> void ! TypeError
277
303
  def on_unload(cpath = :ANY, &block)
278
- raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
304
+ raise TypeError, 'on_unload only accepts strings' unless cpath.is_a?(String) || cpath == :ANY
279
305
 
280
306
  mutex.synchronize do
281
307
  (on_unload_callbacks[cpath] ||= []) << block
@@ -284,7 +310,7 @@ module Zeitwerk::Loader::Config
284
310
 
285
311
  # Logs to `$stdout`, handy shortcut for debugging.
286
312
  #
287
- # @sig () -> void
313
+ #: () -> void
288
314
  def log!
289
315
  @logger = ->(msg) { puts msg }
290
316
  end
@@ -292,73 +318,86 @@ module Zeitwerk::Loader::Config
292
318
  # Returns true if the argument has been configured to be ignored, or is a
293
319
  # descendant of an ignored directory.
294
320
  #
295
- # @sig (String) -> bool
321
+ #: (String) -> bool
296
322
  internal def ignores?(abspath)
297
323
  # Common use case.
298
324
  return false if ignored_paths.empty?
299
325
 
300
- walk_up(abspath) do |path|
326
+ @fs.walk_up(abspath) do |path|
301
327
  return true if ignored_path?(path)
302
- return false if roots.key?(path)
328
+ return false if root_dir?(path)
303
329
  end
304
330
 
305
331
  false
306
332
  end
307
333
 
308
- # @sig (String) -> bool
309
- private def ignored_path?(abspath)
334
+ #: (String) -> bool
335
+ internal def ignored_path?(abspath)
310
336
  ignored_paths.member?(abspath)
311
337
  end
312
338
 
313
- # @sig () -> Array[String]
339
+ #: () -> Array[String]
314
340
  private def actual_roots
315
341
  roots.reject do |root_dir, _root_namespace|
316
- !dir?(root_dir) || ignored_path?(root_dir)
342
+ !@fs.dir?(root_dir) || ignored_path?(root_dir)
317
343
  end
318
344
  end
319
345
 
320
- # @sig (String) -> bool
321
- private def root_dir?(dir)
346
+ #: (String) -> bool
347
+ internal def root_dir?(dir)
322
348
  roots.key?(dir)
323
349
  end
324
350
 
325
- # @sig (String) -> bool
351
+ #: (String) -> bool
352
+ internal def collapse?(dir)
353
+ collapse_dirs.member?(dir)
354
+ end
355
+
356
+ #: (String) -> bool
357
+ internal def collapse_parent?(dir)
358
+ collapse_parents.member?(dir)
359
+ end
360
+
361
+ #: (String) -> bool
326
362
  private def excluded_from_eager_load?(abspath)
327
363
  # Optimize this common use case.
328
364
  return false if eager_load_exclusions.empty?
329
365
 
330
- walk_up(abspath) do |path|
366
+ @fs.walk_up(abspath) do |path|
331
367
  return true if eager_load_exclusions.member?(path)
332
- return false if roots.key?(path)
368
+ return false if root_dir?(path)
333
369
  end
334
370
 
335
371
  false
336
372
  end
337
373
 
338
- # @sig (String) -> bool
339
- private def collapse?(dir)
340
- collapse_dirs.member?(dir)
341
- end
342
-
343
- # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
374
+ #: (String | Pathname | Array[String | Pathname]) -> Array[String]
344
375
  private def expand_paths(paths)
345
376
  paths.flatten.map! { |path| File.expand_path(path) }
346
377
  end
347
378
 
348
- # @sig (Array[String]) -> Array[String]
379
+ #: (Array[String]) -> Array[String]
349
380
  private def expand_glob_patterns(glob_patterns)
350
381
  # Note that Dir.glob works with regular file names just fine. That is,
351
382
  # glob patterns technically need no wildcards.
352
383
  glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
353
384
  end
354
385
 
355
- # @sig () -> void
386
+ #: () -> void
356
387
  private def recompute_ignored_paths
357
388
  ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
358
389
  end
359
390
 
360
- # @sig () -> void
391
+ #: () -> void
361
392
  private def recompute_collapse_dirs
362
393
  collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
363
394
  end
395
+
396
+ #: () -> void
397
+ private def recompute_collapse_parents
398
+ collapse_parents.clear
399
+ collapse_dirs.each do |dir|
400
+ collapse_parents << File.dirname(dir)
401
+ end
402
+ end
364
403
  end
@@ -1,56 +1,55 @@
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.
7
4
  #
8
- # @sig (true | false) -> void
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`.
8
+ #
9
+ #: (?force: boolish) -> void
9
10
  def eager_load(force: false)
10
11
  mutex.synchronize do
11
12
  break if @eager_loaded
12
13
  raise Zeitwerk::SetupRequired unless @setup
13
14
 
14
- log("eager load start") if logger
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)
18
19
  end
19
20
 
20
21
  autoloaded_dirs.each do |autoloaded_dir|
21
- Zeitwerk::Registry.unregister_autoload(autoloaded_dir)
22
+ Zeitwerk::Registry.autoloads.unregister(autoloaded_dir)
22
23
  end
23
24
  autoloaded_dirs.clear
24
25
 
25
26
  @eager_loaded = true
26
27
 
27
- log("eager load end") if logger
28
+ log { 'eager load end' }
28
29
  end
29
30
  end
30
31
 
31
- # @sig (String | Pathname) -> void
32
+ #: (String | Pathname) -> void
32
33
  def eager_load_dir(path)
33
34
  raise Zeitwerk::SetupRequired unless @setup
34
35
 
35
36
  abspath = File.expand_path(path)
36
37
 
37
- raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
38
+ raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)
38
39
 
39
- cnames = []
40
+ paths = []
40
41
 
41
42
  root_namespace = nil
42
- walk_up(abspath) do |dir|
43
+ @fs.walk_up(abspath) do |dir|
43
44
  return if ignored_path?(dir)
44
45
  return if eager_load_exclusions.member?(dir)
45
46
 
46
47
  break if root_namespace = roots[dir]
47
48
 
48
49
  basename = File.basename(dir)
49
- return if hidden?(basename)
50
+ return if @fs.hidden?(basename)
50
51
 
51
- unless collapse?(dir)
52
- cnames << inflector.camelize(basename, dir).to_sym
53
- end
52
+ paths << [basename, dir] unless collapse?(dir)
54
53
  end
55
54
 
56
55
  raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
@@ -58,7 +57,8 @@ module Zeitwerk::Loader::EagerLoad
58
57
  return if @eager_loaded
59
58
 
60
59
  namespace = root_namespace
61
- cnames.reverse_each do |cname|
60
+ paths.reverse_each do |basename, dir|
61
+ cname = cname_for(basename, dir)
62
62
  # Can happen if there are no Ruby files. This is not an error condition,
63
63
  # the directory is actually managed. Could have Ruby files later.
64
64
  return unless namespace.const_defined?(cname, false)
@@ -70,7 +70,7 @@ module Zeitwerk::Loader::EagerLoad
70
70
  actual_eager_load_dir(abspath, namespace)
71
71
  end
72
72
 
73
- # @sig (Module) -> void
73
+ #: (Module) -> void
74
74
  def eager_load_namespace(mod)
75
75
  raise Zeitwerk::SetupRequired unless @setup
76
76
 
@@ -84,7 +84,7 @@ module Zeitwerk::Loader::EagerLoad
84
84
  return unless mod_name
85
85
 
86
86
  actual_roots.each do |root_dir, root_namespace|
87
- if mod.equal?(Object)
87
+ if Object.equal?(mod)
88
88
  # A shortcircuiting test depends on the invocation of this method.
89
89
  # Please keep them in sync if refactored.
90
90
  actual_eager_load_dir(root_dir, root_namespace)
@@ -92,11 +92,11 @@ module Zeitwerk::Loader::EagerLoad
92
92
  eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
93
93
  else
94
94
  root_namespace_name = real_mod_name(root_namespace)
95
- if root_namespace_name.start_with?(mod_name + "::")
95
+ if root_namespace_name.start_with?(mod_name + '::')
96
96
  actual_eager_load_dir(root_dir, root_namespace)
97
97
  elsif mod_name == root_namespace_name
98
98
  actual_eager_load_dir(root_dir, root_namespace)
99
- elsif mod_name.start_with?(root_namespace_name + "::")
99
+ elsif mod_name.start_with?(root_namespace_name + '::')
100
100
  eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
101
101
  else
102
102
  # Unrelated constant hierarchies, do nothing.
@@ -112,60 +112,62 @@ module Zeitwerk::Loader::EagerLoad
112
112
  # The method is implemented as `constantize` for files, in a sense, to be able
113
113
  # to descend orderly and make sure the file is loadable.
114
114
  #
115
- # @sig (String | Pathname) -> void
115
+ #: (String | Pathname) -> void
116
116
  def load_file(path)
117
117
  abspath = File.expand_path(path)
118
118
 
119
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)
120
+ raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
121
121
  raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
122
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
123
+ file_basename = File.basename(abspath)
124
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
127
125
 
128
126
  root_namespace = nil
129
- cnames = []
127
+ paths = []
130
128
 
131
- walk_up(File.dirname(abspath)) do |dir|
129
+ @fs.walk_up(File.dirname(abspath)) do |dir|
132
130
  raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(dir)
133
131
 
134
132
  break if root_namespace = roots[dir]
135
133
 
136
134
  basename = File.basename(dir)
137
- raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
135
+ raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)
138
136
 
139
- unless collapse?(dir)
140
- cnames << inflector.camelize(basename, dir).to_sym
141
- end
137
+ paths << [basename, dir] unless collapse?(dir)
142
138
  end
143
139
 
144
140
  raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
145
141
 
146
142
  namespace = root_namespace
147
- cnames.reverse_each do |cname|
143
+ paths.reverse_each do |basename, dir|
144
+ cname = cname_for(basename, dir)
148
145
  namespace = namespace.const_get(cname, false)
149
146
  end
150
147
 
151
- raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
152
-
153
- 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
154
156
  end
155
157
 
156
158
  # The caller is responsible for making sure `namespace` is the namespace that
157
159
  # corresponds to `dir`.
158
160
  #
159
- # @sig (String, Module, Boolean) -> void
161
+ #: (String, Module, ?force: boolish) -> void
160
162
  private def actual_eager_load_dir(dir, namespace, force: false)
161
163
  honour_exclusions = !force
162
164
  return if honour_exclusions && excluded_from_eager_load?(dir)
163
165
 
164
- log("eager load directory #{dir} start") if logger
166
+ log { "eager load directory #{dir} start" }
165
167
 
166
168
  queue = [[dir, namespace]]
167
169
  while (current_dir, namespace = queue.shift)
168
- ls(current_dir) do |basename, abspath, ftype|
170
+ @fs.ls(current_dir) do |basename, abspath, ftype|
169
171
  next if honour_exclusions && eager_load_exclusions.member?(abspath)
170
172
 
171
173
  if ftype == :file
@@ -173,27 +175,23 @@ module Zeitwerk::Loader::EagerLoad
173
175
  cref.get
174
176
  end
175
177
  else
176
- if collapse?(abspath)
177
- queue << [abspath, namespace]
178
- else
179
- cname = inflector.camelize(basename, abspath).to_sym
180
- queue << [abspath, namespace.const_get(cname, false)]
181
- end
178
+ cname = cname_for(basename, abspath)
179
+ queue << [abspath, namespace.const_get(cname, false)]
182
180
  end
183
181
  end
184
182
  end
185
183
 
186
- log("eager load directory #{dir} end") if logger
184
+ log { "eager load directory #{dir} end" }
187
185
  end
188
186
 
189
187
  # In order to invoke this method, the caller has to ensure `child` is a
190
188
  # strict namespace descendant of `root_namespace`.
191
189
  #
192
- # @sig (Module, String, Module, Boolean) -> void
190
+ #: (Module, String, String, Module) -> void
193
191
  private def eager_load_child_namespace(child, child_name, root_dir, root_namespace)
194
192
  suffix = child_name
195
193
  unless root_namespace.equal?(Object)
196
- suffix = suffix.delete_prefix(real_mod_name(root_namespace) + "::")
194
+ suffix = suffix.delete_prefix(real_mod_name(root_namespace) + '::')
197
195
  end
198
196
 
199
197
  # These directories are at the same namespace level, there may be more if
@@ -206,14 +204,10 @@ module Zeitwerk::Loader::EagerLoad
206
204
  dirs = [root_dir]
207
205
  next_dirs = []
208
206
 
209
- suffix.split("::").each do |segment|
207
+ suffix.split('::').each do |segment|
210
208
  while (dir = dirs.shift)
211
- ls(dir) do |basename, abspath, ftype|
212
- next unless ftype == :directory
213
-
214
- if collapse?(abspath)
215
- dirs << abspath
216
- elsif segment == inflector.camelize(basename, abspath)
209
+ @fs.ls(dir) do |basename, abspath, ftype|
210
+ if ftype == :directory && segment == cname_for(basename, abspath).to_s
217
211
  next_dirs << abspath
218
212
  end
219
213
  end