-
-
Notifications
You must be signed in to change notification settings - Fork 269
/
receive_messages.rb
161 lines (139 loc) · 4.79 KB
/
receive_messages.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
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for multiple messages stubbed on the same object.
#
# @safety
# The autocorrection is marked as unsafe, because it may change the
# order of stubs. This in turn may cause e.g. variables to be called
# before they are defined.
#
# @example
# # bad
# before do
# allow(Service).to receive(:foo).and_return(bar)
# allow(Service).to receive(:baz).and_return(qux)
# end
#
# # good
# before do
# allow(Service).to receive_messages(foo: bar, baz: qux)
# end
#
# # good - ignore same message
# before do
# allow(Service).to receive(:foo).and_return(bar)
# allow(Service).to receive(:foo).and_return(qux)
# end
#
class ReceiveMessages < Base
extend AutoCorrector
include RangeHelp
MSG = 'Use `receive_messages` instead of multiple stubs on lines ' \
'%<loc>s.'
# @!method allow_receive_message?(node)
def_node_matcher :allow_receive_message?, <<~PATTERN
(send (send nil? :allow ...) :to (send (send nil? :receive (sym _)) :and_return !#heredoc_or_splat?))
PATTERN
# @!method allow_argument(node)
def_node_matcher :allow_argument, <<~PATTERN
(send (send nil? :allow $_ ...) ...)
PATTERN
# @!method receive_node(node)
def_node_search :receive_node, <<~PATTERN
$(send (send nil? :receive ...) ...)
PATTERN
# @!method receive_arg(node)
def_node_search :receive_arg, <<~PATTERN
(send (send nil? :receive $_) ...)
PATTERN
# @!method receive_and_return_argument(node)
def_node_matcher :receive_and_return_argument, <<~PATTERN
(send (send nil? :allow ...) :to (send (send nil? :receive (sym $_)) :and_return $_))
PATTERN
def on_begin(node)
repeated_receive_message(node).each do |item, repeated_lines, args|
next if repeated_lines.empty?
register_offense(item, repeated_lines, args)
end
end
private
def repeated_receive_message(node)
node
.children
.select { |child| allow_receive_message?(child) }
.group_by { |child| allow_argument(child) }
.values
.reject(&:one?)
.flat_map { |items| add_repeated_lines_and_arguments(items) }
end
def add_repeated_lines_and_arguments(items)
uniq_items = uniq_items(items)
repeated_lines = uniq_items.map(&:first_line)
uniq_items.map do |item|
[item, repeated_lines - [item.first_line], arguments(uniq_items)]
end
end
def uniq_items(items)
items.select do |item|
items.none? do |i|
receive_arg(item).first == receive_arg(i).first &&
!same_line?(item, i)
end
end
end
def arguments(items)
items.map do |item|
receive_and_return_argument(item) do |receive_arg, return_arg|
"#{normalize_receive_arg(receive_arg)}: " \
"#{normalize_return_arg(return_arg)}"
end
end
end
def normalize_receive_arg(receive_arg)
if requires_quotes?(receive_arg)
"'#{receive_arg}'"
else
receive_arg
end
end
def normalize_return_arg(return_arg)
if return_arg.hash_type? && !return_arg.braces?
"{ #{return_arg.source} }"
else
return_arg.source
end
end
def register_offense(item, repeated_lines, args)
add_offense(item, message: message(repeated_lines)) do |corrector|
if item.loc.line > repeated_lines.max
replace_to_receive_messages(corrector, item, args)
else
corrector.remove(item_range_by_whole_lines(item))
end
end
end
def message(repeated_lines)
format(MSG, loc: repeated_lines)
end
def replace_to_receive_messages(corrector, item, args)
receive_node(item) do |node|
corrector.replace(node,
"receive_messages(#{args.join(', ')})")
end
end
def item_range_by_whole_lines(item)
range_by_whole_lines(item.source_range, include_final_newline: true)
end
def heredoc_or_splat?(node)
((node.str_type? || node.dstr_type?) && node.heredoc?) ||
node.splat_type?
end
def requires_quotes?(value)
value.match?(/^:".*?"|=$|^\W+$/)
end
end
end
end
end