/
random_with_offset.rb
153 lines (132 loc) · 4.62 KB
/
random_with_offset.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
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# This cop checks for the use of randomly generated numbers,
# added/subtracted with integer literals, as well as those with
# Integer#succ and Integer#pred methods. Prefer using ranges instead,
# as it clearly states the intentions.
#
# @example
# # bad
# rand(6) + 1
# 1 + rand(6)
# rand(6) - 1
# 1 - rand(6)
# rand(6).succ
# rand(6).pred
# Random.rand(6) + 1
# Kernel.rand(6) + 1
# rand(0..5) + 1
#
# # good
# rand(1..6)
# rand(1...7)
class RandomWithOffset < Base
extend AutoCorrector
MSG = 'Prefer ranges when generating random numbers instead of ' \
'integers with offsets.'
RESTRICT_ON_SEND = %i[+ - succ pred next].freeze
def_node_matcher :integer_op_rand?, <<~PATTERN
(send
int {:+ :-}
(send
{nil? (const {nil? cbase} :Random) (const {nil? cbase} :Kernel)}
:rand
{int (irange int int) (erange int int)}))
PATTERN
def_node_matcher :rand_op_integer?, <<~PATTERN
(send
(send
{nil? (const {nil? cbase} :Random) (const {nil? cbase} :Kernel)}
:rand
{int (irange int int) (erange int int)})
{:+ :-}
int)
PATTERN
def_node_matcher :rand_modified?, <<~PATTERN
(send
(send
{nil? (const {nil? cbase} :Random) (const {nil? cbase} :Kernel)}
:rand
{int (irange int int) (erange int int)})
{:succ :pred :next})
PATTERN
def on_send(node)
return unless node.receiver
return unless integer_op_rand?(node) ||
rand_op_integer?(node) ||
rand_modified?(node)
add_offense(node) do |corrector|
autocorrect(corrector, node)
end
end
private
def_node_matcher :random_call, <<~PATTERN
{(send (send $_ _ $_) ...)
(send _ _ (send $_ _ $_))}
PATTERN
def autocorrect(corrector, node)
if integer_op_rand?(node)
corrector.replace(node, corrected_integer_op_rand(node))
elsif rand_op_integer?(node)
corrector.replace(node, corrected_rand_op_integer(node))
elsif rand_modified?(node)
corrector.replace(node, corrected_rand_modified(node))
end
end
def corrected_integer_op_rand(node)
random_call(node) do |prefix_node, random_node|
prefix = prefix_from_prefix_node(prefix_node)
left_int, right_int = boundaries_from_random_node(random_node)
offset = to_int(node.receiver)
if node.method?(:+)
"#{prefix}(#{offset + left_int}..#{offset + right_int})"
else
"#{prefix}(#{offset - right_int}..#{offset - left_int})"
end
end
end
def corrected_rand_op_integer(node)
random_call(node) do |prefix_node, random_node|
prefix = prefix_from_prefix_node(prefix_node)
left_int, right_int = boundaries_from_random_node(random_node)
offset = to_int(node.first_argument)
if node.method?(:+)
"#{prefix}(#{left_int + offset}..#{right_int + offset})"
else
"#{prefix}(#{left_int - offset}..#{right_int - offset})"
end
end
end
def corrected_rand_modified(node)
random_call(node) do |prefix_node, random_node|
prefix = prefix_from_prefix_node(prefix_node)
left_int, right_int = boundaries_from_random_node(random_node)
if %i[succ next].include?(node.method_name)
"#{prefix}(#{left_int + 1}..#{right_int + 1})"
elsif node.method?(:pred)
"#{prefix}(#{left_int - 1}..#{right_int - 1})"
end
end
end
def prefix_from_prefix_node(node)
[node&.source, 'rand'].compact.join('.')
end
def boundaries_from_random_node(random_node)
case random_node.type
when :int
[0, to_int(random_node) - 1]
when :irange
[to_int(random_node.begin), to_int(random_node.end)]
when :erange
[to_int(random_node.begin), to_int(random_node.end) - 1]
end
end
def_node_matcher :to_int, <<~PATTERN
(int $_)
PATTERN
end
end
end
end