/
useless_setter_call.rb
173 lines (139 loc) · 5.08 KB
/
useless_setter_call.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
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# This cop checks for setter call to local variable as the final
# expression of a function definition.
#
# @safety
# There are edge cases in which the local variable references a
# value that is also accessible outside the local scope. This is not
# detected by the cop, and it can yield a false positive.
#
# As well, auto-correction is unsafe because the method's
# return value will be changed.
#
# @example
#
# # bad
#
# def something
# x = Something.new
# x.attr = 5
# end
#
# @example
#
# # good
#
# def something
# x = Something.new
# x.attr = 5
# x
# end
class UselessSetterCall < Base
extend AutoCorrector
MSG = 'Useless setter call to local variable `%<variable>s`.'
ASSIGNMENT_TYPES = %i[lvasgn ivasgn cvasgn gvasgn].freeze
def on_def(node)
return unless node.body
last_expr = last_expression(node.body)
return unless setter_call_to_local_variable?(last_expr)
tracker = MethodVariableTracker.new(node.body)
receiver, = *last_expr
variable_name, = *receiver
return unless tracker.contain_local_object?(variable_name)
loc_name = receiver.loc.name
add_offense(loc_name, message: format(MSG, variable: loc_name.source)) do |corrector|
corrector.insert_after(last_expr, "\n#{indent(last_expr)}#{loc_name.source}")
end
end
alias on_defs on_def
private
# @!method setter_call_to_local_variable?(node)
def_node_matcher :setter_call_to_local_variable?, <<~PATTERN
[(send (lvar _) ...) setter_method?]
PATTERN
def last_expression(body)
expression = body.begin_type? ? body.children : body
expression.is_a?(Array) ? expression.last : expression
end
# This class tracks variable assignments in a method body
# and if a variable contains object passed as argument at the end of
# the method.
class MethodVariableTracker
def initialize(body_node)
@body_node = body_node
@local = nil
end
def contain_local_object?(variable_name)
return @local[variable_name] if @local
@local = {}
scan(@body_node) { |node| process_assignment_node(node) }
@local[variable_name]
end
def scan(node, &block)
catch(:skip_children) do
yield node
node.each_child_node { |child_node| scan(child_node, &block) }
end
end
def process_assignment_node(node)
case node.type
when :masgn
process_multiple_assignment(node)
when :or_asgn, :and_asgn
process_logical_operator_assignment(node)
when :op_asgn
process_binary_operator_assignment(node)
when *ASSIGNMENT_TYPES
_, rhs_node = *node
process_assignment(node, rhs_node) if rhs_node
end
end
def process_multiple_assignment(masgn_node)
mlhs_node, mrhs_node = *masgn_node
mlhs_node.children.each_with_index do |lhs_node, index|
next unless ASSIGNMENT_TYPES.include?(lhs_node.type)
lhs_variable_name, = *lhs_node
rhs_node = mrhs_node.children[index]
if mrhs_node.array_type? && rhs_node
process_assignment(lhs_variable_name, rhs_node)
else
@local[lhs_variable_name] = true
end
end
throw :skip_children
end
def process_logical_operator_assignment(asgn_node)
lhs_node, rhs_node = *asgn_node
return unless ASSIGNMENT_TYPES.include?(lhs_node.type)
process_assignment(lhs_node, rhs_node)
throw :skip_children
end
def process_binary_operator_assignment(op_asgn_node)
lhs_node, = *op_asgn_node
return unless ASSIGNMENT_TYPES.include?(lhs_node.type)
lhs_variable_name, = *lhs_node
@local[lhs_variable_name] = true
throw :skip_children
end
def process_assignment(asgn_node, rhs_node)
lhs_variable_name, = *asgn_node
@local[lhs_variable_name] = if rhs_node.variable?
rhs_variable_name, = *rhs_node
@local[rhs_variable_name]
else
constructor?(rhs_node)
end
end
def constructor?(node)
return true if node.literal?
return false unless node.send_type?
node.method?(:new)
end
end
end
end
end
end