rubocop-hq/rubocop

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

Summary

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

module RuboCop
  module Cop
    module Layout
      # Checks the indentation of the first key in a hash literal
      # where the opening brace and the first key are on separate lines. The
      # other keys' indentations are handled by the HashAlignment cop.
      #
      # By default, Hash literals that are arguments in a method call with
      # parentheses, and where the opening curly brace of the hash is on the
      # same line as the opening parenthesis of the method call, shall have
      # their first key indented one step (two spaces) more than the position
      # inside the opening parenthesis.
      #
      # Other hash literals shall have their first key indented one step more
      # than the start of the line where the opening curly brace is.
      #
      # This default style is called 'special_inside_parentheses'. Alternative
      # styles are 'consistent' and 'align_braces'. Here are examples:
      #
      # @example EnforcedStyle: special_inside_parentheses (default)
      #   # The `special_inside_parentheses` style enforces that the first key
      #   # in a hash literal where the opening brace and the first key are on
      #   # separate lines is indented one step (two spaces) more than the
      #   # position inside the opening parentheses.
      #
      #   # bad
      #   hash = {
      #     key: :value
      #   }
      #   and_in_a_method_call({
      #     no: :difference
      #                        })
      #   takes_multi_pairs_hash(x: {
      #     a: 1,
      #     b: 2
      #   },
      #                          y: {
      #                            c: 1,
      #                            d: 2
      #                          })
      #
      #   # good
      #   special_inside_parentheses
      #   hash = {
      #     key: :value
      #   }
      #   but_in_a_method_call({
      #                          its_like: :this
      #                        })
      #   takes_multi_pairs_hash(x: {
      #                            a: 1,
      #                            b: 2
      #                          },
      #                          y: {
      #                            c: 1,
      #                            d: 2
      #                          })
      #
      # @example EnforcedStyle: consistent
      #   # The `consistent` style enforces that the first key in a hash
      #   # literal where the opening brace and the first key are on
      #   # separate lines is indented the same as a hash literal which is not
      #   # defined inside a method call.
      #
      #   # bad
      #   hash = {
      #     key: :value
      #   }
      #   but_in_a_method_call({
      #                          its_like: :this
      #                         })
      #
      #   # good
      #   hash = {
      #     key: :value
      #   }
      #   and_in_a_method_call({
      #     no: :difference
      #   })
      #
      #
      # @example EnforcedStyle: align_braces
      #   # The `align_brackets` style enforces that the opening and closing
      #   # braces are indented to the same position.
      #
      #   # bad
      #   and_now_for_something = {
      #                             completely: :different
      #   }
      #   takes_multi_pairs_hash(x: {
      #     a: 1,
      #     b: 2
      #   },
      #                           y: {
      #                                c: 1,
      #                                d: 2
      #                              })
      #
      #   # good
      #   and_now_for_something = {
      #                             completely: :different
      #                           }
      #   takes_multi_pairs_hash(x: {
      #                               a: 1,
      #                               b: 2
      #                             },
      #                          y: {
      #                               c: 1,
      #                               d: 2
      #                             })
      class FirstHashElementIndentation < Base
        include Alignment
        include ConfigurableEnforcedStyle
        include MultilineElementIndentation
        extend AutoCorrector

        MSG = 'Use %<configured_indentation_width>d spaces for indentation ' \
              'in a hash, relative to %<base_description>s.'

        def on_hash(node)
          check(node, nil) if node.loc.begin
        end

        def on_send(node)
          return if enforce_first_argument_with_fixed_indentation?

          each_argument_node(node, :hash) do |hash_node, left_parenthesis|
            check(hash_node, left_parenthesis)
          end
        end
        alias on_csend on_send

        private

        def autocorrect(corrector, node)
          AlignmentCorrector.correct(corrector, processed_source, node, @column_delta)
        end

        def brace_alignment_style
          :align_braces
        end

        def check(hash_node, left_parenthesis)
          return if ignored_node?(hash_node)

          left_brace = hash_node.loc.begin
          first_pair = hash_node.pairs.first

          if first_pair
            return if same_line?(first_pair, left_brace)

            if separator_style?(first_pair)
              check_based_on_longest_key(hash_node, left_brace, left_parenthesis)
            else
              check_first(first_pair, left_brace, left_parenthesis, 0)
            end
          end

          check_right_brace(hash_node.loc.end, first_pair, left_brace, left_parenthesis)
        end

        def check_right_brace(right_brace, first_pair, left_brace, left_parenthesis)
          # if the right brace is on the same line as the last value, accept
          return if /\S/.match?(right_brace.source_line[0...right_brace.column])

          expected_column, indent_base_type = indent_base(left_brace, first_pair, left_parenthesis)
          @column_delta = expected_column - right_brace.column
          return if @column_delta.zero?

          message = message_for_right_brace(indent_base_type)
          add_offense(right_brace, message: message) do |corrector|
            autocorrect(corrector, right_brace)
          end
        end

        def separator_style?(first_pair)
          separator = first_pair.loc.operator
          key = "Enforced#{separator.is?(':') ? 'Colon' : 'HashRocket'}Style"
          config.for_cop('Layout/HashAlignment')[key] == 'separator'
        end

        def check_based_on_longest_key(hash_node, left_brace, left_parenthesis)
          key_lengths = hash_node.keys.map { |key| key.source_range.length }
          check_first(hash_node.pairs.first, left_brace, left_parenthesis,
                      key_lengths.max - key_lengths.first)
        end

        # Returns the description of what the correct indentation is based on.
        def base_description(indent_base_type)
          case indent_base_type
          when :left_brace_or_bracket
            'the position of the opening brace'
          when :first_column_after_left_parenthesis
            'the first position after the preceding left parenthesis'
          when :parent_hash_key
            'the parent hash key'
          else
            'the start of the line where the left curly brace is'
          end
        end

        def message(base_description)
          format(
            MSG,
            configured_indentation_width: configured_indentation_width,
            base_description: base_description
          )
        end

        def message_for_right_brace(indent_base_type)
          case indent_base_type
          when :left_brace_or_bracket
            'Indent the right brace the same as the left brace.'
          when :first_column_after_left_parenthesis
            'Indent the right brace the same as the first position ' \
            'after the preceding left parenthesis.'
          when :parent_hash_key
            'Indent the right brace the same as the parent hash key.'
          else
            'Indent the right brace the same as the start of the line ' \
            'where the left brace is.'
          end
        end

        def enforce_first_argument_with_fixed_indentation?
          return false unless argument_alignment_config['Enabled']

          argument_alignment_config['EnforcedStyle'] == 'with_fixed_indentation'
        end

        def argument_alignment_config
          config.for_cop('Layout/ArgumentAlignment')
        end
      end
    end
  end
end