Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F50x.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,30 @@
'%(k)s' % {**k}
'%s' % [1, 2, 3]
'%s' % {1, 2, 3}
# F507: literal non-tuple RHS with multiple positional placeholders
'%s %s' % 42 # F507
'%s %s' % 3.14 # F507
'%s %s' % "hello" # F507
'%s %s' % b"hello" # F507
'%s %s' % True # F507
'%s %s' % None # F507
'%s %s' % ... # F507
'%s %s' % f"hello {name}" # F507
# F507: ResolvedPythonType catches compound expressions with known types
'%s %s' % -1 # F507 (unary op on int → int)
'%s %s' % (1 + 2) # F507 (int + int → int)
'%s %s' % (not x) # F507 (not → bool)
'%s %s' % ("a" + "b") # F507 (str + str → str)
'%s %s' % (1 if True else 2) # F507 (int if ... else int → int)
# ok: single placeholder with literal RHS
'%s' % 42
'%s' % "hello"
'%s' % True
# ok: variables/expressions could be tuples at runtime
'%s %s' % banana
'%s %s' % obj.attr
'%s %s' % arr[0]
'%s %s' % get_args()
# ok: ternary/binop where one branch could be a tuple → Unknown
'%s %s' % (a if cond else b)
'%s %s' % (a + b)
15 changes: 15 additions & 0 deletions crates/ruff_linter/src/rules/pyflakes/rules/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::string::ToString;

use ruff_diagnostics::Applicability;
use ruff_python_ast::helpers::contains_effect;
use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType};
use rustc_hash::FxHashSet;

use ruff_macros::{ViolationMetadata, derive_message_formats};
Expand Down Expand Up @@ -757,6 +758,20 @@ pub(crate) fn percent_format_positional_count_mismatch(
location,
);
}
} else if let ResolvedPythonType::Atom(resolved_type) = ResolvedPythonType::from(right) {
// If we can infer a concrete non-tuple type for the RHS, it's always
// a single positional argument. Variables, attribute accesses, calls,
// etc. resolve to `Unknown` and are not flagged because they could be
// tuples at runtime.
if resolved_type != PythonType::Tuple && summary.num_positional != 1 {
checker.report_diagnostic(
PercentFormatPositionalCountMismatch {
wanted: summary.num_positional,
got: 1,
},
location,
);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
assertion_line: 192
---
F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:5:1
Expand All @@ -22,3 +23,167 @@ F507 `%`-format string has 2 placeholder(s) but 3 substitution(s)
7 | '%(bar)s' % {} # F505
8 | '%(bar)s' % {'bar': 1, 'baz': 2} # F504
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:10:1
|
8 | '%(bar)s' % {'bar': 1, 'baz': 2} # F504
9 | '%(bar)s' % (1, 2, 3) # F502
10 | '%s %s' % {'k': 'v'} # F503
| ^^^^^^^^^^^^^^^^^^^^
11 | '%(bar)*s' % {'bar': 'baz'} # F506, F508
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:22:1
|
20 | # ok *args and **kwargs
21 | a = []
22 | '%s %s' % [*a]
| ^^^^^^^^^^^^^^
23 | '%s %s' % (*a,)
24 | k = {}
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:29:1
|
27 | '%s' % {1, 2, 3}
28 | # F507: literal non-tuple RHS with multiple positional placeholders
29 | '%s %s' % 42 # F507
| ^^^^^^^^^^^^
30 | '%s %s' % 3.14 # F507
31 | '%s %s' % "hello" # F507
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:30:1
|
28 | # F507: literal non-tuple RHS with multiple positional placeholders
29 | '%s %s' % 42 # F507
30 | '%s %s' % 3.14 # F507
| ^^^^^^^^^^^^^^
31 | '%s %s' % "hello" # F507
32 | '%s %s' % b"hello" # F507
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:31:1
|
29 | '%s %s' % 42 # F507
30 | '%s %s' % 3.14 # F507
31 | '%s %s' % "hello" # F507
| ^^^^^^^^^^^^^^^^^
32 | '%s %s' % b"hello" # F507
33 | '%s %s' % True # F507
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:32:1
|
30 | '%s %s' % 3.14 # F507
31 | '%s %s' % "hello" # F507
32 | '%s %s' % b"hello" # F507
| ^^^^^^^^^^^^^^^^^^
33 | '%s %s' % True # F507
34 | '%s %s' % None # F507
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:33:1
|
31 | '%s %s' % "hello" # F507
32 | '%s %s' % b"hello" # F507
33 | '%s %s' % True # F507
| ^^^^^^^^^^^^^^
34 | '%s %s' % None # F507
35 | '%s %s' % ... # F507
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:34:1
|
32 | '%s %s' % b"hello" # F507
33 | '%s %s' % True # F507
34 | '%s %s' % None # F507
| ^^^^^^^^^^^^^^
35 | '%s %s' % ... # F507
36 | '%s %s' % f"hello {name}" # F507
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:35:1
|
33 | '%s %s' % True # F507
34 | '%s %s' % None # F507
35 | '%s %s' % ... # F507
| ^^^^^^^^^^^^^
36 | '%s %s' % f"hello {name}" # F507
37 | # F507: ResolvedPythonType catches compound expressions with known types
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:36:1
|
34 | '%s %s' % None # F507
35 | '%s %s' % ... # F507
36 | '%s %s' % f"hello {name}" # F507
| ^^^^^^^^^^^^^^^^^^^^^^^^^
37 | # F507: ResolvedPythonType catches compound expressions with known types
38 | '%s %s' % -1 # F507 (unary op on int → int)
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:38:1
|
36 | '%s %s' % f"hello {name}" # F507
37 | # F507: ResolvedPythonType catches compound expressions with known types
38 | '%s %s' % -1 # F507 (unary op on int → int)
| ^^^^^^^^^^^^
39 | '%s %s' % (1 + 2) # F507 (int + int → int)
40 | '%s %s' % (not x) # F507 (not → bool)
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:39:1
|
37 | # F507: ResolvedPythonType catches compound expressions with known types
38 | '%s %s' % -1 # F507 (unary op on int → int)
39 | '%s %s' % (1 + 2) # F507 (int + int → int)
| ^^^^^^^^^^^^^^^^^
40 | '%s %s' % (not x) # F507 (not → bool)
41 | '%s %s' % ("a" + "b") # F507 (str + str → str)
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:40:1
|
38 | '%s %s' % -1 # F507 (unary op on int → int)
39 | '%s %s' % (1 + 2) # F507 (int + int → int)
40 | '%s %s' % (not x) # F507 (not → bool)
| ^^^^^^^^^^^^^^^^^
41 | '%s %s' % ("a" + "b") # F507 (str + str → str)
42 | '%s %s' % (1 if True else 2) # F507 (int if ... else int → int)
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:41:1
|
39 | '%s %s' % (1 + 2) # F507 (int + int → int)
40 | '%s %s' % (not x) # F507 (not → bool)
41 | '%s %s' % ("a" + "b") # F507 (str + str → str)
| ^^^^^^^^^^^^^^^^^^^^^
42 | '%s %s' % (1 if True else 2) # F507 (int if ... else int → int)
43 | # ok: single placeholder with literal RHS
|

F507 `%`-format string has 2 placeholder(s) but 1 substitution(s)
--> F50x.py:42:1
|
40 | '%s %s' % (not x) # F507 (not → bool)
41 | '%s %s' % ("a" + "b") # F507 (str + str → str)
42 | '%s %s' % (1 if True else 2) # F507 (int if ... else int → int)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
43 | # ok: single placeholder with literal RHS
44 | '%s' % 42
|
Loading