# frozen_string_literal: true require_relative 'base_detector' module Reek module SmellDetectors # # A Data Clump occurs when the same two or three items frequently # appear together in classes and parameter lists, or when a group # of instance variable names start or end with similar substrings. # # The recurrence of the items often means there is duplicate code # spread around to handle them. There may be an abstraction missing # from the code, making the system harder to understand. # # Currently Reek looks for a group of two or more parameters with # the same names that are expected by three or more methods of a class. # # See {file:docs/Data-Clump.md} for details. class DataClump < BaseDetector # # The name of the config field that sets the maximum allowed # copies of any clump. No group of common parameters will be # reported as a DataClump unless there are more than this many # methods containing those parameters. # MAX_COPIES_KEY = 'max_copies' DEFAULT_MAX_COPIES = 2 # # The name of the config field that sets the minimum clump # size. No group of common parameters will be reported as # a DataClump unless it contains at least this many parameters. # MIN_CLUMP_SIZE_KEY = 'min_clump_size' DEFAULT_MIN_CLUMP_SIZE = 2 def self.contexts # :nodoc: [:class, :module] end def self.default_config super.merge( MAX_COPIES_KEY => DEFAULT_MAX_COPIES, MIN_CLUMP_SIZE_KEY => DEFAULT_MIN_CLUMP_SIZE) end # # Checks the given class or module for multiple identical parameter sets. # # @return [Array] # def sniff clumps.map do |clump, methods| methods_length = methods.length smell_warning( lines: methods.map(&:line), message: "takes parameters #{DataClump.print_clump(clump)} " \ "to #{methods_length} methods", parameters: { parameters: clump.map(&:to_s), count: methods_length }) end end # @private def self.print_clump(clump) "[#{clump.map { |parameter| "'#{parameter}'" }.join(', ')}]" end private def max_copies @max_copies ||= value(MAX_COPIES_KEY, context) end def min_clump_size @min_clump_size ||= value(MIN_CLUMP_SIZE_KEY, context) end def candidate_methods @candidate_methods ||= context.node_instance_methods end def candidate_clumps candidate_methods.combination(max_copies + 1).map do |methods| common_argument_names_for(methods) end.select do |clump| clump.length >= min_clump_size end.uniq end # @quality :reek:UtilityFunction def common_argument_names_for(methods) methods.map(&:arg_names).inject(:&).compact.sort end def methods_containing_clump(clump) candidate_methods.select { |method| clump & method.arg_names == clump } end def clumps candidate_clumps.map do |clump| [clump, methods_containing_clump(clump)] end end end end end