# 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 include RangeHelp MSG_EXTRA = 'Extra empty line detected at %s body ' \ '%s.' MSG_MISSING = 'Empty line missing at %s body %s.' MSG_DEFERRED = 'Empty line missing before first %s ' \ 'definition' private # @!method constant_definition?(node) def_node_matcher :constant_definition?, '{class module}' # @!method empty_line_required?(node) def_node_matcher :empty_line_required?, '{def defs class module (send nil? {:private :protected :public})}' def check(node, body, adjusted_first_line: nil) return if valid_body_style?(body) return if node.single_line? first_line = adjusted_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) case style when :beginning_only check_beginning(:empty_lines, first_line) check_ending(:no_empty_lines, last_line) when :ending_only check_beginning(:no_empty_lines, first_line) check_ending(:empty_lines, last_line) else check_beginning(style, first_line) check_ending(style, last_line) end 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(range, message: msg) do |corrector| EmptyLineCorrector.correct(corrector, [style, range]) end end def check_deferred_empty_line(body) node = first_empty_line_required_child(body) return unless node line = previous_line_ignoring_comments(node.first_line) return if processed_source[line].empty? range = source_range(processed_source.buffer, line + 2, 0) add_offense(range, message: deferred_message(node)) do |corrector| EmptyLineCorrector.correct(corrector, [:empty_lines, range]) end 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, kind: self.class::KIND, location: desc) end def deferred_message(node) format(MSG_DEFERRED, type: node.type) end def valid_body_style?(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 body.nil? && style != :no_empty_lines end end end end end