diff options
author | orSolocate <38433858+orSolocate@users.noreply.github.com> | 2022-02-02 00:01:21 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-01 23:01:21 +0100 |
commit | 2d560189f1e2050ce022ec48981cce1f7a88b4d9 (patch) | |
tree | d7f64b569fa2acecce3f8a54850897136ec30c6b /pylint/checkers/modified_iterating_checker.py | |
parent | f7ba0dcfce82dbbd3ef16e3c3a63e67f5f2d823d (diff) | |
download | pylint-git-2d560189f1e2050ce022ec48981cce1f7a88b4d9.tar.gz |
Add `iterating-modified-list` checker for modified lists (#5628)
Diffstat (limited to 'pylint/checkers/modified_iterating_checker.py')
-rw-r--r-- | pylint/checkers/modified_iterating_checker.py | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/pylint/checkers/modified_iterating_checker.py b/pylint/checkers/modified_iterating_checker.py new file mode 100644 index 000000000..711d37bb0 --- /dev/null +++ b/pylint/checkers/modified_iterating_checker.py @@ -0,0 +1,154 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE + +from typing import TYPE_CHECKING, Union + +from astroid import nodes + +from pylint import checkers, interfaces +from pylint.checkers import utils + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +_LIST_MODIFIER_METHODS = {"append", "remove"} +_SET_MODIFIER_METHODS = {"add", "remove"} + + +class ModifiedIterationChecker(checkers.BaseChecker): + """Checks for modified iterators in for loops iterations. + + Currently supports `for` loops for Sets, Dictionaries and Lists. + """ + + __implements__ = interfaces.IAstroidChecker + + name = "modified_iteration" + + msgs = { + "W4701": ( + "Iterated list '%s' is being modified inside for loop body, consider iterating through a copy of it " + "instead.", + "modified-iterating-list", + "Emitted when items are added or removed to a list being iterated through. " + "Doing so can result in unexpected behaviour, that is why it is preferred to use a copy of the list.", + ), + "E4702": ( + "Iterated dict '%s' is being modified inside for loop body, iterate through a copy of it instead.", + "modified-iterating-dict", + "Emitted when items are added or removed to a dict being iterated through. " + "Doing so raises a RuntimeError.", + ), + "E4703": ( + "Iterated set '%s' is being modified inside for loop body, iterate through a copy of it instead.", + "modified-iterating-set", + "Emitted when items are added or removed to a set being iterated through. " + "Doing so raises a RuntimeError.", + ), + } + + options = () + priority = -2 + + @utils.check_messages( + "modified-iterating-list", "modified-iterating-dict", "modified-iterating-set" + ) + def visit_for(self, node: nodes.For) -> None: + iter_obj = node.iter + for body_node in node.body: + self._modified_iterating_check_on_node_and_children(body_node, iter_obj) + + def _modified_iterating_check_on_node_and_children( + self, body_node: nodes.NodeNG, iter_obj: nodes.NodeNG + ) -> None: + """See if node or any of its children raises modified iterating messages.""" + self._modified_iterating_check(body_node, iter_obj) + for child in body_node.get_children(): + self._modified_iterating_check_on_node_and_children(child, iter_obj) + + def _modified_iterating_check( + self, node: nodes.NodeNG, iter_obj: nodes.NodeNG + ) -> None: + msg_id = None + if self._modified_iterating_list_cond(node, iter_obj): + msg_id = "modified-iterating-list" + elif self._modified_iterating_dict_cond(node, iter_obj): + msg_id = "modified-iterating-dict" + elif self._modified_iterating_set_cond(node, iter_obj): + msg_id = "modified-iterating-set" + if msg_id: + self.add_message( + msg_id, + node=node, + args=(iter_obj.name,), + confidence=interfaces.INFERENCE, + ) + + @staticmethod + def _is_node_expr_that_calls_attribute_name(node: nodes.NodeNG) -> bool: + return ( + isinstance(node, nodes.Expr) + and isinstance(node.value, nodes.Call) + and isinstance(node.value.func, nodes.Attribute) + and isinstance(node.value.func.expr, nodes.Name) + ) + + @staticmethod + def _common_cond_list_set( + node: nodes.Expr, + iter_obj: nodes.NodeNG, + infer_val: Union[nodes.List, nodes.Set], + ) -> bool: + return (infer_val == utils.safe_infer(iter_obj)) and ( + node.value.func.expr.name == iter_obj.name + ) + + @staticmethod + def _is_node_assigns_subscript_name(node: nodes.NodeNG) -> bool: + return isinstance(node, nodes.Assign) and ( + isinstance(node.targets[0], nodes.Subscript) + and (isinstance(node.targets[0].value, nodes.Name)) + ) + + def _modified_iterating_list_cond( + self, node: nodes.NodeNG, iter_obj: nodes.NodeNG + ) -> bool: + if not self._is_node_expr_that_calls_attribute_name(node): + return False + infer_val = utils.safe_infer(node.value.func.expr) + if not isinstance(infer_val, nodes.List): + return False + return ( + self._common_cond_list_set(node, iter_obj, infer_val) + and node.value.func.attrname in _LIST_MODIFIER_METHODS + ) + + def _modified_iterating_dict_cond( + self, node: nodes.NodeNG, iter_obj: nodes.NodeNG + ) -> bool: + if not self._is_node_assigns_subscript_name(node): + return False + infer_val = utils.safe_infer(node.targets[0].value) + if not isinstance(infer_val, nodes.Dict): + return False + if infer_val != utils.safe_infer(iter_obj): + return False + return node.targets[0].value.name == iter_obj.name + + def _modified_iterating_set_cond( + self, node: nodes.NodeNG, iter_obj: nodes.NodeNG + ) -> bool: + if not self._is_node_expr_that_calls_attribute_name(node): + return False + infer_val = utils.safe_infer(node.value.func.expr) + if not isinstance(infer_val, nodes.Set): + return False + return ( + self._common_cond_list_set(node, iter_obj, infer_val) + and node.value.func.attrname in _SET_MODIFIER_METHODS + ) + + +def register(linter: "PyLinter") -> None: + linter.register_checker(ModifiedIterationChecker(linter)) |