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.
@@ -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 = ruby?(abspath) ? "file" : "directory"
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}