zeitwerk 2.5.4 → 2.6.18

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,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk::Loader::Helpers
4
- private
5
-
6
4
  # --- Logging -----------------------------------------------------------------------------------
7
5
 
8
6
  # @sig (String) -> void
9
- def log(message)
7
+ private def log(message)
10
8
  method_name = logger.respond_to?(:debug) ? :debug : :call
11
9
  logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
12
10
  end
@@ -14,84 +12,137 @@ module Zeitwerk::Loader::Helpers
14
12
  # --- Files and directories ---------------------------------------------------------------------
15
13
 
16
14
  # @sig (String) { (String, String) -> void } -> void
17
- def ls(dir)
18
- Dir.each_child(dir) do |basename|
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|
19
26
  next if hidden?(basename)
20
27
 
21
28
  abspath = File.join(dir, basename)
22
- next if ignored_paths.member?(abspath)
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
23
44
 
24
45
  # We freeze abspath because that saves allocations when passed later to
25
46
  # File methods. See #125.
26
- yield basename, abspath.freeze
47
+ yield basename, abspath.freeze, ftype
27
48
  end
28
49
  end
29
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
+ #
30
55
  # @sig (String) -> bool
31
- def ruby?(path)
56
+ private def has_at_least_one_ruby_file?(dir)
57
+ to_visit = [dir]
58
+
59
+ while (dir = to_visit.shift)
60
+ children = Dir.children(dir)
61
+
62
+ children.each do |basename|
63
+ next if hidden?(basename)
64
+
65
+ abspath = File.join(dir, basename)
66
+ next if ignored_path?(abspath)
67
+
68
+ if dir?(abspath)
69
+ to_visit << abspath unless roots.key?(abspath)
70
+ else
71
+ return true if ruby?(abspath)
72
+ end
73
+ end
74
+ end
75
+
76
+ false
77
+ end
78
+
79
+ # @sig (String) -> bool
80
+ private def ruby?(path)
32
81
  path.end_with?(".rb")
33
82
  end
34
83
 
35
84
  # @sig (String) -> bool
36
- def dir?(path)
85
+ private def dir?(path)
37
86
  File.directory?(path)
38
87
  end
39
88
 
40
- # @sig String -> bool
41
- def hidden?(basename)
89
+ # @sig (String) -> bool
90
+ private def hidden?(basename)
42
91
  basename.start_with?(".")
43
92
  end
44
93
 
45
- # --- Constants ---------------------------------------------------------------------------------
46
-
47
- # The autoload? predicate takes into account the ancestor chain of the
48
- # receiver, like const_defined? and other methods in the constants API do.
49
- #
50
- # For example, given
51
- #
52
- # class A
53
- # autoload :X, "x.rb"
54
- # end
55
- #
56
- # class B < A
57
- # end
58
- #
59
- # B.autoload?(:X) returns "x.rb".
60
- #
61
- # We need a way to strictly check in parent ignoring ancestors.
62
- #
63
- # @sig (Module, Symbol) -> String?
64
- if method(:autoload?).arity == 1
65
- def strict_autoload_path(parent, cname)
66
- parent.autoload?(cname) if cdef?(parent, cname)
67
- end
68
- else
69
- def strict_autoload_path(parent, cname)
70
- parent.autoload?(cname, false)
94
+ # @sig (String) { (String) -> void } -> void
95
+ private def walk_up(abspath)
96
+ loop do
97
+ yield abspath
98
+ abspath, basename = File.split(abspath)
99
+ break if basename == "/"
71
100
  end
72
101
  end
73
102
 
74
- # @sig (Module, Symbol) -> String
75
- if Symbol.method_defined?(:name)
76
- # Symbol#name was introduced in Ruby 3.0. It returns always the same
77
- # frozen object, so we may save a few string allocations.
78
- def cpath(parent, cname)
79
- Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
103
+ # --- Inflection --------------------------------------------------------------------------------
104
+
105
+ CNAME_VALIDATOR = Module.new
106
+ private_constant :CNAME_VALIDATOR
107
+
108
+ # @raise [Zeitwerk::NameError]
109
+ # @sig (String, String) -> Symbol
110
+ private def cname_for(basename, abspath)
111
+ cname = inflector.camelize(basename, abspath)
112
+
113
+ unless cname.is_a?(String)
114
+ raise TypeError, "#{inflector.class}#camelize must return a String, received #{cname.inspect}"
80
115
  end
81
- else
82
- def cpath(parent, cname)
83
- Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
116
+
117
+ if cname.include?("::")
118
+ raise Zeitwerk::NameError.new(<<~MESSAGE, cname)
119
+ wrong constant name #{cname} inferred by #{inflector.class} from
120
+
121
+ #{abspath}
122
+
123
+ #{inflector.class}#camelize should return a simple constant name without "::"
124
+ MESSAGE
84
125
  end
85
- end
86
126
 
87
- # @sig (Module, Symbol) -> bool
88
- def cdef?(parent, cname)
89
- parent.const_defined?(cname, false)
90
- end
127
+ begin
128
+ CNAME_VALIDATOR.const_defined?(cname, false)
129
+ rescue ::NameError => error
130
+ path_type = ruby?(abspath) ? "file" : "directory"
131
+
132
+ raise Zeitwerk::NameError.new(<<~MESSAGE, error.name)
133
+ #{error.message} inferred by #{inflector.class} from #{path_type}
134
+
135
+ #{abspath}
136
+
137
+ Possible ways to address this:
138
+
139
+ * Tell Zeitwerk to ignore this particular #{path_type}.
140
+ * Tell Zeitwerk to ignore one of its parent directories.
141
+ * Rename the #{path_type} to comply with the naming conventions.
142
+ * Modify the inflector to handle this case.
143
+ MESSAGE
144
+ end
91
145
 
92
- # @raise [NameError]
93
- # @sig (Module, Symbol) -> Object
94
- def cget(parent, cname)
95
- parent.const_get(cname, false)
146
+ cname.to_sym
96
147
  end
97
148
  end