lib/rubocop/cop/layout/empty_line_after_guard_clause.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Layout
# Enforces empty line after guard clause.
#
# This cop allows `# :nocov:` directive after guard clause because
# SimpleCov excludes code from the coverage report by wrapping it in `# :nocov:`:
#
# [source,ruby]
# ----
# def foo
# # :nocov:
# return if condition
# # :nocov:
# bar
# end
# ----
#
# Refer to SimpleCov's documentation for more details:
# https://github.com/simplecov-ruby/simplecov#ignoringskipping-code
#
# @example
#
# # bad
# def foo
# return if need_return?
# bar
# end
#
# # good
# def foo
# return if need_return?
#
# bar
# end
#
# # good
# def foo
# return if something?
# return if something_different?
#
# bar
# end
#
# # also good
# def foo
# if something?
# do_something
# return if need_return?
# end
# end
class EmptyLineAfterGuardClause < Base
include RangeHelp
extend AutoCorrector
extend Util
MSG = 'Add empty line after guard clause.'
END_OF_HEREDOC_LINE = 1
SIMPLE_DIRECTIVE_COMMENT_PATTERN = /\A# *:nocov:\z/.freeze
def on_if(node)
return if correct_style?(node)
return if multiple_statements_on_line?(node)
if node.modifier_form? && (heredoc_node = last_heredoc_argument(node))
if next_line_empty_or_allowed_directive_comment?(heredoc_line(node, heredoc_node))
return
end
add_offense(heredoc_node.loc.heredoc_end) do |corrector|
autocorrect(corrector, heredoc_node)
end
else
return if next_line_empty_or_allowed_directive_comment?(node.last_line)
add_offense(offense_location(node)) { |corrector| autocorrect(corrector, node) }
end
end
private
def autocorrect(corrector, node)
node_range = if heredoc?(node)
range_by_whole_lines(node.loc.heredoc_body)
else
range_by_whole_lines(node.source_range)
end
next_line = node_range.last_line + 1
if next_line_allowed_directive_comment?(next_line)
node_range = processed_source.comment_at_line(next_line)
end
corrector.insert_after(node_range, "\n")
end
def correct_style?(node)
!contains_guard_clause?(node) ||
next_line_rescue_or_ensure?(node) ||
next_sibling_parent_empty_or_else?(node) ||
next_sibling_empty_or_guard_clause?(node)
end
def contains_guard_clause?(node)
node.if_branch&.guard_clause?
end
def next_line_empty_or_allowed_directive_comment?(line)
return true if next_line_empty?(line)
next_line = line + 1
next_line_allowed_directive_comment?(next_line) && next_line_empty?(next_line)
end
def next_line_empty?(line)
processed_source[line].blank?
end
def next_line_allowed_directive_comment?(line)
return false unless (comment = processed_source.comment_at_line(line))
DirectiveComment.new(comment).enabled? || simplecov_directive_comment?(comment)
end
def next_line_rescue_or_ensure?(node)
parent = node.parent
parent.nil? || parent.rescue_type? || parent.ensure_type?
end
def next_sibling_parent_empty_or_else?(node)
next_sibling = node.right_sibling
return true if next_sibling.nil?
parent = next_sibling.parent
parent&.if_type? && parent&.else?
end
def next_sibling_empty_or_guard_clause?(node)
next_sibling = node.right_sibling
return true if next_sibling.nil?
next_sibling.if_type? && contains_guard_clause?(next_sibling)
end
def last_heredoc_argument(node)
n = last_heredoc_argument_node(node)
return n if heredoc?(n)
return unless n.respond_to?(:arguments)
n.arguments.each do |argument|
node = last_heredoc_argument(argument)
return node if node
end
last_heredoc_argument(n.receiver) if n.respond_to?(:receiver)
end
def last_heredoc_argument_node(node)
return node unless node.respond_to?(:if_branch)
if node.if_branch.and_type?
node.if_branch.children.first
elsif use_heredoc_in_condition?(node.condition)
node.condition
else
node.if_branch.children.last
end
end
def heredoc_line(node, heredoc_node)
heredoc_body = heredoc_node.loc.heredoc_body
num_of_heredoc_lines = heredoc_body.last_line - heredoc_body.first_line
node.last_line + num_of_heredoc_lines + END_OF_HEREDOC_LINE
end
def heredoc?(node)
node.respond_to?(:heredoc?) && node.heredoc?
end
def use_heredoc_in_condition?(condition)
condition.descendants.any? do |descendant|
descendant.respond_to?(:heredoc?) && descendant.heredoc?
end
end
def offense_location(node)
if node.loc.respond_to?(:end) && node.loc.end
node.loc.end
else
node
end
end
def multiple_statements_on_line?(node)
parent = node.parent
return false unless parent
parent.begin_type? && parent.single_line?
end
# SimpleCov excludes code from the coverage report by wrapping it in `# :nocov:`:
# https://github.com/simplecov-ruby/simplecov#ignoringskipping-code
def simplecov_directive_comment?(comment)
SIMPLE_DIRECTIVE_COMMENT_PATTERN.match?(comment.text)
end
end
end
end
end