zeitwerk 2.6.0 → 2.6.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,28 +11,28 @@ module Zeitwerk
11
11
  module ExplicitNamespace # :nodoc: all
12
12
  class << self
13
13
  include RealModName
14
+ extend Internal
14
15
 
15
16
  # Maps constant paths that correspond to explicit namespaces according to
16
17
  # the file system, to the loader responsible for them.
17
18
  #
18
- # @private
19
19
  # @sig Hash[String, Zeitwerk::Loader]
20
20
  attr_reader :cpaths
21
+ private :cpaths
21
22
 
22
- # @private
23
23
  # @sig Mutex
24
24
  attr_reader :mutex
25
+ private :mutex
25
26
 
26
- # @private
27
27
  # @sig TracePoint
28
28
  attr_reader :tracer
29
+ private :tracer
29
30
 
30
31
  # Asserts `cpath` corresponds to an explicit namespace for which `loader`
31
32
  # is responsible.
32
33
  #
33
- # @private
34
34
  # @sig (String, Zeitwerk::Loader) -> void
35
- def register(cpath, loader)
35
+ internal def register(cpath, loader)
36
36
  mutex.synchronize do
37
37
  cpaths[cpath] = loader
38
38
  # We check enabled? because, looking at the C source code, enabling an
@@ -41,24 +41,28 @@ module Zeitwerk
41
41
  end
42
42
  end
43
43
 
44
- # @private
45
44
  # @sig (Zeitwerk::Loader) -> void
46
- def unregister_loader(loader)
45
+ internal def unregister_loader(loader)
47
46
  cpaths.delete_if { |_cpath, l| l == loader }
48
47
  disable_tracer_if_unneeded
49
48
  end
50
49
 
51
- private
50
+ # This is an internal method only used by the test suite.
51
+ #
52
+ # @sig (String) -> bool
53
+ internal def registered?(cpath)
54
+ cpaths.key?(cpath)
55
+ end
52
56
 
53
57
  # @sig () -> void
54
- def disable_tracer_if_unneeded
58
+ private def disable_tracer_if_unneeded
55
59
  mutex.synchronize do
56
60
  tracer.disable if cpaths.empty?
57
61
  end
58
62
  end
59
63
 
60
64
  # @sig (TracePoint) -> void
61
- def tracepoint_class_callback(event)
65
+ private def tracepoint_class_callback(event)
62
66
  # If the class is a singleton class, we won't do anything with it so we
63
67
  # can bail out immediately. This is several orders of magnitude faster
64
68
  # than accessing its name.
@@ -5,8 +5,8 @@ module Zeitwerk
5
5
  # @sig (String) -> void
6
6
  def initialize(root_file)
7
7
  namespace = File.basename(root_file, ".rb")
8
- lib_dir = File.dirname(root_file)
9
- @version_file = File.join(lib_dir, namespace, "version.rb")
8
+ root_dir = File.dirname(root_file)
9
+ @version_file = File.join(root_dir, namespace, "version.rb")
10
10
  end
11
11
 
12
12
  # @sig (String, String) -> String
@@ -3,27 +3,31 @@
3
3
  module Zeitwerk
4
4
  # @private
5
5
  class GemLoader < Loader
6
+ include RealModName
7
+
6
8
  # Users should not create instances directly, the public interface is
7
9
  # `Zeitwerk::Loader.for_gem`.
8
10
  private_class_method :new
9
11
 
10
12
  # @private
11
13
  # @sig (String, bool) -> Zeitwerk::GemLoader
12
- def self._new(root_file, warn_on_extra_files:)
13
- new(root_file, warn_on_extra_files: warn_on_extra_files)
14
+ def self.__new(root_file, namespace:, warn_on_extra_files:)
15
+ new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
14
16
  end
15
17
 
16
18
  # @sig (String, bool) -> void
17
- def initialize(root_file, warn_on_extra_files:)
19
+ def initialize(root_file, namespace:, warn_on_extra_files:)
18
20
  super()
19
21
 
20
- @tag = File.basename(root_file, ".rb")
22
+ @tag = File.basename(root_file, ".rb")
23
+ @tag = real_mod_name(namespace) + "-" + @tag unless namespace.equal?(Object)
24
+
21
25
  @inflector = GemInflector.new(root_file)
22
26
  @root_file = File.expand_path(root_file)
23
- @lib = File.dirname(root_file)
27
+ @root_dir = File.dirname(root_file)
24
28
  @warn_on_extra_files = warn_on_extra_files
25
29
 
26
- push_dir(@lib)
30
+ push_dir(@root_dir, namespace: namespace)
27
31
  end
28
32
 
29
33
  # @sig () -> void
@@ -38,12 +42,12 @@ module Zeitwerk
38
42
  def warn_on_extra_files
39
43
  expected_namespace_dir = @root_file.delete_suffix(".rb")
40
44
 
41
- ls(@lib) do |basename, abspath|
45
+ ls(@root_dir) do |basename, abspath|
42
46
  next if abspath == @root_file
43
47
  next if abspath == expected_namespace_dir
44
48
 
45
49
  basename_without_ext = basename.delete_suffix(".rb")
46
- cname = inflector.camelize(basename_without_ext, abspath)
50
+ cname = inflector.camelize(basename_without_ext, abspath).to_sym
47
51
  ftype = dir?(abspath) ? "directory" : "file"
48
52
 
49
53
  warn(<<~EOS)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is a private module.
4
+ module Zeitwerk::Internal
5
+ def internal(method_name)
6
+ private method_name
7
+
8
+ mangled = "__#{method_name}"
9
+ alias_method mangled, method_name
10
+ public mangled
11
+ end
12
+ end
@@ -19,16 +19,19 @@ module Kernel
19
19
  # included in Object, and changes in ancestors don't get propagated into
20
20
  # already existing ancestor chains on Ruby < 3.0.
21
21
  alias_method :zeitwerk_original_require, :require
22
+ class << self
23
+ alias_method :zeitwerk_original_require, :require
24
+ end
22
25
 
23
26
  # @sig (String) -> true | false
24
27
  def require(path)
25
28
  if loader = Zeitwerk::Registry.loader_for(path)
26
29
  if path.end_with?(".rb")
27
30
  required = zeitwerk_original_require(path)
28
- loader.on_file_autoloaded(path) if required
31
+ loader.__on_file_autoloaded(path) if required
29
32
  required
30
33
  else
31
- loader.on_dir_autoloaded(path)
34
+ loader.__on_dir_autoloaded(path)
32
35
  true
33
36
  end
34
37
  else
@@ -36,7 +39,7 @@ module Kernel
36
39
  if required
37
40
  abspath = $LOADED_FEATURES.last
38
41
  if loader = Zeitwerk::Registry.loader_for(abspath)
39
- loader.on_file_autoloaded(abspath)
42
+ loader.__on_file_autoloaded(abspath)
40
43
  end
41
44
  end
42
45
  required
@@ -2,38 +2,46 @@
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)
10
+ internal def on_file_autoloaded(file)
11
11
  cref = autoloads.delete(file)
12
12
  cpath = cpath(*cref)
13
13
 
14
- # If reloading is enabled, we need to put this constant for unloading
15
- # regardless of what cdef? says. In Ruby < 3.1 the internal state is not
16
- # fully cleared. Module#constants still includes it, and you need to
17
- # remove_const. See https://github.com/ruby/ruby/pull/4715.
18
- to_unload[cpath] = [file, cref] if reloading_enabled?
19
14
  Zeitwerk::Registry.unregister_autoload(file)
20
15
 
21
16
  if cdef?(*cref)
22
17
  log("constant #{cpath} loaded from file #{file}") if logger
18
+ to_unload[cpath] = [file, cref] if reloading_enabled?
23
19
  run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
24
20
  else
25
- raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
21
+ msg = "expected file #{file} to define constant #{cpath}, but didn't"
22
+ log(msg) if logger
23
+
24
+ # Ruby still keeps the autoload defined, but we remove it because the
25
+ # contract in Zeitwerk is more strict.
26
+ crem(*cref)
27
+
28
+ # Since the expected constant was not defined, there is nothing to unload.
29
+ # However, if the exception is rescued and reloading is enabled, we still
30
+ # need to deleted the file from $LOADED_FEATURES.
31
+ to_unload[cpath] = [file, cref] if reloading_enabled?
32
+
33
+ raise Zeitwerk::NameError.new(msg, cref.last)
26
34
  end
27
35
  end
28
36
 
29
37
  # Invoked from our decorated Kernel#require when a managed directory is
30
38
  # autoloaded.
31
39
  #
32
- # @private
33
40
  # @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.
41
+ internal def on_dir_autoloaded(dir)
42
+ # Module#autoload does not serialize concurrent requires in CRuby < 3.2, and
43
+ # we handle directories ourselves without going through Kernel#require, so
44
+ # the callback needs to account for concurrency.
37
45
  #
38
46
  # Multi-threading would introduce a race condition here in which thread t1
39
47
  # autovivifies the module, and while autoloads for its children are being
@@ -42,8 +50,8 @@ module Zeitwerk::Loader::Callbacks
42
50
  # Without the mutex and subsequent delete call, t2 would reset the module.
43
51
  # That not only would reassign the constant (undesirable per se) but, worse,
44
52
  # the module object created by t2 wouldn't have any of the autoloads for its
45
- # children, since t1 would have correctly deleted its lazy_subdirs entry.
46
- mutex2.synchronize do
53
+ # children, since t1 would have correctly deleted its namespace_dirs entry.
54
+ dirs_autoload_monitor.synchronize do
47
55
  if cref = autoloads.delete(dir)
48
56
  autovivified_module = cref[0].const_set(cref[1], Module.new)
49
57
  cpath = autovivified_module.name
@@ -71,9 +79,9 @@ module Zeitwerk::Loader::Callbacks
71
79
  # @private
72
80
  # @sig (Module) -> void
73
81
  def on_namespace_loaded(namespace)
74
- if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
75
- subdirs.each do |subdir|
76
- set_autoloads_in_dir(subdir, namespace)
82
+ if dirs = namespace_dirs.delete(real_mod_name(namespace))
83
+ dirs.each do |dir|
84
+ define_autoloads_for_dir(dir, namespace)
77
85
  end
78
86
  end
79
87
  end
@@ -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
8
- # order, easily handle duplicates, and also be able to have a fast lookup,
9
- # needed for detecting nested paths.
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
- # "/Users/fxn/blog/app/assets" => true,
12
- # "/Users/fxn/blog/app/channels" => true,
18
+ # "/Users/fxn/blog/app/channels" => Object,
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
- # @sig Hash[String, true]
20
- attr_reader :root_dirs
21
-
22
- # @sig #camelize
23
- attr_accessor :inflector
28
+ # @sig Hash[String, Module]
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
@@ -136,12 +143,30 @@ module Zeitwerk::Loader::Config
136
143
  @tag = tag.to_s
137
144
  end
138
145
 
139
- # Absolute paths of the root directories. This is a read-only collection,
140
- # please push here via `push_dir`.
146
+ # If `namespaces` is falsey (default), returns an array with the absolute
147
+ # paths of the root directories as strings. If truthy, returns a hash table
148
+ # instead. Keys are the absolute paths of the root directories as strings,
149
+ # values are their corresponding namespaces, class or module objects.
141
150
  #
142
- # @sig () -> Array[String]
143
- def dirs
144
- root_dirs.keys.freeze
151
+ # If `ignored` is falsey (default), ignored root directories are filtered out.
152
+ #
153
+ # These are read-only collections, please add to them with `push_dir`.
154
+ #
155
+ # @sig () -> Array[String] | Hash[String, Module]
156
+ def dirs(namespaces: false, ignored: false)
157
+ if namespaces
158
+ if ignored || ignored_paths.empty?
159
+ roots.clone
160
+ else
161
+ roots.reject { |root_dir, _namespace| ignored_path?(root_dir) }
162
+ end
163
+ else
164
+ if ignored || ignored_paths.empty?
165
+ roots.keys
166
+ else
167
+ roots.keys.reject { |root_dir| ignored_path?(root_dir) }
168
+ end
169
+ end.freeze
145
170
  end
146
171
 
147
172
  # You need to call this method before setup in order to be able to reload.
@@ -264,57 +289,76 @@ module Zeitwerk::Loader::Config
264
289
  @logger = ->(msg) { puts msg }
265
290
  end
266
291
 
267
- # @private
292
+ # Returns true if the argument has been configured to be ignored, or is a
293
+ # descendant of an ignored directory.
294
+ #
268
295
  # @sig (String) -> bool
269
- def ignores?(abspath)
270
- ignored_paths.any? do |ignored_path|
271
- ignored_path == abspath || (dir?(ignored_path) && abspath.start_with?(ignored_path + "/"))
296
+ internal def ignores?(abspath)
297
+ # Common use case.
298
+ return false if ignored_paths.empty?
299
+
300
+ walk_up(abspath) do |path|
301
+ return true if ignored_path?(path)
302
+ return false if roots.key?(path)
272
303
  end
304
+
305
+ false
273
306
  end
274
307
 
275
- private
308
+ # @sig (String) -> bool
309
+ private def ignored_path?(abspath)
310
+ ignored_paths.member?(abspath)
311
+ end
276
312
 
277
313
  # @sig () -> Array[String]
278
- def actual_root_dirs
279
- root_dirs.reject do |root_dir, _namespace|
280
- !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)
281
317
  end
282
318
  end
283
319
 
284
320
  # @sig (String) -> bool
285
- def root_dir?(dir)
286
- root_dirs.key?(dir)
321
+ private def root_dir?(dir)
322
+ roots.key?(dir)
287
323
  end
288
324
 
289
325
  # @sig (String) -> bool
290
- def excluded_from_eager_load?(abspath)
291
- eager_load_exclusions.member?(abspath)
326
+ private def excluded_from_eager_load?(abspath)
327
+ # Optimize this common use case.
328
+ return false if eager_load_exclusions.empty?
329
+
330
+ walk_up(abspath) do |path|
331
+ return true if eager_load_exclusions.member?(path)
332
+ return false if roots.key?(path)
333
+ end
334
+
335
+ false
292
336
  end
293
337
 
294
338
  # @sig (String) -> bool
295
- def collapse?(dir)
339
+ private def collapse?(dir)
296
340
  collapse_dirs.member?(dir)
297
341
  end
298
342
 
299
343
  # @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
300
- def expand_paths(paths)
344
+ private def expand_paths(paths)
301
345
  paths.flatten.map! { |path| File.expand_path(path) }
302
346
  end
303
347
 
304
348
  # @sig (Array[String]) -> Array[String]
305
- def expand_glob_patterns(glob_patterns)
349
+ private def expand_glob_patterns(glob_patterns)
306
350
  # Note that Dir.glob works with regular file names just fine. That is,
307
351
  # glob patterns technically need no wildcards.
308
352
  glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
309
353
  end
310
354
 
311
355
  # @sig () -> void
312
- def recompute_ignored_paths
356
+ private def recompute_ignored_paths
313
357
  ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
314
358
  end
315
359
 
316
360
  # @sig () -> void
317
- def recompute_collapse_dirs
361
+ private def recompute_collapse_dirs
318
362
  collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
319
363
  end
320
364
  end