class RuboCop::Cop::InternalAffairs::NodeMatcherDirective


PATTERN
…
def_node_matcher :foo?, <<~PATTERN
# @!method foo?(node)
# good
PATTERN
…
def_node_matcher :foo?, <<~PATTERN
# bad
@example
method.
directive so that editors are able to find the dynamically defined
Checks that node matcher definitions are tagged with a YARD ‘@!method`

def add_newline?(node)

def add_newline?(node)
  # Determine if a blank line should be inserted before the new directive
  # in order to spread out pattern matchers
  return false if node.sibling_index&.zero?
  return false unless node.parent
  prev_sibling = node.parent.child_nodes[node.sibling_index - 1]
  return false unless prev_sibling && pattern_matcher?(prev_sibling)
  node.loc.line == last_line(prev_sibling) + 1
end

def correct_method_directive(corrector, directive, actual_name)

def correct_method_directive(corrector, directive, actual_name)
  correct = "@!method #{remove_receiver(actual_name)}"
  current_name = (directive[:receiver] || '') + directive[:method_name]
  regexp = /@!method\s+#{Regexp.escape(current_name)}/
  replacement = directive[:node_method].text.gsub(regexp, correct)
  corrector.replace(directive[:node_method], replacement)
end

def directive_offense_type(directive, actual_name)

def directive_offense_type(directive, actual_name)
  return :missing_directive unless directive
  return :wrong_scope if wrong_scope(directive, actual_name)
  return :no_scope if no_scope(directive, actual_name)
  # The method directive being prefixed by 'self.' is always an offense.
  # The matched method_name does not contain the receiver but the
  # def_node_match method name may so it must be removed.
  if directive[:method_name] != remove_receiver(actual_name) || directive[:receiver]
    :wrong_name
  end
end

def formatted_message(offense_type, directive, actual_name, method_name)

rubocop:disable Metrics/MethodLength
def formatted_message(offense_type, directive, actual_name, method_name)
  case offense_type
  when :wrong_name
    # Add the receiver to the name when showing an offense
    current_name = if directive[:receiver]
                     directive[:receiver] + directive[:method_name]
                   else
                     directive[:method_name]
                   end
    # The correct name will never include a receiver, remove it
    format(MSG_WRONG_NAME, expected: remove_receiver(actual_name), actual: current_name)
  when :wrong_scope
    MSG_WRONG_SCOPE_SELF
  when :no_scope
    MSG_MISSING_SCOPE_SELF
  when :missing_directive
    format(MSG, method: method_name)
  end
end

def group_comments(comments)

def group_comments(comments)
  result = []
  comments.each.with_index do |comment, index|
    # Grab the scope directive if it is preceded by a method directive
    if comment.text.include?('@!method')
      result << if (next_comment = comments[index + 1])&.text&.include?('@!scope')
                  [comment, next_comment]
                else
                  [comment, nil]
                end
    end
  end
  result
end

def insert_method_directive(corrector, node, actual_name)

def insert_method_directive(corrector, node, actual_name)
  # If the pattern matcher uses arguments (`%1`, `%2`, etc.), include them in the directive
  arguments = pattern_arguments(node.arguments[1].source)
  range = range_with_surrounding_space(node.source_range, side: :left, newlines: false)
  indentation = range.source.match(/^\s*/)[0]
  directive = "#{indentation}# @!method #{actual_name}(#{arguments.join(', ')})\n"
  directive = "\n#{directive}" if add_newline?(node)
  corrector.insert_before(range, directive)
end

def insert_scope_directive(corrector, node)

def insert_scope_directive(corrector, node)
  range = range_with_surrounding_space(node.source_range, side: :left, newlines: false)
  indentation = range.source.match(/^\s*/)[0]
  directive = "\n#{indentation}# @!scope class"
  corrector.insert_after(node, directive)
end

def last_line(node)

def last_line(node)
  if node.last_argument.heredoc?
    node.last_argument.loc.heredoc_end.line
  else
    node.loc.last_line
  end
end

def method_directives(node)

def method_directives(node)
  comments = processed_source.ast_with_comments[node]
  group_comments(comments).filter_map do |comment_method, comment_scope|
    match = comment_method.text.match(REGEXP_METHOD)
    next unless match
    {
      node_method: comment_method,
      node_scope: comment_scope,
      method_name: match[:method_name],
      args: match[:args],
      receiver: match[:receiver],
      has_scope_directive: comment_scope&.text&.match?(REGEXP_SCOPE)
    }
  end
end

def no_scope(directive, actual_name)

def no_scope(directive, actual_name)
  actual_name.start_with?('self.') && !directive[:has_scope_directive]
end

def on_send(node)

def on_send(node)
  return if node.arguments.none?
  return unless valid_method_name?(node)
  actual_name = node.first_argument.value.to_s
  # Ignore cases where the method has a receiver that isn't self
  return if actual_name.include?('.') && !actual_name.start_with?('self.')
  directives = method_directives(node)
  return too_many_directives(node) if directives.size > 1
  process_directive(node, actual_name, directives.first)
end

def pattern_arguments(pattern)

def pattern_arguments(pattern)
  arguments = %w[node]
  max_pattern_var = pattern.scan(/(?<=%)\d+/).map(&:to_i).max
  max_pattern_var&.times { |i| arguments << "arg#{i + 1}" }
  arguments
end

def process_directive(node, actual_name, directive)

def process_directive(node, actual_name, directive)
  return unless (offense_type = directive_offense_type(directive, actual_name))
  register_offense(offense_type, node, directive, actual_name)
end

def register_offense(offense_type, node, directive, actual_name)

def register_offense(offense_type, node, directive, actual_name)
  message = formatted_message(offense_type, directive, actual_name, node.method_name)
  add_offense(node, message: message) do |corrector|
    case offense_type
    when :wrong_name
      correct_method_directive(corrector, directive, actual_name)
    when :wrong_scope
      remove_scope_directive(corrector, directive)
    when :no_scope
      insert_scope_directive(corrector, directive[:node_method])
    when :missing_directive
      insert_method_directive(corrector, node, actual_name)
    end
  end
end

def remove_receiver(current)

def remove_receiver(current)
  current.delete_prefix('self.')
end

def remove_scope_directive(corrector, directive)

def remove_scope_directive(corrector, directive)
  range = range_by_whole_lines(
    directive[:node_scope].source_range,
    include_final_newline: true
  )
  corrector.remove(range)
end

def too_many_directives(node)

def too_many_directives(node)
  add_offense(node, message: MSG_TOO_MANY)
end

def valid_method_name?(node)

def valid_method_name?(node)
  node.first_argument.str_type? || node.first_argument.sym_type?
end

def wrong_scope(directive, actual_name)

def wrong_scope(directive, actual_name)
  !actual_name.start_with?('self.') && directive[:has_scope_directive]
end