lib/rubocop/cop/layout/redundant_line_break.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Layout
# Checks whether certain expressions, e.g. method calls, that could fit
# completely on a single line, are broken up into multiple lines unnecessarily.
#
# @example any configuration
# # bad
# foo(
# a,
# b
# )
#
# puts 'string that fits on ' \
# 'a single line'
#
# things
# .select { |thing| thing.cond? }
# .join('-')
#
# # good
# foo(a, b)
#
# puts 'string that fits on a single line'
#
# things.select { |thing| thing.cond? }.join('-')
#
# @example InspectBlocks: false (default)
# # good
# foo(a) do |x|
# puts x
# end
#
# @example InspectBlocks: true
# # bad
# foo(a) do |x|
# puts x
# end
#
# # good
# foo(a) { |x| puts x }
#
class RedundantLineBreak < Base
include CheckAssignment
extend AutoCorrector
MSG = 'Redundant line break detected.'
def on_lvasgn(node)
super unless end_with_percent_blank_string?(processed_source)
end
def on_send(node)
# Include "the whole expression".
node = node.parent while node.parent&.send_type? ||
convertible_block?(node) ||
node.parent.is_a?(RuboCop::AST::BinaryOperatorNode)
return unless offense?(node) && !part_of_ignored_node?(node)
register_offense(node)
end
alias on_csend on_send
private
def end_with_percent_blank_string?(processed_source)
processed_source.buffer.source.end_with?("%\n\n")
end
def check_assignment(node, _rhs)
return unless offense?(node)
register_offense(node)
end
def register_offense(node)
add_offense(node) do |corrector|
corrector.replace(node, to_single_line(node.source).strip)
end
ignore_node(node)
end
def offense?(node)
return false if !node.multiline? || too_long?(node) || !suitable_as_single_line?(node)
return require_backslash?(node) if node.and_type? || node.or_type?
!index_access_call_chained?(node) && !configured_to_not_be_inspected?(node)
end
def require_backslash?(node)
processed_source.lines[node.loc.operator.line - 1].end_with?('\\')
end
def index_access_call_chained?(node)
return false unless node.send_type? && node.method?(:[])
node.children.first.send_type? && node.children.first.method?(:[])
end
def configured_to_not_be_inspected?(node)
return true if other_cop_takes_precedence?(node)
!cop_config['InspectBlocks'] && (node.block_type? ||
any_descendant?(node, :block, &:multiline?))
end
def other_cop_takes_precedence?(node)
single_line_block_chain_enabled? && any_descendant?(node, :block) do |block_node|
block_node.parent.send_type? && block_node.parent.loc.dot && !block_node.multiline?
end
end
def single_line_block_chain_enabled?
@config.for_cop('Layout/SingleLineBlockChain')['Enabled']
end
def suitable_as_single_line?(node)
!comment_within?(node) &&
node.each_descendant(:if, :case, :kwbegin, :def, :defs).none? &&
node.each_descendant(:dstr, :str).none? { |n| n.heredoc? || n.value.include?("\n") } &&
node.each_descendant(:begin, :sym).none? { |b| !b.single_line? }
end
def convertible_block?(node)
parent = node.parent
parent&.block_type? && node == parent.send_node &&
(node.parenthesized? || !node.arguments?)
end
def comment_within?(node)
comment_line_numbers = processed_source.comments.map { |comment| comment.loc.line }
comment_line_numbers.any? do |comment_line_number|
comment_line_number >= node.first_line && comment_line_number <= node.last_line
end
end
def too_long?(node)
lines = processed_source.lines[(node.first_line - 1)...node.last_line]
to_single_line(lines.join("\n")).length > max_line_length
end
def to_single_line(source)
source
.gsub(/" *\\\n\s*'/, %q(" + ')) # Double quote, backslash, and then single quote
.gsub(/' *\\\n\s*"/, %q(' + ")) # Single quote, backslash, and then double quote
.gsub(/(["']) *\\\n\s*\1/, '') # Double or single quote, backslash, then same quote
.gsub(/\n\s*(?=(&)?\.\w)/, '') # Extra space within method chaining which includes `&.`
.gsub(/\s*\\?\n\s*/, ' ') # Any other line break, with or without backslash
end
def max_line_length
config.for_cop('Layout/LineLength')['Max']
end
end
end
end
end