lib/rubocop/cop/style/signal_exception.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Checks for uses of `fail` and `raise`.
#
# @example EnforcedStyle: only_raise (default)
# # The `only_raise` style enforces the sole use of `raise`.
# # bad
# begin
# fail
# rescue Exception
# # handle it
# end
#
# def watch_out
# fail
# rescue Exception
# # handle it
# end
#
# Kernel.fail
#
# # good
# begin
# raise
# rescue Exception
# # handle it
# end
#
# def watch_out
# raise
# rescue Exception
# # handle it
# end
#
# Kernel.raise
#
# @example EnforcedStyle: only_fail
# # The `only_fail` style enforces the sole use of `fail`.
# # bad
# begin
# raise
# rescue Exception
# # handle it
# end
#
# def watch_out
# raise
# rescue Exception
# # handle it
# end
#
# Kernel.raise
#
# # good
# begin
# fail
# rescue Exception
# # handle it
# end
#
# def watch_out
# fail
# rescue Exception
# # handle it
# end
#
# Kernel.fail
#
# @example EnforcedStyle: semantic
# # The `semantic` style enforces the use of `fail` to signal an
# # exception, then will use `raise` to trigger an offense after
# # it has been rescued.
# # bad
# begin
# raise
# rescue Exception
# # handle it
# end
#
# def watch_out
# # Error thrown
# rescue Exception
# fail
# end
#
# Kernel.fail
# Kernel.raise
#
# # good
# begin
# fail
# rescue Exception
# # handle it
# end
#
# def watch_out
# fail
# rescue Exception
# raise 'Preferably with descriptive message'
# end
#
# explicit_receiver.fail
# explicit_receiver.raise
class SignalException < Base
include ConfigurableEnforcedStyle
extend AutoCorrector
FAIL_MSG = 'Use `fail` instead of `raise` to signal exceptions.'
RAISE_MSG = 'Use `raise` instead of `fail` to rethrow exceptions.'
RESTRICT_ON_SEND = %i[raise fail].freeze
# @!method kernel_call?(node, name)
def_node_matcher :kernel_call?, '(send (const {nil? cbase} :Kernel) %1 ...)'
# @!method custom_fail_methods(node)
def_node_search :custom_fail_methods, '{(def :fail ...) (defs _ :fail ...)}'
def on_rescue(node)
return unless style == :semantic
begin_node, *rescue_nodes, _else_node = *node
check_scope(:raise, begin_node)
rescue_nodes.each do |rescue_node|
check_scope(:fail, rescue_node)
allow(:raise, rescue_node)
end
end
def on_send(node)
case style
when :semantic
check_send(:raise, node) unless ignored_node?(node)
when :only_raise
return if custom_fail_defined?
check_send(:fail, node)
when :only_fail
check_send(:raise, node)
end
end
private
def custom_fail_defined?
return @custom_fail_defined if defined?(@custom_fail_defined)
ast = processed_source.ast
@custom_fail_defined = ast && custom_fail_methods(ast).any?
end
def message(method_name)
case style
when :semantic
method_name == :fail ? RAISE_MSG : FAIL_MSG
when :only_raise
'Always use `raise` to signal exceptions.'
when :only_fail
'Always use `fail` to signal exceptions.'
end
end
def check_scope(method_name, node)
return unless node
each_command_or_kernel_call(method_name, node) do |send_node|
next if ignored_node?(send_node)
add_offense(send_node.loc.selector, message: message(method_name)) do |corrector|
autocorrect(corrector, send_node)
end
ignore_node(send_node)
end
end
def check_send(method_name, node)
return unless node && command_or_kernel_call?(method_name, node)
add_offense(node.loc.selector, message: message(method_name)) do |corrector|
autocorrect(corrector, node)
end
end
def autocorrect(corrector, node)
name =
case style
when :semantic
command_or_kernel_call?(:raise, node) ? 'fail' : 'raise'
when :only_raise then 'raise'
when :only_fail then 'fail'
end
corrector.replace(node.loc.selector, name)
end
def command_or_kernel_call?(name, node)
return false unless node.method?(name)
node.command?(name) || kernel_call?(node, name)
end
def allow(method_name, node)
each_command_or_kernel_call(method_name, node) { |send_node| ignore_node(send_node) }
end
def each_command_or_kernel_call(method_name, node)
on_node(:send, node, :rescue) do |send_node|
yield send_node if command_or_kernel_call?(method_name, send_node)
end
end
end
end
end
end