/
shadowed_argument.rb
176 lines (148 loc) · 5.13 KB
/
shadowed_argument.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
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# This cop checks for shadowed arguments.
#
# This cop has `IgnoreImplicitReferences` configuration option.
# It means argument shadowing is used in order to pass parameters
# to zero arity `super` when `IgnoreImplicitReferences` is `true`.
#
# @example
#
# # bad
# do_something do |foo|
# foo = 42
# puts foo
# end
#
# def do_something(foo)
# foo = 42
# puts foo
# end
#
# # good
# do_something do |foo|
# foo = foo + 42
# puts foo
# end
#
# def do_something(foo)
# foo = foo + 42
# puts foo
# end
#
# def do_something(foo)
# puts foo
# end
#
# @example IgnoreImplicitReferences: false (default)
#
# # bad
# def do_something(foo)
# foo = 42
# super
# end
#
# def do_something(foo)
# foo = super
# bar
# end
#
# @example IgnoreImplicitReferences: true
#
# # good
# def do_something(foo)
# foo = 42
# super
# end
#
# def do_something(foo)
# foo = super
# bar
# end
#
class ShadowedArgument < Base
MSG = 'Argument `%<argument>s` was shadowed by a local variable before it was used.'
# @!method uses_var?(node)
def_node_search :uses_var?, '(lvar %)'
def self.joining_forces
VariableForce
end
def after_leaving_scope(scope, _variable_table)
scope.variables.each_value { |variable| check_argument(variable) }
end
private
def check_argument(argument)
return unless argument.method_argument? || argument.block_argument?
# Block local variables, i.e., variables declared after ; inside
# |...| aren't really arguments.
return if argument.explicit_block_local_variable?
shadowing_assignment(argument) do |node|
message = format(MSG, argument: argument.name)
add_offense(node, message: message)
end
end
def shadowing_assignment(argument)
return unless argument.referenced?
assignment_without_argument_usage(argument) do |node, location_known|
assignment_without_usage_pos = node.source_range.begin_pos
references = argument_references(argument)
# If argument was referenced before it was reassigned
# then it's not shadowed
next if references.any? do |reference|
next true if !reference.explicit? && ignore_implicit_references?
reference_pos(reference.node) <= assignment_without_usage_pos
end
yield location_known ? node : argument.declaration_node
end
end
# Find the first argument assignment, which doesn't reference the
# argument at the rhs. If the assignment occurs inside a branch or
# block, it is impossible to tell whether it's executed, so precise
# shadowing location is not known.
#
def assignment_without_argument_usage(argument)
argument.assignments.reduce(true) do |location_known, assignment|
assignment_node = assignment.meta_assignment_node || assignment.node
# Shorthand assignments always use their arguments
next false if assignment_node.shorthand_asgn?
node_within_block_or_conditional =
node_within_block_or_conditional?(assignment_node.parent, argument.scope.node)
unless uses_var?(assignment_node, argument.name)
# It's impossible to decide whether a branch or block is executed,
# so the precise reassignment location is undecidable.
next false if node_within_block_or_conditional
yield(assignment.node, location_known)
break
end
location_known
end
end
def reference_pos(node)
node = node.parent if node.parent.masgn_type?
node.source_range.begin_pos
end
# Check whether the given node is nested into block or conditional.
#
def node_within_block_or_conditional?(node, stop_search_node)
return false if node == stop_search_node
node.conditional? || node.block_type? ||
node_within_block_or_conditional?(node.parent, stop_search_node)
end
# Get argument references without assignments' references
#
def argument_references(argument)
assignment_references = argument.assignments.flat_map(&:references).map(&:source_range)
argument.references.reject do |ref|
next false unless ref.explicit?
assignment_references.include?(ref.node.source_range)
end
end
def ignore_implicit_references?
cop_config['IgnoreImplicitReferences']
end
end
end
end
end