[flake8-bugbear] Skip B905 and B912 if <2 iterables and no starred arguments (#20998)

Closes #20997

This will _decrease_ the number of diagnostics emitted for
[zip-without-explicit-strict
(B905)](https://docs.astral.sh/ruff/rules/zip-without-explicit-strict/#zip-without-explicit-strict-b905),
since previously it triggered on any `zip` call no matter the number of
arguments. It may _increase_ the number of diagnostics for
[map-without-explicit-strict
(B912)](https://docs.astral.sh/ruff/rules/map-without-explicit-strict/#map-without-explicit-strict-b912)
since it will now trigger on a single starred argument where before it
would not. However, the latter rule is in `preview` so this is
acceptable.

Note - we do not need to make any changes to
[batched-without-explicit-strict
(B911)](https://docs.astral.sh/ruff/rules/batched-without-explicit-strict/#batched-without-explicit-strict-b911)
since that just takes a single iterable.

I am doing this in one PR rather than two because we should keep the
behavior of these rules consistent with one another.

For review: apologies for the unreadability of the snapshot for `B905`.
Unfortunately I saw no way of keeping a small diff and a correct fixture
(the fixture labeled a whole block as `# Error` whereas now several in
the block became `# Ok`).Probably simplest to just view the actual
snapshot - it's relatively small.
This commit is contained in:
Dylan 2025-10-20 18:35:32 -05:00 committed by GitHub
parent 281ae8eb34
commit d363d49ab7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 122 additions and 153 deletions

View file

@ -1,12 +1,10 @@
from itertools import count, cycle, repeat
# Errors
zip()
zip(range(3))
zip("a", "b")
zip("a", "b", *zip("c"))
zip(zip("a"), strict=False)
zip(zip("a", strict=True))
zip(zip("a", "b"), strict=False)
zip(zip("a", strict=True),"b")
# OK
zip(range(3), strict=True)
@ -27,3 +25,10 @@ zip([1, 2, 3], repeat(1, times=4))
import builtins
# Still an error even though it uses the qualified name
builtins.zip([1, 2, 3])
# Regression https://github.com/astral-sh/ruff/issues/20997
# Ok
zip()
zip(range(3))
# Error
zip(*lot_of_iterators)

View file

@ -31,3 +31,6 @@ map(lambda x, y: x + y, [1, 2, 3], cycle([1, 2, 3]))
map(lambda x, y: x + y, [1, 2, 3], repeat(1))
map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=None))
map(lambda x, y: x + y, [1, 2, 3], count())
# Regression https://github.com/astral-sh/ruff/issues/20997
map(f, *lots_of_iterators)

View file

@ -9,7 +9,7 @@ use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
/// Checks for `map` calls without an explicit `strict` parameter when called with two or more iterables.
/// Checks for `map` calls without an explicit `strict` parameter when called with two or more iterables, or any starred argument.
///
/// This rule applies to Python 3.14 and later, where `map` accepts a `strict` keyword
/// argument. For details, see: [Whats New in Python 3.14](https://docs.python.org/dev/whatsnew/3.14.html).
@ -62,7 +62,12 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
if semantic.match_builtin_expr(&call.func, "map")
&& call.arguments.find_keyword("strict").is_none()
&& call.arguments.args.len() >= 3 // function + at least 2 iterables
&& (
// at least 2 iterables (+ 1 function)
call.arguments.args.len() >= 3
// or a starred argument
|| call.arguments.args.iter().any(ast::Expr::is_starred_expr)
)
&& !any_infinite_iterables(call.arguments.args.iter().skip(1), semantic)
{
checker

View file

@ -9,7 +9,7 @@ use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
/// Checks for `zip` calls without an explicit `strict` parameter.
/// Checks for `zip` calls without an explicit `strict` parameter when called with two or more iterables, or any starred argument.
///
/// ## Why is this bad?
/// By default, if the iterables passed to `zip` are of different lengths, the
@ -58,6 +58,12 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
if semantic.match_builtin_expr(&call.func, "zip")
&& call.arguments.find_keyword("strict").is_none()
&& (
// at least 2 iterables
call.arguments.args.len() >= 2
// or a starred argument
|| call.arguments.args.iter().any(ast::Expr::is_starred_expr)
)
&& !any_infinite_iterables(call.arguments.args.iter(), semantic)
{
checker

View file

@ -1,204 +1,140 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
assertion_line: 156
---
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:4:1
|
3 | # Errors
4 | zip()
| ^^^^^
5 | zip(range(3))
6 | zip("a", "b")
4 | zip("a", "b")
| ^^^^^^^^^^^^^
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
|
help: Add explicit value for parameter `strict=`
1 | from itertools import count, cycle, repeat
2 |
3 | # Errors
- zip()
4 + zip(strict=False)
5 | zip(range(3))
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
- zip("a", "b")
4 + zip("a", "b", strict=False)
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
7 | zip(zip("a", strict=True),"b")
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:5:1
|
3 | # Errors
4 | zip()
5 | zip(range(3))
| ^^^^^^^^^^^^^
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
4 | zip("a", "b")
5 | zip("a", "b", *zip("c"))
| ^^^^^^^^^^^^^^^^^^^^^^^^
6 | zip(zip("a", "b"), strict=False)
7 | zip(zip("a", strict=True),"b")
|
help: Add explicit value for parameter `strict=`
2 |
3 | # Errors
4 | zip()
- zip(range(3))
5 + zip(range(3), strict=False)
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
4 | zip("a", "b")
- zip("a", "b", *zip("c"))
5 + zip("a", "b", *zip("c"), strict=False)
6 | zip(zip("a", "b"), strict=False)
7 | zip(zip("a", strict=True),"b")
8 |
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:6:1
--> B905.py:6:5
|
4 | zip()
5 | zip(range(3))
6 | zip("a", "b")
| ^^^^^^^^^^^^^
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
4 | zip("a", "b")
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
| ^^^^^^^^^^^^^
7 | zip(zip("a", strict=True),"b")
|
help: Add explicit value for parameter `strict=`
3 | # Errors
4 | zip()
5 | zip(range(3))
- zip("a", "b")
6 + zip("a", "b", strict=False)
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
4 | zip("a", "b")
5 | zip("a", "b", *zip("c"))
- zip(zip("a", "b"), strict=False)
6 + zip(zip("a", "b", strict=False), strict=False)
7 | zip(zip("a", strict=True),"b")
8 |
9 | # OK
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:7:1
|
5 | zip(range(3))
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
| ^^^^^^^^^^^^^^^^^^^^^^^^
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
7 | zip(zip("a", strict=True),"b")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 |
9 | # OK
|
help: Add explicit value for parameter `strict=`
4 | zip()
5 | zip(range(3))
6 | zip("a", "b")
- zip("a", "b", *zip("c"))
7 + zip("a", "b", *zip("c"), strict=False)
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
10 |
4 | zip("a", "b")
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
- zip(zip("a", strict=True),"b")
7 + zip(zip("a", strict=True),"b", strict=False)
8 |
9 | # OK
10 | zip(range(3), strict=True)
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:7:16
|
5 | zip(range(3))
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
| ^^^^^^^^
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
|
help: Add explicit value for parameter `strict=`
4 | zip()
5 | zip(range(3))
6 | zip("a", "b")
- zip("a", "b", *zip("c"))
7 + zip("a", "b", *zip("c", strict=False))
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
10 |
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:8:5
|
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
| ^^^^^^^^
9 | zip(zip("a", strict=True))
|
help: Add explicit value for parameter `strict=`
5 | zip(range(3))
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
- zip(zip("a"), strict=False)
8 + zip(zip("a", strict=False), strict=False)
9 | zip(zip("a", strict=True))
10 |
11 | # OK
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:9:1
--> B905.py:22:1
|
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
10 |
11 | # OK
|
help: Add explicit value for parameter `strict=`
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
- zip(zip("a", strict=True))
9 + zip(zip("a", strict=True), strict=False)
10 |
11 | # OK
12 | zip(range(3), strict=True)
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:24:1
|
23 | # Errors (limited iterators).
24 | zip([1, 2, 3], repeat(1, 1))
21 | # Errors (limited iterators).
22 | zip([1, 2, 3], repeat(1, 1))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
25 | zip([1, 2, 3], repeat(1, times=4))
23 | zip([1, 2, 3], repeat(1, times=4))
|
help: Add explicit value for parameter `strict=`
21 | zip([1, 2, 3], repeat(1, times=None))
22 |
23 | # Errors (limited iterators).
19 | zip([1, 2, 3], repeat(1, times=None))
20 |
21 | # Errors (limited iterators).
- zip([1, 2, 3], repeat(1, 1))
24 + zip([1, 2, 3], repeat(1, 1), strict=False)
25 | zip([1, 2, 3], repeat(1, times=4))
26 |
27 | import builtins
22 + zip([1, 2, 3], repeat(1, 1), strict=False)
23 | zip([1, 2, 3], repeat(1, times=4))
24 |
25 | import builtins
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:25:1
--> B905.py:23:1
|
23 | # Errors (limited iterators).
24 | zip([1, 2, 3], repeat(1, 1))
25 | zip([1, 2, 3], repeat(1, times=4))
21 | # Errors (limited iterators).
22 | zip([1, 2, 3], repeat(1, 1))
23 | zip([1, 2, 3], repeat(1, times=4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26 |
27 | import builtins
24 |
25 | import builtins
|
help: Add explicit value for parameter `strict=`
22 |
23 | # Errors (limited iterators).
24 | zip([1, 2, 3], repeat(1, 1))
20 |
21 | # Errors (limited iterators).
22 | zip([1, 2, 3], repeat(1, 1))
- zip([1, 2, 3], repeat(1, times=4))
25 + zip([1, 2, 3], repeat(1, times=4), strict=False)
26 |
27 | import builtins
28 | # Still an error even though it uses the qualified name
23 + zip([1, 2, 3], repeat(1, times=4), strict=False)
24 |
25 | import builtins
26 | # Still an error even though it uses the qualified name
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:29:1
--> B905.py:34:1
|
27 | import builtins
28 | # Still an error even though it uses the qualified name
29 | builtins.zip([1, 2, 3])
| ^^^^^^^^^^^^^^^^^^^^^^^
32 | zip(range(3))
33 | # Error
34 | zip(*lot_of_iterators)
| ^^^^^^^^^^^^^^^^^^^^^^
|
help: Add explicit value for parameter `strict=`
26 |
27 | import builtins
28 | # Still an error even though it uses the qualified name
- builtins.zip([1, 2, 3])
29 + builtins.zip([1, 2, 3], strict=False)
31 | zip()
32 | zip(range(3))
33 | # Error
- zip(*lot_of_iterators)
34 + zip(*lot_of_iterators, strict=False)
note: This is an unsafe fix and may change runtime behavior

View file

@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
assertion_line: 112
---
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:5:1
@ -147,3 +146,18 @@ help: Add explicit value for parameter `strict=`
19 | # OK
20 | map(lambda x: x, [1, 2, 3], strict=True)
note: This is an unsafe fix and may change runtime behavior
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:36:1
|
35 | # Regression https://github.com/astral-sh/ruff/issues/20997
36 | map(f, *lots_of_iterators)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add explicit value for parameter `strict=`
33 | map(lambda x, y: x + y, [1, 2, 3], count())
34 |
35 | # Regression https://github.com/astral-sh/ruff/issues/20997
- map(f, *lots_of_iterators)
36 + map(f, *lots_of_iterators, strict=False)
note: This is an unsafe fix and may change runtime behavior