From 03899dcba37c7429084486600ed7f3b12bea20dc Mon Sep 17 00:00:00 2001 From: Martin Imre Date: Thu, 11 Apr 2024 21:52:52 +0200 Subject: [PATCH] [`flake8-bugbear`] Implement `loop-iterator-mutation` (`B909`) (#9578) ## Summary This PR adds the implementation for the current [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear)'s B038 rule. The B038 rule checks for mutation of loop iterators in the body of a for loop and alerts when found. Rational: Editing the loop iterator can lead to undesired behavior and is probably a bug in most cases. Closes #9511. Note there will be a second iteration of B038 implemented in `flake8-bugbear` soon, and this PR currently only implements the weakest form of the rule. I'd be happy to also implement the further improvements to B038 here in ruff :slightly_smiling_face: See https://github.com/PyCQA/flake8-bugbear/issues/454 for more information on the planned improvements. ## Test Plan Re-using the same test file that I've used for `flake8-bugbear`, which is included in this PR (look for the `B038.py` file). Note: this is my first time using `rust` (beside `rustlings`) - I'd be very happy about thorough feedback on what I could've done better :slightly_smiling_face: - Bring it on :grinning: --- .../test/fixtures/flake8_bugbear/B909.py | 160 ++++++++ .../ast/analyze/deferred_for_loops.rs | 3 + .../src/checkers/ast/analyze/statement.rs | 1 + crates/ruff_linter/src/codes.rs | 1 + .../src/rules/flake8_bugbear/mod.rs | 1 + .../rules/loop_iterator_mutation.rs | 295 +++++++++++++++ .../src/rules/flake8_bugbear/rules/mod.rs | 2 + ...__flake8_bugbear__tests__B909_B909.py.snap | 341 ++++++++++++++++++ ruff.schema.json | 1 + 9 files changed, 805 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py create mode 100644 crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs create mode 100644 crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B909_B909.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py new file mode 100644 index 0000000000000..68afaf87fb257 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py @@ -0,0 +1,160 @@ +""" +Should emit: +B909 - on lines 11, 25, 26, 40, 46 +""" + +# lists + +some_list = [1, 2, 3] +some_other_list = [1, 2, 3] +for elem in some_list: + # errors + some_list.remove(0) + del some_list[2] + some_list.append(elem) + some_list.sort() + some_list.reverse() + some_list.clear() + some_list.extend([1, 2]) + some_list.insert(1, 1) + some_list.pop(1) + some_list.pop() + + # conditional break should error + if elem == 2: + some_list.remove(0) + if elem == 3: + break + + # non-errors + some_other_list.remove(elem) + del some_list + del some_other_list + found_idx = some_list.index(elem) + some_list = 3 + + # unconditional break should not error + if elem == 2: + some_list.remove(elem) + break + + +# dicts +mydicts = {"a": {"foo": 1, "bar": 2}} + +for elem in mydicts: + # errors + mydicts.popitem() + mydicts.setdefault("foo", 1) + mydicts.update({"foo": "bar"}) + + # no errors + elem.popitem() + elem.setdefault("foo", 1) + elem.update({"foo": "bar"}) + +# sets + +myset = {1, 2, 3} + +for _ in myset: + # errors + myset.update({4, 5}) + myset.intersection_update({4, 5}) + myset.difference_update({4, 5}) + myset.symmetric_difference_update({4, 5}) + myset.add(4) + myset.discard(3) + + # no errors + del myset + + +# members +class A: + some_list: list + + def __init__(self, ls): + self.some_list = list(ls) + + +a = A((1, 2, 3)) +# ensure member accesses are handled as errors +for elem in a.some_list: + a.some_list.remove(0) + del a.some_list[2] + + +# Augassign should error + +foo = [1, 2, 3] +bar = [4, 5, 6] +for _ in foo: + foo *= 2 + foo += bar + foo[1] = 9 + foo[1:2] = bar + foo[1:2:3] = bar + +foo = {1, 2, 3} +bar = {4, 5, 6} +for _ in foo: # should error + foo |= bar + foo &= bar + foo -= bar + foo ^= bar + + +# more tests for unconditional breaks - should not error +for _ in foo: + foo.remove(1) + for _ in bar: + bar.remove(1) + break + break + +# should not error +for _ in foo: + foo.remove(1) + for _ in bar: + ... + break + +# should error (?) +for _ in foo: + foo.remove(1) + if bar: + bar.remove(1) + break + break + +# should error +for _ in foo: + if bar: + pass + else: + foo.remove(1) + +# should error +for elem in some_list: + if some_list.pop() == 2: + pass + +# should not error +for elem in some_list: + if some_list.pop() == 2: + break + +# should error +for elem in some_list: + if some_list.pop() == 2: + pass + else: + break + +# should not error +for elem in some_list: + del some_list[elem] + some_list[elem] = 1 + some_list.remove(elem) + some_list.discard(elem) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs index ee2378dd8bd0e..0aad5987dc1ce 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs @@ -30,6 +30,9 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) { if checker.enabled(Rule::EnumerateForLoop) { flake8_simplify::rules::enumerate_for_loop(checker, stmt_for); } + if checker.enabled(Rule::LoopIteratorMutation) { + flake8_bugbear::rules::loop_iterator_mutation(checker, stmt_for); + } } } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 141a0ec9e96ef..96dcf5df55681 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1268,6 +1268,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.any_enabled(&[ Rule::EnumerateForLoop, Rule::IncorrectDictIterator, + Rule::LoopIteratorMutation, Rule::UnnecessaryEnumerate, Rule::UnusedLoopControlVariable, Rule::YieldInForLoop, diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 988f0118b1329..3bde1d732a614 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -378,6 +378,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bugbear, "035") => (RuleGroup::Stable, rules::flake8_bugbear::rules::StaticKeyDictComprehension), (Flake8Bugbear, "904") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept), (Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict), + (Flake8Bugbear, "909") => (RuleGroup::Preview, rules::flake8_bugbear::rules::LoopIteratorMutation), // flake8-blind-except (Flake8BlindExcept, "001") => (RuleGroup::Stable, rules::flake8_blind_except::rules::BlindExcept), diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs index cbcbed801176d..4049d4a0149eb 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs @@ -61,6 +61,7 @@ mod tests { #[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))] #[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))] #[test_case(Rule::UselessExpression, Path::new("B018.py"))] + #[test_case(Rule::LoopIteratorMutation, Path::new("B909.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs new file mode 100644 index 0000000000000..3c5b31ce7ae98 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs @@ -0,0 +1,295 @@ +use std::collections::HashMap; + +use ruff_diagnostics::Diagnostic; +use ruff_diagnostics::Violation; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::comparable::ComparableExpr; +use ruff_python_ast::name::UnqualifiedName; +use ruff_python_ast::{ + visitor::{self, Visitor}, + Arguments, Expr, ExprAttribute, ExprCall, ExprSubscript, Stmt, StmtAssign, StmtAugAssign, + StmtBreak, StmtDelete, StmtFor, StmtIf, +}; +use ruff_text_size::TextRange; + +use crate::checkers::ast::Checker; +use crate::fix::snippet::SourceCodeSnippet; + +/// ## What it does +/// Checks for mutations to an iterable during a loop iteration. +/// +/// ## Why is this bad? +/// When iterating over an iterable, mutating the iterable can lead to unexpected +/// behavior, like skipping elements or infinite loops. +/// +/// ## Example +/// ```python +/// items = [1, 2, 3] +/// +/// for item in items: +/// print(item) +/// +/// # Create an infinite loop by appending to the list. +/// items.append(item) +/// ``` +/// +/// ## References +/// - [Python documentation: Mutable Sequence Types](https://docs.python.org/3/library/stdtypes.html#typesseq-mutable) +#[violation] +pub struct LoopIteratorMutation { + name: Option, +} + +impl Violation for LoopIteratorMutation { + #[derive_message_formats] + fn message(&self) -> String { + let LoopIteratorMutation { name } = self; + + if let Some(name) = name.as_ref().and_then(SourceCodeSnippet::full_display) { + format!("Mutation to loop iterable `{name}` during iteration") + } else { + format!("Mutation to loop iterable during iteration") + } + } +} + +/// B909 +pub(crate) fn loop_iterator_mutation(checker: &mut Checker, stmt_for: &StmtFor) { + let StmtFor { + target, + iter, + body, + orelse: _, + is_async: _, + range: _, + } = stmt_for; + + if !matches!(iter.as_ref(), Expr::Name(_) | Expr::Attribute(_)) { + return; + } + + // Collect mutations to the iterable. + let mutations = { + let mut visitor = LoopMutationsVisitor::new(iter, target); + visitor.visit_body(body); + visitor.mutations + }; + + // Create a diagnostic for each mutation. + for mutation in mutations.values().flatten() { + let name = UnqualifiedName::from_expr(iter) + .map(|name| name.to_string()) + .map(SourceCodeSnippet::new); + checker + .diagnostics + .push(Diagnostic::new(LoopIteratorMutation { name }, *mutation)); + } +} + +/// Returns `true` if the method mutates when called on an iterator. +fn is_mutating_function(function_name: &str) -> bool { + matches!( + function_name, + "append" + | "sort" + | "reverse" + | "remove" + | "clear" + | "extend" + | "insert" + | "pop" + | "popitem" + | "setdefault" + | "update" + | "intersection_update" + | "difference_update" + | "symmetric_difference_update" + | "add" + | "discard" + ) +} + +/// A visitor to collect mutations to a variable in a loop. +#[derive(Debug, Clone)] +struct LoopMutationsVisitor<'a> { + iter: &'a Expr, + target: &'a Expr, + mutations: HashMap>, + branches: Vec, + branch: u8, +} + +impl<'a> LoopMutationsVisitor<'a> { + /// Initialize the visitor. + fn new(iter: &'a Expr, target: &'a Expr) -> Self { + Self { + iter, + target, + mutations: HashMap::new(), + branches: vec![0], + branch: 0, + } + } + + /// Register a mutation. + fn add_mutation(&mut self, range: TextRange) { + self.mutations.entry(self.branch).or_default().push(range); + } + + /// Handle, e.g., `del items[0]`. + fn handle_delete(&mut self, range: TextRange, targets: &[Expr]) { + for target in targets { + if let Expr::Subscript(ExprSubscript { + range: _, + value, + slice, + ctx: _, + }) = target + { + // Find, e.g., `del items[0]`. + if ComparableExpr::from(self.iter) == ComparableExpr::from(value) { + // But allow, e.g., `for item in items: del items[item]`. + if ComparableExpr::from(self.target) != ComparableExpr::from(slice) { + self.add_mutation(range); + } + } + } + } + } + + /// Handle, e.g., `items[0] = 1`. + fn handle_assign(&mut self, range: TextRange, targets: &[Expr]) { + for target in targets { + if let Expr::Subscript(ExprSubscript { + range: _, + value, + slice, + ctx: _, + }) = target + { + // Find, e.g., `items[0] = 1`. + if ComparableExpr::from(self.iter) == ComparableExpr::from(value) { + // But allow, e.g., `for item in items: items[item] = 1`. + if ComparableExpr::from(self.target) != ComparableExpr::from(slice) { + self.add_mutation(range); + } + } + } + } + } + + /// Handle, e.g., `items += [1]`. + fn handle_aug_assign(&mut self, range: TextRange, target: &Expr) { + if ComparableExpr::from(self.iter) == ComparableExpr::from(target) { + self.add_mutation(range); + } + } + + /// Handle, e.g., `items.append(1)`. + fn handle_call(&mut self, func: &Expr, arguments: &Arguments) { + if let Expr::Attribute(ExprAttribute { + range, + value, + attr, + ctx: _, + }) = func + { + if is_mutating_function(attr.as_str()) { + // Find, e.g., `items.remove(1)`. + if ComparableExpr::from(self.iter) == ComparableExpr::from(value) { + // But allow, e.g., `for item in items: items.remove(item)`. + if matches!(attr.as_str(), "remove" | "discard" | "pop") { + if arguments.len() == 1 { + if let [arg] = &*arguments.args { + if ComparableExpr::from(self.target) == ComparableExpr::from(arg) { + return; + } + } + } + } + + self.add_mutation(*range); + } + } + } + } +} + +/// `Visitor` to collect all used identifiers in a statement. +impl<'a> Visitor<'a> for LoopMutationsVisitor<'a> { + fn visit_stmt(&mut self, stmt: &'a Stmt) { + match stmt { + // Ex) `del items[0]` + Stmt::Delete(StmtDelete { range, targets }) => { + self.handle_delete(*range, targets); + visitor::walk_stmt(self, stmt); + } + + // Ex) `items[0] = 1` + Stmt::Assign(StmtAssign { range, targets, .. }) => { + self.handle_assign(*range, targets); + visitor::walk_stmt(self, stmt); + } + + // Ex) `items += [1]` + Stmt::AugAssign(StmtAugAssign { range, target, .. }) => { + self.handle_aug_assign(*range, target); + visitor::walk_stmt(self, stmt); + } + + // Ex) `if True: items.append(1)` + Stmt::If(StmtIf { + test, + body, + elif_else_clauses, + .. + }) => { + // Handle the `if` branch. + self.branch += 1; + self.branches.push(self.branch); + self.visit_expr(test); + self.visit_body(body); + self.branches.pop(); + + // Handle the `elif` and `else` branches. + for clause in elif_else_clauses { + self.branch += 1; + self.branches.push(self.branch); + if let Some(test) = &clause.test { + self.visit_expr(test); + } + self.visit_body(&clause.body); + self.branches.pop(); + } + } + + // On break, clear the mutations for the current branch. + Stmt::Break(StmtBreak { range: _ }) => { + if let Some(mutations) = self.mutations.get_mut(&self.branch) { + mutations.clear(); + } + visitor::walk_stmt(self, stmt); + } + + // Avoid recursion for class and function definitions. + Stmt::ClassDef(_) | Stmt::FunctionDef(_) => {} + + // Default case. + _ => { + visitor::walk_stmt(self, stmt); + } + } + } + + fn visit_expr(&mut self, expr: &'a Expr) { + // Ex) `items.append(1)` + if let Expr::Call(ExprCall { + func, arguments, .. + }) = expr + { + self.handle_call(func, arguments); + } + + visitor::walk_expr(self, expr); + } +} diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs index 281d6020ab6d9..111eb5f18b72c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs @@ -12,6 +12,7 @@ pub(crate) use function_call_in_argument_default::*; pub(crate) use function_uses_loop_variable::*; pub(crate) use getattr_with_constant::*; pub(crate) use jump_statement_in_finally::*; +pub(crate) use loop_iterator_mutation::*; pub(crate) use loop_variable_overrides_iterator::*; pub(crate) use mutable_argument_default::*; pub(crate) use no_explicit_stacklevel::*; @@ -47,6 +48,7 @@ mod function_call_in_argument_default; mod function_uses_loop_variable; mod getattr_with_constant; mod jump_statement_in_finally; +mod loop_iterator_mutation; mod loop_variable_overrides_iterator; mod mutable_argument_default; mod no_explicit_stacklevel; diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B909_B909.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B909_B909.py.snap new file mode 100644 index 0000000000000..7f70841c6c066 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B909_B909.py.snap @@ -0,0 +1,341 @@ +--- +source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +--- +B909.py:12:5: B909 Mutation to loop iterable `some_list` during iteration + | +10 | for elem in some_list: +11 | # errors +12 | some_list.remove(0) + | ^^^^^^^^^^^^^^^^ B909 +13 | del some_list[2] +14 | some_list.append(elem) + | + +B909.py:13:5: B909 Mutation to loop iterable `some_list` during iteration + | +11 | # errors +12 | some_list.remove(0) +13 | del some_list[2] + | ^^^^^^^^^^^^^^^^ B909 +14 | some_list.append(elem) +15 | some_list.sort() + | + +B909.py:14:5: B909 Mutation to loop iterable `some_list` during iteration + | +12 | some_list.remove(0) +13 | del some_list[2] +14 | some_list.append(elem) + | ^^^^^^^^^^^^^^^^ B909 +15 | some_list.sort() +16 | some_list.reverse() + | + +B909.py:15:5: B909 Mutation to loop iterable `some_list` during iteration + | +13 | del some_list[2] +14 | some_list.append(elem) +15 | some_list.sort() + | ^^^^^^^^^^^^^^ B909 +16 | some_list.reverse() +17 | some_list.clear() + | + +B909.py:16:5: B909 Mutation to loop iterable `some_list` during iteration + | +14 | some_list.append(elem) +15 | some_list.sort() +16 | some_list.reverse() + | ^^^^^^^^^^^^^^^^^ B909 +17 | some_list.clear() +18 | some_list.extend([1, 2]) + | + +B909.py:17:5: B909 Mutation to loop iterable `some_list` during iteration + | +15 | some_list.sort() +16 | some_list.reverse() +17 | some_list.clear() + | ^^^^^^^^^^^^^^^ B909 +18 | some_list.extend([1, 2]) +19 | some_list.insert(1, 1) + | + +B909.py:18:5: B909 Mutation to loop iterable `some_list` during iteration + | +16 | some_list.reverse() +17 | some_list.clear() +18 | some_list.extend([1, 2]) + | ^^^^^^^^^^^^^^^^ B909 +19 | some_list.insert(1, 1) +20 | some_list.pop(1) + | + +B909.py:19:5: B909 Mutation to loop iterable `some_list` during iteration + | +17 | some_list.clear() +18 | some_list.extend([1, 2]) +19 | some_list.insert(1, 1) + | ^^^^^^^^^^^^^^^^ B909 +20 | some_list.pop(1) +21 | some_list.pop() + | + +B909.py:20:5: B909 Mutation to loop iterable `some_list` during iteration + | +18 | some_list.extend([1, 2]) +19 | some_list.insert(1, 1) +20 | some_list.pop(1) + | ^^^^^^^^^^^^^ B909 +21 | some_list.pop() + | + +B909.py:21:5: B909 Mutation to loop iterable `some_list` during iteration + | +19 | some_list.insert(1, 1) +20 | some_list.pop(1) +21 | some_list.pop() + | ^^^^^^^^^^^^^ B909 +22 | +23 | # conditional break should error + | + +B909.py:25:9: B909 Mutation to loop iterable `some_list` during iteration + | +23 | # conditional break should error +24 | if elem == 2: +25 | some_list.remove(0) + | ^^^^^^^^^^^^^^^^ B909 +26 | if elem == 3: +27 | break + | + +B909.py:47:5: B909 Mutation to loop iterable `mydicts` during iteration + | +45 | for elem in mydicts: +46 | # errors +47 | mydicts.popitem() + | ^^^^^^^^^^^^^^^ B909 +48 | mydicts.setdefault("foo", 1) +49 | mydicts.update({"foo": "bar"}) + | + +B909.py:48:5: B909 Mutation to loop iterable `mydicts` during iteration + | +46 | # errors +47 | mydicts.popitem() +48 | mydicts.setdefault("foo", 1) + | ^^^^^^^^^^^^^^^^^^ B909 +49 | mydicts.update({"foo": "bar"}) + | + +B909.py:49:5: B909 Mutation to loop iterable `mydicts` during iteration + | +47 | mydicts.popitem() +48 | mydicts.setdefault("foo", 1) +49 | mydicts.update({"foo": "bar"}) + | ^^^^^^^^^^^^^^ B909 +50 | +51 | # no errors + | + +B909.py:62:5: B909 Mutation to loop iterable `myset` during iteration + | +60 | for _ in myset: +61 | # errors +62 | myset.update({4, 5}) + | ^^^^^^^^^^^^ B909 +63 | myset.intersection_update({4, 5}) +64 | myset.difference_update({4, 5}) + | + +B909.py:63:5: B909 Mutation to loop iterable `myset` during iteration + | +61 | # errors +62 | myset.update({4, 5}) +63 | myset.intersection_update({4, 5}) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ B909 +64 | myset.difference_update({4, 5}) +65 | myset.symmetric_difference_update({4, 5}) + | + +B909.py:64:5: B909 Mutation to loop iterable `myset` during iteration + | +62 | myset.update({4, 5}) +63 | myset.intersection_update({4, 5}) +64 | myset.difference_update({4, 5}) + | ^^^^^^^^^^^^^^^^^^^^^^^ B909 +65 | myset.symmetric_difference_update({4, 5}) +66 | myset.add(4) + | + +B909.py:65:5: B909 Mutation to loop iterable `myset` during iteration + | +63 | myset.intersection_update({4, 5}) +64 | myset.difference_update({4, 5}) +65 | myset.symmetric_difference_update({4, 5}) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B909 +66 | myset.add(4) +67 | myset.discard(3) + | + +B909.py:66:5: B909 Mutation to loop iterable `myset` during iteration + | +64 | myset.difference_update({4, 5}) +65 | myset.symmetric_difference_update({4, 5}) +66 | myset.add(4) + | ^^^^^^^^^ B909 +67 | myset.discard(3) + | + +B909.py:67:5: B909 Mutation to loop iterable `myset` during iteration + | +65 | myset.symmetric_difference_update({4, 5}) +66 | myset.add(4) +67 | myset.discard(3) + | ^^^^^^^^^^^^^ B909 +68 | +69 | # no errors + | + +B909.py:84:5: B909 Mutation to loop iterable `a.some_list` during iteration + | +82 | # ensure member accesses are handled as errors +83 | for elem in a.some_list: +84 | a.some_list.remove(0) + | ^^^^^^^^^^^^^^^^^^ B909 +85 | del a.some_list[2] + | + +B909.py:85:5: B909 Mutation to loop iterable `a.some_list` during iteration + | +83 | for elem in a.some_list: +84 | a.some_list.remove(0) +85 | del a.some_list[2] + | ^^^^^^^^^^^^^^^^^^ B909 + | + +B909.py:93:5: B909 Mutation to loop iterable `foo` during iteration + | +91 | bar = [4, 5, 6] +92 | for _ in foo: +93 | foo *= 2 + | ^^^^^^^^ B909 +94 | foo += bar +95 | foo[1] = 9 + | + +B909.py:94:5: B909 Mutation to loop iterable `foo` during iteration + | +92 | for _ in foo: +93 | foo *= 2 +94 | foo += bar + | ^^^^^^^^^^ B909 +95 | foo[1] = 9 +96 | foo[1:2] = bar + | + +B909.py:95:5: B909 Mutation to loop iterable `foo` during iteration + | +93 | foo *= 2 +94 | foo += bar +95 | foo[1] = 9 + | ^^^^^^^^^^ B909 +96 | foo[1:2] = bar +97 | foo[1:2:3] = bar + | + +B909.py:96:5: B909 Mutation to loop iterable `foo` during iteration + | +94 | foo += bar +95 | foo[1] = 9 +96 | foo[1:2] = bar + | ^^^^^^^^^^^^^^ B909 +97 | foo[1:2:3] = bar + | + +B909.py:97:5: B909 Mutation to loop iterable `foo` during iteration + | +95 | foo[1] = 9 +96 | foo[1:2] = bar +97 | foo[1:2:3] = bar + | ^^^^^^^^^^^^^^^^ B909 +98 | +99 | foo = {1, 2, 3} + | + +B909.py:102:5: B909 Mutation to loop iterable `foo` during iteration + | +100 | bar = {4, 5, 6} +101 | for _ in foo: # should error +102 | foo |= bar + | ^^^^^^^^^^ B909 +103 | foo &= bar +104 | foo -= bar + | + +B909.py:103:5: B909 Mutation to loop iterable `foo` during iteration + | +101 | for _ in foo: # should error +102 | foo |= bar +103 | foo &= bar + | ^^^^^^^^^^ B909 +104 | foo -= bar +105 | foo ^= bar + | + +B909.py:104:5: B909 Mutation to loop iterable `foo` during iteration + | +102 | foo |= bar +103 | foo &= bar +104 | foo -= bar + | ^^^^^^^^^^ B909 +105 | foo ^= bar + | + +B909.py:105:5: B909 Mutation to loop iterable `foo` during iteration + | +103 | foo &= bar +104 | foo -= bar +105 | foo ^= bar + | ^^^^^^^^^^ B909 + | + +B909.py:125:5: B909 Mutation to loop iterable `foo` during iteration + | +123 | # should error (?) +124 | for _ in foo: +125 | foo.remove(1) + | ^^^^^^^^^^ B909 +126 | if bar: +127 | bar.remove(1) + | + +B909.py:136:9: B909 Mutation to loop iterable `foo` during iteration + | +134 | pass +135 | else: +136 | foo.remove(1) + | ^^^^^^^^^^ B909 +137 | +138 | # should error + | + +B909.py:140:8: B909 Mutation to loop iterable `some_list` during iteration + | +138 | # should error +139 | for elem in some_list: +140 | if some_list.pop() == 2: + | ^^^^^^^^^^^^^ B909 +141 | pass + | + +B909.py:150:8: B909 Mutation to loop iterable `some_list` during iteration + | +148 | # should error +149 | for elem in some_list: +150 | if some_list.pop() == 2: + | ^^^^^^^^^^^^^ B909 +151 | pass +152 | else: + | diff --git a/ruff.schema.json b/ruff.schema.json index a7c2d7cea8986..198970035e1f5 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2727,6 +2727,7 @@ "B90", "B904", "B905", + "B909", "BLE", "BLE0", "BLE00",