summaryrefslogtreecommitdiff
path: root/pylint/checkers/refactoring/implicit_booleaness_checker.py
diff options
context:
space:
mode:
authorJaehoon Hwang <jaehoonhwang@users.noreply.github.com>2021-10-09 23:18:46 -0700
committerGitHub <noreply@github.com>2021-10-10 08:18:46 +0200
commit220e27dc5bdd6bdd9dbee56d5c7d33a946c8ad17 (patch)
treec3f34d216acce183b92b7007f4a38811169d4c66 /pylint/checkers/refactoring/implicit_booleaness_checker.py
parent661703f3c9f030420d6559433b621abdc27ac7b5 (diff)
downloadpylint-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.py123
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]