From d363d49ab7f5a10647b8ba120b0213999cfac4cc Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 20 Oct 2025 18:35:32 -0500 Subject: [PATCH] [`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. --- .../test/fixtures/flake8_bugbear/B905.py | 13 +- .../test/fixtures/flake8_bugbear/B912.py | 3 + .../rules/map_without_explicit_strict.rs | 9 +- .../rules/zip_without_explicit_strict.rs | 8 +- ...rules__flake8_bugbear__tests__B905.py.snap | 226 +++++++----------- ...bugbear__tests__preview__B912_B912.py.snap | 16 +- 6 files changed, 122 insertions(+), 153 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B905.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B905.py index 031773b92e..02ed07daf5 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B905.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B905.py @@ -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) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B912.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B912.py index 32c7afb2f8..72bbf67cb8 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B912.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B912.py @@ -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) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs index 13c6806433..0eba117469 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs @@ -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: [What’s 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 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index a432bbdce9..102d6b0a57 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -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 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B905.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B905.py.snap index 8ea572c657..b6b4640583 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B905.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B905.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B912_B912.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B912_B912.py.snap index 98a2147bba..32047fa101 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B912_B912.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B912_B912.py.snap @@ -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