class RuboCop::Cop::InternalAffairs::NodeMatcherDirective

Checks that node matcher definitions are tagged with a YARD `@!method` directive so that editors are able to find the dynamically defined method.

@example

# bad
def_node_matcher :foo?, <<~PATTERN
  ...
PATTERN

# good
# @!method foo?(node)
def_node_matcher :foo?, <<~PATTERN
  ...
PATTERN

Constants

MSG
MSG_TOO_MANY
MSG_WRONG_NAME
REGEXP
RESTRICT_ON_SEND

Public Instance Methods

on_send(node) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 39
def on_send(node)
  return if node.arguments.none?
  return unless valid_method_name?(node)

  actual_name = node.arguments.first.value
  directives = method_directives(node)
  return too_many_directives(node) if directives.size > 1

  directive = directives.first
  return if directive_correct?(directive, actual_name)

  register_offense(node, directive, actual_name)
end

Private Instance Methods

add_newline?(node) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 117
def add_newline?(node)
  # Determine if a blank line should be inserted before the new directive
  # in order to spread out pattern matchers
  return if node.sibling_index&.zero?
  return unless node.parent

  prev_sibling = node.parent.child_nodes[node.sibling_index - 1]
  return unless prev_sibling && pattern_matcher?(prev_sibling)

  node.loc.line == last_line(prev_sibling) + 1
end
correct_directive(corrector, directive, actual_name) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 137
def correct_directive(corrector, directive, actual_name)
  correct = "@!method #{actual_name}"
  regexp = /@!method\s+#{Regexp.escape(directive[:method_name])}/

  replacement = directive[:node].text.gsub(regexp, correct)
  corrector.replace(directive[:node], replacement)
end
directive_correct?(directive, actual_name) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 74
def directive_correct?(directive, actual_name)
  directive && directive[:method_name] == actual_name.to_s
end
formatted_message(directive, actual_name, method_name) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 90
def formatted_message(directive, actual_name, method_name)
  if directive
    format(MSG_WRONG_NAME, expected: actual_name, actual: directive[:method_name])
  else
    format(MSG, method: method_name)
  end
end
insert_directive(corrector, node, actual_name) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 98
def insert_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.loc.expression, 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
last_line(node) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 129
def last_line(node)
  if node.last_argument.heredoc?
    node.last_argument.loc.heredoc_end.line
  else
    node.loc.last_line
  end
end
method_directives(node) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 59
def method_directives(node)
  comments = processed_source.ast_with_comments[node]

  comments.map do |comment|
    match = comment.text.match(REGEXP)
    next unless match

    { node: comment, method_name: match[:method_name], args: match[:args] }
  end.compact
end
pattern_arguments(pattern) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 110
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
register_offense(node, directive, actual_name) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 78
def register_offense(node, directive, actual_name)
  message = formatted_message(directive, actual_name, node.method_name)

  add_offense(node, message: message) do |corrector|
    if directive
      correct_directive(corrector, directive, actual_name)
    else
      insert_directive(corrector, node, actual_name)
    end
  end
end
too_many_directives(node) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 70
def too_many_directives(node)
  add_offense(node, message: MSG_TOO_MANY)
end
valid_method_name?(node) click to toggle source
# File lib/rubocop/cop/internal_affairs/node_matcher_directive.rb, line 55
def valid_method_name?(node)
  node.arguments.first.str_type? || node.arguments.first.sym_type?
end