diff options
Diffstat (limited to 'pylint/checkers/refactoring.py')
-rw-r--r-- | pylint/checkers/refactoring.py | 95 |
1 files changed, 94 insertions, 1 deletions
diff --git a/pylint/checkers/refactoring.py b/pylint/checkers/refactoring.py index 4d93299d9..9a66a71e7 100644 --- a/pylint/checkers/refactoring.py +++ b/pylint/checkers/refactoring.py @@ -36,6 +36,7 @@ """Looks for code which can be refactored.""" import builtins import collections +import copy import itertools import tokenize from functools import reduce @@ -126,6 +127,8 @@ def _is_test_condition( parent = parent or node.parent if isinstance(parent, (astroid.While, astroid.If, astroid.IfExp, astroid.Assert)): return node is parent.test or parent.test.parent_of(node) + if isinstance(parent, astroid.Comprehension): + return node in parent.ifs return _is_call_of_name(parent, "bool") and parent.parent_of(node) @@ -157,6 +160,16 @@ class RefactoringChecker(checkers.BaseTokenChecker): "simplify-boolean-expression", "Emitted when redundant pre-python 2.5 ternary syntax is used.", ), + "R1726": ( + "Boolean condition '%s' may be simplified to '%s'", + "simplifiable-condition", + "Emitted when a boolean condition is able to be simplified.", + ), + "R1727": ( + "Boolean condition '%s' will always evaluate to '%s'", + "condition-evals-to-constant", + "Emitted when a boolean condition can be simplified to a constant value.", + ), "R1702": ( "Too many nested blocks (%s/%s)", "too-many-nested-blocks", @@ -361,6 +374,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): self._elifs = [] self._nested_blocks_msg = None self._reported_swap_nodes = set() + self._can_simplify_bool_op = False def open(self): # do this in open since config not fully initialized in __init__ @@ -992,13 +1006,92 @@ class RefactoringChecker(checkers.BaseTokenChecker): self.add_message("chained-comparison", node=node) break + @staticmethod + def _apply_boolean_simplification_rules(operator, values): + """Removes irrelevant values or returns shortcircuiting values + + This function applies the following two rules: + 1) an OR expression with True in it will always be true, and the + reverse for AND + + 2) False values in OR expressions are only relevant if all values are + false, and the reverse for AND""" + simplified_values = [] + + for subnode in values: + inferred_bool = None + if not next(subnode.nodes_of_class(astroid.Name), False): + inferred = utils.safe_infer(subnode) + if inferred: + inferred_bool = inferred.bool_value() + + if not isinstance(inferred_bool, bool): + simplified_values.append(subnode) + elif (operator == "or") == inferred_bool: + return [subnode] + + return simplified_values or [astroid.Const(operator == "and")] + + def _simplify_boolean_operation(self, bool_op): + """Attempts to simplify a boolean operation + + Recursively applies simplification on the operator terms, + and keeps track of whether reductions have been made.""" + children = list(bool_op.get_children()) + intermediate = [ + self._simplify_boolean_operation(child) + if isinstance(child, astroid.BoolOp) + else child + for child in children + ] + result = self._apply_boolean_simplification_rules(bool_op.op, intermediate) + if len(result) < len(children): + self._can_simplify_bool_op = True + if len(result) == 1: + return result[0] + simplified_bool_op = copy.copy(bool_op) + simplified_bool_op.postinit(result) + return simplified_bool_op + + def _check_simplifiable_condition(self, node): + """Check if a boolean condition can be simplified. + + Variables will not be simplified, even in the value can be inferred, + and expressions like '3 + 4' will remain expanded.""" + if not _is_test_condition(node): + return + + self._can_simplify_bool_op = False + simplified_expr = self._simplify_boolean_operation(node) + + if not self._can_simplify_bool_op: + return + + if not next(simplified_expr.nodes_of_class(astroid.Name), False): + self.add_message( + "condition-evals-to-constant", + node=node, + args=(node.as_string(), simplified_expr.as_string()), + ) + else: + self.add_message( + "simplifiable-condition", + node=node, + args=(node.as_string(), simplified_expr.as_string()), + ) + @utils.check_messages( - "consider-merging-isinstance", "consider-using-in", "chained-comparison" + "consider-merging-isinstance", + "consider-using-in", + "chained-comparison", + "simplifiable-condition", + "condition-evals-to-constant", ) def visit_boolop(self, node): self._check_consider_merging_isinstance(node) self._check_consider_using_in(node) self._check_chained_comparison(node) + self._check_simplifiable_condition(node) @staticmethod def _is_simple_assignment(node): |