Skip to content

Commit e9683c4

Browse files
timmartinjacobtylerwalls
authored andcommitted
Suppress unnecessary-list-index-lookup for whole loop on write (#6845)
The ``unnecessary-list-index-lookup`` checker was assuming that if the subscript was written to then it would only invalidate access on later lines, but this is not necessarily the case if an inner loop is nested inside the one being checked. Fixes #6818 Co-authored-by: Jacob Walls <[email protected]>
1 parent 4fc721d commit e9683c4

File tree

5 files changed

+116
-18
lines changed

5 files changed

+116
-18
lines changed

doc/whatsnew/2/2.14/full.rst

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ What's New in Pylint 2.14.2?
55
----------------------------
66
Release date: TBA
77

8+
* Fixed a false positive in ``unnecessary-list-index-lookup`` and ``unnecessary-dict-index-lookup``
9+
when the subscript is updated in the body of a nested loop.
10+
11+
Closes #6818
12+
813
* Fixed a false positive for ``used-before-assignment`` when a try block returns
914
but an except handler defines a name via type annotation.
1015

pylint/checkers/refactoring/refactoring_checker.py

+92-18
Original file line numberDiff line numberDiff line change
@@ -1921,14 +1921,30 @@ def _check_unnecessary_dict_index_lookup(
19211921
return
19221922
iterating_object_name = node.iter.func.expr.as_string()
19231923

1924+
# Store potential violations. These will only be reported if we don't
1925+
# discover any writes to the collection during the loop.
1926+
messages = []
1927+
19241928
# Verify that the body of the for loop uses a subscript
19251929
# with the object that was iterated. This uses some heuristics
19261930
# in order to make sure that the same object is used in the
19271931
# for body.
19281932

19291933
children = (
1930-
node.body if isinstance(node, nodes.For) else node.parent.get_children()
1934+
node.body
1935+
if isinstance(node, nodes.For)
1936+
else list(node.parent.get_children())
1937+
)
1938+
1939+
# Check if there are any for / while loops within the loop in question;
1940+
# If so, we will be more conservative about reporting errors as we
1941+
# can't yet do proper control flow analysis to be sure when
1942+
# reassignment will affect us
1943+
nested_loops = itertools.chain.from_iterable(
1944+
child.nodes_of_class((nodes.For, nodes.While)) for child in children
19311945
)
1946+
has_nested_loops = next(nested_loops, None) is not None
1947+
19321948
for child in children:
19331949
for subscript in child.nodes_of_class(nodes.Subscript):
19341950
if not isinstance(subscript.value, (nodes.Name, nodes.Attribute)):
@@ -1968,11 +1984,19 @@ def _check_unnecessary_dict_index_lookup(
19681984
# defined and compare that to the for loop's line number
19691985
continue
19701986

1971-
self.add_message(
1972-
"unnecessary-dict-index-lookup",
1973-
node=subscript,
1974-
args=(node.target.elts[1].as_string()),
1975-
)
1987+
if has_nested_loops:
1988+
messages.append(
1989+
{
1990+
"node": subscript,
1991+
"variable": node.target.elts[1].as_string(),
1992+
}
1993+
)
1994+
else:
1995+
self.add_message(
1996+
"unnecessary-dict-index-lookup",
1997+
node=subscript,
1998+
args=(node.target.elts[1].as_string(),),
1999+
)
19762000

19772001
# Case where .items is assigned to single var (i.e., for item in d.items())
19782002
elif isinstance(value, nodes.Subscript):
@@ -1999,11 +2023,31 @@ def _check_unnecessary_dict_index_lookup(
19992023
inferred = utils.safe_infer(value.slice)
20002024
if not isinstance(inferred, nodes.Const) or inferred.value != 0:
20012025
continue
2002-
self.add_message(
2003-
"unnecessary-dict-index-lookup",
2004-
node=subscript,
2005-
args=("1".join(value.as_string().rsplit("0", maxsplit=1)),),
2006-
)
2026+
2027+
if has_nested_loops:
2028+
messages.append(
2029+
{
2030+
"node": subscript,
2031+
"variable": "1".join(
2032+
value.as_string().rsplit("0", maxsplit=1)
2033+
),
2034+
}
2035+
)
2036+
else:
2037+
self.add_message(
2038+
"unnecessary-dict-index-lookup",
2039+
node=subscript,
2040+
args=(
2041+
"1".join(value.as_string().rsplit("0", maxsplit=1)),
2042+
),
2043+
)
2044+
2045+
for message in messages:
2046+
self.add_message(
2047+
"unnecessary-dict-index-lookup",
2048+
node=message["node"],
2049+
args=(message["variable"],),
2050+
)
20072051

20082052
def _check_unnecessary_list_index_lookup(
20092053
self, node: nodes.For | nodes.Comprehension
@@ -2029,9 +2073,25 @@ def _check_unnecessary_list_index_lookup(
20292073
iterating_object_name = node.iter.args[0].name
20302074
value_variable = node.target.elts[1]
20312075

2076+
# Store potential violations. These will only be reported if we don't
2077+
# discover any writes to the collection during the loop.
2078+
bad_nodes = []
2079+
20322080
children = (
2033-
node.body if isinstance(node, nodes.For) else node.parent.get_children()
2081+
node.body
2082+
if isinstance(node, nodes.For)
2083+
else list(node.parent.get_children())
2084+
)
2085+
2086+
# Check if there are any for / while loops within the loop in question;
2087+
# If so, we will be more conservative about reporting errors as we
2088+
# can't yet do proper control flow analysis to be sure when
2089+
# reassignment will affect us
2090+
nested_loops = itertools.chain.from_iterable(
2091+
child.nodes_of_class((nodes.For, nodes.While)) for child in children
20342092
)
2093+
has_nested_loops = next(nested_loops, None) is not None
2094+
20352095
for child in children:
20362096
for subscript in child.nodes_of_class(nodes.Subscript):
20372097
if isinstance(node, nodes.For) and _is_part_of_assignment_target(
@@ -2070,9 +2130,23 @@ def _check_unnecessary_list_index_lookup(
20702130
# reassigned on a later line, so it can't be used.
20712131
continue
20722132

2073-
self.add_message(
2074-
"unnecessary-list-index-lookup",
2075-
node=subscript,
2076-
args=(node.target.elts[1].name,),
2077-
confidence=HIGH,
2078-
)
2133+
if has_nested_loops:
2134+
# Have found a likely issue, but since there are nested
2135+
# loops we don't want to report this unless we get to the
2136+
# end of the loop without updating the collection
2137+
bad_nodes.append(subscript)
2138+
else:
2139+
self.add_message(
2140+
"unnecessary-list-index-lookup",
2141+
node=subscript,
2142+
args=(node.target.elts[1].name,),
2143+
confidence=HIGH,
2144+
)
2145+
2146+
for subscript in bad_nodes:
2147+
self.add_message(
2148+
"unnecessary-list-index-lookup",
2149+
node=subscript,
2150+
args=(node.target.elts[1].name,),
2151+
confidence=HIGH,
2152+
)

tests/functional/u/unnecessary/unnecessary_dict_index_lookup.py

+6
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,9 @@ class Foo:
124124
d = {'a': 1, 'b': 2, 'c': 3}
125125
for key, val in d.items():
126126
([d[key], x], y) = ([1, 2], 3)
127+
128+
# Regression test for https://github.com/PyCQA/pylint/issues/6818
129+
d = {'a': 1, 'b': 2, 'c': 3}
130+
for key, val in d.items():
131+
while d[key] > 0:
132+
d[key] -= 1

tests/functional/u/unnecessary/unnecessary_list_index_lookup.py

+12
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,15 @@ def process_list_again(data):
6262
num_list = [1, 2, 3]
6363
for a, b in enumerate(num_list):
6464
([x, num_list[a]], _) = ([5, 6], 1)
65+
66+
# Regression test for https://github.com/PyCQA/pylint/issues/6818
67+
updated_list = [1, 2, 3]
68+
for idx, val in enumerate(updated_list):
69+
while updated_list[idx] > 0:
70+
updated_list[idx] -= 1
71+
72+
updated_list = [1, 2, 3]
73+
for idx, val in enumerate(updated_list):
74+
print(updated_list[idx]) # [unnecessary-list-index-lookup]
75+
updated_list[idx] -= 1
76+
print(updated_list[idx])
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
unnecessary-list-index-lookup:8:10:8:22::Unnecessary list index lookup, use 'val' instead:HIGH
22
unnecessary-list-index-lookup:43:52:43:64::Unnecessary list index lookup, use 'val' instead:HIGH
33
unnecessary-list-index-lookup:46:10:46:22::Unnecessary list index lookup, use 'val' instead:HIGH
4+
unnecessary-list-index-lookup:74:10:74:27::Unnecessary list index lookup, use 'val' instead:HIGH

0 commit comments

Comments
 (0)