Skip to content

Commit dd8f358

Browse files
committed
New checker unnecessary-list-index-lookup (#4525)
1 parent 182cc53 commit dd8f358

File tree

9 files changed

+172
-2
lines changed

9 files changed

+172
-2
lines changed

ChangeLog

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
Pylint's ChangeLog
33
------------------
44

5+
What's New in Pylint 2.14.0?
6+
============================
7+
Release date: TBA
8+
9+
* Added new checker ``unnecessary-list-index-lookup`` for indexing into a list while
10+
iterating over ``enumerate()``.
11+
12+
Closes #4525
13+
14+
515
What's New in Pylint 2.13.0?
616
============================
717
Release date: TBA

doc/whatsnew/2.14.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
***************************
2+
What's New in Pylint 2.13
3+
***************************
4+
5+
:Release: 2.14
6+
:Date: TBA
7+
8+
Summary -- Release highlights
9+
=============================
10+
11+
New checkers
12+
============
13+
14+
* Added new checker ``unnecessary-list-index-lookup`` for indexing into a list while
15+
iterating over ``enumerate()``.
16+
17+
Closes #4525

doc/whatsnew/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ High level descriptions of the most important changes between major Pylint versi
99
.. toctree::
1010
:maxdepth: 1
1111

12+
2.14.rst
1213
2.13.rst
1314
2.12.rst
1415
2.11.rst

pylint/checkers/refactoring/refactoring_checker.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pylint import utils as lint_utils
1717
from pylint.checkers import utils
1818
from pylint.checkers.utils import node_frame_class
19+
from pylint.interfaces import HIGH
1920

2021
KNOWN_INFINITE_ITERATORS = {"itertools.count"}
2122
BUILTIN_EXIT_FUNCS = frozenset(("quit", "exit"))
@@ -430,6 +431,13 @@ class RefactoringChecker(checkers.BaseTokenChecker):
430431
"Emitted when using dict() to create an empty dictionary instead of the literal {}. "
431432
"The literal is faster as it avoids an additional function call.",
432433
),
434+
"R1736": (
435+
"Unnecessary list index lookup, use '%s' instead",
436+
"unnecessary-list-index-lookup",
437+
"Emitted when iterating over an enumeration and accessing the "
438+
"value by index lookup. "
439+
"The value can be accessed directly instead.",
440+
),
433441
}
434442
options = (
435443
(
@@ -628,10 +636,12 @@ def _check_redefined_argument_from_local(self, name_node):
628636
"redefined-argument-from-local",
629637
"too-many-nested-blocks",
630638
"unnecessary-dict-index-lookup",
639+
"unnecessary-list-index-lookup",
631640
)
632641
def visit_for(self, node: nodes.For) -> None:
633642
self._check_nested_blocks(node)
634643
self._check_unnecessary_dict_index_lookup(node)
644+
self._check_unnecessary_list_index_lookup(node)
635645

636646
for name in node.target.nodes_of_class(nodes.AssignName):
637647
self._check_redefined_argument_from_local(name)
@@ -1548,10 +1558,15 @@ def _check_consider_using_join(self, aug_assign):
15481558
def visit_augassign(self, node: nodes.AugAssign) -> None:
15491559
self._check_consider_using_join(node)
15501560

1551-
@utils.check_messages("unnecessary-comprehension", "unnecessary-dict-index-lookup")
1561+
@utils.check_messages(
1562+
"unnecessary-comprehension",
1563+
"unnecessary-dict-index-lookup",
1564+
"unnecessary-list-index-lookup",
1565+
)
15521566
def visit_comprehension(self, node: nodes.Comprehension) -> None:
15531567
self._check_unnecessary_comprehension(node)
15541568
self._check_unnecessary_dict_index_lookup(node)
1569+
self._check_unnecessary_list_index_lookup(node)
15551570

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

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)