-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rules): Implement flake8-bugbear B038 rule
The B038 rule checks for mutation of loop iterators in the body of a for loop and alerts when found. Editing the loop iterator can lead to undesired behavior and is probably a bug in most cases.
- Loading branch information
Showing
9 changed files
with
406 additions
and
0 deletions.
There are no files selected for viewing
75 changes: 75 additions & 0 deletions
75
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B038.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
""" | ||
Should emit: | ||
B999 - on lines 11, 25, 26, 40, 46 | ||
""" | ||
|
||
|
||
some_list = [1, 2, 3] | ||
for elem in some_list: | ||
print(elem) | ||
if elem % 2 == 0: | ||
some_list.remove(elem) # should error | ||
|
||
some_list = [1, 2, 3] | ||
some_other_list = [1, 2, 3] | ||
for elem in some_list: | ||
print(elem) | ||
if elem % 2 == 0: | ||
some_other_list.remove(elem) # should not error | ||
del some_other_list | ||
|
||
|
||
some_list = [1, 2, 3] | ||
for elem in some_list: | ||
print(elem) | ||
if elem % 2 == 0: | ||
del some_list[2] # should error | ||
del some_list | ||
|
||
|
||
class A: | ||
some_list: list | ||
|
||
def __init__(self, ls): | ||
self.some_list = list(ls) | ||
|
||
|
||
a = A((1, 2, 3)) | ||
for elem in a.some_list: | ||
print(elem) | ||
if elem % 2 == 0: | ||
a.some_list.remove(elem) # should error | ||
|
||
a = A((1, 2, 3)) | ||
for elem in a.some_list: | ||
print(elem) | ||
if elem % 2 == 0: | ||
del a.some_list[2] # should error | ||
|
||
|
||
|
||
some_list = [1, 2, 3] | ||
for elem in some_list: | ||
print(elem) | ||
if elem == 2: | ||
found_idx = some_list.index(elem) # should not error | ||
some_list.append(elem) # should error | ||
some_list.sort() # should error | ||
some_list.reverse() # should error | ||
some_list.clear() # should error | ||
some_list.extend([1,2]) # should error | ||
some_list.insert(1, 1) # should error | ||
some_list.pop(1) # should error | ||
some_list.pop() # should error | ||
some_list = 3 # should error | ||
break | ||
|
||
|
||
|
||
mydicts = {'a': {'foo': 1, 'bar': 2}} | ||
|
||
for mydict in mydicts: | ||
if mydicts.get('a', ''): | ||
print(mydict['foo']) # should not error | ||
mydicts.popitem() # should error | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
185 changes: 185 additions & 0 deletions
185
crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutated.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
use ruff_diagnostics::Violation; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::{ | ||
visitor::{self, Visitor}, | ||
Expr, ExprAttribute, ExprCall, ExprContext, ExprName, ExprSubscript, Stmt, StmtDelete, StmtFor, | ||
}; | ||
use ruff_text_size::Ranged; | ||
|
||
use crate::checkers::ast::Checker; | ||
use ruff_diagnostics::Diagnostic; | ||
|
||
static MUTATING_FUNCTIONS: &'static [&'static str] = &[ | ||
"append", "sort", "reverse", "remove", "clear", "extend", "insert", "pop", "popitem", | ||
]; | ||
|
||
/// ## What it does | ||
/// Checks for mutation of the iterator of a loop in the loop's body | ||
/// | ||
/// ## Why is this bad? | ||
/// Changing the structure that is being iterated over will usually lead to | ||
/// unintended behavior as not all elements will be addressed. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// some_list = [1,2,3] | ||
/// for i in some_list: | ||
/// some_list.remove(i) # this will lead to not all elements being printed | ||
/// print(i) | ||
/// ``` | ||
/// | ||
/// | ||
/// ## References | ||
/// - [Python documentation: Mutable Sequence Types](https://docs.python.org/3/library/stdtypes.html#typesseq-mutable) | ||
#[violation] | ||
pub struct LoopIteratorMutated; | ||
|
||
impl Violation for LoopIteratorMutated { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
format!("editing a loop's mutable iterable often leads to unexpected results/bugs") | ||
} | ||
} | ||
|
||
fn _to_name_str(node: &Expr) -> String { | ||
match node { | ||
Expr::Name(ExprName { id, .. }) => { | ||
return id.to_string(); | ||
} | ||
Expr::Attribute(ExprAttribute { | ||
range: _, | ||
value, | ||
attr, | ||
.. | ||
}) => { | ||
let mut inner = _to_name_str(value); | ||
match inner.as_str() { | ||
"" => { | ||
return "".into(); | ||
} | ||
_ => { | ||
inner.push_str("."); | ||
inner.push_str(attr); | ||
return inner; | ||
} | ||
} | ||
} | ||
Expr::Call(ExprCall { range: _, func, .. }) => { | ||
return _to_name_str(func); | ||
} | ||
_ => { | ||
return "".into(); | ||
} | ||
} | ||
} | ||
// B038 | ||
pub(crate) fn loop_iterator_mutated(checker: &mut Checker, stmt_for: &StmtFor) { | ||
let StmtFor { | ||
target: _, | ||
iter, | ||
body, | ||
orelse: _, | ||
is_async: _, | ||
range: _, | ||
} = stmt_for; | ||
let name; | ||
|
||
match iter.as_ref() { | ||
Expr::Name(ExprName { .. }) => { | ||
name = _to_name_str(iter.as_ref()); | ||
} | ||
Expr::Attribute(ExprAttribute { .. }) => { | ||
name = _to_name_str(iter.as_ref()); | ||
} | ||
_ => { | ||
println!("Shouldn't happen"); | ||
return; | ||
} | ||
} | ||
let mut visitor = LoopMutationsVisitor { | ||
name: &name, | ||
mutations: Vec::new(), | ||
}; | ||
visitor.visit_body(body); | ||
for mutation in visitor.mutations { | ||
checker | ||
.diagnostics | ||
.push(Diagnostic::new(LoopIteratorMutated {}, mutation.range())); | ||
} | ||
} | ||
struct LoopMutationsVisitor<'a> { | ||
name: &'a String, | ||
mutations: Vec<Box<dyn Ranged + 'a>>, | ||
} | ||
|
||
/// `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 { | ||
Stmt::Delete(StmtDelete { range, targets }) => { | ||
for target in targets { | ||
let name; | ||
match target { | ||
Expr::Subscript(ExprSubscript { | ||
range: _, | ||
value, | ||
slice: _, | ||
ctx: _, | ||
}) => { | ||
name = _to_name_str(value); | ||
} | ||
|
||
Expr::Attribute(_) | Expr::Name(_) => { | ||
name = _to_name_str(target); | ||
} | ||
_ => { | ||
name = String::new(); | ||
visitor::walk_expr(self, target); | ||
} | ||
} | ||
if self.name.eq(&name) { | ||
self.mutations.push(Box::new(range)); | ||
} | ||
} | ||
} | ||
_ => { | ||
visitor::walk_stmt(self, stmt); | ||
} | ||
} | ||
} | ||
|
||
fn visit_expr(&mut self, expr: &'a Expr) { | ||
match expr { | ||
Expr::Name(ExprName { range: _, id, ctx }) => { | ||
if self.name.eq(id) { | ||
match ctx { | ||
ExprContext::Del => { | ||
self.mutations.push(Box::new(expr)); | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
Expr::Call(ExprCall { | ||
range: _, | ||
func, | ||
arguments: _, | ||
}) => match func.as_ref() { | ||
Expr::Attribute(ExprAttribute { | ||
range: _, | ||
value, | ||
attr, | ||
ctx: _, | ||
}) => { | ||
let name = _to_name_str(value); | ||
if self.name.eq(&name) && MUTATING_FUNCTIONS.contains(&attr.as_str()) { | ||
self.mutations.push(Box::new(expr)); | ||
} | ||
} | ||
_ => {} | ||
}, | ||
_ => {} | ||
} | ||
visitor::walk_expr(self, expr); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.