Skip to content

Commit 8d21ab2

Browse files
authored
New checker unnecessary-list-index-lookup (#4525) (#5834)
1 parent 737d45f commit 8d21ab2

File tree

10 files changed

+163
-2
lines changed

10 files changed

+163
-2
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Release date: TBA
1616

1717
Closes #578
1818

19+
* Added new checker ``unnecessary-list-index-lookup`` for indexing into a list while
20+
iterating over ``enumerate()``.
21+
22+
Closes #4525
1923

2024
* Fix false negative for ``no-member`` when attempting to assign an instance
2125
attribute to itself without any prior assignment.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
letters = ['a', 'b', 'c']
2+
3+
for index, letter in enumerate(letters):
4+
print(letters[index]) # [unnecessary-list-index-lookup]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
letters = ['a', 'b', 'c']
2+
3+
for index, letter in enumerate(letters):
4+
print(letter)

doc/whatsnew/2.14.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ New checkers
1818

1919
Closes #578
2020

21+
* Added new checker ``unnecessary-list-index-lookup`` for indexing into a list while
22+
iterating over ``enumerate()``.
23+
24+
Closes #4525
25+
2126
Removed checkers
2227
================
2328

pylint/checkers/refactoring/refactoring_checker.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from pylint import utils as lint_utils
1919
from pylint.checkers import utils
2020
from pylint.checkers.utils import node_frame_class
21+
from pylint.interfaces import HIGH
2122

2223
if sys.version_info >= (3, 8):
2324
from functools import cached_property
@@ -437,6 +438,13 @@ class RefactoringChecker(checkers.BaseTokenChecker):
437438
"Emitted when using dict() to create an empty dictionary instead of the literal {}. "
438439
"The literal is faster as it avoids an additional function call.",
439440
),
441+
"R1736": (
442+
"Unnecessary list index lookup, use '%s' instead",
443+
"unnecessary-list-index-lookup",
444+
"Emitted when iterating over an enumeration and accessing the "
445+
"value by index lookup. "
446+
"The value can be accessed directly instead.",
447+
),
440448
}
441449
options = (
442450
(
@@ -635,10 +643,12 @@ def _check_redefined_argument_from_local(self, name_node):
635643
"redefined-argument-from-local",
636644
"too-many-nested-blocks",
637645
"unnecessary-dict-index-lookup",
646+
"unnecessary-list-index-lookup",
638647
)
639648
def visit_for(self, node: nodes.For) -> None:
640649
self._check_nested_blocks(node)
641650
self._check_unnecessary_dict_index_lookup(node)
651+
self._check_unnecessary_list_index_lookup(node)
642652

643653
for name in node.target.nodes_of_class(nodes.AssignName):
644654
self._check_redefined_argument_from_local(name)
@@ -1556,10 +1566,15 @@ def _check_consider_using_join(self, aug_assign):
15561566
def visit_augassign(self, node: nodes.AugAssign) -> None:
15571567
self._check_consider_using_join(node)
15581568

1559-
@utils.check_messages("unnecessary-comprehension", "unnecessary-dict-index-lookup")
1569+
@utils.check_messages(
1570+
"unnecessary-comprehension",
1571+
"unnecessary-dict-index-lookup",
1572+
"unnecessary-list-index-lookup",
1573+
)
15601574
def visit_comprehension(self, node: nodes.Comprehension) -> None:
15611575
self._check_unnecessary_comprehension(node)
15621576
self._check_unnecessary_dict_index_lookup(node)
1577+
self._check_unnecessary_list_index_lookup(node)
15631578

15641579
def _check_unnecessary_comprehension(self, node: nodes.Comprehension) -> None:
15651580
if (
@@ -1963,3 +1978,72 @@ def _check_unnecessary_dict_index_lookup(
19631978
node=subscript,
19641979
args=("1".join(value.as_string().rsplit("0", maxsplit=1)),),
19651980
)
1981+
1982+
def _check_unnecessary_list_index_lookup(
1983+
self, node: Union[nodes.For, nodes.Comprehension]
1984+
) -> None:
1985+
if (
1986+
not isinstance(node.iter, nodes.Call)
1987+
or not isinstance(node.iter.func, nodes.Name)
1988+
or not node.iter.func.name == "enumerate"
1989+
or not isinstance(node.iter.args[0], nodes.Name)
1990+
):
1991+
return
1992+
1993+
if not isinstance(node.target, nodes.Tuple) or len(node.target.elts) < 2:
1994+
# enumerate() result is being assigned without destructuring
1995+
return
1996+
1997+
iterating_object_name = node.iter.args[0].name
1998+
value_variable = node.target.elts[1]
1999+
2000+
children = (
2001+
node.body if isinstance(node, nodes.For) else node.parent.get_children()
2002+
)
2003+
for child in children:
2004+
for subscript in child.nodes_of_class(nodes.Subscript):
2005+
if isinstance(node, nodes.For) and (
2006+
isinstance(subscript.parent, nodes.Assign)
2007+
and subscript in subscript.parent.targets
2008+
or isinstance(subscript.parent, nodes.AugAssign)
2009+
and subscript == subscript.parent.target
2010+
):
2011+
# Ignore this subscript if it is the target of an assignment
2012+
# Early termination; after reassignment index lookup will be necessary
2013+
return
2014+
2015+
if isinstance(subscript.parent, nodes.Delete):
2016+
# Ignore this subscript if it's used with the delete keyword
2017+
return
2018+
2019+
index = subscript.slice
2020+
if isinstance(index, nodes.Name):
2021+
if (
2022+
index.name != node.target.elts[0].name
2023+
or iterating_object_name != subscript.value.as_string()
2024+
):
2025+
continue
2026+
2027+
if (
2028+
isinstance(node, nodes.For)
2029+
and index.lookup(index.name)[1][-1].lineno > node.lineno
2030+
):
2031+
# Ignore this subscript if it has been redefined after
2032+
# the for loop.
2033+
continue
2034+
2035+
if (
2036+
isinstance(node, nodes.For)
2037+
and index.lookup(value_variable.name)[1][-1].lineno
2038+
> node.lineno
2039+
):
2040+
# The variable holding the value from iteration has been
2041+
# reassigned on a later line, so it can't be used.
2042+
continue
2043+
2044+
self.add_message(
2045+
"unnecessary-list-index-lookup",
2046+
node=subscript,
2047+
args=(node.target.elts[1].name,),
2048+
confidence=HIGH,
2049+
)

tests/functional/c/consider/consider_using_enumerate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Emit a message for iteration through range and len is encountered."""
22

3-
# pylint: disable=missing-docstring, import-error, useless-object-inheritance, unsubscriptable-object, too-few-public-methods
3+
# pylint: disable=missing-docstring, import-error, useless-object-inheritance, unsubscriptable-object, too-few-public-methods, unnecessary-list-index-lookup
44

55
def bad():
66
iterable = [1, 2, 3]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Tests for unnecessary-list-index-lookup."""
2+
3+
# pylint: disable=missing-docstring, too-few-public-methods, expression-not-assigned, line-too-long, unused-variable
4+
5+
my_list = ['a', 'b']
6+
7+
for idx, val in enumerate(my_list):
8+
print(my_list[idx]) # [unnecessary-list-index-lookup]
9+
10+
for idx, _ in enumerate(my_list):
11+
print(my_list[0])
12+
if idx > 0:
13+
print(my_list[idx - 1])
14+
15+
for idx, val in enumerate(my_list):
16+
del my_list[idx]
17+
18+
for idx, val in enumerate(my_list):
19+
my_list[idx] = 42
20+
21+
for vals in enumerate(my_list):
22+
# This could be refactored, but too complex to infer
23+
print(my_list[vals[0]])
24+
25+
def process_list(data):
26+
for index, value in enumerate(data):
27+
index = 1
28+
print(data[index])
29+
30+
def process_list_again(data):
31+
for index, value in enumerate(data):
32+
value = 1
33+
print(data[index]) # Can't use value here, it's been redefined
34+
35+
other_list = [1, 2]
36+
for idx, val in enumerate(my_list):
37+
print(other_list[idx])
38+
39+
OTHER_INDEX = 0
40+
for idx, val in enumerate(my_list):
41+
print(my_list[OTHER_INDEX])
42+
43+
result = [val for idx, val in enumerate(my_list) if my_list[idx] == 'a'] # [unnecessary-list-index-lookup]
44+
result = [val for idx, val in enumerate(my_list) if idx > 0 and my_list[idx - 1] == 'a']
45+
result = [val for idx, val in enumerate(my_list) if other_list[idx] == 'a']
46+
result = [my_list[idx] for idx, val in enumerate(my_list)] # [unnecessary-list-index-lookup]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
unnecessary-list-index-lookup:8:10:8:22::Unnecessary list index lookup, use 'val' instead:HIGH
2+
unnecessary-list-index-lookup:43:52:43:64::Unnecessary list index lookup, use 'val' instead:HIGH
3+
unnecessary-list-index-lookup:46:10:46:22::Unnecessary list index lookup, use 'val' instead:HIGH
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Tests for unnecessary-list-index-lookup with assignment expressions."""
2+
3+
# pylint: disable=missing-docstring, too-few-public-methods, expression-not-assigned, line-too-long, unused-variable
4+
5+
my_list = ['a', 'b']
6+
7+
for idx, val in enumerate(my_list):
8+
if (val := 42) and my_list[idx] == 'b':
9+
print(1)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
min_pyver=3.8

0 commit comments

Comments
 (0)