# frozen_string_literal: true require_relative 'base_detector' module Reek module SmellDetectors # # Control Coupling occurs when a method or block checks the value of # a parameter in order to decide which execution path to take. The # offending parameter is often called a Control Couple. # # A simple example would be the quoted parameter # in the following method: # # def write(quoted) # if quoted # write_quoted(@value) # else # puts @value # end # end # # Control Coupling is a kind of duplication, because the calling method # already knows which path should be taken. # # Control Coupling reduces the code's flexibility by creating a # dependency between the caller and callee: # any change to the possible values of the controlling parameter must # be reflected on both sides of the call. # # A Control Couple also reveals a loss of simplicity: the called # method probably has more than one responsibility, # because it includes at least two different code paths. # # One possible solution is to use the Strategy Pattern # to pass into the callee what must be done. This is # not considered to be control coupling because the # callee will do the same thing with the strategy, # whatever it happens to be. Sometimes in Ruby the # strategy may actually just be a block passed in, and # that remains next to where the caller invokes it in # the source code. # # See {file:docs/Control-Parameter.md} for details. class ControlParameter < BaseDetector # # Checks whether the given method chooses its execution path # by testing the value of one of its parameters. # # @return [Array] # # :reek:FeatureEnvy def sniff ControlParameterCollector.new(context).control_parameters.map do |control_parameter| argument = control_parameter.name.to_s smell_warning( context: context, lines: control_parameter.lines, message: "is controlled by argument '#{argument}'", parameters: { argument: argument }) end end # # Collects information about a single control parameter. # class FoundControlParameter def initialize(param, occurences) @param = param @occurences = occurences end def smells? occurences.any? end def lines occurences.map(&:line) end def name param.to_s end private attr_reader :occurences, :param end private_constant :FoundControlParameter # Finds cases of ControlParameter in a particular node for a particular parameter class ControlParameterFinder CONDITIONAL_NODE_TYPES = [:if, :case, :and, :or].freeze def initialize(node, param) @node = node @param = param end def find_matches return [] if legitimite_uses? nested_finders.flat_map(&:find_matches) + uses_of_param_in_condition end def legitimite_uses? return true if uses_param_in_body? return true if uses_param_in_call_in_condition? return true if nested_finders.any?(&:legitimite_uses?) false end private attr_reader :node, :param def conditional_nodes node.body_nodes(CONDITIONAL_NODE_TYPES) end def nested_finders @nested_finders ||= conditional_nodes.flat_map do |node| self.class.new(node, param) end end def uses_param_in_call_in_condition? return unless condition condition.each_node(:send) do |inner| next unless regular_call_involving_param? inner return true end false end def uses_of_param_in_condition return [] unless condition condition.each_node(:lvar).select { |inner| inner.var_name == param } end def condition return nil unless CONDITIONAL_NODE_TYPES.include? node.type node.condition end def regular_call_involving_param?(call_node) call_involving_param?(call_node) && !comparison_call?(call_node) end def comparison_call?(call_node) comparison_method_names.include? call_node.name end def comparison_method_names [:==, :!=, :=~] end def call_involving_param?(call_node) call_node.each_node(:lvar).any? { |it| it.var_name == param } end def uses_param_in_body? nodes = node.body_nodes([:lvar], CONDITIONAL_NODE_TYPES) nodes.any? { |lvar_node| lvar_node.var_name == param } end end private_constant :ControlParameterFinder # # Collects all control parameters in a given context. # class ControlParameterCollector def initialize(context) @context = context end def control_parameters potential_parameters. map { |param| FoundControlParameter.new(param, find_matches(param)) }. select(&:smells?) end private attr_reader :context def potential_parameters context.exp.parameter_names end def find_matches(param) ControlParameterFinder.new(context.exp, param).find_matches end end private_constant :ControlParameterCollector end end end