rubocop-hq/rubocop

View on GitHub
lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

module RuboCop
  module Cop
    # Autocorrection logic for the closing brace of a literal either
    # on the same line as the last contained elements, or a new line.
    class MultilineLiteralBraceCorrector
      include MultilineLiteralBraceLayout
      include RangeHelp

      def self.correct(corrector, node, processed_source)
        new(corrector, node, processed_source).call
      end

      def initialize(corrector, node, processed_source)
        @corrector = corrector
        @node = node
        @processed_source = processed_source
      end

      def call
        if closing_brace_on_same_line?(node)
          correct_same_line_brace(corrector)
        else
          # When a comment immediately before the closing brace gets in the
          # way of an easy correction, the offense is reported but not auto-
          # corrected. The user must handle the delicate decision of where to
          # put the comment.
          return if new_line_needed_before_closing_brace?(node)

          end_range = last_element_range_with_trailing_comma(node).end

          correct_next_line_brace(corrector, end_range)
          correct_heredoc_argument_method_chain(corrector, end_range)
        end
      end

      private

      attr_reader :corrector, :node, :processed_source

      def correct_same_line_brace(corrector)
        corrector.insert_before(node.loc.end, "\n")
      end

      def correct_next_line_brace(corrector, end_range)
        corrector.remove(range_with_surrounding_space(node.loc.end, side: :left))
        corrector.insert_before(end_range, content_if_comment_present(corrector, node))
      end

      def correct_heredoc_argument_method_chain(corrector, end_range)
        return unless (parent = node.parent)
        return unless use_heredoc_argument_method_chain?(parent)

        chained_method = range_between(parent.loc.dot.begin_pos, parent.source_range.end_pos)

        corrector.remove(chained_method)
        corrector.insert_after(end_range, chained_method.source)
      end

      def content_if_comment_present(corrector, node)
        range = range_with_surrounding_space(
          children(node).last.source_range,
          side: :right
        ).end.resize(1)
        if range.source == '#'
          select_content_to_be_inserted_after_last_element(corrector, node)
        else
          node.loc.end.source
        end
      end

      def use_heredoc_argument_method_chain?(parent)
        return false unless node.respond_to?(:first_argument)
        return false unless (first_argument = node.first_argument)

        parent.call_type? && first_argument.str_type? && first_argument.heredoc?
      end

      def select_content_to_be_inserted_after_last_element(corrector, node)
        range = range_between(
          node.loc.end.begin_pos,
          range_by_whole_lines(node.source_range).end.end_pos
        )

        remove_trailing_content_of_comment(corrector, range)
        range.source
      end

      def remove_trailing_content_of_comment(corrector, range)
        corrector.remove(range)
      end

      def last_element_range_with_trailing_comma(node)
        trailing_comma_range = last_element_trailing_comma_range(node)
        if trailing_comma_range
          children(node).last.source_range.join(trailing_comma_range)
        else
          children(node).last.source_range
        end
      end

      def last_element_trailing_comma_range(node)
        range = range_with_surrounding_space(
          children(node).last.source_range,
          side: :right
        ).end.resize(1)

        range.source == ',' ? range : nil
      end
    end
  end
end