-
-
Notifications
You must be signed in to change notification settings - Fork 119
/
helpers.rb
191 lines (157 loc) · 5.05 KB
/
helpers.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# frozen_string_literal: true
module Zeitwerk::Loader::Helpers
# --- Logging -----------------------------------------------------------------------------------
# @sig (String) -> void
private def log(message)
method_name = logger.respond_to?(:debug) ? :debug : :call
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
end
# --- Files and directories ---------------------------------------------------------------------
# @sig (String) { (String, String) -> void } -> void
private def ls(dir)
children = Dir.children(dir)
# The order in which a directory is listed depends on the file system.
#
# Since client code may run in different platforms, it seems convenient to
# order directory entries. This provides consistent eager loading across
# platforms, for example.
children.sort!
children.each do |basename|
next if hidden?(basename)
abspath = File.join(dir, basename)
next if ignored_path?(abspath)
if dir?(abspath)
next if roots.key?(abspath)
next if !has_at_least_one_ruby_file?(abspath)
else
next unless ruby?(abspath)
end
# We freeze abspath because that saves allocations when passed later to
# File methods. See #125.
yield basename, abspath.freeze
end
end
# @sig (String) -> bool
private def has_at_least_one_ruby_file?(dir)
to_visit = [dir]
while dir = to_visit.shift
ls(dir) do |_basename, abspath|
if dir?(abspath)
to_visit << abspath
else
return true
end
end
end
false
end
# @sig (String) -> bool
private def ruby?(path)
path.end_with?(".rb")
end
# @sig (String) -> bool
private def dir?(path)
File.directory?(path)
end
# @sig (String) -> bool
private def hidden?(basename)
basename.start_with?(".")
end
# @sig (String) { (String) -> void } -> void
private def walk_up(abspath)
loop do
yield abspath
abspath, basename = File.split(abspath)
break if basename == "/"
end
end
# --- Constants ---------------------------------------------------------------------------------
# The autoload? predicate takes into account the ancestor chain of the
# receiver, like const_defined? and other methods in the constants API do.
#
# For example, given
#
# class A
# autoload :X, "x.rb"
# end
#
# class B < A
# end
#
# B.autoload?(:X) returns "x.rb".
#
# We need a way to strictly check in parent ignoring ancestors.
#
# @sig (Module, Symbol) -> String?
if method(:autoload?).arity == 1
private def strict_autoload_path(parent, cname)
parent.autoload?(cname) if cdef?(parent, cname)
end
else
private def strict_autoload_path(parent, cname)
parent.autoload?(cname, false)
end
end
# @sig (Module, Symbol) -> String
if Symbol.method_defined?(:name)
# Symbol#name was introduced in Ruby 3.0. It returns always the same
# frozen object, so we may save a few string allocations.
private def cpath(parent, cname)
Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
end
else
private def cpath(parent, cname)
Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
end
end
# @sig (Module, Symbol) -> bool
private def cdef?(parent, cname)
parent.const_defined?(cname, false)
end
# @raise [NameError]
# @sig (Module, Symbol) -> Object
private def cget(parent, cname)
parent.const_get(cname, false)
end
# @raise [NameError]
# @sig (Module, Symbol) -> Object
private def crem(parent, cname)
parent.__send__(:remove_const, cname)
end
CNAME_VALIDATOR = Module.new
private_constant :CNAME_VALIDATOR
# @raise [Zeitwerk::NameError]
# @sig (String, String) -> Symbol
private def cname_for(basename, abspath, namespace)
cname = if inflector.method(:camelize).arity == 3
inflector.camelize(basename, abspath, namespace)
else
inflector.camelize(basename, abspath)
end
unless cname.is_a?(String)
raise TypeError, "#{inflector.class}#camelize must return a String, received #{cname.inspect}"
end
if cname.include?("::")
raise Zeitwerk::NameError.new(<<~MESSAGE, cname)
wrong constant name #{cname} inferred by #{inflector.class} from
#{abspath}
#{inflector.class}#camelize should return a simple constant name without "::"
MESSAGE
end
begin
CNAME_VALIDATOR.const_defined?(cname, false)
rescue ::NameError => error
path_type = ruby?(abspath) ? "file" : "directory"
raise Zeitwerk::NameError.new(<<~MESSAGE, error.name)
#{error.message} inferred by #{inflector.class} from #{path_type}
#{abspath}
Possible ways to address this:
* Tell Zeitwerk to ignore this particular #{path_type}.
* Tell Zeitwerk to ignore one of its parent directories.
* Rename the #{path_type} to comply with the naming conventions.
* Modify the inflector to handle this case.
MESSAGE
end
cname.to_sym
end
end