forked from rubocop/rubocop
/
non_deterministic_require_order.rb
185 lines (160 loc) · 5.98 KB
/
non_deterministic_require_order.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
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# `Dir[...]` and `Dir.glob(...)` do not make any guarantees about
# the order in which files are returned. The final order is
# determined by the operating system and file system.
# This means that using them in cases where the order matters,
# such as requiring files, can lead to intermittent failures
# that are hard to debug. To ensure this doesn't happen,
# always sort the list.
#
# `Dir.glob` and `Dir[]` sort globbed results by default in Ruby 3.0.
# So all bad cases are acceptable when Ruby 3.0 or higher are used.
#
# NOTE: This cop will be deprecated and removed when supporting only Ruby 3.0 and higher.
#
# @safety
# This cop is unsafe in the case where sorting files changes existing
# expected behavior.
#
# @example
#
# # bad
# Dir["./lib/**/*.rb"].each do |file|
# require file
# end
#
# # good
# Dir["./lib/**/*.rb"].sort.each do |file|
# require file
# end
#
# # bad
# Dir.glob(Rails.root.join(__dir__, 'test', '*.rb')) do |file|
# require file
# end
#
# # good
# Dir.glob(Rails.root.join(__dir__, 'test', '*.rb')).sort.each do |file|
# require file
# end
#
# # bad
# Dir['./lib/**/*.rb'].each(&method(:require))
#
# # good
# Dir['./lib/**/*.rb'].sort.each(&method(:require))
#
# # bad
# Dir.glob(Rails.root.join('test', '*.rb'), &method(:require))
#
# # good
# Dir.glob(Rails.root.join('test', '*.rb')).sort.each(&method(:require))
#
# # good - Respect intent if `sort` keyword option is specified in Ruby 3.0 or higher.
# Dir.glob(Rails.root.join(__dir__, 'test', '*.rb'), sort: false).each(&method(:require))
#
class NonDeterministicRequireOrder < Base
extend AutoCorrector
MSG = 'Sort files before requiring them.'
def on_block(node)
return if target_ruby_version >= 3.0
return unless node.body
return unless unsorted_dir_loop?(node.send_node)
loop_variable(node.arguments) do |var_name|
return unless var_is_required?(node.body, var_name)
add_offense(node.send_node) { |corrector| correct_block(corrector, node.send_node) }
end
end
def on_numblock(node)
return if target_ruby_version >= 3.0
return unless node.body
return unless unsorted_dir_loop?(node.send_node)
node.argument_list
.filter { |argument| var_is_required?(node.body, argument.name) }
.each do
add_offense(node.send_node) { |corrector| correct_block(corrector, node.send_node) }
end
end
def on_block_pass(node)
return if target_ruby_version >= 3.0
return unless method_require?(node)
return unless unsorted_dir_pass?(node.parent)
parent_node = node.parent
add_offense(parent_node) do |corrector|
if parent_node.arguments.last&.block_pass_type?
correct_block_pass(corrector, parent_node)
else
correct_block(corrector, parent_node)
end
end
end
private
def correct_block(corrector, node)
if unsorted_dir_block?(node)
corrector.replace(node, "#{node.source}.sort.each")
else
source = node.receiver.source
corrector.replace(node, "#{source}.sort.each")
end
end
def correct_block_pass(corrector, node)
if unsorted_dir_glob_pass?(node)
block_arg = node.arguments.last
corrector.remove(last_arg_range(node))
corrector.insert_after(node, ".sort.each(#{block_arg.source})")
else
corrector.replace(node.loc.selector, 'sort.each')
end
end
# Returns range of last argument including comma and whitespace.
#
# @return [Parser::Source::Range]
#
def last_arg_range(node)
node.arguments.last.source_range.with(
begin_pos: node.arguments[-2].source_range.end_pos
)
end
def unsorted_dir_loop?(node)
unsorted_dir_block?(node) || unsorted_dir_each?(node)
end
def unsorted_dir_pass?(node)
unsorted_dir_glob_pass?(node) || unsorted_dir_each_pass?(node)
end
# @!method unsorted_dir_block?(node)
def_node_matcher :unsorted_dir_block?, <<~PATTERN
(send (const {nil? cbase} :Dir) :glob ...)
PATTERN
# @!method unsorted_dir_each?(node)
def_node_matcher :unsorted_dir_each?, <<~PATTERN
(send (send (const {nil? cbase} :Dir) {:[] :glob} ...) :each)
PATTERN
# @!method method_require?(node)
def_node_matcher :method_require?, <<~PATTERN
(block-pass (send nil? :method (sym {:require :require_relative})))
PATTERN
# @!method unsorted_dir_glob_pass?(node)
def_node_matcher :unsorted_dir_glob_pass?, <<~PATTERN
(send (const {nil? cbase} :Dir) :glob ...
(block-pass (send nil? :method (sym {:require :require_relative}))))
PATTERN
# @!method unsorted_dir_each_pass?(node)
def_node_matcher :unsorted_dir_each_pass?, <<~PATTERN
(send (send (const {nil? cbase} :Dir) {:[] :glob} ...) :each
(block-pass (send nil? :method (sym {:require :require_relative}))))
PATTERN
# @!method loop_variable(node)
def_node_matcher :loop_variable, <<~PATTERN
(args (arg $_))
PATTERN
# @!method var_is_required?(node, name)
def_node_search :var_is_required?, <<~PATTERN
(send nil? {:require :require_relative} (lvar %1))
PATTERN
end
end
end
end