forked from rubocop/rubocop
-
Notifications
You must be signed in to change notification settings - Fork 0
/
duplicate_methods.rb
244 lines (209 loc) · 6.82 KB
/
duplicate_methods.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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# frozen_string_literal: true
require 'set'
module RuboCop
module Cop
module Lint
# This cop checks for duplicated instance (or singleton) method
# definitions.
#
# @example
#
# # bad
#
# def foo
# 1
# end
#
# def foo
# 2
# end
#
# @example
#
# # bad
#
# def foo
# 1
# end
#
# alias foo bar
#
# @example
#
# # good
#
# def foo
# 1
# end
#
# def bar
# 2
# end
#
# @example
#
# # good
#
# def foo
# 1
# end
#
# alias bar foo
class DuplicateMethods < Cop
MSG = 'Method `%<method>s` is defined at both %<defined>s and ' \
'%<current>s.'
METHOD_DEF_METHODS = %i[alias_method attr_reader attr_writer
attr_accessor attr].to_set.freeze
def initialize(config = nil, options = nil)
super
@definitions = {}
end
def on_def(node)
# if a method definition is inside an if, it is very likely
# that a different definition is used depending on platform, etc.
return if node.ancestors.any?(&:if_type?)
return if possible_dsl?(node)
found_instance_method(node, node.method_name)
end
def on_defs(node)
return if node.ancestors.any?(&:if_type?)
return if possible_dsl?(node)
if node.receiver.const_type?
_, const_name = *node.receiver
check_const_receiver(node, node.method_name, const_name)
elsif node.receiver.self_type?
check_self_receiver(node, node.method_name)
end
end
def_node_matcher :method_alias?, <<~PATTERN
(alias (sym $_name) sym)
PATTERN
def on_alias(node)
return unless (name = method_alias?(node))
return if node.ancestors.any?(&:if_type?)
return if possible_dsl?(node)
found_instance_method(node, name)
end
def_node_matcher :alias_method?, <<~PATTERN
(send nil? :alias_method (sym $_name) _)
PATTERN
def_node_matcher :sym_name, '(sym $_name)'
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def on_send(node)
return unless METHOD_DEF_METHODS.include?(node.method_name)
if (name = alias_method?(node))
return unless name
return if node.ancestors.any?(&:if_type?)
return if possible_dsl?(node)
found_instance_method(node, name)
elsif (attr = node.attribute_accessor?)
on_attr(node, *attr)
end
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
private
def check_const_receiver(node, name, const_name)
qualified = lookup_constant(node, const_name)
return unless qualified
found_method(node, "#{qualified}.#{name}")
end
def check_self_receiver(node, name)
enclosing = node.parent_module_name
return unless enclosing
found_method(node, "#{enclosing}.#{name}")
end
def message_for_dup(node, method_name)
format(MSG, method: method_name, defined: source_location(@definitions[method_name]),
current: source_location(node))
end
def found_instance_method(node, name)
return unless (scope = node.parent_module_name)
if scope =~ /\A#<Class:(.*)>\Z/
found_method(node, "#{Regexp.last_match(1)}.#{name}")
else
found_method(node, "#{scope}##{name}")
end
end
def found_method(node, method_name)
if @definitions.key?(method_name)
loc = case node.type
when :def, :defs
node.loc.keyword.join(node.loc.name)
else
node.loc.expression
end
message = message_for_dup(node, method_name)
add_offense(node, location: loc, message: message)
else
@definitions[method_name] = node
end
end
def on_attr(node, attr_name, args)
case attr_name
when :attr
writable = args.size == 2 && args.last.true_type?
found_attr(node, [args.first], readable: true, writable: writable)
when :attr_reader
found_attr(node, args, readable: true)
when :attr_writer
found_attr(node, args, writable: true)
when :attr_accessor
found_attr(node, args, readable: true, writable: true)
end
end
def found_attr(node, args, readable: false, writable: false)
args.each do |arg|
name = sym_name(arg)
next unless name
found_instance_method(node, name) if readable
found_instance_method(node, "#{name}=") if writable
end
end
def lookup_constant(node, const_name)
# this method is quite imperfect and can be fooled
# to do much better, we would need to do global analysis of the whole
# codebase
node.each_ancestor(:class, :module, :casgn) do |ancestor|
namespace, mod_name = *ancestor.defined_module
loop do
if mod_name == const_name
return qualified_name(ancestor.parent_module_name,
namespace,
mod_name)
end
break if namespace.nil?
namespace, mod_name = *namespace
end
end
end
def qualified_name(enclosing, namespace, mod_name)
if enclosing != 'Object'
if namespace
"#{enclosing}::#{namespace.const_name}::#{mod_name}"
else
"#{enclosing}::#{mod_name}"
end
elsif namespace
"#{namespace.const_name}::#{mod_name}"
else
mod_name
end
end
def possible_dsl?(node)
# DSL methods may evaluate a block in the context of a newly created
# class or module
# Assume that if a method definition is inside any block call which
# we can't identify, it could be a DSL
node.each_ancestor(:block).any? do |ancestor|
ancestor.method_name != :class_eval && !ancestor.class_constructor?
end
end
def source_location(node)
range = node.location.expression
path = smart_path(range.source_buffer.name)
"#{path}:#{range.line}"
end
end
end
end
end