class RuboCop::Cop::Style::CaseLikeIf

Identifies places where ‘if-elsif` constructions can be replaced with `case-when`.

@safety

This cop is unsafe. `case` statements use `===` for equality,
so if the original conditional used a different equality operator, the
behavior may be different.

@example MinBranchesCount: 3 (default)

# bad
if status == :active
  perform_action
elsif status == :inactive || status == :hibernating
  check_timeout
elsif status == :invalid
  report_invalid
else
  final_action
end

# good
case status
when :active
  perform_action
when :inactive, :hibernating
  check_timeout
when :invalid
  report_invalid
else
  final_action
end

@example MinBranchesCount: 4

# good
if status == :active
  perform_action
elsif status == :inactive || status == :hibernating
  check_timeout
elsif status == :invalid
  report_invalid
else
  final_action
end

Constants

MSG

Public Instance Methods

on_if(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 57
def on_if(node)
  return unless should_check?(node)

  target = find_target(node.condition)
  return unless target

  conditions = []
  convertible = true

  branch_conditions(node).each do |branch_condition|
    return false if regexp_with_working_captures?(branch_condition)

    conditions << []
    convertible = collect_conditions(branch_condition, target, conditions.last)
    break unless convertible
  end

  return unless convertible

  add_offense(node) { |corrector| autocorrect(corrector, node) }
end

Private Instance Methods

autocorrect(corrector, node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 81
def autocorrect(corrector, node)
  target = find_target(node.condition)

  corrector.insert_before(node, "case #{target.source}\n#{indent(node)}")

  branch_conditions(node).each do |branch_condition|
    conditions = []
    collect_conditions(branch_condition, target, conditions)

    range = correction_range(branch_condition)
    branch_replacement = "when #{conditions.map(&:source).join(', ')}"
    corrector.replace(range, branch_replacement)
  end
end
branch_conditions(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 231
def branch_conditions(node)
  conditions = []
  while node&.if_type? && !node.ternary?
    conditions << node.condition
    node = node.else_branch
  end
  conditions
end
class_reference?(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 249
def class_reference?(node)
  node.const_type? && node.children[1].match?(/[[:lower:]]/)
end
collect_conditions(node, target, conditions) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 166
def collect_conditions(node, target, conditions)
  condition =
    case node.type
    when :begin
      return collect_conditions(node.children.first, target, conditions)
    when :or
      return collect_conditions(node.lhs, target, conditions) &&
             collect_conditions(node.rhs, target, conditions)
    when :match_with_lvasgn
      lhs, rhs = *node
      condition_from_binary_op(lhs, rhs, target)
    when :send
      condition_from_send_node(node, target)
    end

  conditions << condition if condition
end
condition_from_binary_op(lhs, rhs, target) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 220
def condition_from_binary_op(lhs, rhs, target)
  lhs = deparenthesize(lhs)
  rhs = deparenthesize(rhs)

  if lhs == target
    rhs
  elsif rhs == target
    lhs
  end
end
condition_from_equality_node(node, target) click to toggle source

rubocop:enable Metrics/CyclomaticComplexity

# File lib/rubocop/cop/style/case_like_if.rb, line 202
def condition_from_equality_node(node, target)
  lhs, _method, rhs = *node
  condition = condition_from_binary_op(lhs, rhs, target)
  condition if condition && !class_reference?(condition)
end
condition_from_include_or_cover_node(node, target) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 213
def condition_from_include_or_cover_node(node, target)
  return unless (receiver = node.receiver)

  receiver = deparenthesize(receiver)
  receiver if receiver.range_type? && node.first_argument == target
end
condition_from_match_node(node, target) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 208
def condition_from_match_node(node, target)
  lhs, _method, rhs = *node
  condition_from_binary_op(lhs, rhs, target)
end
condition_from_send_node(node, target) click to toggle source

rubocop:disable Metrics/CyclomaticComplexity

# File lib/rubocop/cop/style/case_like_if.rb, line 185
def condition_from_send_node(node, target)
  case node.method_name
  when :is_a?
    node.first_argument if node.receiver == target
  when :==, :eql?, :equal?
    condition_from_equality_node(node, target)
  when :=~, :match, :match?
    condition_from_match_node(node, target)
  when :===
    lhs, _method, rhs = *node
    lhs if rhs == target
  when :include?, :cover?
    condition_from_include_or_cover_node(node, target)
  end
end
const_reference?(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 240
def const_reference?(node)
  return false unless node.const_type?

  name = node.children[1].to_s

  # We can no be sure if, e.g. `C`, represents a constant or a class reference
  name.length > 1 && name == name.upcase
end
correction_range(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 258
def correction_range(node)
  range_between(node.parent.loc.keyword.begin_pos, node.source_range.end_pos)
end
deparenthesize(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 253
def deparenthesize(node)
  node = node.children.last while node.begin_type?
  node
end
find_target(node) click to toggle source

rubocop:disable Metrics/MethodLength

# File lib/rubocop/cop/style/case_like_if.rb, line 102
def find_target(node)
  case node.type
  when :begin
    find_target(node.children.first)
  when :or
    find_target(node.lhs)
  when :match_with_lvasgn
    lhs, rhs = *node
    if lhs.regexp_type?
      rhs
    elsif rhs.regexp_type?
      lhs
    end
  when :send
    find_target_in_send_node(node)
  end
end
find_target_in_equality_node(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 136
def find_target_in_equality_node(node)
  argument = node.first_argument
  receiver = node.receiver
  return unless argument && receiver

  if argument.literal? || const_reference?(argument)
    receiver
  elsif receiver.literal? || const_reference?(receiver)
    argument
  end
end
find_target_in_include_or_cover_node(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 148
def find_target_in_include_or_cover_node(node)
  return unless (receiver = node.receiver)

  node.first_argument if deparenthesize(receiver).range_type?
end
find_target_in_match_node(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 154
def find_target_in_match_node(node)
  argument = node.first_argument
  receiver = node.receiver
  return unless receiver

  if receiver.regexp_type?
    argument
  elsif argument.regexp_type?
    receiver
  end
end
find_target_in_send_node(node) click to toggle source

rubocop:enable Metrics/MethodLength

# File lib/rubocop/cop/style/case_like_if.rb, line 121
def find_target_in_send_node(node)
  case node.method_name
  when :is_a?
    node.receiver
  when :==, :eql?, :equal?
    find_target_in_equality_node(node)
  when :===
    node.first_argument
  when :include?, :cover?
    find_target_in_include_or_cover_node(node)
  when :match, :match?, :=~
    find_target_in_match_node(node)
  end
end
regexp_with_named_captures?(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 274
def regexp_with_named_captures?(node)
  node.regexp_type? && node.each_capture(named: true).count.positive?
end
regexp_with_working_captures?(node) click to toggle source

Named captures work with ‘=~` (if regexp is on lhs) and with `match` (both sides)

# File lib/rubocop/cop/style/case_like_if.rb, line 263
def regexp_with_working_captures?(node)
  case node.type
  when :match_with_lvasgn
    lhs, _rhs = *node
    node.loc.selector.source == '=~' && regexp_with_named_captures?(lhs)
  when :send
    lhs, method, rhs = *node
    method == :match && [lhs, rhs].any? { |n| regexp_with_named_captures?(n) }
  end
end
should_check?(node) click to toggle source
# File lib/rubocop/cop/style/case_like_if.rb, line 96
def should_check?(node)
  !node.unless? && !node.elsif? && !node.modifier_form? && !node.ternary? &&
    node.elsif_conditional? && min_branches_count?(node)
end