diff options
author | Jaehoon Hwang <jaehoonhwang@users.noreply.github.com> | 2021-10-09 23:18:46 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-10 08:18:46 +0200 |
commit | 220e27dc5bdd6bdd9dbee56d5c7d33a946c8ad17 (patch) | |
tree | c3f34d216acce183b92b7007f4a38811169d4c66 /pylint/checkers/refactoring/implicit_booleaness_checker.py | |
parent | 661703f3c9f030420d6559433b621abdc27ac7b5 (diff) | |
download | pylint-git-220e27dc5bdd6bdd9dbee56d5c7d33a946c8ad17.tar.gz |
Rename `len-as-condition` to `use-implicit-booleaness-not-len` (#5132)
Rename `len-as-condition` to be more general for new checker
`use-implicit-booleaness-not-comparison`
* Refactor `LenChecker` class -> `ImplicitBooleanessChecker`o
* Rename test files/`len_checker.py`/`__init__.py` to reflect new name.
* Add `len-as-condition` as `old_names` for `use-implicit-booleaness-not-len`
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
Diffstat (limited to 'pylint/checkers/refactoring/implicit_booleaness_checker.py')
-rw-r--r-- | pylint/checkers/refactoring/implicit_booleaness_checker.py | 123 |
1 files changed, 123 insertions, 0 deletions
diff --git a/pylint/checkers/refactoring/implicit_booleaness_checker.py b/pylint/checkers/refactoring/implicit_booleaness_checker.py new file mode 100644 index 000000000..98605635c --- /dev/null +++ b/pylint/checkers/refactoring/implicit_booleaness_checker.py @@ -0,0 +1,123 @@ +# 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 List + +import astroid +from astroid import nodes + +from pylint import checkers, interfaces +from pylint.checkers import utils + + +class ImplicitBooleanessChecker(checkers.BaseChecker): + """Checks for incorrect usage of len() inside conditions. + Pep8 states: + For sequences, (strings, lists, tuples), use the fact that empty sequences are false. + + Yes: if not seq: + if seq: + + No: if len(seq): + if not len(seq): + + Problems detected: + * if len(sequence): + * if not len(sequence): + * elif len(sequence): + * elif not len(sequence): + * while len(sequence): + * while not len(sequence): + * assert len(sequence): + * assert not len(sequence): + * bool(len(sequence)) + """ + + __implements__ = (interfaces.IAstroidChecker,) + + # configuration section name + name = "refactoring" + msgs = { + "C1802": ( + "Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty", + "use-implicit-booleaness-not-len", + "Used when Pylint detects that len(sequence) is being used " + "without explicit comparison inside a condition to determine if a sequence is empty. " + "Instead of coercing the length to a boolean, either " + "rely on the fact that empty sequences are false or " + "compare the length against a scalar.", + {"old_names": [("C1801", "len-as-condition")]}, + ), + } + + priority = -2 + options = () + + @utils.check_messages("use-implicit-booleaness-not-len") + def visit_call(self, node: nodes.Call) -> None: + # a len(S) call is used inside a test condition + # could be if, while, assert or if expression statement + # e.g. `if len(S):` + if not utils.is_call_of_name(node, "len"): + return + # the len() call could also be nested together with other + # boolean operations, e.g. `if z or len(x):` + parent = node.parent + while isinstance(parent, nodes.BoolOp): + parent = parent.parent + # we're finally out of any nested boolean operations so check if + # this len() call is part of a test condition + if not utils.is_test_condition(node, parent): + return + len_arg = node.args[0] + generator_or_comprehension = ( + nodes.ListComp, + nodes.SetComp, + nodes.DictComp, + nodes.GeneratorExp, + ) + if isinstance(len_arg, generator_or_comprehension): + # The node is a generator or comprehension as in len([x for x in ...]) + self.add_message("use-implicit-booleaness-not-len", node=node) + return + try: + instance = next(len_arg.infer()) + except astroid.InferenceError: + # Probably undefined-variable, abort check + return + mother_classes = self.base_classes_of_node(instance) + affected_by_pep8 = any( + t in mother_classes for t in ("str", "tuple", "list", "set") + ) + if "range" in mother_classes or ( + affected_by_pep8 and not self.instance_has_bool(instance) + ): + self.add_message("use-implicit-booleaness-not-len", node=node) + + @staticmethod + def instance_has_bool(class_def: nodes.ClassDef) -> bool: + try: + class_def.getattr("__bool__") + return True + except astroid.AttributeInferenceError: + ... + return False + + @utils.check_messages("use-implicit-booleaness-not-len") + def visit_unaryop(self, node: nodes.UnaryOp) -> None: + """`not len(S)` must become `not S` regardless if the parent block + is a test condition or something else (boolean expression) + e.g. `if not len(S):`""" + if ( + isinstance(node, nodes.UnaryOp) + and node.op == "not" + and utils.is_call_of_name(node.operand, "len") + ): + self.add_message("use-implicit-booleaness-not-len", node=node) + + @staticmethod + def base_classes_of_node(instance: nodes.ClassDef) -> List[nodes.Name]: + """Return all the classes names that a ClassDef inherit from including 'object'.""" + try: + return [instance.name] + [x.name for x in instance.ancestors()] + except TypeError: + return [instance.name] |