From b011d87cc58d7effe077c52aa587e661fc3670d8 Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Thu, 6 May 2021 18:47:26 +0800 Subject: [PATCH 01/13] Implemented new check `consider-using-dict-items` --- CONTRIBUTORS.txt | 2 + ChangeLog | 5 ++ doc/whatsnew/2.9.rst | 3 + .../refactoring/recommendation_checker.py | 71 ++++++++++++++++++- pylint/checkers/typecheck.py | 4 +- .../c/consider/consider_using_dict_items.py | 17 +++++ .../c/consider/consider_using_dict_items.txt | 2 + 7 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 tests/functional/c/consider/consider_using_dict_items.py create mode 100644 tests/functional/c/consider/consider_using_dict_items.txt diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index fb422cf7ba..7d1cd868b7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -484,3 +484,5 @@ contributors: * Jiajunsu (victor): contributor * Andrew Haigh (nelfin): contributor + +* Pang Yu Shao (yushao2): contributor diff --git a/ChangeLog b/ChangeLog index de7a504220..20842af444 100644 --- a/ChangeLog +++ b/ChangeLog @@ -33,6 +33,11 @@ modules are added. * Fix raising false-positive ``no-member`` on abstract properties +* ``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? =========================== diff --git a/doc/whatsnew/2.9.rst b/doc/whatsnew/2.9.rst index a0053e98f0..906a6abfc7 100644 --- a/doc/whatsnew/2.9.rst +++ b/doc/whatsnew/2.9.rst @@ -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 ============= diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index b1175f03e0..0bcd2cbf15 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -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 @@ -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 @@ -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 diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 8ce0c9171e..f64960a218 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1439,8 +1439,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 diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py new file mode 100644 index 0000000000..9b06772834 --- /dev/null +++ b/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) diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt new file mode 100644 index 0000000000..46662e5ce9 --- /dev/null +++ b/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() From b92a5738160572c2089012e6ca18a6cf207c6e6f Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Sun, 9 May 2021 23:12:24 +0800 Subject: [PATCH 02/13] Added additional logic and tests for ``consider-using-dict-items`` --- ChangeLog | 2 +- doc/whatsnew/2.9.rst | 2 +- .../refactoring/recommendation_checker.py | 53 ++++++++++--------- .../c/consider/consider_using_dict_items.py | 19 +++++-- .../c/consider/consider_using_dict_items.txt | 1 + 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/ChangeLog b/ChangeLog index 20842af444..b9e279ab9a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -33,7 +33,7 @@ modules are added. * Fix raising false-positive ``no-member`` on abstract properties -* ``consider-using-dict-items``, emitted when iterating over dictionary keys and then +* New checker ``consider-using-dict-items``. Emitted when iterating over dictionary keys and then indexing the same dictionary with the key within loop body. Closes #3389 diff --git a/doc/whatsnew/2.9.rst b/doc/whatsnew/2.9.rst index 906a6abfc7..8f5bd43f2f 100644 --- a/doc/whatsnew/2.9.rst +++ b/doc/whatsnew/2.9.rst @@ -12,7 +12,7 @@ Summary -- Release highlights New checkers ============ -* ``consider-using-dict-items``, emitted when iterating over dictionary keys and then +* ``consider-using-dict-items``: Emitted when iterating over dictionary keys and then indexing the same dictionary with the key within loop body. Other Changes diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 0bcd2cbf15..608620726f 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -1,6 +1,8 @@ # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/master/LICENSE +from typing import cast + import astroid from pylint import checkers, interfaces @@ -29,8 +31,8 @@ class RecommendationChecker(checkers.BaseChecker): "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. " + "Emitted when iterating over the keys of a dictionary and accessing the " + "value by index lookup." "Both the key and value can be accessed by iterating using the .items() " "method of the dictionary instead.", ), @@ -62,11 +64,11 @@ def visit_call(self, node): self.add_message("consider-iterating-dictionary", node=node) @utils.check_messages("consider-using-enumerate", "consider-using-dict-items") - def visit_for(self, node): + def visit_for(self, node: astroid.For) -> None: self._check_consider_using_enumerate(node) self._check_consider_using_dict_items(node) - def _check_consider_using_enumerate(self, node): + def _check_consider_using_enumerate(self, node: astroid.For) -> None: """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 @@ -132,8 +134,8 @@ def _check_consider_using_enumerate(self, node): 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.""" + def _check_consider_using_dict_items(self, node: astroid.For) -> None: + """Emit a convention when dict key used to index dict.""" # 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. @@ -143,24 +145,21 @@ def _check_consider_using_dict_items(self, node): 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 - ): + if not isinstance(inferred, (astroid.Dict, 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": + if not ( + isinstance(node.iter.func, astroid.Attribute) + and 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 - ): + if not isinstance(inferred, (astroid.BoundMethod, astroid.Dict)): return - iterating_object_name = node.iter.as_string().split(".")[0] + iterating_object_name = node.iter.as_string().partition(".")[0] # Verify that the body of the for loop uses a subscript # with the object that was iterated. This uses some heuristics @@ -168,23 +167,25 @@ def _check_consider_using_dict_items(self, node): # for body. for child in node.body: for subscript in child.nodes_of_class(astroid.Subscript): + subscript = cast(astroid.Subscript, 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): + if ( + not isinstance(value, astroid.Name) + or value.name != node.target.name + or iterating_object_name != subscript.value.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. + last_definition_lineno = value.lookup(value.name)[1][-1].lineno + if last_definition_lineno > node.lineno: + # Ignore this subscript if it has been redefined after + # the for loop. This checks for the line number using .lookup() + # to get the line number where the iterating object was last + # defined and compare that to the for loop's line number continue self.add_message("consider-using-dict-items", node=node) return diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py index 9b06772834..c8166835c7 100644 --- a/tests/functional/c/consider/consider_using_dict_items.py +++ b/tests/functional/c/consider/consider_using_dict_items.py @@ -1,13 +1,13 @@ """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 +# pylint: disable=missing-docstring,unsubscriptable-object def bad(): a_dict = {1:1, 2:2, 3:3} - for k in a_dict:# [consider-using-dict-items] + 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] + for k in another_dict: # [consider-using-dict-items] print(another_dict[k]) @@ -15,3 +15,16 @@ def good(): a_dict = {1:1, 2:2, 3:3} for k in a_dict: print(k) + +out_of_scope_dict = dict() + +def another_bad(): + for k in out_of_scope_dict: # [consider-using-dict-items] + print(out_of_scope_dict[k]) + +def another_good(): + for k in out_of_scope_dict: + k = 1 + k = 2 + k = 3 + print(out_of_scope_dict[k]) diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index 46662e5ce9..a86b74e978 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -1,2 +1,3 @@ consider-using-dict-items:7:4:bad:Consider iterating with .items() consider-using-dict-items:10:4:bad:Consider iterating with .items() +consider-using-dict-items:22:4:another_bad:Consider iterating with .items() From b9af784f7f8a7a294ead2fe3051a87e5516e8746 Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Tue, 11 May 2021 01:50:02 +0800 Subject: [PATCH 03/13] Implemented ``consider-using-dict-items`` on comprehension - Also, added accompanying test cases --- .../refactoring/recommendation_checker.py | 91 ++++++++++++++----- .../c/consider/consider_using_dict_items.py | 35 ++++++- .../c/consider/consider_using_dict_items.txt | 10 ++ 3 files changed, 112 insertions(+), 24 deletions(-) diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 608620726f..01c7f36445 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -1,7 +1,7 @@ # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/master/LICENSE -from typing import cast +from typing import Optional, Union, cast import astroid @@ -9,6 +9,32 @@ from pylint.checkers import utils +def _check_if_dict_keys_used( + node: Union[astroid.For, astroid.Comprehension] +) -> Optional[str]: + if not isinstance(node.iter, astroid.Call): + # Is it a dictionary? + if not isinstance(node.iter, (astroid.Name, astroid.Attribute)): + return None + inferred = utils.safe_infer(node.iter) + if not isinstance(inferred, (astroid.Dict, astroid.DictComp)): + return None + iterating_object_name = node.iter.as_string() + + else: + # Is it a proper keys call? + if ( + isinstance(node.iter.func, astroid.Name) + or node.iter.func.attrname != "keys" + ): + return None + inferred = utils.safe_infer(node.iter.func) + if not isinstance(inferred, (astroid.BoundMethod, astroid.Dict)): + return None + iterating_object_name = node.iter.as_string().partition(".")[0] + return iterating_object_name + + class RecommendationChecker(checkers.BaseChecker): __implements__ = (interfaces.IAstroidChecker,) @@ -140,26 +166,9 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: # 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, astroid.DictComp)): - return - iterating_object_name = node.iter.as_string() - - else: - # Is it a proper keys call? - if not ( - isinstance(node.iter.func, astroid.Attribute) - and node.iter.func.attrname != "keys" - ): - return - inferred = utils.safe_infer(node.iter.func) - if not isinstance(inferred, (astroid.BoundMethod, astroid.Dict)): - return - iterating_object_name = node.iter.as_string().partition(".")[0] + iterating_object_name = _check_if_dict_keys_used(node) + if iterating_object_name is None: + return # Verify that the body of the for loop uses a subscript # with the object that was iterated. This uses some heuristics @@ -168,7 +177,8 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: for child in node.body: for subscript in child.nodes_of_class(astroid.Subscript): subscript = cast(astroid.Subscript, subscript) - if not isinstance(subscript.value, astroid.Name): + + if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)): continue value = subscript.slice @@ -177,8 +187,9 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: if ( not isinstance(value, astroid.Name) or value.name != node.target.name - or iterating_object_name != subscript.value.name + or iterating_object_name != subscript.value.as_string() ): + continue last_definition_lineno = value.lookup(value.name)[1][-1].lineno if last_definition_lineno > node.lineno: @@ -187,5 +198,39 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: # to get the line number where the iterating object was last # defined and compare that to the for loop's line number continue + if ( + isinstance(subscript.parent, astroid.Assign) + and subscript in subscript.parent.targets + ): + # Ignore this subscript if it is the target of an assignment + continue + + self.add_message("consider-using-dict-items", node=node) + return + + @utils.check_messages("consider-using-dict-items") + def visit_comprehension(self, node: astroid.Comprehension) -> None: + iterating_object_name = _check_if_dict_keys_used(node) + if iterating_object_name is None: + return + + children = list(node.parent.get_children()) + if node.ifs: + children += node.ifs + for child in children: + for subscript in child.nodes_of_class(astroid.Subscript): + subscript = cast(astroid.Subscript, subscript) + if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)): + continue + + value = subscript.slice + if isinstance(value, astroid.Index): + value = value.value + if ( + not isinstance(value, astroid.Name) + or value.name != node.target.name + or iterating_object_name != subscript.value.as_string() + ): + continue self.add_message("consider-using-dict-items", node=node) return diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py index c8166835c7..e1940bbffb 100644 --- a/tests/functional/c/consider/consider_using_dict_items.py +++ b/tests/functional/c/consider/consider_using_dict_items.py @@ -1,6 +1,6 @@ """Emit a message for iteration through dict keys and subscripting dict with key.""" -# pylint: disable=missing-docstring,unsubscriptable-object +# pylint: disable=missing-docstring,unsubscriptable-object,too-few-public-methods def bad(): a_dict = {1:1, 2:2, 3:3} @@ -28,3 +28,36 @@ def another_good(): k = 2 k = 3 print(out_of_scope_dict[k]) + + +b_dict = {} +for k2 in b_dict: # Should not emit warning, key access necessary + b_dict[k2] = 2 + +for k3 in b_dict: # [consider-using-dict-items] + val = b_dict[k3] + +for k4 in b_dict.keys(): # [consider-iterating-dictionary,consider-using-dict-items] + val = b_dict[k4] + +class Foo: + c_dict = {} + +for k5 in Foo.c_dict: # [consider-using-dict-items] + val = Foo.c_dict[k5] + +for k6 in b_dict: # [consider-using-dict-items] + val = b_dict[k6] + b_dict[k6] = 2 + +c_dict={} + +val = [(k7, b_dict[k7]) for k7 in b_dict] # [consider-using-dict-items] +val = any(True for k8 in b_dict if b_dict[k8]) # [consider-using-dict-items] +val = {k9: b_dict[k9] for k9 in b_dict} # [consider-using-dict-items] +val = [(k7, b_dict[k7]) for k7 in Foo.c_dict] +val = any(True for k8 in Foo.c_dict if b_dict[k8]) +val = [(k7, c_dict[k7]) for k7 in Foo.c_dict] +val = any(True for k8 in Foo.c_dict if c_dict[k8]) +val = [(k7, Foo.c_dict[k7]) for k7 in Foo.c_dict] # [consider-using-dict-items] +val = any(True for k8 in Foo.c_dict if Foo.c_dict[k8]) # [consider-using-dict-items] diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index a86b74e978..d79fed6886 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -1,3 +1,13 @@ consider-using-dict-items:7:4:bad:Consider iterating with .items() consider-using-dict-items:10:4:bad:Consider iterating with .items() consider-using-dict-items:22:4:another_bad:Consider iterating with .items() +consider-using-dict-items:37:0::Consider iterating with .items() +consider-iterating-dictionary:40:10::Consider iterating the dictionary directly instead of calling .keys() +consider-using-dict-items:40:0::Consider iterating with .items() +consider-using-dict-items:46:0::Consider iterating with .items() +consider-using-dict-items:49:0::Consider iterating with .items() +consider-using-dict-items:55:0::Consider iterating with .items() +consider-using-dict-items:56:0::Consider iterating with .items() +consider-using-dict-items:57:0::Consider iterating with .items() +consider-using-dict-items:62:0::Consider iterating with .items() +consider-using-dict-items:63:0::Consider iterating with .items() From d2a85db588f48a7b259a913cb105353fe06ba87e Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Tue, 11 May 2021 10:11:12 +0800 Subject: [PATCH 04/13] Grouped test items --- .../c/consider/consider_using_dict_items.py | 26 +++++++++++++------ .../c/consider/consider_using_dict_items.txt | 16 ++++++------ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py index e1940bbffb..0a51f2d788 100644 --- a/tests/functional/c/consider/consider_using_dict_items.py +++ b/tests/functional/c/consider/consider_using_dict_items.py @@ -34,6 +34,11 @@ def another_good(): for k2 in b_dict: # Should not emit warning, key access necessary b_dict[k2] = 2 +# Warning should be emitted in this case +for k6 in b_dict: # [consider-using-dict-items] + val = b_dict[k6] + b_dict[k6] = 2 + for k3 in b_dict: # [consider-using-dict-items] val = b_dict[k3] @@ -43,21 +48,26 @@ def another_good(): class Foo: c_dict = {} +# Should emit warning when iterating over a dict attribute of a class for k5 in Foo.c_dict: # [consider-using-dict-items] val = Foo.c_dict[k5] -for k6 in b_dict: # [consider-using-dict-items] - val = b_dict[k6] - b_dict[k6] = 2 +# Should emit warning within a list/dict comprehension +val = {k9: b_dict[k9] for k9 in b_dict} # [consider-using-dict-items] +val = [(k7, b_dict[k7]) for k7 in b_dict] # [consider-using-dict-items] -c_dict={} +# Should emit warning even when using dict attribute of a class within comprehension +val = [(k7, Foo.c_dict[k7]) for k7 in Foo.c_dict] # [consider-using-dict-items] +val = any(True for k8 in Foo.c_dict if Foo.c_dict[k8]) # [consider-using-dict-items] -val = [(k7, b_dict[k7]) for k7 in b_dict] # [consider-using-dict-items] +# Should emit warning when dict access done in ``if`` portion of comprehension val = any(True for k8 in b_dict if b_dict[k8]) # [consider-using-dict-items] -val = {k9: b_dict[k9] for k9 in b_dict} # [consider-using-dict-items] + +# Should NOT emit warning whey key used to access a different dict val = [(k7, b_dict[k7]) for k7 in Foo.c_dict] val = any(True for k8 in Foo.c_dict if b_dict[k8]) + +# Should NOT emit warning, essentially same check as above +c_dict={} val = [(k7, c_dict[k7]) for k7 in Foo.c_dict] val = any(True for k8 in Foo.c_dict if c_dict[k8]) -val = [(k7, Foo.c_dict[k7]) for k7 in Foo.c_dict] # [consider-using-dict-items] -val = any(True for k8 in Foo.c_dict if Foo.c_dict[k8]) # [consider-using-dict-items] diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index d79fed6886..d159b1c2fc 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -1,13 +1,13 @@ consider-using-dict-items:7:4:bad:Consider iterating with .items() consider-using-dict-items:10:4:bad:Consider iterating with .items() consider-using-dict-items:22:4:another_bad:Consider iterating with .items() -consider-using-dict-items:37:0::Consider iterating with .items() -consider-iterating-dictionary:40:10::Consider iterating the dictionary directly instead of calling .keys() -consider-using-dict-items:40:0::Consider iterating with .items() -consider-using-dict-items:46:0::Consider iterating with .items() -consider-using-dict-items:49:0::Consider iterating with .items() -consider-using-dict-items:55:0::Consider iterating with .items() +consider-using-dict-items:38:0::Consider iterating with .items() +consider-using-dict-items:42:0::Consider iterating with .items() +consider-iterating-dictionary:45:10::Consider iterating the dictionary directly instead of calling .keys() +consider-using-dict-items:45:0::Consider iterating with .items() +consider-using-dict-items:52:0::Consider iterating with .items() consider-using-dict-items:56:0::Consider iterating with .items() consider-using-dict-items:57:0::Consider iterating with .items() -consider-using-dict-items:62:0::Consider iterating with .items() -consider-using-dict-items:63:0::Consider iterating with .items() +consider-using-dict-items:60:0::Consider iterating with .items() +consider-using-dict-items:61:0::Consider iterating with .items() +consider-using-dict-items:64:0::Consider iterating with .items() From c30df89ebbf119b0ac336732bf9539235602a50f Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Tue, 11 May 2021 10:13:20 +0800 Subject: [PATCH 05/13] Refactored utility function --- .../refactoring/recommendation_checker.py | 32 ++----------------- pylint/checkers/utils.py | 30 +++++++++++++++++ 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 01c7f36445..1ba229b30c 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -1,7 +1,7 @@ # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/master/LICENSE -from typing import Optional, Union, cast +from typing import cast import astroid @@ -9,32 +9,6 @@ from pylint.checkers import utils -def _check_if_dict_keys_used( - node: Union[astroid.For, astroid.Comprehension] -) -> Optional[str]: - if not isinstance(node.iter, astroid.Call): - # Is it a dictionary? - if not isinstance(node.iter, (astroid.Name, astroid.Attribute)): - return None - inferred = utils.safe_infer(node.iter) - if not isinstance(inferred, (astroid.Dict, astroid.DictComp)): - return None - iterating_object_name = node.iter.as_string() - - else: - # Is it a proper keys call? - if ( - isinstance(node.iter.func, astroid.Name) - or node.iter.func.attrname != "keys" - ): - return None - inferred = utils.safe_infer(node.iter.func) - if not isinstance(inferred, (astroid.BoundMethod, astroid.Dict)): - return None - iterating_object_name = node.iter.as_string().partition(".")[0] - return iterating_object_name - - class RecommendationChecker(checkers.BaseChecker): __implements__ = (interfaces.IAstroidChecker,) @@ -166,7 +140,7 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: # that the object which is iterated is used as a subscript in the # body of the for. - iterating_object_name = _check_if_dict_keys_used(node) + iterating_object_name = utils.get_iterating_dictionary_name(node) if iterating_object_name is None: return @@ -210,7 +184,7 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: @utils.check_messages("consider-using-dict-items") def visit_comprehension(self, node: astroid.Comprehension) -> None: - iterating_object_name = _check_if_dict_keys_used(node) + iterating_object_name = utils.get_iterating_dictionary_name(node) if iterating_object_name is None: return diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 6c11eeb9d9..8aae906433 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1492,3 +1492,33 @@ def is_assign_name_annotated_with(node: astroid.AssignName, typing_name: str) -> ): return True return False + + +def get_iterating_dictionary_name( + node: Union[astroid.For, astroid.Comprehension] +) -> Optional[str]: + """Get the name of the dictionary which keys are being iterated over on + a ``astroid.For`` or ``astroid.Comprehension`` node. + + If the iterating object is not either the keys method of a dictionary + or a dictionary itself, this returns None + """ + if isinstance(node.iter, astroid.Call): + # Is it a proper keys call? + if ( + isinstance(node.iter.func, astroid.Name) + or node.iter.func.attrname != "keys" + ): + return None + inferred = safe_infer(node.iter.func) + if not isinstance(inferred, (astroid.BoundMethod, astroid.Dict)): + return None + return node.iter.as_string().partition(".")[0] + + # Is it a dictionary? + if not isinstance(node.iter, (astroid.Name, astroid.Attribute)): + return None + inferred = safe_infer(node.iter) + if not isinstance(inferred, (astroid.Dict, astroid.DictComp)): + return None + return node.iter.as_string() From 08ba8502e5a4bf8913b69200f45d3c6492fa31ea Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Tue, 11 May 2021 10:27:17 +0800 Subject: [PATCH 06/13] Added logic to check for augassign statements --- .../checkers/refactoring/recommendation_checker.py | 8 +++++--- .../c/consider/consider_using_dict_items.py | 3 +++ .../c/consider/consider_using_dict_items.txt | 14 +++++++------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 1ba229b30c..bc70680790 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -135,7 +135,7 @@ def _check_consider_using_enumerate(self, node: astroid.For) -> None: return def _check_consider_using_dict_items(self, node: astroid.For) -> None: - """Emit a convention when dict key used to index dict.""" + """Add message when accessing dict values by index lookup.""" # 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. @@ -173,8 +173,10 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: # defined and compare that to the for loop's line number continue if ( - isinstance(subscript.parent, astroid.Assign) + isinstance(subscript.parent, (astroid.Assign)) and subscript in subscript.parent.targets + or isinstance(subscript.parent, (astroid.AugAssign)) + and subscript == subscript.parent.target ): # Ignore this subscript if it is the target of an assignment continue @@ -190,7 +192,7 @@ def visit_comprehension(self, node: astroid.Comprehension) -> None: children = list(node.parent.get_children()) if node.ifs: - children += node.ifs + children.extend(node.ifs) for child in children: for subscript in child.nodes_of_class(astroid.Subscript): subscript = cast(astroid.Subscript, subscript) diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py index 0a51f2d788..65e11a4c2d 100644 --- a/tests/functional/c/consider/consider_using_dict_items.py +++ b/tests/functional/c/consider/consider_using_dict_items.py @@ -34,6 +34,9 @@ def another_good(): for k2 in b_dict: # Should not emit warning, key access necessary b_dict[k2] = 2 +for k2 in b_dict: # Should not emit warning, key access necessary (augassign) + b_dict[k2] += 2 + # Warning should be emitted in this case for k6 in b_dict: # [consider-using-dict-items] val = b_dict[k6] diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index d159b1c2fc..fedd8a8ecd 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -1,13 +1,13 @@ consider-using-dict-items:7:4:bad:Consider iterating with .items() consider-using-dict-items:10:4:bad:Consider iterating with .items() consider-using-dict-items:22:4:another_bad:Consider iterating with .items() -consider-using-dict-items:38:0::Consider iterating with .items() -consider-using-dict-items:42:0::Consider iterating with .items() -consider-iterating-dictionary:45:10::Consider iterating the dictionary directly instead of calling .keys() +consider-using-dict-items:41:0::Consider iterating with .items() consider-using-dict-items:45:0::Consider iterating with .items() -consider-using-dict-items:52:0::Consider iterating with .items() -consider-using-dict-items:56:0::Consider iterating with .items() -consider-using-dict-items:57:0::Consider iterating with .items() +consider-iterating-dictionary:48:10::Consider iterating the dictionary directly instead of calling .keys() +consider-using-dict-items:48:0::Consider iterating with .items() +consider-using-dict-items:55:0::Consider iterating with .items() +consider-using-dict-items:59:0::Consider iterating with .items() consider-using-dict-items:60:0::Consider iterating with .items() -consider-using-dict-items:61:0::Consider iterating with .items() +consider-using-dict-items:63:0::Consider iterating with .items() consider-using-dict-items:64:0::Consider iterating with .items() +consider-using-dict-items:67:0::Consider iterating with .items() From 31bbd174c21f81978805975ae99815d89c588f91 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 11 May 2021 12:57:38 +0200 Subject: [PATCH 07/13] Minor style changes --- .../refactoring/recommendation_checker.py | 2 -- pylint/checkers/utils.py | 6 ++--- .../c/consider/consider_using_dict_items.py | 19 ++++++++-------- .../c/consider/consider_using_dict_items.txt | 22 +++++++++---------- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index bc70680790..2be19e4f5e 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -1,6 +1,5 @@ # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/master/LICENSE - from typing import cast import astroid @@ -163,7 +162,6 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: or value.name != node.target.name or iterating_object_name != subscript.value.as_string() ): - continue last_definition_lineno = value.lookup(value.name)[1][-1].lineno if last_definition_lineno > node.lineno: diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 8aae906433..64fa300ae8 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1498,13 +1498,13 @@ def get_iterating_dictionary_name( node: Union[astroid.For, astroid.Comprehension] ) -> Optional[str]: """Get the name of the dictionary which keys are being iterated over on - a ``astroid.For`` or ``astroid.Comprehension`` node. + a `astroid.For` or `astroid.Comprehension` node. If the iterating object is not either the keys method of a dictionary - or a dictionary itself, this returns None + or a dictionary itself, this returns None. """ + # Is it a proper keys call? if isinstance(node.iter, astroid.Call): - # Is it a proper keys call? if ( isinstance(node.iter.func, astroid.Name) or node.iter.func.attrname != "keys" diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py index 65e11a4c2d..db3626e10a 100644 --- a/tests/functional/c/consider/consider_using_dict_items.py +++ b/tests/functional/c/consider/consider_using_dict_items.py @@ -1,25 +1,24 @@ """Emit a message for iteration through dict keys and subscripting dict with key.""" - # pylint: disable=missing-docstring,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] + 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] + for k in another_dict: # [consider-using-dict-items] print(another_dict[k]) def good(): - a_dict = {1:1, 2:2, 3:3} + a_dict = {1: 1, 2: 2, 3: 3} for k in a_dict: print(k) out_of_scope_dict = dict() def another_bad(): - for k in out_of_scope_dict: # [consider-using-dict-items] + for k in out_of_scope_dict: # [consider-using-dict-items] print(out_of_scope_dict[k]) def another_good(): @@ -34,7 +33,7 @@ def another_good(): for k2 in b_dict: # Should not emit warning, key access necessary b_dict[k2] = 2 -for k2 in b_dict: # Should not emit warning, key access necessary (augassign) +for k2 in b_dict: # Should not emit warning, key access necessary (AugAssign) b_dict[k2] += 2 # Warning should be emitted in this case @@ -60,8 +59,8 @@ class Foo: val = [(k7, b_dict[k7]) for k7 in b_dict] # [consider-using-dict-items] # Should emit warning even when using dict attribute of a class within comprehension -val = [(k7, Foo.c_dict[k7]) for k7 in Foo.c_dict] # [consider-using-dict-items] -val = any(True for k8 in Foo.c_dict if Foo.c_dict[k8]) # [consider-using-dict-items] +val = [(k7, Foo.c_dict[k7]) for k7 in Foo.c_dict] # [consider-using-dict-items] +val = any(True for k8 in Foo.c_dict if Foo.c_dict[k8]) # [consider-using-dict-items] # Should emit warning when dict access done in ``if`` portion of comprehension val = any(True for k8 in b_dict if b_dict[k8]) # [consider-using-dict-items] @@ -71,6 +70,6 @@ class Foo: val = any(True for k8 in Foo.c_dict if b_dict[k8]) # Should NOT emit warning, essentially same check as above -c_dict={} +c_dict = {} val = [(k7, c_dict[k7]) for k7 in Foo.c_dict] val = any(True for k8 in Foo.c_dict if c_dict[k8]) diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index fedd8a8ecd..d5fd1f821d 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -1,13 +1,13 @@ -consider-using-dict-items:7:4:bad:Consider iterating with .items() -consider-using-dict-items:10:4:bad:Consider iterating with .items() -consider-using-dict-items:22:4:another_bad:Consider iterating with .items() -consider-using-dict-items:41:0::Consider iterating with .items() -consider-using-dict-items:45:0::Consider iterating with .items() -consider-iterating-dictionary:48:10::Consider iterating the dictionary directly instead of calling .keys() -consider-using-dict-items:48:0::Consider iterating with .items() -consider-using-dict-items:55:0::Consider iterating with .items() +consider-using-dict-items:6:4:bad:Consider iterating with .items() +consider-using-dict-items:9:4:bad:Consider iterating with .items() +consider-using-dict-items:21:4:another_bad:Consider iterating with .items() +consider-using-dict-items:40:0::Consider iterating with .items() +consider-using-dict-items:44:0::Consider iterating with .items() +consider-iterating-dictionary:47:10::Consider iterating the dictionary directly instead of calling .keys() +consider-using-dict-items:47:0::Consider iterating with .items() +consider-using-dict-items:54:0::Consider iterating with .items() +consider-using-dict-items:58:0::Consider iterating with .items() consider-using-dict-items:59:0::Consider iterating with .items() -consider-using-dict-items:60:0::Consider iterating with .items() +consider-using-dict-items:62:0::Consider iterating with .items() consider-using-dict-items:63:0::Consider iterating with .items() -consider-using-dict-items:64:0::Consider iterating with .items() -consider-using-dict-items:67:0::Consider iterating with .items() +consider-using-dict-items:66:0::Consider iterating with .items() From e8161a5148d16f7cdda8b97568b36bfe65cab57e Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Tue, 11 May 2021 19:53:02 +0800 Subject: [PATCH 08/13] Added more test cases to check for accessing other dicts --- .../functional/c/consider/consider_using_dict_items.py | 10 +++++++++- .../c/consider/consider_using_dict_items.txt | 10 +++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py index db3626e10a..f45d5eca41 100644 --- a/tests/functional/c/consider/consider_using_dict_items.py +++ b/tests/functional/c/consider/consider_using_dict_items.py @@ -54,6 +54,15 @@ class Foo: for k5 in Foo.c_dict: # [consider-using-dict-items] val = Foo.c_dict[k5] +c_dict = {} + +# Should NOT emit warning whey key used to access a different dict +for k5 in Foo.c_dict: # This is fine + val = b_dict[k5] + +for k5 in Foo.c_dict: # This is fine + val = c_dict[k5] + # Should emit warning within a list/dict comprehension val = {k9: b_dict[k9] for k9 in b_dict} # [consider-using-dict-items] val = [(k7, b_dict[k7]) for k7 in b_dict] # [consider-using-dict-items] @@ -70,6 +79,5 @@ class Foo: val = any(True for k8 in Foo.c_dict if b_dict[k8]) # Should NOT emit warning, essentially same check as above -c_dict = {} val = [(k7, c_dict[k7]) for k7 in Foo.c_dict] val = any(True for k8 in Foo.c_dict if c_dict[k8]) diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index d5fd1f821d..00451fb4d4 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -6,8 +6,8 @@ consider-using-dict-items:44:0::Consider iterating with .items() consider-iterating-dictionary:47:10::Consider iterating the dictionary directly instead of calling .keys() consider-using-dict-items:47:0::Consider iterating with .items() consider-using-dict-items:54:0::Consider iterating with .items() -consider-using-dict-items:58:0::Consider iterating with .items() -consider-using-dict-items:59:0::Consider iterating with .items() -consider-using-dict-items:62:0::Consider iterating with .items() -consider-using-dict-items:63:0::Consider iterating with .items() -consider-using-dict-items:66:0::Consider iterating with .items() +consider-using-dict-items:67:0::Consider iterating with .items() +consider-using-dict-items:68:0::Consider iterating with .items() +consider-using-dict-items:71:0::Consider iterating with .items() +consider-using-dict-items:72:0::Consider iterating with .items() +consider-using-dict-items:75:0::Consider iterating with .items() From f54b22d7a4db26c449d8c446d28f5c2723e77dd4 Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Tue, 11 May 2021 19:59:55 +0800 Subject: [PATCH 09/13] Cleaned up logic based on PR review --- pylint/checkers/refactoring/recommendation_checker.py | 4 ---- pylint/checkers/utils.py | 11 +++++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 2be19e4f5e..920842890e 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -155,8 +155,6 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: continue value = subscript.slice - if isinstance(value, astroid.Index): - value = value.value if ( not isinstance(value, astroid.Name) or value.name != node.target.name @@ -198,8 +196,6 @@ def visit_comprehension(self, node: astroid.Comprehension) -> None: continue value = subscript.slice - if isinstance(value, astroid.Index): - value = value.value if ( not isinstance(value, astroid.Name) or value.name != node.target.name diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 64fa300ae8..acaff104ac 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1504,12 +1504,11 @@ def get_iterating_dictionary_name( or a dictionary itself, this returns None. """ # Is it a proper keys call? - if isinstance(node.iter, astroid.Call): - if ( - isinstance(node.iter.func, astroid.Name) - or node.iter.func.attrname != "keys" - ): - return None + if ( + isinstance(node.iter, astroid.Call) + and isinstance(node.iter.func, astroid.Attribute) + and node.iter.func.attrname == "keys" + ): inferred = safe_infer(node.iter.func) if not isinstance(inferred, (astroid.BoundMethod, astroid.Dict)): return None From b1fb77b8bec724a0a3b6b03996753b256ca4d660 Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Tue, 11 May 2021 21:58:18 +0800 Subject: [PATCH 10/13] Added test case for .keys() within comprehension --- tests/functional/c/consider/consider_using_dict_items.py | 5 ++++- tests/functional/c/consider/consider_using_dict_items.txt | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py index f45d5eca41..d0af4fe0ac 100644 --- a/tests/functional/c/consider/consider_using_dict_items.py +++ b/tests/functional/c/consider/consider_using_dict_items.py @@ -1,5 +1,5 @@ """Emit a message for iteration through dict keys and subscripting dict with key.""" -# pylint: disable=missing-docstring,unsubscriptable-object,too-few-public-methods +# pylint: disable=line-too-long,missing-docstring,unsubscriptable-object,too-few-public-methods def bad(): a_dict = {1: 1, 2: 2, 3: 3} @@ -81,3 +81,6 @@ class Foo: # Should NOT emit warning, essentially same check as above val = [(k7, c_dict[k7]) for k7 in Foo.c_dict] val = any(True for k8 in Foo.c_dict if c_dict[k8]) + +# Should emit warning, using .keys() of Foo.c_dict +val = any(True for k8 in Foo.c_dict.keys() if Foo.c_dict[k8]) # [consider-iterating-dictionary,consider-using-dict-items] diff --git a/tests/functional/c/consider/consider_using_dict_items.txt b/tests/functional/c/consider/consider_using_dict_items.txt index 00451fb4d4..8ead82351b 100644 --- a/tests/functional/c/consider/consider_using_dict_items.txt +++ b/tests/functional/c/consider/consider_using_dict_items.txt @@ -11,3 +11,5 @@ consider-using-dict-items:68:0::Consider iterating with .items() consider-using-dict-items:71:0::Consider iterating with .items() consider-using-dict-items:72:0::Consider iterating with .items() consider-using-dict-items:75:0::Consider iterating with .items() +consider-iterating-dictionary:86:25::Consider iterating the dictionary directly instead of calling .keys() +consider-using-dict-items:86:0::Consider iterating with .items() From c79d112a96fa00c19e34024c713491df00999648 Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Tue, 11 May 2021 22:08:12 +0800 Subject: [PATCH 11/13] Revised codes based on review --- .../refactoring/recommendation_checker.py | 7 +++--- pylint/checkers/utils.py | 24 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 920842890e..fba4ab8b11 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -153,8 +153,9 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)): continue - value = subscript.slice + if isinstance(value, astroid.Index): + value = value.value if ( not isinstance(value, astroid.Name) or value.name != node.target.name @@ -185,7 +186,6 @@ def visit_comprehension(self, node: astroid.Comprehension) -> None: iterating_object_name = utils.get_iterating_dictionary_name(node) if iterating_object_name is None: return - children = list(node.parent.get_children()) if node.ifs: children.extend(node.ifs) @@ -194,8 +194,9 @@ def visit_comprehension(self, node: astroid.Comprehension) -> None: subscript = cast(astroid.Subscript, subscript) if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)): continue - value = subscript.slice + if isinstance(value, astroid.Index): + value = value.value if ( not isinstance(value, astroid.Name) or value.name != node.target.name diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index acaff104ac..470a1e68b9 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1504,20 +1504,18 @@ def get_iterating_dictionary_name( or a dictionary itself, this returns None. """ # Is it a proper keys call? - if ( + is_iterating_dict_keys = ( isinstance(node.iter, astroid.Call) and isinstance(node.iter.func, astroid.Attribute) and node.iter.func.attrname == "keys" - ): - inferred = safe_infer(node.iter.func) - if not isinstance(inferred, (astroid.BoundMethod, astroid.Dict)): - return None - return node.iter.as_string().partition(".")[0] + and isinstance(safe_infer(node.iter.func), astroid.BoundMethod) + ) + # Is it a proper dictionary? + is_iterating_dict = isinstance( + node.iter, (astroid.Name, astroid.Attribute) + ) and isinstance(safe_infer(node.iter), astroid.Dict) - # Is it a dictionary? - if not isinstance(node.iter, (astroid.Name, astroid.Attribute)): - return None - inferred = safe_infer(node.iter) - if not isinstance(inferred, (astroid.Dict, astroid.DictComp)): - return None - return node.iter.as_string() + if is_iterating_dict or is_iterating_dict_keys: + return node.iter.as_string().partition(".keys")[0] + + return None From 797b050525a8fe4ed615b15505733da6e781a199 Mon Sep 17 00:00:00 2001 From: Pang Yu Shao Date: Tue, 11 May 2021 23:33:53 +0800 Subject: [PATCH 12/13] Improved readability --- .../refactoring/recommendation_checker.py | 4 ++++ pylint/checkers/utils.py | 21 +++++++++++-------- .../c/consider/consider_using_dict_items.py | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index fba4ab8b11..2ff58b8fe8 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -153,6 +153,7 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)): continue + value = subscript.slice if isinstance(value, astroid.Index): value = value.value @@ -186,14 +187,17 @@ def visit_comprehension(self, node: astroid.Comprehension) -> None: iterating_object_name = utils.get_iterating_dictionary_name(node) if iterating_object_name is None: return + children = list(node.parent.get_children()) if node.ifs: children.extend(node.ifs) for child in children: for subscript in child.nodes_of_class(astroid.Subscript): subscript = cast(astroid.Subscript, subscript) + if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)): continue + value = subscript.slice if isinstance(value, astroid.Index): value = value.value diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 470a1e68b9..8cf97d8074 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1504,18 +1504,21 @@ def get_iterating_dictionary_name( or a dictionary itself, this returns None. """ # Is it a proper keys call? - is_iterating_dict_keys = ( + if ( isinstance(node.iter, astroid.Call) and isinstance(node.iter.func, astroid.Attribute) and node.iter.func.attrname == "keys" - and isinstance(safe_infer(node.iter.func), astroid.BoundMethod) - ) - # Is it a proper dictionary? - is_iterating_dict = isinstance( - node.iter, (astroid.Name, astroid.Attribute) - ) and isinstance(safe_infer(node.iter), astroid.Dict) + ): + inferred = safe_infer(node.iter.func) + if not isinstance(inferred, astroid.BoundMethod): + return None + return node.iter.as_string().rpartition(".keys")[0] - if is_iterating_dict or is_iterating_dict_keys: - return node.iter.as_string().partition(".keys")[0] + # Is it a dictionary? + if isinstance(node.iter, (astroid.Name, astroid.Attribute)): + inferred = safe_infer(node.iter) + if not isinstance(inferred, astroid.Dict): + return None + return node.iter.as_string() return None diff --git a/tests/functional/c/consider/consider_using_dict_items.py b/tests/functional/c/consider/consider_using_dict_items.py index d0af4fe0ac..1d6c86741b 100644 --- a/tests/functional/c/consider/consider_using_dict_items.py +++ b/tests/functional/c/consider/consider_using_dict_items.py @@ -83,4 +83,4 @@ class Foo: val = any(True for k8 in Foo.c_dict if c_dict[k8]) # Should emit warning, using .keys() of Foo.c_dict -val = any(True for k8 in Foo.c_dict.keys() if Foo.c_dict[k8]) # [consider-iterating-dictionary,consider-using-dict-items] +val = any(True for k8 in Foo.c_dict.keys() if Foo.c_dict[k8]) # [consider-iterating-dictionary,consider-using-dict-items] From d9ccab139233faac37361026bd8dbc1316523430 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 11 May 2021 21:46:18 +0200 Subject: [PATCH 13/13] Last small style changes --- pylint/checkers/refactoring/recommendation_checker.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 2ff58b8fe8..6efc4190c4 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -171,9 +171,9 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None: # defined and compare that to the for loop's line number continue if ( - isinstance(subscript.parent, (astroid.Assign)) + isinstance(subscript.parent, astroid.Assign) and subscript in subscript.parent.targets - or isinstance(subscript.parent, (astroid.AugAssign)) + or isinstance(subscript.parent, astroid.AugAssign) and subscript == subscript.parent.target ): # Ignore this subscript if it is the target of an assignment @@ -207,5 +207,6 @@ def visit_comprehension(self, node: astroid.Comprehension) -> None: or iterating_object_name != subscript.value.as_string() ): continue + self.add_message("consider-using-dict-items", node=node) return