Skip to content

Commit

Permalink
Implemented new check consider-using-dict-items (pylint-dev#3389)
Browse files Browse the repository at this point in the history
  • Loading branch information
yushao2 committed May 8, 2021
1 parent 7167442 commit b38c035
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CONTRIBUTORS.txt
Expand Up @@ -482,3 +482,5 @@ contributors:
* das-intensity: contributor

* Jiajunsu (victor): contributor

* Pang Yu Shao (yushao2): contributor
5 changes: 5 additions & 0 deletions ChangeLog
Expand Up @@ -23,6 +23,11 @@ Release date: 2021-04-26
Closes #4166
Closes #4415

* ``consider-using-dict-items``, emitted when iterating over dictionary keys and then
indexing the same dictionary with the key within loop body.

Closes #3389


What's New in Pylint 2.8.2?
===========================
Expand Down
3 changes: 3 additions & 0 deletions doc/whatsnew/2.9.rst
Expand Up @@ -12,6 +12,9 @@ Summary -- Release highlights
New checkers
============

* ``consider-using-dict-items``, emitted when iterating over dictionary keys and then
indexing the same dictionary with the key within loop body.

Other Changes
=============

Expand Down
71 changes: 70 additions & 1 deletion pylint/checkers/refactoring/recommendation_checker.py
Expand Up @@ -26,6 +26,14 @@ class RecommendationChecker(checkers.BaseChecker):
"method. It is enough to just iterate through the dictionary itself, as "
'in "for key in dictionary".',
),
"C0206": (
"Consider iterating with .items()",
"consider-using-dict-items",
"Emitted when the keys of a dictionary are iterated and the items of the "
"dictionary is accessed by indexing with the key in each iteration. "
"Both the key and value can be accessed by iterating using the .items() "
"method of the dictionary instead.",
),
}

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

@utils.check_messages("consider-using-enumerate")
@utils.check_messages("consider-using-enumerate", "consider-using-dict-items")
def visit_for(self, node):
self._check_consider_using_enumerate(node)
self._check_consider_using_dict_items(node)

def _check_consider_using_enumerate(self, node):
"""Emit a convention whenever range and len are used for indexing."""
# Verify that we have a `range([start], len(...), [stop])` call and
# that the object which is iterated is used as a subscript in the
Expand Down Expand Up @@ -119,3 +131,60 @@ def visit_for(self, node):
continue
self.add_message("consider-using-enumerate", node=node)
return

def _check_consider_using_dict_items(self, node):
"""Emit a convention whenever range and len are used for indexing."""
# Verify that we have a .keys() call and
# that the object which is iterated is used as a subscript in the
# body of the for.

if not isinstance(node.iter, astroid.Call):
# Is it a dictionary?
if not isinstance(node.iter, astroid.Name):
return
inferred = utils.safe_infer(node.iter)
if not isinstance(inferred, astroid.Dict) and not isinstance(
inferred, astroid.DictComp
):
return
iterating_object_name = node.iter.as_string()

else:
# Is it a proper keys call?
if isinstance(node.iter.func, astroid.Name):
return
if node.iter.func.attrname != "keys":
return
inferred = utils.safe_infer(node.iter.func)
if not isinstance(inferred, astroid.BoundMethod) or not isinstance(
inferred.bound, astroid.Dict
):
return
iterating_object_name = node.iter.as_string().split(".")[0]

# Verify that the body of the for loop uses a subscript
# with the object that was iterated. This uses some heuristics
# in order to make sure that the same object is used in the
# for body.
for child in node.body:
for subscript in child.nodes_of_class(astroid.Subscript):
if not isinstance(subscript.value, astroid.Name):
continue

value = subscript.slice
if isinstance(value, astroid.Index):
value = value.value
if not isinstance(value, astroid.Name):
continue
if value.name != node.target.name:
continue
if iterating_object_name != subscript.value.name:
continue
if subscript.value.scope() != node.scope():
# Ignore this subscript if it's not in the same
# scope. This means that in the body of the for
# loop, another scope was created, where the same
# name for the iterating object was used.
continue
self.add_message("consider-using-dict-items", node=node)
return
4 changes: 2 additions & 2 deletions pylint/checkers/typecheck.py
Expand Up @@ -1436,8 +1436,8 @@ def visit_call(self, node):
args=(display_name, callable_name),
)

for name in kwparams:
defval, assigned = kwparams[name]
for name, val in kwparams.items():
defval, assigned = val
if (
defval is None
and not assigned
Expand Down
17 changes: 17 additions & 0 deletions tests/functional/c/consider/consider_using_dict_items.py
@@ -0,0 +1,17 @@
"""Emit a message for iteration through dict keys and subscripting dict with key."""

# pylint: disable=missing-docstring, import-error, useless-object-inheritance, unsubscriptable-object, too-few-public-methods

def bad():
a_dict = {1:1, 2:2, 3:3}
for k in a_dict:# [consider-using-dict-items]
print(a_dict[k])
another_dict = dict()
for k in another_dict:# [consider-using-dict-items]
print(another_dict[k])


def good():
a_dict = {1:1, 2:2, 3:3}
for k in a_dict:
print(k)
2 changes: 2 additions & 0 deletions tests/functional/c/consider/consider_using_dict_items.txt
@@ -0,0 +1,2 @@
consider-using-dict-items:7:4:bad:Consider iterating with .items()
consider-using-dict-items:10:4:bad:Consider iterating with .items()

0 comments on commit b38c035

Please sign in to comment.