lib/rubocop/cop/lint/shadowed_exception.rb



# encoding: utf-8
# frozen_string_literal: true

module RuboCop
  module Cop
    module Lint
      # This cop checks for a rescued exception that get shadowed by a
      # less specific exception being rescued before a more specific
      # exception is rescued.
      #
      # @example
      # # bad
      # begin
      #   something
      # rescue Exception
      #   handle_exception
      # rescue StandardError
      #   handle_standard_error
      # end
      #
      # #good
      # begin
      #   something
      # rescue StandardError
      #   handle_standard_error
      # rescue Exception
      #   handle_exception
      # end
      class ShadowedException < Cop
        MSG = 'Do not shadow rescued Exceptions'.freeze

        def on_rescue(node)
          return if rescue_modifier?(node)
          _body, *rescues, _else = *node
          rescued_groups = rescues.each_with_object([]) do |group, exceptions|
            rescue_group, = *group

            exceptions << evaluate_exceptions(rescue_group)
          end

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

          return if rescue_group_rescues_multiple_levels &&
                    rescued_groups == sort_rescued_groups(rescued_groups)

          add_offense(node, :expression)
        end

        private

        def rescue_modifier?(node)
          node && node.rescue_type? &&
            (node.parent.nil? || !(node.parent.kwbegin_type? ||
             node.parent.ensure_type?))
        end

        def contains_multiple_levels_of_exceptions?(group)
          group.any? do |exception|
            higher_exception = false
            group.each_with_index do |_e, i|
              higher_exception ||= group[i] < exception
            end

            higher_exception
          end
        end

        def evaluate_exceptions(rescue_group)
          if rescue_group
            rescued_exceptions = rescue_group.source.delete(' ').split(',')

            rescued_exceptions.each_with_object([]) do |exception, converted|
              begin
                converted << instance_eval(exception, __FILE__, __LINE__)
              rescue StandardError, ScriptError
                next
              end
            end
          else
            # treat an empty `rescue` as `rescue StandardError`
            [StandardError]
          end
        end

        def sort_rescued_groups(groups)
          groups.sort do |x, y|
            x <=> y || 0
          end
        end
      end
    end
  end
end