1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
from typing import List
import astroid
from pylint import checkers, interfaces
from pylint.checkers import utils
class LenChecker(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 = {
"C1801": (
"Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty",
"len-as-condition",
"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.",
)
}
priority = -2
options = ()
@utils.check_messages("len-as-condition")
def visit_call(self, node):
# 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, astroid.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 = (
astroid.ListComp,
astroid.SetComp,
astroid.DictComp,
astroid.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("len-as-condition", node=node)
return
try:
instance = next(len_arg.infer())
except astroid.InferenceError:
# Probably undefined-varible, 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("len-as-condition", node=node)
@staticmethod
def instance_has_bool(class_def: astroid.ClassDef) -> bool:
try:
class_def.getattr("__bool__")
return True
except astroid.AttributeInferenceError:
...
return False
@utils.check_messages("len-as-condition")
def visit_unaryop(self, node):
"""`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, astroid.UnaryOp)
and node.op == "not"
and utils.is_call_of_name(node.operand, "len")
):
self.add_message("len-as-condition", node=node)
@staticmethod
def base_classes_of_node(instance: astroid.nodes.ClassDef) -> List[astroid.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]
|