rubocop-hq/rubocop

View on GitHub
lib/rubocop/cop/layout/multiline_block_layout.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
# frozen_string_literal: true

module RuboCop
  module Cop
    module Layout
      # Checks whether the multiline do end blocks have a newline
      # after the start of the block. Additionally, it checks whether the block
      # arguments, if any, are on the same line as the start of the
      # block. Putting block arguments on separate lines, because the whole
      # line would otherwise be too long, is accepted.
      #
      # @example
      #   # bad
      #   blah do |i| foo(i)
      #     bar(i)
      #   end
      #
      #   # bad
      #   blah do
      #     |i| foo(i)
      #     bar(i)
      #   end
      #
      #   # good
      #   blah do |i|
      #     foo(i)
      #     bar(i)
      #   end
      #
      #   # bad
      #   blah { |i| foo(i)
      #     bar(i)
      #   }
      #
      #   # good
      #   blah { |i|
      #     foo(i)
      #     bar(i)
      #   }
      #
      #   # good
      #   blah { |
      #     long_list,
      #     of_parameters,
      #     that_would_not,
      #     fit_on_one_line
      #   |
      #     foo(i)
      #     bar(i)
      #   }
      class MultilineBlockLayout < Base
        include RangeHelp
        extend AutoCorrector

        MSG = 'Block body expression is on the same line as the block start.'
        ARG_MSG = 'Block argument expression is not on the same line as the block start.'
        PIPE_SIZE = '|'.length

        def on_block(node)
          return if node.single_line?

          unless args_on_beginning_line?(node) || line_break_necessary_in_args?(node)
            add_offense_for_expression(node, node.arguments, ARG_MSG)
          end

          return unless node.body && same_line?(node.loc.begin, node.body)

          add_offense_for_expression(node, node.body, MSG)
        end

        alias on_numblock on_block

        private

        def args_on_beginning_line?(node)
          !node.arguments? || node.loc.begin.line == node.arguments.loc.last_line
        end

        def line_break_necessary_in_args?(node)
          needed_length_for_args(node) > max_line_length
        end

        def needed_length_for_args(node)
          node.source_range.column +
            characters_needed_for_space_and_pipes(node) +
            node.source.lines.first.chomp.length +
            block_arg_string(node, node.arguments).length
        end

        def characters_needed_for_space_and_pipes(node)
          if node.source.lines.first.end_with?("|\n")
            PIPE_SIZE
          else
            (PIPE_SIZE * 2) + 1
          end
        end

        def add_offense_for_expression(node, expr, msg)
          expression = expr.source_range
          range = range_between(expression.begin_pos, expression.end_pos)

          add_offense(range, message: msg) { |corrector| autocorrect(corrector, node) }
        end

        def autocorrect(corrector, node)
          unless args_on_beginning_line?(node)
            autocorrect_arguments(corrector, node)
            expr_before_body = node.arguments.source_range.end
          end

          return unless node.body

          expr_before_body ||= node.loc.begin

          return unless same_line?(expr_before_body, node.body)

          autocorrect_body(corrector, node, node.body)
        end

        def autocorrect_arguments(corrector, node)
          end_pos = range_with_surrounding_space(
            node.arguments.source_range,
            side: :right,
            newlines: false
          ).end_pos
          range = range_between(node.loc.begin.end.begin_pos, end_pos)
          corrector.replace(range, " |#{block_arg_string(node, node.arguments)}|")
        end

        def autocorrect_body(corrector, node, block_body)
          first_node = if block_body.begin_type? && !block_body.source.start_with?('(')
                         block_body.children.first
                       else
                         block_body
                       end

          block_start_col = node.source_range.column

          corrector.insert_before(first_node, "\n  #{' ' * block_start_col}")
        end

        def block_arg_string(node, args)
          arg_string = args.children.map do |arg|
            if arg.mlhs_type?
              "(#{block_arg_string(node, arg)})"
            else
              arg.source
            end
          end.join(', ')
          arg_string += ',' if include_trailing_comma?(node.arguments)
          arg_string
        end

        def include_trailing_comma?(args)
          arg_count = args.each_descendant(:arg).to_a.size
          arg_count == 1 && args.source.include?(',')
        end
      end
    end
  end
end