Skip to content

Commit 8ab9ab5

Browse files
Emit modified-iterating-list|dict|set for literals or usage of del (#6652)
1 parent fe63da4 commit 8ab9ab5

File tree

6 files changed

+72
-27
lines changed

6 files changed

+72
-27
lines changed

doc/whatsnew/2/2.15/index.rst

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ False positives fixed
3131
False negatives fixed
3232
=====================
3333

34+
* Emit ``modified-iterating-list`` and analogous messages for dicts and sets when iterating
35+
literals, or when using the ``del`` keyword.
36+
37+
Closes #6648
38+
3439

3540
Other bug fixes
3641
===============

pylint/checkers/modified_iterating_checker.py

+31-4
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ class ModifiedIterationChecker(checkers.BaseChecker):
5656
)
5757
def visit_for(self, node: nodes.For) -> None:
5858
iter_obj = node.iter
59-
if isinstance(iter_obj, nodes.Name):
60-
for body_node in node.body:
61-
self._modified_iterating_check_on_node_and_children(body_node, iter_obj)
59+
for body_node in node.body:
60+
self._modified_iterating_check_on_node_and_children(body_node, iter_obj)
6261

6362
def _modified_iterating_check_on_node_and_children(
6463
self, body_node: nodes.NodeNG, iter_obj: nodes.NodeNG
@@ -72,7 +71,19 @@ def _modified_iterating_check(
7271
self, node: nodes.NodeNG, iter_obj: nodes.NodeNG
7372
) -> None:
7473
msg_id = None
75-
if self._modified_iterating_list_cond(node, iter_obj):
74+
if isinstance(node, nodes.Delete) and any(
75+
self._deleted_iteration_target_cond(t, iter_obj) for t in node.targets
76+
):
77+
inferred = utils.safe_infer(iter_obj)
78+
if isinstance(inferred, nodes.List):
79+
msg_id = "modified-iterating-list"
80+
elif isinstance(inferred, nodes.Dict):
81+
msg_id = "modified-iterating-dict"
82+
elif isinstance(inferred, nodes.Set):
83+
msg_id = "modified-iterating-set"
84+
elif not isinstance(iter_obj, nodes.Name):
85+
pass
86+
elif self._modified_iterating_list_cond(node, iter_obj):
7687
msg_id = "modified-iterating-list"
7788
elif self._modified_iterating_dict_cond(node, iter_obj):
7889
msg_id = "modified-iterating-dict"
@@ -150,6 +161,22 @@ def _modified_iterating_set_cond(
150161
and node.value.func.attrname in _SET_MODIFIER_METHODS
151162
)
152163

164+
def _deleted_iteration_target_cond(
165+
self, node: nodes.DelName, iter_obj: nodes.NodeNG
166+
) -> bool:
167+
if not isinstance(node, nodes.DelName):
168+
return False
169+
if not isinstance(iter_obj.parent, nodes.For):
170+
return False
171+
if not isinstance(
172+
iter_obj.parent.target, (nodes.AssignName, nodes.BaseContainer)
173+
):
174+
return False
175+
return any(
176+
t == node.name
177+
for t in utils.find_assigned_names_recursive(iter_obj.parent.target)
178+
)
179+
153180

154181
def register(linter: PyLinter) -> None:
155182
linter.register_checker(ModifiedIterationChecker(linter))

pylint/checkers/utils.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import re
1313
import string
1414
import warnings
15-
from collections.abc import Iterable
15+
from collections.abc import Iterable, Iterator
1616
from functools import lru_cache, partial
1717
from re import Match
1818
from typing import TYPE_CHECKING, Callable, TypeVar
@@ -1792,3 +1792,15 @@ def in_for_else_branch(parent: nodes.NodeNG, stmt: nodes.Statement) -> bool:
17921792
return isinstance(parent, nodes.For) and any(
17931793
else_stmt.parent_of(stmt) or else_stmt == stmt for else_stmt in parent.orelse
17941794
)
1795+
1796+
1797+
def find_assigned_names_recursive(
1798+
target: nodes.AssignName | nodes.BaseContainer,
1799+
) -> Iterator[str]:
1800+
"""Yield the names of assignment targets, accounting for nested ones."""
1801+
if isinstance(target, nodes.AssignName):
1802+
if target.name is not None:
1803+
yield target.name
1804+
elif isinstance(target, nodes.BaseContainer):
1805+
for elt in target.elts:
1806+
yield from find_assigned_names_recursive(elt)

pylint/checkers/variables.py

+4-19
Original file line numberDiff line numberDiff line change
@@ -1245,13 +1245,12 @@ def leave_functiondef(self, node: nodes.FunctionDef) -> None:
12451245

12461246
global_names = _flattened_scope_names(node.nodes_of_class(nodes.Global))
12471247
nonlocal_names = _flattened_scope_names(node.nodes_of_class(nodes.Nonlocal))
1248-
comprehension_target_names: list[str] = []
1248+
comprehension_target_names: set[str] = set()
12491249

12501250
for comprehension_scope in node.nodes_of_class(nodes.ComprehensionScope):
12511251
for generator in comprehension_scope.generators:
1252-
self._find_assigned_names_recursive(
1253-
generator.target, comprehension_target_names
1254-
)
1252+
for name in utils.find_assigned_names_recursive(generator.target):
1253+
comprehension_target_names.add(name)
12551254

12561255
for name, stmts in not_consumed.items():
12571256
self._check_is_unused(
@@ -1460,20 +1459,6 @@ def _should_node_be_skipped(
14601459

14611460
return False
14621461

1463-
def _find_assigned_names_recursive(
1464-
self,
1465-
target: nodes.AssignName | nodes.BaseContainer,
1466-
target_names: list[str],
1467-
) -> None:
1468-
"""Update `target_names` in place with the names of assignment
1469-
targets, recursively (to account for nested assignments).
1470-
"""
1471-
if isinstance(target, nodes.AssignName):
1472-
target_names.append(target.name)
1473-
elif isinstance(target, nodes.BaseContainer):
1474-
for elt in target.elts:
1475-
self._find_assigned_names_recursive(elt, target_names)
1476-
14771462
# pylint: disable=too-many-return-statements
14781463
def _check_consumer(
14791464
self,
@@ -2275,7 +2260,7 @@ def _check_is_unused(
22752260
stmt,
22762261
global_names,
22772262
nonlocal_names: Iterable[str],
2278-
comprehension_target_names: list[str],
2263+
comprehension_target_names: Iterable[str],
22792264
) -> None:
22802265
# Ignore some special names specified by user configuration.
22812266
if self._is_name_ignored(stmt, name):

tests/functional/m/modified_iterating.py

+12
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@
4747
item_set.remove(4) # [modified-iterating-set]
4848
item_list.remove(1) # [modified-iterating-list]
4949

50+
for item in [1, 2, 3]:
51+
del item # [modified-iterating-list]
52+
53+
for inner_first, inner_second in [[1, 2], [1, 2]]:
54+
del inner_second # [modified-iterating-list]
55+
56+
for k in my_dict:
57+
del k # [modified-iterating-dict]
58+
59+
for element in item_set:
60+
del element # [modified-iterating-set]
61+
5062
# Check for nested for loops and changes to iterators
5163
for l in item_list:
5264
item_list.append(1) # [modified-iterating-list]

tests/functional/m/modified_iterating.txt

+7-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ modified-iterating-set:39:4:39:27::Iterated set 'item_set' is being modified ins
55
modified-iterating-list:46:8:46:27::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE
66
modified-iterating-set:47:8:47:26::Iterated set 'item_set' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE
77
modified-iterating-list:48:4:48:23::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE
8-
modified-iterating-list:52:4:52:23::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE
9-
modified-iterating-list:55:12:55:31::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE
10-
modified-iterating-list:57:16:57:35::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE
8+
modified-iterating-list:51:4:51:12::Iterated list 'list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE
9+
modified-iterating-list:54:4:54:20::Iterated list 'list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE
10+
modified-iterating-dict:57:4:57:9::Iterated dict 'my_dict' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE
11+
modified-iterating-set:60:4:60:15::Iterated set 'item_set' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE
12+
modified-iterating-list:64:4:64:23::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE
13+
modified-iterating-list:67:12:67:31::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE
14+
modified-iterating-list:69:16:69:35::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE

0 commit comments

Comments
 (0)