rubocop-hq/rubocop

View on GitHub
lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb

Summary

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

module RuboCop
  module Cop
    # Common functionality for checking the closing brace of a literal is
    # either on the same line as the last contained elements or a new line.
    module MultilineLiteralBraceLayout
      include ConfigurableEnforcedStyle

      private

      def check_brace_layout(node)
        return if ignored_literal?(node)

        # If the last node is or contains a conflicting HEREDOC, we don't want
        # to adjust the brace layout because this will result in invalid code.
        return if last_line_heredoc?(node.children.last)

        check(node)
      end

      # Returns true for the case
      #   [a,
      #    b # comment
      #   ].some_method
      def new_line_needed_before_closing_brace?(node)
        last_element_line = last_element_range_with_trailing_comma(node).last_line

        last_element_commented = processed_source.comment_at_line(last_element_line)

        last_element_commented && (node.chained? || node.argument?)
      end

      def check(node)
        case style
        when :symmetrical then check_symmetrical(node)
        when :new_line then check_new_line(node)
        when :same_line then check_same_line(node)
        end
      end

      def check_new_line(node)
        return unless closing_brace_on_same_line?(node)

        add_offense(node.loc.end, message: self.class::ALWAYS_NEW_LINE_MESSAGE) do |corrector|
          MultilineLiteralBraceCorrector.correct(corrector, node, processed_source)
        end
      end

      def check_same_line(node)
        return if closing_brace_on_same_line?(node)

        add_offense(node.loc.end, message: self.class::ALWAYS_SAME_LINE_MESSAGE) do |corrector|
          MultilineLiteralBraceCorrector.correct(corrector, node, processed_source)
        end
      end

      def check_symmetrical(node)
        if opening_brace_on_same_line?(node)
          return if closing_brace_on_same_line?(node)

          add_offense(node.loc.end, message: self.class::SAME_LINE_MESSAGE) do |corrector|
            MultilineLiteralBraceCorrector.correct(corrector, node, processed_source)
          end
        else
          return unless closing_brace_on_same_line?(node)

          add_offense(node.loc.end, message: self.class::NEW_LINE_MESSAGE) do |corrector|
            MultilineLiteralBraceCorrector.correct(corrector, node, processed_source)
          end
        end
      end

      def empty_literal?(node)
        children(node).empty?
      end

      def implicit_literal?(node)
        !node.loc.begin
      end

      def ignored_literal?(node)
        implicit_literal?(node) || empty_literal?(node) || node.single_line?
      end

      def children(node)
        node.children
      end

      # This method depends on the fact that we have guarded
      # against implicit and empty literals.
      def opening_brace_on_same_line?(node)
        same_line?(node.loc.begin, children(node).first)
      end

      # This method depends on the fact that we have guarded
      # against implicit and empty literals.
      def closing_brace_on_same_line?(node)
        node.loc.end.line == children(node).last.last_line
      end

      # Starting with the parent node and recursively for the parent node's
      # children, check if the node is a HEREDOC and if the HEREDOC ends below
      # or on the last line of the parent node.
      #
      # Example:
      #
      #   # node is `b: ...` parameter
      #   # last_line_heredoc?(node) => false
      #   foo(a,
      #     b: {
      #       a: 1,
      #       c: <<-EOM
      #         baz
      #       EOM
      #     }
      #   )
      #
      #   # node is `b: ...` parameter
      #   # last_line_heredoc?(node) => true
      #   foo(a,
      #     b: <<-EOM
      #       baz
      #     EOM
      #   )
      def last_line_heredoc?(node, parent = nil)
        parent ||= node

        if node.respond_to?(:loc) &&
           node.loc.respond_to?(:heredoc_end) &&
           node.loc.heredoc_end.last_line >= parent.last_line
          return true
        end

        return false unless node.respond_to?(:children)

        node.children.any? { |child| last_line_heredoc?(child, parent) }
      end
    end
  end
end