diff --git a/crates/ruff/resources/test/fixtures/flake8_bugbear/B905.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B905.py index 1a01b5ebac327..4d94cabbceea6 100644 --- a/crates/ruff/resources/test/fixtures/flake8_bugbear/B905.py +++ b/crates/ruff/resources/test/fixtures/flake8_bugbear/B905.py @@ -1,3 +1,6 @@ +from itertools import count, cycle, repeat + +# Errors zip() zip(range(3)) zip("a", "b") @@ -5,6 +8,18 @@ zip(zip("a"), strict=False) zip(zip("a", strict=True)) +# OK zip(range(3), strict=True) zip("a", "b", strict=False) zip("a", "b", "c", strict=True) + +# OK (infinite iterators). +zip([1, 2, 3], cycle("ABCDEF")) +zip([1, 2, 3], count()) +zip([1, 2, 3], repeat(1)) +zip([1, 2, 3], repeat(1, None)) +zip([1, 2, 3], repeat(1, times=None)) + +# Errors (limited iterators). +zip([1, 2, 3], repeat(1, 1)) +zip([1, 2, 3], repeat(1, times=4)) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 32c6c61fdc919..b39a02fa8321c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2633,7 +2633,9 @@ where if self.enabled(Rule::ZipWithoutExplicitStrict) && self.settings.target_version >= PythonVersion::Py310 { - flake8_bugbear::rules::zip_without_explicit_strict(self, expr, func, keywords); + flake8_bugbear::rules::zip_without_explicit_strict( + self, expr, func, args, keywords, + ); } if self.enabled(Rule::NoExplicitStacklevel) { flake8_bugbear::rules::no_explicit_stacklevel(self, func, args, keywords); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index e1314239e081e..3bbc2efbaced6 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -2,6 +2,8 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; +use ruff_python_semantic::model::SemanticModel; use crate::checkers::ast::Checker; @@ -20,6 +22,7 @@ pub(crate) fn zip_without_explicit_strict( checker: &mut Checker, expr: &Expr, func: &Expr, + args: &[Expr], kwargs: &[Keyword], ) { if let Expr::Name(ast::ExprName { id, .. }) = func { @@ -28,6 +31,9 @@ pub(crate) fn zip_without_explicit_strict( && !kwargs .iter() .any(|keyword| keyword.arg.as_ref().map_or(false, |name| name == "strict")) + && !args + .iter() + .any(|arg| is_infinite_iterator(arg, checker.semantic_model())) { checker .diagnostics @@ -35,3 +41,40 @@ pub(crate) fn zip_without_explicit_strict( } } } + +/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to +/// `itertools.cycle` or similar). +fn is_infinite_iterator(arg: &Expr, model: &SemanticModel) -> bool { + let Expr::Call(ast::ExprCall { func, args, keywords, .. }) = &arg else { + return false; + }; + + return model + .resolve_call_path(func) + .map_or(false, |call_path| match call_path.as_slice() { + ["itertools", "cycle" | "count"] => true, + ["itertools", "repeat"] => { + // Ex) `itertools.repeat(1)` + if keywords.is_empty() && args.len() == 1 { + return true; + } + + // Ex) `itertools.repeat(1, None)` + if args.len() == 2 && is_const_none(&args[1]) { + return true; + } + + // Ex) `iterools.repeat(1, times=None)` + for keyword in keywords { + if keyword.arg.as_ref().map_or(false, |name| name == "times") { + if is_const_none(&keyword.value) { + return true; + } + } + } + + false + } + _ => false, + }); +} diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B905_B905.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B905_B905.py.snap index d18f3715463d7..76dcda7de4bb9 100644 --- a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B905_B905.py.snap +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B905_B905.py.snap @@ -1,70 +1,88 @@ --- source: crates/ruff/src/rules/flake8_bugbear/mod.rs --- -B905.py:1:1: B905 `zip()` without an explicit `strict=` parameter +B905.py:4:1: B905 `zip()` without an explicit `strict=` parameter | -1 | zip() +4 | # Errors +5 | zip() | ^^^^^ B905 -2 | zip(range(3)) -3 | zip("a", "b") +6 | zip(range(3)) +7 | zip("a", "b") | -B905.py:2:1: B905 `zip()` without an explicit `strict=` parameter +B905.py:5:1: B905 `zip()` without an explicit `strict=` parameter | -2 | zip() -3 | zip(range(3)) +5 | # Errors +6 | zip() +7 | zip(range(3)) | ^^^^^^^^^^^^^ B905 -4 | zip("a", "b") -5 | zip("a", "b", *zip("c")) +8 | zip("a", "b") +9 | zip("a", "b", *zip("c")) | -B905.py:3:1: B905 `zip()` without an explicit `strict=` parameter - | -3 | zip() -4 | zip(range(3)) -5 | zip("a", "b") - | ^^^^^^^^^^^^^ B905 -6 | zip("a", "b", *zip("c")) -7 | zip(zip("a"), strict=False) - | +B905.py:6:1: B905 `zip()` without an explicit `strict=` parameter + | + 6 | zip() + 7 | zip(range(3)) + 8 | zip("a", "b") + | ^^^^^^^^^^^^^ B905 + 9 | zip("a", "b", *zip("c")) +10 | zip(zip("a"), strict=False) + | -B905.py:4:1: B905 `zip()` without an explicit `strict=` parameter - | -4 | zip(range(3)) -5 | zip("a", "b") -6 | zip("a", "b", *zip("c")) - | ^^^^^^^^^^^^^^^^^^^^^^^^ B905 -7 | zip(zip("a"), strict=False) -8 | zip(zip("a", strict=True)) - | +B905.py:7:1: B905 `zip()` without an explicit `strict=` parameter + | + 7 | zip(range(3)) + 8 | zip("a", "b") + 9 | zip("a", "b", *zip("c")) + | ^^^^^^^^^^^^^^^^^^^^^^^^ B905 +10 | zip(zip("a"), strict=False) +11 | zip(zip("a", strict=True)) + | -B905.py:4:16: B905 `zip()` without an explicit `strict=` parameter - | -4 | zip(range(3)) -5 | zip("a", "b") -6 | zip("a", "b", *zip("c")) - | ^^^^^^^^ B905 -7 | zip(zip("a"), strict=False) -8 | zip(zip("a", strict=True)) - | +B905.py:7:16: B905 `zip()` without an explicit `strict=` parameter + | + 7 | zip(range(3)) + 8 | zip("a", "b") + 9 | zip("a", "b", *zip("c")) + | ^^^^^^^^ B905 +10 | zip(zip("a"), strict=False) +11 | zip(zip("a", strict=True)) + | -B905.py:5:5: B905 `zip()` without an explicit `strict=` parameter - | -5 | zip("a", "b") -6 | zip("a", "b", *zip("c")) -7 | zip(zip("a"), strict=False) - | ^^^^^^^^ B905 -8 | zip(zip("a", strict=True)) - | +B905.py:8:5: B905 `zip()` without an explicit `strict=` parameter + | + 8 | zip("a", "b") + 9 | zip("a", "b", *zip("c")) +10 | zip(zip("a"), strict=False) + | ^^^^^^^^ B905 +11 | zip(zip("a", strict=True)) + | -B905.py:6:1: B905 `zip()` without an explicit `strict=` parameter +B905.py:9:1: B905 `zip()` without an explicit `strict=` parameter | - 6 | zip("a", "b", *zip("c")) - 7 | zip(zip("a"), strict=False) - 8 | zip(zip("a", strict=True)) + 9 | zip("a", "b", *zip("c")) +10 | zip(zip("a"), strict=False) +11 | zip(zip("a", strict=True)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ B905 - 9 | -10 | zip(range(3), strict=True) +12 | +13 | # OK + | + +B905.py:24:1: B905 `zip()` without an explicit `strict=` parameter + | +24 | # Errors (limited iterators). +25 | zip([1, 2, 3], repeat(1, 1)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905 +26 | zip([1, 2, 3], repeat(1, times=4)) + | + +B905.py:25:1: B905 `zip()` without an explicit `strict=` parameter + | +25 | # Errors (limited iterators). +26 | zip([1, 2, 3], repeat(1, 1)) +27 | zip([1, 2, 3], repeat(1, times=4)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905 |