This repository has been archived by the owner on Feb 19, 2024. It is now read-only.
forked from rubocop/rubocop
/
non_deterministic_require_order.rb
156 lines (136 loc) · 4.49 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
# 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.
#
# @example
#
# # bad
# Dir["./lib/**/*.rb"].each do |file|
# require file
# end
#
# # good
# Dir["./lib/**/*.rb"].sort.each do |file|
# require file
# end
#
# @example
#
# # 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
#
# @example
#
# # bad
# Dir['./lib/**/*.rb'].each(&method(:require))
#
# # good
# Dir['./lib/**/*.rb'].sort.each(&method(:require))
#
# @example
#
# # bad
# Dir.glob(Rails.root.join('test', '*.rb'), &method(:require))
#
# # good
# Dir.glob(Rails.root.join('test', '*.rb')).sort.each(&method(:require))
#
class NonDeterministicRequireOrder < Cop
MSG = 'Sort files before requiring them.'
def on_block(node)
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)
end
end
def on_block_pass(node)
return unless method_require?(node)
return unless unsorted_dir_pass?(node.parent)
add_offense(node.parent)
end
def autocorrect(node)
return correct_block_pass(node) if node.arguments.last&.block_pass_type?
if unsorted_dir_block?(node)
lambda do |corrector|
corrector.replace(node, "#{node.source}.sort.each")
end
else
lambda do |corrector|
source = node.receiver.source
corrector.replace(node, "#{source}.sort.each")
end
end
end
private
def correct_block_pass(node)
if unsorted_dir_glob_pass?(node)
lambda do |corrector|
block_arg = node.arguments.last
corrector.remove(last_arg_range(node))
corrector.insert_after(node, ".sort.each(#{block_arg.source})")
end
else
lambda do |corrector|
corrector.replace(node.loc.selector, 'sort.each')
end
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
def_node_matcher :unsorted_dir_block?, <<~PATTERN
(send (const {nil? cbase} :Dir) :glob ...)
PATTERN
def_node_matcher :unsorted_dir_each?, <<~PATTERN
(send (send (const {nil? cbase} :Dir) {:[] :glob} ...) :each)
PATTERN
def_node_matcher :method_require?, <<~PATTERN
(block-pass (send nil? :method (sym :require)))
PATTERN
def_node_matcher :unsorted_dir_glob_pass?, <<~PATTERN
(send (const {nil? cbase} :Dir) :glob ...
(block-pass (send nil? :method (sym :require))))
PATTERN
def_node_matcher :unsorted_dir_each_pass?, <<~PATTERN
(send (send (const {nil? cbase} :Dir) {:[] :glob} ...) :each
(block-pass (send nil? :method (sym :require))))
PATTERN
def_node_matcher :loop_variable, <<~PATTERN
(args (arg $_))
PATTERN
def_node_search :var_is_required?, <<~PATTERN
(send nil? :require (lvar %1))
PATTERN
end
end
end
end