Skip to content

Commit 901e751

Browse files
committed
Implemented new check consider-using-dict-items (#3389)
1 parent 7167442 commit 901e751

File tree

6 files changed

+96
-3
lines changed

6 files changed

+96
-3
lines changed

CONTRIBUTORS.txt

+2
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,5 @@ contributors:
482482
* das-intensity: contributor
483483

484484
* Jiajunsu (victor): contributor
485+
486+
* Pang Yu Shao (yushao2): contributor

doc/whatsnew/2.9.rst

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Summary -- Release highlights
1212
New checkers
1313
============
1414

15+
* ``consider-using-dict-items``, emitted when iterating over dictionary keys and then
16+
indexing the same dictionary with the key within loop body.
17+
1518
Other Changes
1619
=============
1720

pylint/checkers/refactoring/recommendation_checker.py

+70-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ class RecommendationChecker(checkers.BaseChecker):
2626
"method. It is enough to just iterate through the dictionary itself, as "
2727
'in "for key in dictionary".',
2828
),
29+
"C0206": (
30+
"Consider iterating with .items()",
31+
"consider-using-dict-items",
32+
"Emitted when the keys of a dictionary are iterated and the items of the "
33+
"dictionary is accessed by indexing with the key in each iteration. "
34+
"Both the key and value can be accessed by iterating using the .items() "
35+
"method of the dictionary instead.",
36+
),
2937
}
3038

3139
@staticmethod
@@ -53,8 +61,12 @@ def visit_call(self, node):
5361
if isinstance(node.parent, (astroid.For, astroid.Comprehension)):
5462
self.add_message("consider-iterating-dictionary", node=node)
5563

56-
@utils.check_messages("consider-using-enumerate")
64+
@utils.check_messages("consider-using-enumerate", "consider-using-dict-items")
5765
def visit_for(self, node):
66+
self._check_consider_using_enumerate(node)
67+
self._check_consider_using_dict_items(node)
68+
69+
def _check_consider_using_enumerate(self, node):
5870
"""Emit a convention whenever range and len are used for indexing."""
5971
# Verify that we have a `range([start], len(...), [stop])` call and
6072
# that the object which is iterated is used as a subscript in the
@@ -119,3 +131,60 @@ def visit_for(self, node):
119131
continue
120132
self.add_message("consider-using-enumerate", node=node)
121133
return
134+
135+
def _check_consider_using_dict_items(self, node):
136+
"""Emit a convention whenever range and len are used for indexing."""
137+
# Verify that we have a .keys() call and
138+
# that the object which is iterated is used as a subscript in the
139+
# body of the for.
140+
141+
if not isinstance(node.iter, astroid.Call):
142+
# Is it a dictionary?
143+
if not isinstance(node.iter, astroid.Name):
144+
return
145+
inferred = utils.safe_infer(node.iter)
146+
if not isinstance(inferred, astroid.Dict) and not isinstance(
147+
inferred, astroid.DictComp
148+
):
149+
return
150+
iterating_object_name = node.iter.as_string()
151+
152+
else:
153+
# Is it a proper keys call?
154+
if isinstance(node.iter.func, astroid.Name):
155+
return
156+
if node.iter.func.attrname != "keys":
157+
return
158+
inferred = utils.safe_infer(node.iter.func)
159+
if not isinstance(inferred, astroid.BoundMethod) or not isinstance(
160+
inferred.bound, astroid.Dict
161+
):
162+
return
163+
iterating_object_name = node.iter.as_string().split(".")[0]
164+
165+
# Verify that the body of the for loop uses a subscript
166+
# with the object that was iterated. This uses some heuristics
167+
# in order to make sure that the same object is used in the
168+
# for body.
169+
for child in node.body:
170+
for subscript in child.nodes_of_class(astroid.Subscript):
171+
if not isinstance(subscript.value, astroid.Name):
172+
continue
173+
174+
value = subscript.slice
175+
if isinstance(value, astroid.Index):
176+
value = value.value
177+
if not isinstance(value, astroid.Name):
178+
continue
179+
if value.name != node.target.name:
180+
continue
181+
if iterating_object_name != subscript.value.name:
182+
continue
183+
if subscript.value.scope() != node.scope():
184+
# Ignore this subscript if it's not in the same
185+
# scope. This means that in the body of the for
186+
# loop, another scope was created, where the same
187+
# name for the iterating object was used.
188+
continue
189+
self.add_message("consider-using-dict-items", node=node)
190+
return

pylint/checkers/typecheck.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1436,8 +1436,8 @@ def visit_call(self, node):
14361436
args=(display_name, callable_name),
14371437
)
14381438

1439-
for name in kwparams:
1440-
defval, assigned = kwparams[name]
1439+
for name, val in kwparams.items():
1440+
defval, assigned = val
14411441
if (
14421442
defval is None
14431443
and not assigned
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Emit a message for iteration through dict keys and subscripting dict with key."""
2+
3+
# pylint: disable=missing-docstring, import-error, useless-object-inheritance, unsubscriptable-object, too-few-public-methods
4+
5+
def bad():
6+
a_dict = {1:1, 2:2, 3:3}
7+
for k in a_dict:# [consider-using-dict-items]
8+
print(a_dict[k])
9+
another_dict = dict()
10+
for k in another_dict:# [consider-using-dict-items]
11+
print(another_dict[k])
12+
13+
14+
def good():
15+
a_dict = {1:1, 2:2, 3:3}
16+
for k in a_dict:
17+
print(k)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
consider-using-dict-items:7:4:bad:Consider iterating with .items()
2+
consider-using-dict-items:10:4:bad:Consider iterating with .items()

0 commit comments

Comments
 (0)