lib/rubocop/cop/layout/indent_first_array_element.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Layout
      # This cop checks the indentation of the first element in an array literal
      # where the opening bracket and the first element are on separate lines.
      # The other elements' indentations are handled by the AlignArray cop.
      #
      # By default, array literals that are arguments in a method call with
      # parentheses, and where the opening square bracket of the array is on the
      # same line as the opening parenthesis of the method call, shall have
      # their first element indented one step (two spaces) more than the
      # position inside the opening parenthesis.
      #
      # Other array literals shall have their first element indented one step
      # more than the start of the line where the opening square bracket is.
      #
      # This default style is called 'special_inside_parentheses'. Alternative
      # styles are 'consistent' and 'align_brackets'. Here are examples:
      #
      # @example EnforcedStyle: special_inside_parentheses (default)
      #   # The `special_inside_parentheses` style enforces that the first
      #   # element in an array literal where the opening bracket and first
      #   # element are on seprate lines is indented one step (two spaces) more
      #   # than the position inside the opening parenthesis.
      #
      #   #bad
      #   array = [
      #     :value
      #   ]
      #   and_in_a_method_call([
      #     :no_difference
      #                        ])
      #
      #   #good
      #   array = [
      #     :value
      #   ]
      #   but_in_a_method_call([
      #                          :its_like_this
      #                        ])
      #
      # @example EnforcedStyle: consistent
      #   # The `consistent` style enforces that the first element in an array
      #   # literal where the opening bracket and the first element are on
      #   # seprate lines is indented the same as an array literal which is not
      #   # defined inside a method call.
      #
      #   #bad
      #   # consistent
      #   array = [
      #     :value
      #   ]
      #   but_in_a_method_call([
      #                          :its_like_this
      #   ])
      #
      #   #good
      #   array = [
      #     :value
      #   ]
      #   and_in_a_method_call([
      #     :no_difference
      #   ])
      #
      # @example EnforcedStyle: align_brackets
      #   # The `align_brackets` style enforces that the opening and closing
      #   # brackets are indented to the same position.
      #
      #   #bad
      #   # align_brackets
      #   and_now_for_something = [
      #                             :completely_different
      #   ]
      #
      #   #good
      #   # align_brackets
      #   and_now_for_something = [
      #                             :completely_different
      #                           ]
      class IndentFirstArrayElement < Cop
        include Alignment
        include ConfigurableEnforcedStyle
        include MultilineElementIndentation

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

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

        def on_send(node)
          each_argument_node(node, :array) do |array_node, left_parenthesis|
            check(array_node, left_parenthesis)
          end
        end
        alias on_csend on_send

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

        private

        def brace_alignment_style
          :align_brackets
        end

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

          left_bracket = array_node.loc.begin
          first_elem = array_node.values.first
          if first_elem
            return if first_elem.source_range.line == left_bracket.line

            check_first(first_elem, left_bracket, left_parenthesis, 0)
          end

          check_right_bracket(array_node.loc.end, left_bracket,
                              left_parenthesis)
        end

        def check_right_bracket(right_bracket, left_bracket, left_parenthesis)
          # if the right bracket is on the same line as the last value, accept
          return if right_bracket.source_line[0...right_bracket.column] =~ /\S/

          expected_column = base_column(left_bracket, left_parenthesis)
          @column_delta = expected_column - right_bracket.column
          return if @column_delta.zero?

          msg = if style == :align_brackets
                  'Indent the right bracket the same as the left bracket.'
                elsif style == :special_inside_parentheses && left_parenthesis
                  'Indent the right bracket the same as the first position ' \
                  'after the preceding left parenthesis.'
                else
                  'Indent the right bracket the same as the start of the line' \
                  ' where the left bracket is.'
                end
          add_offense(right_bracket, location: right_bracket, message: msg)
        end

        # Returns the description of what the correct indentation is based on.
        def base_description(left_parenthesis)
          if style == :align_brackets
            'the position of the opening bracket'
          elsif left_parenthesis && style == :special_inside_parentheses
            'the first position after the preceding left parenthesis'
          else
            'the start of the line where the left square bracket is'
          end
        end

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