class RuboCop::Cop::Lint::ShadowedException

Checks for a rescued exception that get shadowed by a less specific exception being rescued before a more specific exception is rescued.

An exception is considered shadowed if it is rescued after its ancestor is, or if it and its ancestor are both rescued in the same `rescue` statement. In both cases, the more specific rescue is unnecessary because it is covered by rescuing the less specific exception. (ie. `rescue Exception, StandardError` has the same behavior whether `StandardError` is included or not, because all “StandardError“s are rescued by `rescue Exception`).

@example

# bad

begin
  something
rescue Exception
  handle_exception
rescue StandardError
  handle_standard_error
end

# bad
begin
  something
rescue Exception, StandardError
  handle_error
end

# good

begin
  something
rescue StandardError
  handle_standard_error
rescue Exception
  handle_exception
end

# good, however depending on runtime environment.
#
# This is a special case for system call errors.
# System dependent error code depends on runtime environment.
# For example, whether `Errno::EAGAIN` and `Errno::EWOULDBLOCK` are
# the same error code or different error code depends on environment.
# This good case is for `Errno::EAGAIN` and `Errno::EWOULDBLOCK` with
# the same error code.
begin
  something
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
  handle_standard_error
end

Constants

MSG

Public Instance Methods

on_rescue(node) click to toggle source
# File lib/rubocop/cop/lint/shadowed_exception.rb, line 67
def on_rescue(node)
  return if rescue_modifier?(node)

  _body, *rescues, _else = *node
  rescued_groups = rescued_groups_for(rescues)

  rescue_group_rescues_multiple_levels = rescued_groups.any? do |group|
    contains_multiple_levels_of_exceptions?(group)
  end

  return if !rescue_group_rescues_multiple_levels && sorted?(rescued_groups)

  add_offense(offense_range(rescues))
end

Private Instance Methods

compare_exceptions(exception, other_exception) click to toggle source
# File lib/rubocop/cop/lint/shadowed_exception.rb, line 101
def compare_exceptions(exception, other_exception)
  if system_call_err?(exception) && system_call_err?(other_exception)
    # This condition logic is for special case.
    # System dependent error code depends on runtime environment.
    # For example, whether `Errno::EAGAIN` and `Errno::EWOULDBLOCK` are
    # the same error code or different error code depends on runtime
    # environment. This checks the error code for that.
    exception.const_get(:Errno) != other_exception.const_get(:Errno) &&
      exception <=> other_exception
  else
    exception && other_exception && exception <=> other_exception
  end
end
contains_multiple_levels_of_exceptions?(group) click to toggle source
# File lib/rubocop/cop/lint/shadowed_exception.rb, line 94
def contains_multiple_levels_of_exceptions?(group)
  # Always treat `Exception` as the highest level exception.
  return true if group.size > 1 && group.include?(Exception)

  group.combination(2).any? { |a, b| compare_exceptions(a, b) }
end
evaluate_exceptions(group) click to toggle source
# File lib/rubocop/cop/lint/shadowed_exception.rb, line 119
def evaluate_exceptions(group)
  rescued_exceptions = group.exceptions

  if rescued_exceptions.any?
    rescued_exceptions.each_with_object([]) do |exception, converted|
      # FIXME: Workaround `rubocop:disable` comment for JRuby.
      #        https://github.com/jruby/jruby/issues/6642
      # rubocop:disable Style/RedundantBegin
      begin
        RuboCop::Util.silence_warnings do
          # Avoid printing deprecation warnings about constants
          converted << Kernel.const_get(exception.source)
        end
      rescue NameError
        converted << nil
      end
      # rubocop:enable Style/RedundantBegin
    end
  else
    # treat an empty `rescue` as `rescue StandardError`
    [StandardError]
  end
end
find_shadowing_rescue(rescues) click to toggle source
# File lib/rubocop/cop/lint/shadowed_exception.rb, line 158
def find_shadowing_rescue(rescues)
  rescued_groups = rescued_groups_for(rescues)
  rescued_groups.zip(rescues).each do |group, res|
    return res if contains_multiple_levels_of_exceptions?(group)
  end

  rescued_groups.each_cons(2).with_index do |group_pair, i|
    return rescues[i] unless sorted?(group_pair)
  end
end
offense_range(rescues) click to toggle source
# File lib/rubocop/cop/lint/shadowed_exception.rb, line 84
def offense_range(rescues)
  shadowing_rescue = find_shadowing_rescue(rescues)
  expression = shadowing_rescue.loc.expression
  range_between(expression.begin_pos, expression.end_pos)
end
rescued_groups_for(rescues) click to toggle source
# File lib/rubocop/cop/lint/shadowed_exception.rb, line 90
def rescued_groups_for(rescues)
  rescues.map { |group| evaluate_exceptions(group) }
end
sorted?(rescued_groups) click to toggle source
# File lib/rubocop/cop/lint/shadowed_exception.rb, line 143
def sorted?(rescued_groups)
  rescued_groups.each_cons(2).all? do |x, y|
    if x.include?(Exception)
      false
    elsif y.include?(Exception) ||
          # consider sorted if a group is empty or only contains
          # `nil`s
          x.none? || y.none?
      true
    else
      (x <=> y || 0) <= 0
    end
  end
end
system_call_err?(error) click to toggle source
# File lib/rubocop/cop/lint/shadowed_exception.rb, line 115
def system_call_err?(error)
  error && error.ancestors[1] == SystemCallError
end