Skip to content

Commit 94be910

Browse files
committed
Merge pull request rubocop#2237 from rrosenblum/fix_format_parameter_mismatch
Fix format parameter mismatch
2 parents 11ac74c + 012e189 commit 94be910

File tree

3 files changed

+113
-52
lines changed

3 files changed

+113
-52
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
* [#2213](https://github.com/bbatsov/rubocop/issues/2213): Write to cache with binary encoding to avoid transcoding exceptions in some locales. ([@jonas054][])
1717
* [#2218](https://github.com/bbatsov/rubocop/issues/2218): Fix loading config error when safe yaml is only partially loaded. ([@maxjacobson][])
1818
* [#2161](https://github.com/bbatsov/rubocop/issues/2161): Allow an explicit receiver (except `Kernel`) in `Style/SignalException`. ([@lumeet][])
19+
* [#2237](https://github.com/bbatsov/rubocop/pull/2237): Allow `Lint/FormatParameterMismatch` to be called using `Kernel.format` and `Kernel.sprintf`. ([@rrosenblum][])
20+
* [#2234](https://github.com/bbatsov/rubocop/issues/2234): Do not register an offense for `Lint/FormatParameterMismatch` when the format string is a variable. ([@rrosenblum][])
1921

2022
## 0.34.0 (05/09/2015)
2123

lib/rubocop/cop/lint/format_parameter_mismatch.rb

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ module Lint
1414
class FormatParameterMismatch < Cop
1515
# http://rubular.com/r/CvpbxkcTzy
1616
MSG = 'Number arguments (%i) to `%s` mismatches expected fields (%i).'
17-
# rubocop:disable Metrics/LineLength
18-
FIELD_REGEX = /(%(([\s#+-0\*])?(\d*)?(.\d+)?(\.)?[bBdiouxXeEfgGaAcps]|%))/
19-
NAMED_FIELD_REGEX = /%\{[_a-zA-Z][_a-zA-Z]+\}/
20-
21-
def fields_regex
22-
FIELD_REGEX
23-
end
17+
FIELD_REGEX = /(%(([\s#+-0\*])?(\d*)?(.\d+)?(\.)?[bBdiouxXeEfgGaAcps]|%))/.freeze # rubocop:disable Metrics/LineLength
18+
NAMED_FIELD_REGEX = /%\{[_a-zA-Z][_a-zA-Z]+\}/.freeze
19+
KERNEL = 'Kernel'.freeze
20+
SHOVEL = '<<'.freeze
21+
PERCENT = '%'.freeze
22+
PERCENT_PERCENT = '%%'.freeze
23+
SPLAT = '*'.freeze
24+
STRING_TYPES = [:str, :dstr].freeze
2425

2526
def on_send(node)
2627
add_offense(node, :selector) if offending_node?(node)
@@ -29,18 +30,29 @@ def on_send(node)
2930
private
3031

3132
def offending_node?(node)
33+
return false unless called_on_string?(node)
34+
3235
if sprintf?(node) || format?(node) || percent?(node)
3336
if named_mode?(node) || node_with_splat_args?(node)
3437
false
3538
else
36-
num_of_args_for_format, num_of_expected_fields = count_matches(node)
37-
num_of_expected_fields != num_of_args_for_format
39+
num_of_format_args, num_of_expected_fields = count_matches(node)
40+
num_of_expected_fields != num_of_format_args
3841
end
3942
else
4043
false
4144
end
4245
end
4346

47+
def called_on_string?(node)
48+
receiver_node, _method, format_string, = *node
49+
if receiver_node.nil? || receiver_node.const_type?
50+
format_string && format_string.str_type?
51+
else
52+
receiver_node.str_type?
53+
end
54+
end
55+
4456
def named_mode?(node)
4557
receiver_node, _method_name, *args = *node
4658

@@ -50,11 +62,7 @@ def named_mode?(node)
5062
receiver_node
5163
end
5264

53-
relevant_node
54-
.loc
55-
.expression
56-
.source
57-
.scan(NAMED_FIELD_REGEX).count > 0
65+
relevant_node.loc.expression.source.scan(NAMED_FIELD_REGEX).size > 0
5866
end
5967

6068
def node_with_splat_args?(node)
@@ -68,7 +76,7 @@ def node_with_splat_args?(node)
6876
def heredoc?(node)
6977
_receiver, _name, args = *node
7078

71-
args.loc.expression.source[0, 2] == '<<'
79+
args.loc.expression.source[0, 2] == SHOVEL
7280
end
7381

7482
def count_matches(node)
@@ -80,7 +88,7 @@ def count_matches(node)
8088
elsif percent?(node)
8189
first_child_argument = args.first
8290

83-
if first_child_argument.type == :array
91+
if first_child_argument.array_type?
8492
number_of_args_for_format = args.first.child_nodes.size
8593
number_of_expected_fields = expected_fields_count(receiver_node)
8694
else
@@ -95,10 +103,13 @@ def count_matches(node)
95103
def format_method?(name, node)
96104
receiver, method_name, *args = *node
97105

98-
# commands have no explicit receiver
99-
return false unless !receiver && method_name == name
106+
if receiver && receiver.const_type?
107+
return false unless receiver.loc.name.is?(KERNEL)
108+
end
109+
110+
return false unless method_name == name
100111

101-
args.size > 1 && :str == args.first.type
112+
args.size > 1 && args.first.str_type?
102113
end
103114

104115
def expected_fields_count(node)
@@ -107,8 +118,8 @@ def expected_fields_count(node)
107118
.expression
108119
.source
109120
.scan(FIELD_REGEX)
110-
.select { |x| x.first != '%%' }
111-
.reduce(0) { |a, e| a + (e[2] == '*' ? 2 : 1) }
121+
.select { |x| x.first != PERCENT_PERCENT }
122+
.reduce(0) { |a, e| a + (e[2] == SPLAT ? 2 : 1) }
112123
end
113124

114125
def format?(node)
@@ -123,10 +134,10 @@ def percent?(node)
123134
receiver_node, method_name, *arg_nodes = *node
124135

125136
percent = method_name == :% &&
126-
([:str, :dstr].include?(receiver_node.type) ||
127-
arg_nodes[0].type == :array)
137+
(STRING_TYPES.include?(receiver_node.type) ||
138+
arg_nodes[0].array_type?)
128139

129-
if percent && [:str, :dstr].include?(receiver_node.type)
140+
if percent && STRING_TYPES.include?(receiver_node.type)
130141
return false if heredoc?(node)
131142
end
132143

@@ -137,7 +148,7 @@ def message(node)
137148
_receiver, method_name, *_args = *node
138149
num_args_for_format, num_expected_fields = count_matches(node)
139150

140-
method_name = 'String#%' if '%' == method_name.to_s
151+
method_name = 'String#%' if PERCENT == method_name.to_s
141152
format(MSG, num_args_for_format, method_name, num_expected_fields)
142153
end
143154
end

spec/rubocop/cop/lint/format_parameter_mismatch_spec.rb

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,53 @@
55
describe RuboCop::Cop::Lint::FormatParameterMismatch do
66
subject(:cop) { described_class.new }
77

8+
shared_examples 'variables' do |variable|
9+
it 'does not register an offense for % called on a variable' do
10+
inspect_source(cop, ["#{variable} = '%s'",
11+
"#{variable} % [foo]"])
12+
13+
expect(cop.messages).to be_empty
14+
end
15+
16+
it 'does not register an offense for format called on a variable' do
17+
inspect_source(cop, ["#{variable} = '%s'",
18+
"format(#{variable}, foo)"])
19+
20+
expect(cop.messages).to be_empty
21+
end
22+
23+
it 'does not register an offense for format called on a variable' do
24+
inspect_source(cop, ["#{variable} = '%s'",
25+
"sprintf(#{variable}, foo)"])
26+
27+
expect(cop.messages).to be_empty
28+
end
29+
end
30+
31+
it_behaves_like 'variables', 'CONST'
32+
it_behaves_like 'variables', 'var'
33+
it_behaves_like 'variables', '@var'
34+
it_behaves_like 'variables', '@@var'
35+
it_behaves_like 'variables', '$var'
36+
37+
it 'registers an offense when calling Kernel.format ' \
38+
'and the fields do not match' do
39+
inspect_source(cop, 'Kernel.format("%s %s", 1)')
40+
expect(cop.offenses.size).to eq(1)
41+
42+
msg = ['Number arguments (1) to `format` mismatches expected fields (2).']
43+
expect(cop.messages).to eq(msg)
44+
end
45+
46+
it 'registers an offense when calling Kernel.sprintf ' \
47+
'and the fields do not match' do
48+
inspect_source(cop, 'Kernel.sprintf("%s %s", 1)')
49+
expect(cop.offenses.size).to eq(1)
50+
51+
msg = ['Number arguments (1) to `sprintf` mismatches expected fields (2).']
52+
expect(cop.messages).to eq(msg)
53+
end
54+
855
it 'registers an offense when there are less arguments than expected' do
956
inspect_source(cop, 'format("%s %s", 1)')
1057
expect(cop.offenses.size).to eq(1)
@@ -38,15 +85,16 @@
3885
end
3986

4087
it 'registers offense with sprintf' do
41-
inspect_source(cop, 'format("%s %s", 1, 2, 3)')
88+
inspect_source(cop, 'sprintf("%s %s", 1, 2, 3)')
4289
expect(cop.offenses.size).to eq(1)
4390

44-
msg = ['Number arguments (3) to `format` mismatches expected fields (2).']
91+
msg = ['Number arguments (3) to `sprintf` mismatches expected fields (2).']
4592
expect(cop.messages).to eq(msg)
4693
end
4794

4895
it 'correctly parses different sprintf formats' do
49-
inspect_source(cop, 'format("%020x%+g:% g %%%#20.8x %#.0e", 1, 2, 3, 4, 5)')
96+
inspect_source(cop,
97+
'sprintf("%020x%+g:% g %%%#20.8x %#.0e", 1, 2, 3, 4, 5)')
5098
expect(cop.offenses).to be_empty
5199
end
52100

@@ -130,53 +178,53 @@
130178
end
131179

132180
it 'finds the correct number of fields' do
133-
expect(''.scan(cop.fields_regex).size)
181+
expect(''.scan(described_class::FIELD_REGEX).size)
134182
.to eq(0)
135-
expect('%s'.scan(cop.fields_regex).size)
183+
expect('%s'.scan(described_class::FIELD_REGEX).size)
136184
.to eq(1)
137-
expect('%s %s'.scan(cop.fields_regex).size)
185+
expect('%s %s'.scan(described_class::FIELD_REGEX).size)
138186
.to eq(2)
139-
expect('%s %s %%'.scan(cop.fields_regex).size)
187+
expect('%s %s %%'.scan(described_class::FIELD_REGEX).size)
140188
.to eq(3)
141-
expect('%s %s %%'.scan(cop.fields_regex).size)
189+
expect('%s %s %%'.scan(described_class::FIELD_REGEX).size)
142190
.to eq(3)
143-
expect('% d'.scan(cop.fields_regex).size)
191+
expect('% d'.scan(described_class::FIELD_REGEX).size)
144192
.to eq(1)
145-
expect('%+d'.scan(cop.fields_regex).size)
193+
expect('%+d'.scan(described_class::FIELD_REGEX).size)
146194
.to eq(1)
147-
expect('%d'.scan(cop.fields_regex).size)
195+
expect('%d'.scan(described_class::FIELD_REGEX).size)
148196
.to eq(1)
149-
expect('%+o'.scan(cop.fields_regex).size)
197+
expect('%+o'.scan(described_class::FIELD_REGEX).size)
150198
.to eq(1)
151-
expect('%#o'.scan(cop.fields_regex).size)
199+
expect('%#o'.scan(described_class::FIELD_REGEX).size)
152200
.to eq(1)
153-
expect('%.0e'.scan(cop.fields_regex).size)
201+
expect('%.0e'.scan(described_class::FIELD_REGEX).size)
154202
.to eq(1)
155-
expect('%#.0e'.scan(cop.fields_regex).size)
203+
expect('%#.0e'.scan(described_class::FIELD_REGEX).size)
156204
.to eq(1)
157-
expect('% 020d'.scan(cop.fields_regex).size)
205+
expect('% 020d'.scan(described_class::FIELD_REGEX).size)
158206
.to eq(1)
159-
expect('%20d'.scan(cop.fields_regex).size)
207+
expect('%20d'.scan(described_class::FIELD_REGEX).size)
160208
.to eq(1)
161-
expect('%+20d'.scan(cop.fields_regex).size)
209+
expect('%+20d'.scan(described_class::FIELD_REGEX).size)
162210
.to eq(1)
163-
expect('%020d'.scan(cop.fields_regex).size)
211+
expect('%020d'.scan(described_class::FIELD_REGEX).size)
164212
.to eq(1)
165-
expect('%+020d'.scan(cop.fields_regex).size)
213+
expect('%+020d'.scan(described_class::FIELD_REGEX).size)
166214
.to eq(1)
167-
expect('% 020d'.scan(cop.fields_regex).size)
215+
expect('% 020d'.scan(described_class::FIELD_REGEX).size)
168216
.to eq(1)
169-
expect('%-20d'.scan(cop.fields_regex).size)
217+
expect('%-20d'.scan(described_class::FIELD_REGEX).size)
170218
.to eq(1)
171-
expect('%-+20d'.scan(cop.fields_regex).size)
219+
expect('%-+20d'.scan(described_class::FIELD_REGEX).size)
172220
.to eq(1)
173-
expect('%- 20d'.scan(cop.fields_regex).size)
221+
expect('%- 20d'.scan(described_class::FIELD_REGEX).size)
174222
.to eq(1)
175-
expect('%020x'.scan(cop.fields_regex).size)
223+
expect('%020x'.scan(described_class::FIELD_REGEX).size)
176224
.to eq(1)
177-
expect('%#20.8x'.scan(cop.fields_regex).size)
225+
expect('%#20.8x'.scan(described_class::FIELD_REGEX).size)
178226
.to eq(1)
179-
expect('%+g:% g:%-g'.scan(cop.fields_regex).size)
227+
expect('%+g:% g:%-g'.scan(described_class::FIELD_REGEX).size)
180228
.to eq(3)
181229
end
182230
end

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy