# 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