/
format_parameter_mismatch.rb
190 lines (151 loc) · 5.3 KB
/
format_parameter_mismatch.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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# This lint sees if there is a mismatch between the number of
# expected fields for format/sprintf/#% and what is actually
# passed as arguments.
#
# In addition it checks whether different formats are used in the same
# format string. Do not mix numbered, unnumbered, and named formats in
# the same format string.
#
# @example
#
# # bad
#
# format('A value: %s and another: %i', a_value)
#
# @example
#
# # good
#
# format('A value: %s and another: %i', a_value, another)
#
# @example
#
# # bad
#
# format('Unnumbered format: %s and numbered: %2$s', a_value, another)
#
# @example
#
# # good
#
# format('Numbered format: %1$s and numbered %2$s', a_value, another)
class FormatParameterMismatch < Cop
# http://rubular.com/r/CvpbxkcTzy
MSG = "Number of arguments (%<arg_num>i) to `%<method>s` doesn't " \
'match the number of fields (%<field_num>i).'
MSG_INVALID = 'Format string is invalid because formatting sequence types ' \
'(numbered, named or unnumbered) are mixed.'
KERNEL = 'Kernel'
SHOVEL = '<<'
STRING_TYPES = %i[str dstr].freeze
def on_send(node)
return unless format_string?(node)
if invalid_format_string?(node)
add_offense(node, location: :selector, message: MSG_INVALID)
return
end
return unless offending_node?(node)
add_offense(node, location: :selector)
end
private
def format_string?(node)
called_on_string?(node) && method_with_format_args?(node)
end
def invalid_format_string?(node)
!RuboCop::Cop::Utils::FormatString.new(node.source).valid?
end
def offending_node?(node)
return false if splat_args?(node)
num_of_format_args, num_of_expected_fields = count_matches(node)
return false if num_of_format_args == :unknown
matched_arguments_count?(num_of_expected_fields, num_of_format_args)
end
def matched_arguments_count?(expected, passed)
if passed.negative?
expected < passed.abs
else
expected != passed
end
end
def_node_matcher :called_on_string?, <<~PATTERN
{(send {nil? const_type?} _ (str _) ...)
(send (str ...) ...)}
PATTERN
def method_with_format_args?(node)
sprintf?(node) || format?(node) || percent?(node)
end
def splat_args?(node)
return false if percent?(node)
node.arguments.drop(1).any?(&:splat_type?)
end
def heredoc?(node)
node.first_argument.source[0, 2] == SHOVEL
end
def count_matches(node)
if countable_format?(node)
count_format_matches(node)
elsif countable_percent?(node)
count_percent_matches(node)
else
[:unknown] * 2
end
end
def countable_format?(node)
(sprintf?(node) || format?(node)) && !heredoc?(node)
end
def countable_percent?(node)
percent?(node) && node.first_argument.array_type?
end
def count_format_matches(node)
[node.arguments.count - 1, expected_fields_count(node.first_argument)]
end
def count_percent_matches(node)
[node.first_argument.child_nodes.count,
expected_fields_count(node.receiver)]
end
def format_method?(name, node)
return false if node.const_receiver? &&
!node.receiver.loc.name.is?(KERNEL)
return false unless node.method?(name)
node.arguments.size > 1 && node.first_argument.str_type?
end
def expected_fields_count(node)
return :unknown unless node.str_type?
format_string = RuboCop::Cop::Utils::FormatString.new(node.source)
return 1 if format_string.named_interpolation?
max_digit_dollar_num = format_string.max_digit_dollar_num
return max_digit_dollar_num if max_digit_dollar_num&.nonzero?
format_string
.format_sequences
.reject(&:percent?)
.reduce(0) { |acc, seq| acc + seq.arity }
end
def format?(node)
format_method?(:format, node)
end
def sprintf?(node)
format_method?(:sprintf, node)
end
def percent?(node)
receiver = node.receiver
percent = node.method?(:%) &&
(STRING_TYPES.include?(receiver.type) ||
node.first_argument.array_type?)
return false if percent && STRING_TYPES.include?(receiver.type) &&
heredoc?(node)
percent
end
def message(node)
num_args_for_format, num_expected_fields = count_matches(node)
method_name = node.method?(:%) ? 'String#%' : node.method_name
format(MSG, arg_num: num_args_for_format, method: method_name,
field_num: num_expected_fields)
end
end
end
end
end