lib/rubocop/cop/mixin/empty_lines_around_body.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Layout
      # Common functionality for checking if presence/absence of empty lines
      # around some kind of body matches the configuration.
      module EmptyLinesAroundBody
        extend NodePattern::Macros
        include ConfigurableEnforcedStyle

        MSG_EXTRA = 'Extra empty line detected at %s body %s.'.freeze
        MSG_MISSING = 'Empty line missing at %s body %s.'.freeze
        MSG_DEFERRED = 'Empty line missing before first %s definition'.freeze

        def_node_matcher :constant_definition?, '{class module}'
        def_node_matcher :empty_line_required?, '{def defs class module}'

        def autocorrect(args)
          offense_style, range = args
          lambda do |corrector|
            case offense_style
            when :no_empty_lines then
              corrector.remove(range)
            when :empty_lines then
              corrector.insert_before(range, "\n")
            end
          end
        end

        private

        def check(node, body)
          # When style is `empty_lines`, if the body is empty, we don't enforce
          # the presence OR absence of an empty line
          # But if style is `no_empty_lines`, there must not be an empty line
          return unless body || style == :no_empty_lines
          return if node.single_line?

          first_line = node.source_range.first_line
          last_line = node.source_range.last_line

          case style
          when :empty_lines_except_namespace
            check_empty_lines_except_namespace(body, first_line, last_line)
          when :empty_lines_special
            check_empty_lines_special(body, first_line, last_line)
          else
            check_both(style, first_line, last_line)
          end
        end

        def check_empty_lines_except_namespace(body, first_line, last_line)
          if namespace?(body, with_one_child: true)
            check_both(:no_empty_lines, first_line, last_line)
          else
            check_both(:empty_lines, first_line, last_line)
          end
        end

        def check_empty_lines_special(body, first_line, last_line)
          return unless body
          if namespace?(body, with_one_child: true)
            check_both(:no_empty_lines, first_line, last_line)
          else
            if first_child_requires_empty_line?(body)
              check_beginning(:empty_lines, first_line)
            else
              check_beginning(:no_empty_lines, first_line)
              check_deferred_empty_line(body)
            end
            check_ending(:empty_lines, last_line)
          end
        end

        def check_both(style, first_line, last_line)
          check_beginning(style, first_line)
          check_ending(style, last_line)
        end

        def check_beginning(style, first_line)
          check_source(style, first_line, 'beginning')
        end

        def check_ending(style, last_line)
          check_source(style, last_line - 2, 'end')
        end

        def check_source(style, line_no, desc)
          case style
          when :no_empty_lines
            check_line(style, line_no, message(MSG_EXTRA, desc), &:empty?)
          when :empty_lines
            check_line(style, line_no, message(MSG_MISSING, desc)) do |line|
              !line.empty?
            end
          end
        end

        def check_line(style, line, msg)
          return unless yield(processed_source.lines[line])

          offset = style == :empty_lines && msg.include?('end.') ? 2 : 1
          range = source_range(processed_source.buffer, line + offset, 0)
          add_offense([style, range], range, msg)
        end

        def check_deferred_empty_line(body)
          node = first_empty_line_required_child(body)
          return unless node

          line = previous_line_ignoring_comments(node.loc.first_line)
          return if processed_source[line].empty?

          range = source_range(processed_source.buffer, line + 2, 0)
          add_offense([:empty_lines, range], range, deferred_message(node))
        end

        def namespace?(body, with_one_child: false)
          if body.begin_type?
            return false if with_one_child
            body.children.all? { |child| constant_definition?(child) }
          else
            constant_definition?(body)
          end
        end

        def first_child_requires_empty_line?(body)
          if body.begin_type?
            empty_line_required?(body.children.first)
          else
            empty_line_required?(body)
          end
        end

        def first_empty_line_required_child(body)
          if body.begin_type?
            body.children.find { |child| empty_line_required?(child) }
          elsif empty_line_required?(body)
            body
          end
        end

        def previous_line_ignoring_comments(send_line)
          (send_line - 2).downto(0) do |line|
            return line unless comment_line?(processed_source[line])
          end
          0
        end

        def message(type, desc)
          format(type, self.class::KIND, desc)
        end

        def deferred_message(node)
          format(MSG_DEFERRED, node.type)
        end
      end
    end
  end
end