diff options
author | Dani Alcala <112832187+clavedeluna@users.noreply.github.com> | 2022-11-17 10:28:10 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-17 14:28:10 +0100 |
commit | 325e0d45dd3f6e721de9f08e6b3908f31278534d (patch) | |
tree | 926dd1d918515f38c0ab043edfcf50141535530c /pylint/extensions | |
parent | ed99ffd02eb107f5cd289c5c13175a8480c81f67 (diff) | |
download | pylint-git-325e0d45dd3f6e721de9f08e6b3908f31278534d.tar.gz |
Fix false negatives for-any-all (#7707)
Diffstat (limited to 'pylint/extensions')
-rw-r--r-- | pylint/extensions/for_any_all.py | 100 |
1 files changed, 93 insertions, 7 deletions
diff --git a/pylint/extensions/for_any_all.py b/pylint/extensions/for_any_all.py index e6ab41c3f..257b5d37f 100644 --- a/pylint/extensions/for_any_all.py +++ b/pylint/extensions/for_any_all.py @@ -11,7 +11,12 @@ from typing import TYPE_CHECKING from astroid import nodes from pylint.checkers import BaseChecker -from pylint.checkers.utils import only_required_for_messages, returns_bool +from pylint.checkers.utils import ( + assigned_bool, + only_required_for_messages, + returns_bool, +) +from pylint.interfaces import HIGH if TYPE_CHECKING: from pylint.lint.pylinter import PyLinter @@ -36,19 +41,100 @@ class ConsiderUsingAnyOrAllChecker(BaseChecker): return if_children = list(node.body[0].get_children()) - if not len(if_children) == 2: # The If node has only a comparison and return - return - if not returns_bool(if_children[1]): + if any(isinstance(child, nodes.If) for child in if_children): + # an if node within the if-children indicates an elif clause, + # suggesting complex logic. return - # Check for terminating boolean return right after the loop node_after_loop = node.next_sibling() - if returns_bool(node_after_loop): + + if self._assigned_reassigned_returned(node, if_children, node_after_loop): + final_return_bool = node_after_loop.value.name + suggested_string = self._build_suggested_string(node, final_return_bool) + self.add_message( + "consider-using-any-or-all", + node=node, + args=suggested_string, + confidence=HIGH, + ) + return + + if self._if_statement_returns_bool(if_children, node_after_loop): final_return_bool = node_after_loop.value.value suggested_string = self._build_suggested_string(node, final_return_bool) self.add_message( - "consider-using-any-or-all", node=node, args=suggested_string + "consider-using-any-or-all", + node=node, + args=suggested_string, + confidence=HIGH, ) + return + + @staticmethod + def _if_statement_returns_bool( + if_children: list[nodes.NodeNG], node_after_loop: nodes.NodeNG + ) -> bool: + """Detect for-loop, if-statement, return pattern: + + Ex: + def any_uneven(items): + for item in items: + if not item % 2 == 0: + return True + return False + """ + if not len(if_children) == 2: + # The If node has only a comparison and return + return False + if not returns_bool(if_children[1]): + return False + + # Check for terminating boolean return right after the loop + return returns_bool(node_after_loop) + + @staticmethod + def _assigned_reassigned_returned( + node: nodes.For, if_children: list[nodes.NodeNG], node_after_loop: nodes.NodeNG + ) -> bool: + """Detect boolean-assign, for-loop, re-assign, return pattern: + + Ex: + def check_lines(lines, max_chars): + long_line = False + for line in lines: + if len(line) > max_chars: + long_line = True + # no elif / else statement + return long_line + """ + node_before_loop = node.previous_sibling() + + if not assigned_bool(node_before_loop): + # node before loop isn't assigning to boolean + return False + + assign_children = [x for x in if_children if isinstance(x, nodes.Assign)] + if not assign_children: + # if-nodes inside loop aren't assignments + return False + + # We only care for the first assign node of the if-children. Otherwise it breaks the pattern. + first_target = assign_children[0].targets[0] + target_before_loop = node_before_loop.targets[0] + + if not ( + isinstance(first_target, nodes.AssignName) + and isinstance(target_before_loop, nodes.AssignName) + ): + return False + + node_before_loop_name = node_before_loop.targets[0].name + return ( + first_target.name == node_before_loop_name + and isinstance(node_after_loop, nodes.Return) + and isinstance(node_after_loop.value, nodes.Name) + and node_after_loop.value.name == node_before_loop_name + ) @staticmethod def _build_suggested_string(node: nodes.For, final_return_bool: bool) -> str: |