lib/rubocop/cop/layout/end_alignment.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Layout
      # This cop checks whether the end keywords are aligned properly.
      #
      # Three modes are supported through the `EnforcedStyleAlignWith`
      # configuration parameter:
      #
      # If it's set to `keyword` (which is the default), the `end`
      # shall be aligned with the start of the keyword (if, class, etc.).
      #
      # If it's set to `variable` the `end` shall be aligned with the
      # left-hand-side of the variable assignment, if there is one.
      #
      # If it's set to `start_of_line`, the `end` shall be aligned with the
      # start of the line where the matching keyword appears.
      #
      # This `Layout/EndAlignment` cop aligns with keywords (e.g. `if`, `while`, `case`)
      # by default. On the other hand, `Layout/BeginEndAlignment` cop aligns with
      # `EnforcedStyleAlignWith: start_of_line` by default due to `||= begin` tends
      # to align with the start of the line. These style can be configured by each cop.
      #
      # @example EnforcedStyleAlignWith: keyword (default)
      #   # bad
      #
      #   variable = if true
      #       end
      #
      #   # good
      #
      #   variable = if true
      #              end
      #
      #   variable =
      #     if true
      #     end
      #
      # @example EnforcedStyleAlignWith: variable
      #   # bad
      #
      #   variable = if true
      #       end
      #
      #   # good
      #
      #   variable = if true
      #   end
      #
      #   variable =
      #     if true
      #     end
      #
      # @example EnforcedStyleAlignWith: start_of_line
      #   # bad
      #
      #   variable = if true
      #       end
      #
      #   puts(if true
      #        end)
      #
      #   # good
      #
      #   variable = if true
      #   end
      #
      #   puts(if true
      #   end)
      #
      #   variable =
      #     if true
      #     end
      class EndAlignment < Base
        include CheckAssignment
        include EndKeywordAlignment
        include RangeHelp
        extend AutoCorrector

        def on_class(node)
          check_other_alignment(node)
        end

        def on_module(node)
          check_other_alignment(node)
        end

        def on_if(node)
          check_other_alignment(node) unless node.ternary?
        end

        def on_while(node)
          check_other_alignment(node)
        end

        def on_until(node)
          check_other_alignment(node)
        end

        def on_case(node)
          if node.argument?
            check_asgn_alignment(node.parent, node)
          else
            check_other_alignment(node)
          end
        end

        private

        def autocorrect(corrector, node)
          AlignmentCorrector.align_end(corrector, processed_source, node, alignment_node(node))
        end

        def check_assignment(node, rhs)
          # If there are method calls chained to the right hand side of the
          # assignment, we let rhs be the receiver of those method calls before
          # we check if it's an if/unless/while/until.
          return unless (rhs = first_part_of_call_chain(rhs))
          return unless rhs.conditional?
          return if rhs.if_type? && rhs.ternary?

          check_asgn_alignment(node, rhs)
        end

        def check_asgn_alignment(outer_node, inner_node)
          align_with = {
            keyword: inner_node.loc.keyword,
            start_of_line: start_line_range(inner_node),
            variable: asgn_variable_align_with(outer_node, inner_node)
          }

          check_end_kw_alignment(inner_node, align_with)
          ignore_node(inner_node)
        end

        def asgn_variable_align_with(outer_node, inner_node)
          expr = outer_node.source_range

          if line_break_before_keyword?(expr, inner_node)
            inner_node.loc.keyword
          else
            range_between(expr.begin_pos, inner_node.loc.keyword.end_pos)
          end
        end

        def check_other_alignment(node)
          align_with = {
            keyword: node.loc.keyword,
            variable: node.loc.keyword,
            start_of_line: start_line_range(node)
          }
          check_end_kw_alignment(node, align_with)
        end

        def alignment_node(node)
          case style
          when :keyword
            node
          when :variable
            alignment_node_for_variable_style(node)
          else
            start_line_range(node)
          end
        end

        def alignment_node_for_variable_style(node)
          return node.parent if node.case_type? && node.argument?

          assignment = node.ancestors.find(&:assignment_or_similar?)
          if assignment && !line_break_before_keyword?(assignment.source_range,
                                                       node)
            assignment
          else
            # Fall back to 'keyword' style if this node is not on the RHS of an
            # assignment, or if it is but there's a line break between LHS and
            # RHS.
            node
          end
        end
      end
    end
  end
end