Class: RuboCop::Cop::Lint::DuplicateMethods

Inherits:
Base
  • Object
show all
Includes:
ProjectIndexHelp
Defined in:
lib/rubocop/cop/lint/duplicate_methods.rb

Overview

Checks for duplicated instance (or singleton) method definitions.

NOTE: Aliasing a method to itself is allowed, as it indicates that the developer intends to suppress Ruby's method redefinition warnings. See https://bugs.ruby-lang.org/issues/13574.

By default the cop can only detect duplicates within a single file. When AllCops/UseProjectIndex is enabled and the rubydex gem is installed, the cop additionally consults the project-wide index and reports methods whose duplicate definition lives in another file.

NOTE: The project index does not record whether a definition in another file is wrapped in a conditional, so a platform-specific redefinition in another file may still be reported. Aliasing the method to itself (see above) before redefining marks the redefinition as intentional and is respected across files.

Examples:


# bad
def foo
  1
end

def foo
  2
end

# bad
def foo
  1
end

alias foo bar

# good
def foo
  1
end

def bar
  2
end

# good
def foo
  1
end

alias bar foo

# good
alias foo foo
def foo
  1
end

# good
alias_method :foo, :foo
def foo
  1
end

# bad
class MyClass
  extend Forwardable

  # or with: `def_instance_delegator`, `def_delegators`, `def_instance_delegators`
  def_delegator :delegation_target, :delegated_method_name

  def delegated_method_name
  end
end

# good
class MyClass
  extend Forwardable

  def_delegator :delegation_target, :delegated_method_name

  def non_duplicated_delegated_method_name
  end
end

AllCops:ActiveSupportExtensionsEnabled: false (default)


# good
def foo
  1
end

delegate :foo, to: :bar

AllCops:ActiveSupportExtensionsEnabled: true


# bad
def foo
  1
end

delegate :foo, to: :bar

# good
def foo
  1
end

delegate :baz, to: :bar

# good - delegate with splat arguments is ignored
def foo
  1
end

delegate :foo, **options

# good - delegate inside a condition is ignored
def foo
  1
end

if cond
  delegate :foo, to: :bar
end

DelegatingMethods: ['delegate', 'expose'] (default: ['delegate'])

# A project's own `delegate`-shaped macros can be registered so the
# methods they define are recognized (with
# `AllCops/ActiveSupportExtensionsEnabled: true`).

# bad
def foo
  1
end

expose :foo, to: :bar

Constant Summary collapse

MSG =
'Method `%<method>s` is defined at both %<defined>s and %<current>s.'
INDEXABLE_METHOD_NAME =

Method names the cop registers that can be looked up in the project index: a fully qualified namespace followed by # (instance) or . (singleton) and the method name.

/\A(?<owner>[A-Z]\w*(?:::[A-Z]\w*)*)(?<separator>[#.])(?<name>[^#.]+)\z/.freeze

Constants included from ProjectIndexHelp

ProjectIndexHelp::BUILTIN_DOCUMENT_URI, ProjectIndexHelp::FILE_URI_PREFIX, ProjectIndexHelp::WINDOWS_DRIVE_PREFIX

Constants inherited from Base

Base::RESTRICT_ON_SEND

Instance Attribute Summary

Attributes inherited from Base

#config, #processed_source, #project_index

Instance Method Summary collapse

Methods included from ProjectIndexHelp

#external_dependency_checksum

Methods inherited from Base

#active_support_extensions_enabled?, #add_global_offense, #add_offense, #always_autocorrect?, autocorrect_incompatible_with, badge, #begin_investigation, #callbacks_needed, callbacks_needed, #config_to_allow_offenses, #config_to_allow_offenses=, #contextual_autocorrect?, #cop_config, cop_name, #cop_name, department, documentation_url, exclude_from_registry, #excluded_file?, #external_dependency_checksum, inherited, #inspect, joining_forces, lint?, match?, #message, #offenses, #on_investigation_end, #on_other_file, #parse, #parser_engine, #ready, #relevant_file?, requires_gem, #string_literals_frozen_by_default?, support_autocorrect?, support_multiple_source?, #target_gem_version, #target_rails_version, #target_ruby_version

Methods included from ExcludeLimit

cop_dir_for, #exclude_limit, read_limits

Methods included from AutocorrectLogic

#autocorrect?, #autocorrect_enabled?, #autocorrect_requested?, #autocorrect_with_disable_uncorrectable?, #correctable?, #disable_uncorrectable?, #safe_autocorrect?

Methods included from IgnoredNode

#ignore_node, #ignored_node?, #part_of_ignored_node?

Methods included from Util

silence_warnings

Constructor Details

#initialize(config = nil, options = nil) ⇒ DuplicateMethods

No RESTRICT_ON_SEND: the delegating method names are configurable (DelegatingMethods), so on_send must see every call. The handler returns quickly for calls that are not method definitions, and benchmarks show no measurable cost over the previous fixed list.



159
160
161
162
163
164
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 159

def initialize(config = nil, options = nil)
  super
  @definitions = {}
  @scopes = Hash.new { |hash, key| hash[key] = [] }
  @self_aliased = Set.new
end

Instance Method Details

#alias_method?(node) ⇒ Object



211
212
213
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 211

def_node_matcher :alias_method?, <<~PATTERN
  (send nil? :alias_method (sym $_name) (sym $_original_name))
PATTERN

#class_new_block?(node) ⇒ Object



255
256
257
258
259
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 255

def_node_matcher :class_new_block?, <<~PATTERN
  (block
    (send (const _ :Class) :new ...)
    ...)
PATTERN

#class_or_module_new_block?(node) ⇒ Object



248
249
250
251
252
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 248

def_node_matcher :class_or_module_new_block?, <<~PATTERN
  (block
    (send (const _ {:Class :Module}) :new ...)
    ...)
PATTERN

#delegate_args(node) ⇒ Object

Matches the argument shape of an Active Support delegate call (delegate :a, :b, to: :target), regardless of the method name; the name is checked separately against DelegatingMethods.



219
220
221
222
223
224
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 219

def_node_matcher :delegate_args, <<~PATTERN
  (send nil? _
    ({sym str} $_)+
    (hash <(pair (sym :to) {sym str}) ...>)
  )
PATTERN

#delegator?(node) ⇒ Object



227
228
229
230
231
232
233
234
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 227

def_node_matcher :delegator?, <<~PATTERN
  (send nil? {:def_delegator :def_instance_delegator}
    {
      {sym str} ({sym str} $_) |
      {sym str} {sym str} ({sym str} $_)
    }
  )
PATTERN

#delegators?(node) ⇒ Object



237
238
239
240
241
242
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 237

def_node_matcher :delegators?, <<~PATTERN
  (send nil? {:def_delegators :def_instance_delegators}
    {sym str}
    ({sym str} $_)+
  )
PATTERN

#method_alias?(node) ⇒ Object



193
194
195
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 193

def_node_matcher :method_alias?, <<~PATTERN
  (alias (sym $_name) (sym $_original_name))
PATTERN

#on_alias(node) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 197

def on_alias(node)
  name, original_name = method_alias?(node)
  return unless name && original_name

  if name == original_name
    track_self_alias(node, name)
    return
  end
  return if node.ancestors.any?(&:if_type?)

  found_instance_method(node, name)
end

#on_def(node) ⇒ Object



173
174
175
176
177
178
179
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 173

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.each_ancestor.any?(&:if_type?)

  found_instance_method(node, node.method_name)
end

#on_defs(node) ⇒ Object



181
182
183
184
185
186
187
188
189
190
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 181

def on_defs(node)
  return if node.each_ancestor.any?(&:if_type?)

  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

#on_new_investigationObject



166
167
168
169
170
171
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 166

def on_new_investigation
  # The self-alias trick declares an intentional redefinition only within
  # the file that uses it, so the tracked names do not carry over.
  @self_aliased = Set.new
  super
end

#on_send(node) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 261

def on_send(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  name, original_name = alias_method?(node)

  if name && original_name
    if name == original_name
      track_self_alias(node, name)
      return
    end
    return if inside_condition?(node)

    found_instance_method(node, name)
  elsif (attr = node.attribute_accessor?)
    on_attr(node, *attr)
  elsif delegating_method?(node) && (names = delegate_args(node))
    return if inside_condition?(node)

    on_delegate(node, names)
  elsif (name = delegator?(node))
    return if inside_condition?(node)

    found_instance_method(node, name)
  elsif (names = delegators?(node))
    return if inside_condition?(node)

    names.each { |name| found_instance_method(node, name) }
  end
end

#sym_name(node) ⇒ Object



245
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 245

def_node_matcher :sym_name, '(sym $_name)'