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. With AllCops/ActiveSupportExtensionsEnabled: true, Active Support's redefinition markers (silence_redefinition_of_method and redefine_method) are honored the same way.

NOTE: Methods defined with define_method are not recorded in the project index, so a duplicate whose other definition uses define_method cannot be detected across files.

Cross-file duplicates whose other definition lives in a file matching one of the AllowedCrossFilePaths patterns are not reported. This suits files that redefine application methods but are never loaded together with them, such as standalone scripts. Patterns are matched with the same glob (or regexp) semantics as Exclude, against the other file's path relative to the directory of the .rubocop.yml that configures the cop; absolute patterns are matched against the absolute path. Offenses inside such files themselves are best silenced with an ordinary per-cop Exclude.

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

# good - Active Support's redefinition markers signal an intentional
# redefinition of a method defined in another file
silence_redefinition_of_method :foo
def foo
  1
end

AllowedCrossFilePaths: ['script/**/*'] (default: [])

# Cross-file duplicates whose other definition lives in a file
# matching one of the patterns are not reported.

# good - assuming `AppHelper#format` is also defined in
# `script/backfill.rb`, which is never loaded with this file
class AppHelper
  def format
  end
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?, #skipped_unsafe_correction_with_disable_uncorrectable?

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.



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

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

Instance Method Details

#active_support_redefinition_marker(node) ⇒ Object

Matches Active Support's markers of an intentional method redefinition: silence_redefinition_of_method :name suppresses Ruby's redefinition warning for a subsequent definition of name, and redefine_method(:name) { ... } both silences the warning and redefines.



283
284
285
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 283

def_node_matcher :active_support_redefinition_marker, <<~PATTERN
  (send nil? {:silence_redefinition_of_method :redefine_method} ({sym str} $_) ...)
PATTERN

#alias_method?(node) ⇒ Object



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

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

#class_new_block?(node) ⇒ Object



298
299
300
301
302
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 298

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

#class_or_module_new_block?(node) ⇒ Object



291
292
293
294
295
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 291

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.



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

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

#delegator?(node) ⇒ Object



261
262
263
264
265
266
267
268
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 261

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



271
272
273
274
275
276
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 271

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

#method_alias?(node) ⇒ Object



227
228
229
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 227

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

#on_alias(node) ⇒ Object



231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 231

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

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

  found_instance_method(node, name)
end

#on_def(node) ⇒ Object



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

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



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

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



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

def on_new_investigation
  # The self-alias trick and Active Support's redefinition markers declare
  # an intentional redefinition only within the file that uses them, so the
  # tracked names do not carry over.
  @intentionally_redefined = Set.new
  super
end

#on_send(node) ⇒ Object

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



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/rubocop/cop/lint/duplicate_methods.rb', line 304

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_intentional_redefinition(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) }
  elsif (name = active_support_redefinition_marker(node))
    # `redefine_method` takes a block; scope resolution must start from the
    # block node, since `parent_module_name` cannot see through a block
    # ancestor that is not a module constructor.
    track_intentional_redefinition(node.block_node || node, name) if
      active_support_extensions_enabled?
  end
end

#sym_name(node) ⇒ Object



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

def_node_matcher :sym_name, '(sym $_name)'