zeitwerk 2.7.4 → 2.8.0
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.
- checksums.yaml +4 -4
- data/README.md +150 -71
- data/lib/zeitwerk/core_ext/kernel.rb +1 -1
- data/lib/zeitwerk/cref/map.rb +1 -1
- data/lib/zeitwerk/cref.rb +8 -1
- data/lib/zeitwerk/error.rb +12 -1
- data/lib/zeitwerk/gem_inflector.rb +3 -3
- data/lib/zeitwerk/gem_loader.rb +6 -6
- data/lib/zeitwerk/inflector.rb +8 -8
- data/lib/zeitwerk/loader/callbacks.rb +4 -4
- data/lib/zeitwerk/loader/config.rb +73 -27
- data/lib/zeitwerk/loader/eager_load.rb +42 -48
- data/lib/zeitwerk/loader/file_system.rb +212 -0
- data/lib/zeitwerk/loader/helpers.rb +2 -101
- data/lib/zeitwerk/loader.rb +195 -162
- data/lib/zeitwerk/real_mod_name.rb +1 -1
- data/lib/zeitwerk/registry/loaders.rb +2 -2
- data/lib/zeitwerk/registry.rb +30 -4
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -13
- metadata +4 -6
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This private class encapsulates interactions with the file system.
|
|
4
|
+
#
|
|
5
|
+
# It is used to list directories and check file types, and it encodes the
|
|
6
|
+
# conventions documented in the README.
|
|
7
|
+
#
|
|
8
|
+
# @private
|
|
9
|
+
class Zeitwerk::Loader::FileSystem # :nodoc:
|
|
10
|
+
#: (Zeitwerk::Loader) -> void
|
|
11
|
+
def initialize(loader)
|
|
12
|
+
@loader = loader
|
|
13
|
+
end
|
|
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
|
+
#
|
|
29
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
30
|
+
def ls(dir, collapse: true, &)
|
|
31
|
+
children = relevant_dir_entries(dir)
|
|
32
|
+
|
|
33
|
+
# The order in which a directory is listed depends on the file system.
|
|
34
|
+
#
|
|
35
|
+
# Since client code may run on different platforms, it seems convenient to
|
|
36
|
+
# sort directory entries. This provides more deterministic behavior, with
|
|
37
|
+
# consistent eager loading in particular.
|
|
38
|
+
children.sort_by!(&:first)
|
|
39
|
+
|
|
40
|
+
children.each do |basename, abspath, ftype|
|
|
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, &)
|
|
47
|
+
next
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
yield basename, abspath, ftype
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#: (String) { (String) -> void } -> void
|
|
56
|
+
def walk_up(abspath)
|
|
57
|
+
loop do
|
|
58
|
+
yield abspath
|
|
59
|
+
abspath, basename = File.split(abspath)
|
|
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
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
nsfile
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Encodes the documented conventions.
|
|
104
|
+
#
|
|
105
|
+
#: (String) -> Symbol?
|
|
106
|
+
def supported_ftype?(abspath)
|
|
107
|
+
if rb_extension?(abspath)
|
|
108
|
+
:file # By convention, we can avoid a syscall here.
|
|
109
|
+
elsif dir?(abspath)
|
|
110
|
+
:directory
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
#: (String) -> bool
|
|
115
|
+
def rb_extension?(path)
|
|
116
|
+
path.end_with?('.rb')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
#: (String) -> bool
|
|
120
|
+
def dir?(path)
|
|
121
|
+
File.directory?(path)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#: (String) -> bool
|
|
125
|
+
def hidden?(basename)
|
|
126
|
+
basename.start_with?('.')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Looks for a Ruby file using breadth-first search. This type of search is
|
|
132
|
+
# important to list as less directories as possible and return fast in the
|
|
133
|
+
# common case in which there are Ruby files in the passed directory.
|
|
134
|
+
#
|
|
135
|
+
#: (String) -> bool
|
|
136
|
+
def has_at_least_one_ruby_file?(dir)
|
|
137
|
+
to_visit = [dir]
|
|
138
|
+
|
|
139
|
+
while (dir = to_visit.shift)
|
|
140
|
+
relevant_dir_entries(dir) do |_, abspath, ftype|
|
|
141
|
+
return true if ftype == :file
|
|
142
|
+
to_visit << abspath
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
false
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
150
|
+
#: (String) -> [[String, String, Symbol]]
|
|
151
|
+
def relevant_dir_entries(dir)
|
|
152
|
+
return enum_for(__method__, dir).to_a unless block_given?
|
|
153
|
+
|
|
154
|
+
each_ruby_file_or_directory(dir) do |basename, abspath, ftype|
|
|
155
|
+
next if @loader.__ignored_path?(abspath)
|
|
156
|
+
|
|
157
|
+
if ftype == :file
|
|
158
|
+
yield basename, abspath, ftype
|
|
159
|
+
else
|
|
160
|
+
# Conceptually, root directories represent a separate project tree.
|
|
161
|
+
yield basename, abspath, ftype unless @loader.__root_dir?(abspath)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Dir.scan is more efficient in common platforms, but it is going to take a
|
|
167
|
+
# while for it to be available.
|
|
168
|
+
#
|
|
169
|
+
# The following compatibility methods have the same semantics but are written
|
|
170
|
+
# to favor the performance of the Ruby fallback, which can save syscalls.
|
|
171
|
+
#
|
|
172
|
+
# In particular, by convention, any directory entry with a .rb extension is
|
|
173
|
+
# assumed to be a file or a symlink to a file.
|
|
174
|
+
#
|
|
175
|
+
# These methods also freeze abspaths because that saves allocations when
|
|
176
|
+
# passed later to File methods. See https://github.com/fxn/zeitwerk/pull/125.
|
|
177
|
+
|
|
178
|
+
if Dir.respond_to?(:scan) # Available in Ruby 4.1.
|
|
179
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
180
|
+
def each_ruby_file_or_directory(dir)
|
|
181
|
+
Dir.scan(dir) do |basename, ftype|
|
|
182
|
+
next if hidden?(basename)
|
|
183
|
+
|
|
184
|
+
if rb_extension?(basename)
|
|
185
|
+
abspath = File.join(dir, basename).freeze
|
|
186
|
+
yield basename, abspath, :file # By convention.
|
|
187
|
+
elsif ftype == :directory
|
|
188
|
+
abspath = File.join(dir, basename).freeze
|
|
189
|
+
yield basename, abspath, :directory
|
|
190
|
+
elsif ftype == :link
|
|
191
|
+
abspath = File.join(dir, basename).freeze
|
|
192
|
+
yield basename, abspath, :directory if dir?(abspath)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
else
|
|
197
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
198
|
+
def each_ruby_file_or_directory(dir)
|
|
199
|
+
Dir.each_child(dir) do |basename|
|
|
200
|
+
next if hidden?(basename)
|
|
201
|
+
|
|
202
|
+
if rb_extension?(basename)
|
|
203
|
+
abspath = File.join(dir, basename).freeze
|
|
204
|
+
yield basename, abspath, :file # By convention.
|
|
205
|
+
else
|
|
206
|
+
abspath = File.join(dir, basename).freeze
|
|
207
|
+
yield basename, abspath, :directory if dir?(abspath)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -1,105 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zeitwerk::Loader::Helpers
|
|
4
|
-
# --- Logging -----------------------------------------------------------------------------------
|
|
5
|
-
|
|
6
|
-
#: (to_s() -> String) -> void
|
|
7
|
-
private def log(message)
|
|
8
|
-
method_name = logger.respond_to?(:debug) ? :debug : :call
|
|
9
|
-
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# --- Files and directories ---------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
#: (String) { (String, String, Symbol) -> void } -> void
|
|
15
|
-
private def ls(dir)
|
|
16
|
-
children = Dir.children(dir)
|
|
17
|
-
|
|
18
|
-
# The order in which a directory is listed depends on the file system.
|
|
19
|
-
#
|
|
20
|
-
# Since client code may run in different platforms, it seems convenient to
|
|
21
|
-
# order directory entries. This provides consistent eager loading across
|
|
22
|
-
# platforms, for example.
|
|
23
|
-
children.sort!
|
|
24
|
-
|
|
25
|
-
children.each do |basename|
|
|
26
|
-
next if hidden?(basename)
|
|
27
|
-
|
|
28
|
-
abspath = File.join(dir, basename)
|
|
29
|
-
next if ignored_path?(abspath)
|
|
30
|
-
|
|
31
|
-
if dir?(abspath)
|
|
32
|
-
next if roots.key?(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
|
|
40
|
-
else
|
|
41
|
-
next unless ruby?(abspath)
|
|
42
|
-
ftype = :file
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# We freeze abspath because that saves allocations when passed later to
|
|
46
|
-
# File methods. See #125.
|
|
47
|
-
yield basename, abspath.freeze, ftype
|
|
48
|
-
end
|
|
49
|
-
end
|
|
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
|
-
#
|
|
55
|
-
#: (String) -> bool
|
|
56
|
-
private def has_at_least_one_ruby_file?(dir)
|
|
57
|
-
to_visit = [dir]
|
|
58
|
-
|
|
59
|
-
while (dir = to_visit.shift)
|
|
60
|
-
Dir.each_child(dir) do |basename|
|
|
61
|
-
next if hidden?(basename)
|
|
62
|
-
|
|
63
|
-
abspath = File.join(dir, basename)
|
|
64
|
-
next if ignored_path?(abspath)
|
|
65
|
-
|
|
66
|
-
if dir?(abspath)
|
|
67
|
-
to_visit << abspath unless roots.key?(abspath)
|
|
68
|
-
else
|
|
69
|
-
return true if ruby?(abspath)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
false
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
#: (String) -> bool
|
|
78
|
-
private def ruby?(path)
|
|
79
|
-
path.end_with?(".rb")
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
#: (String) -> bool
|
|
83
|
-
private def dir?(path)
|
|
84
|
-
File.directory?(path)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
#: (String) -> bool
|
|
88
|
-
private def hidden?(basename)
|
|
89
|
-
basename.start_with?(".")
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
#: (String) { (String) -> void } -> void
|
|
93
|
-
private def walk_up(abspath)
|
|
94
|
-
loop do
|
|
95
|
-
yield abspath
|
|
96
|
-
abspath, basename = File.split(abspath)
|
|
97
|
-
break if basename == "/"
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# --- Inflection --------------------------------------------------------------------------------
|
|
102
|
-
|
|
103
4
|
CNAME_VALIDATOR = Module.new #: Module
|
|
104
5
|
private_constant :CNAME_VALIDATOR
|
|
105
6
|
|
|
@@ -111,7 +12,7 @@ module Zeitwerk::Loader::Helpers
|
|
|
111
12
|
raise TypeError, "#{inflector.class}#camelize must return a String, received #{cname.inspect}"
|
|
112
13
|
end
|
|
113
14
|
|
|
114
|
-
if cname.include?(
|
|
15
|
+
if cname.include?('::')
|
|
115
16
|
raise Zeitwerk::NameError.new(<<~MESSAGE, cname)
|
|
116
17
|
wrong constant name #{cname} inferred by #{inflector.class} from
|
|
117
18
|
|
|
@@ -124,7 +25,7 @@ module Zeitwerk::Loader::Helpers
|
|
|
124
25
|
begin
|
|
125
26
|
CNAME_VALIDATOR.const_defined?(cname, false)
|
|
126
27
|
rescue ::NameError => error
|
|
127
|
-
path_type =
|
|
28
|
+
path_type = @fs.rb_extension?(abspath) ? 'file' : 'directory'
|
|
128
29
|
|
|
129
30
|
raise Zeitwerk::NameError.new(<<~MESSAGE, error.name)
|
|
130
31
|
#{error.message} inferred by #{inflector.class} from #{path_type}
|