forked from rubocop/rubocop
-
Notifications
You must be signed in to change notification settings - Fork 0
/
node_matcher_directive.rb
151 lines (123 loc) · 4.89 KB
/
node_matcher_directive.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# frozen_string_literal: true
module RuboCop
module Cop
module InternalAffairs
# 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
#
class NodeMatcherDirective < Base
extend AutoCorrector
include RangeHelp
MSG = 'Preceed `%<method>s` with a `@!method` YARD directive.'
MSG_WRONG_NAME = '`@!method` YARD directive has invalid method name, ' \
'use `%<expected>s` instead of `%<actual>s`.'
MSG_TOO_MANY = 'Multiple `@!method` YARD directives found for this matcher.'
RESTRICT_ON_SEND = %i[def_node_matcher def_node_search].to_set.freeze
REGEXP = /^\s*#\s*@!method\s+(?<method_name>[a-z0-9_]+[?!]?)(?:\((?<args>.*)\))?/.freeze
# @!method pattern_matcher?(node)
def_node_matcher :pattern_matcher?, <<~PATTERN
(send _ %RESTRICT_ON_SEND {str sym} {str dstr})
PATTERN
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
def valid_method_name?(node)
node.arguments.first.str_type? || node.arguments.first.sym_type?
end
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
def too_many_directives(node)
add_offense(node, message: MSG_TOO_MANY)
end
def directive_correct?(directive, actual_name)
directive && directive[:method_name] == actual_name.to_s
end
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
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
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(
range: 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
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 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
def last_line(node)
if node.last_argument.heredoc?
node.last_argument.loc.heredoc_end.line
else
node.loc.last_line
end
end
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
end
end
end
end