@@ -7,11 +7,20 @@ use ruff_text_size::Ranged;
7
7
use crate :: checkers:: ast:: Checker ;
8
8
9
9
/// ## What it does
10
- /// Checks for `set` being modified during the iteration on the set .
10
+ /// Checks for loops in which a `set` is modified during iteration.
11
11
///
12
12
/// ## Why is this bad?
13
- /// If `set` is modified during the iteration, it will cause `RuntimeError`.
14
- /// This could be fixed by using temporal copy of the set to iterate.
13
+ /// If a `set` is modified during iteration, it will cause a `RuntimeError`.
14
+ ///
15
+ /// If you need to modify a `set` within a loop, consider iterating over a copy
16
+ /// of the `set` instead.
17
+ ///
18
+ /// ## Known problems
19
+ /// This rule favors false negatives over false positives. Specifically, it
20
+ /// will only detect variables that can be inferred to be a `set` type based on
21
+ /// local type inference, and will only detect modifications that are made
22
+ /// directly on the variable itself (e.g., `set.add()`), as opposed to
23
+ /// modifications within other function calls (e.g., `some_function(set)`).
15
24
///
16
25
/// ## Example
17
26
/// ```python
@@ -37,26 +46,17 @@ pub struct ModifiedIteratingSet {
37
46
impl AlwaysFixableViolation for ModifiedIteratingSet {
38
47
#[ derive_message_formats]
39
48
fn message ( & self ) -> String {
40
- format ! (
41
- "Iterated set `{}` is being modified inside for loop body." ,
42
- self . name
43
- )
49
+ let ModifiedIteratingSet { name } = self ;
50
+ format ! ( "Iterated set `{name}` is modified within the `for` loop" , )
44
51
}
45
52
46
53
fn fix_title ( & self ) -> String {
47
- format ! ( "Consider iterating through a copy of it instead." )
54
+ let ModifiedIteratingSet { name } = self ;
55
+ format ! ( "Iterate over a copy of `{name}`" )
48
56
}
49
57
}
50
58
51
- fn is_method_modifying ( identifier : & str ) -> bool {
52
- ( identifier == "add" )
53
- || ( identifier == "clear" )
54
- || ( identifier == "discard" )
55
- || ( identifier == "pop" )
56
- || ( identifier == "remove" )
57
- }
58
-
59
- // PLE4703
59
+ /// PLE4703
60
60
pub ( crate ) fn modified_iterating_set ( checker : & mut Checker , for_stmt : & StmtFor ) {
61
61
let Some ( name) = for_stmt. iter . as_name_expr ( ) else {
62
62
return ;
@@ -69,8 +69,7 @@ pub(crate) fn modified_iterating_set(checker: &mut Checker, for_stmt: &StmtFor)
69
69
return ;
70
70
}
71
71
72
- let mut is_modified = false ;
73
- for stmt in & for_stmt. body {
72
+ if for_stmt. body . iter ( ) . any ( |stmt| {
74
73
// name_of_set.modify_method()
75
74
// ^---------^ ^-----------^
76
75
// value attr
@@ -79,40 +78,42 @@ pub(crate) fn modified_iterating_set(checker: &mut Checker, for_stmt: &StmtFor)
79
78
// ^-------------------------^
80
79
// expr, stmt
81
80
let Stmt :: Expr ( ast:: StmtExpr { value : expr, .. } ) = stmt else {
82
- continue ;
81
+ return false ;
83
82
} ;
84
83
85
84
let Some ( func) = expr. as_call_expr ( ) . map ( |exprcall| & exprcall. func ) else {
86
- continue ;
85
+ return false ;
87
86
} ;
88
87
89
88
let Expr :: Attribute ( ast:: ExprAttribute { value, attr, .. } ) = func. as_ref ( ) else {
90
- continue ;
89
+ return false ;
91
90
} ;
92
91
93
92
let Some ( value) = value. as_name_expr ( ) else {
94
- continue ;
93
+ return false ;
95
94
} ;
96
95
97
96
let Some ( binding_id_value) = checker. semantic ( ) . only_binding ( value) else {
98
- continue ;
97
+ return false ;
99
98
} ;
100
- if binding_id == binding_id_value && is_method_modifying ( attr. as_str ( ) ) {
101
- is_modified = true ;
102
- }
103
- }
104
99
105
- if is_modified {
100
+ binding_id == binding_id_value && modifies_set ( attr. as_str ( ) )
101
+ } ) {
106
102
let mut diagnostic = Diagnostic :: new (
107
103
ModifiedIteratingSet {
108
104
name : name. id . clone ( ) ,
109
105
} ,
110
106
for_stmt. range ( ) ,
111
107
) ;
112
108
diagnostic. set_fix ( Fix :: unsafe_edit ( Edit :: range_replacement (
113
- format ! ( "{}.copy()" , name . id ) ,
109
+ format ! ( "{}.copy()" , checker . locator ( ) . slice ( name ) ) ,
114
110
name. range ( ) ,
115
111
) ) ) ;
116
112
checker. diagnostics . push ( diagnostic) ;
117
113
}
118
114
}
115
+
116
+ /// Returns `true` if the method modifies the set.
117
+ fn modifies_set ( identifier : & str ) -> bool {
118
+ matches ! ( identifier, "add" | "clear" | "discard" | "pop" | "remove" )
119
+ }
0 commit comments