Skip to content

Commit 4714a65

Browse files
PaulRenvoisePCManticore
authored andcommitted
Make len-as-condition only fire when a len(x) call is made without an explicit comparison
This commit reduce the scope of `len-as-condition` to only care when a `len(SEQUENCE)` call is made without an explicit comparison, as stated in PEP8.
1 parent 7b5e0e7 commit 4714a65

File tree

6 files changed

+181
-185
lines changed

6 files changed

+181
-185
lines changed

CONTRIBUTORS.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,4 +278,6 @@ contributors:
278278

279279
* Goudcode: contributor
280280

281+
* Paul Renvoise : contributor
282+
281283
* Bluesheeptoken: contributor

ChangeLog

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ What's New in Pylint 2.4.0?
77

88
Release date: TBA
99

10+
* ``len-as-condition`` now only fires when a ``len(x)`` call is made without an explicit comparison
11+
12+
The message and description accompanying this checker has been changed
13+
reflect this new behavior, by explicitly asking to either rely on the
14+
fact that empty sequence are false or to compare the length with a scalar.
15+
16+
Close #2684
17+
1018
* ``assigning-non-slot`` not emitted for classes with unknown base classes.
1119

1220
Close #2807
@@ -30,7 +38,6 @@ Release date: TBA
3038
This check is emitted when ``pylint`` finds a class variable that conflicts with a slot
3139
name, which would raise a ``ValueError`` at runtime.
3240

33-
3441
* Fix issue with pylint name in output of python -m pylint --version
3542

3643
Close #2764

doc/whatsnew/2.4.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,32 @@ New checkers
2828
Other Changes
2929
=============
3030

31+
* ``len-as-condition`` now only fires when a ``len(x)`` call is made without an explicit comparison.
32+
33+
The message and description accompanying this checker has been changed
34+
reflect this new behavior, by explicitly asking to either rely on the
35+
fact that empty sequence are false or to compare the length with a scalar.
36+
37+
OK::
38+
39+
if len(x) == 0:
40+
pass
41+
42+
while not len(x) == 0:
43+
pass
44+
45+
assert len(x) > 5, message
46+
47+
KO::
48+
49+
if not len(x):
50+
pass
51+
52+
while len(x) and other_cond:
53+
pass
54+
55+
assert len(x), message
56+
3157
* A file is now read from stdin if the ``--from-stdin`` flag is used on the
3258
command line. In addition to the ``--from-stdin`` flag a (single) file
3359
name needs to be specified on the command line, which is needed for the

pylint/checkers/refactoring.py

Lines changed: 76 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,69 @@ def _if_statement_is_always_returning(if_node, returning_node_class):
4747
return False
4848

4949

50+
def _is_len_call(node):
51+
"""Checks if node is len(SOMETHING)."""
52+
return (
53+
isinstance(node, astroid.Call)
54+
and isinstance(node.func, astroid.Name)
55+
and node.func.name == "len"
56+
)
57+
58+
59+
def _is_constant_zero(node):
60+
return isinstance(node, astroid.Const) and node.value == 0
61+
62+
63+
def _node_is_test_condition(node):
64+
""" Checks if node is an if, while, assert or if expression statement."""
65+
return isinstance(node, (astroid.If, astroid.While, astroid.Assert, astroid.IfExp))
66+
67+
68+
def _is_trailing_comma(tokens, index):
69+
"""Check if the given token is a trailing comma
70+
71+
:param tokens: Sequence of modules tokens
72+
:type tokens: list[tokenize.TokenInfo]
73+
:param int index: Index of token under check in tokens
74+
:returns: True if the token is a comma which trails an expression
75+
:rtype: bool
76+
"""
77+
token = tokens[index]
78+
if token.exact_type != tokenize.COMMA:
79+
return False
80+
# Must have remaining tokens on the same line such as NEWLINE
81+
left_tokens = itertools.islice(tokens, index + 1, None)
82+
same_line_remaining_tokens = list(
83+
itertools.takewhile(
84+
lambda other_token, _token=token: other_token.start[0] == _token.start[0],
85+
left_tokens,
86+
)
87+
)
88+
# Note: If the newline is tokenize.NEWLINE and not tokenize.NL
89+
# then the newline denotes the end of expression
90+
is_last_element = all(
91+
other_token.type in (tokenize.NEWLINE, tokenize.COMMENT)
92+
for other_token in same_line_remaining_tokens
93+
)
94+
if not same_line_remaining_tokens or not is_last_element:
95+
return False
96+
97+
def get_curline_index_start():
98+
"""Get the index denoting the start of the current line"""
99+
for subindex, token in enumerate(reversed(tokens[:index])):
100+
# See Lib/tokenize.py and Lib/token.py in cpython for more info
101+
if token.type in (tokenize.NEWLINE, tokenize.NL):
102+
return index - subindex
103+
return 0
104+
105+
curline_start = get_curline_index_start()
106+
expected_tokens = {"return", "yield"}
107+
for prevtoken in tokens[curline_start:index]:
108+
if "=" in prevtoken.string or prevtoken.string in expected_tokens:
109+
return True
110+
return False
111+
112+
50113
class RefactoringChecker(checkers.BaseTokenChecker):
51114
"""Looks for code which can be refactored
52115
@@ -355,7 +418,7 @@ def process_tokens(self, tokens):
355418
# tokens[index][2] is the actual position and also is
356419
# reported by IronPython.
357420
self._elifs.extend([tokens[index][2], tokens[index + 1][2]])
358-
elif is_trailing_comma(tokens, index):
421+
elif _is_trailing_comma(tokens, index):
359422
if self.linter.is_message_enabled("trailing-comma-tuple"):
360423
self.add_message("trailing-comma-tuple", line=token.start[0])
361424

@@ -1239,28 +1302,6 @@ def visit_unaryop(self, node):
12391302
)
12401303

12411304

1242-
def _is_len_call(node):
1243-
"""Checks if node is len(SOMETHING)."""
1244-
return (
1245-
isinstance(node, astroid.Call)
1246-
and isinstance(node.func, astroid.Name)
1247-
and node.func.name == "len"
1248-
)
1249-
1250-
1251-
def _is_constant_zero(node):
1252-
return isinstance(node, astroid.Const) and node.value == 0
1253-
1254-
1255-
def _has_constant_value(node, value):
1256-
return isinstance(node, astroid.Const) and node.value == value
1257-
1258-
1259-
def _node_is_test_condition(node):
1260-
""" Checks if node is an if, while, assert or if expression statement."""
1261-
return isinstance(node, (astroid.If, astroid.While, astroid.Assert, astroid.IfExp))
1262-
1263-
12641305
class LenChecker(checkers.BaseChecker):
12651306
"""Checks for incorrect usage of len() inside conditions.
12661307
Pep8 states:
@@ -1275,11 +1316,12 @@ class LenChecker(checkers.BaseChecker):
12751316
Problems detected:
12761317
* if len(sequence):
12771318
* if not len(sequence):
1278-
* if len(sequence) == 0:
1279-
* if len(sequence) != 0:
1280-
* if len(sequence) > 0:
1281-
* if len(sequence) < 1:
1282-
* if len(sequence) <= 0:
1319+
* elif len(sequence):
1320+
* elif not len(sequence):
1321+
* while len(sequence):
1322+
* while not len(sequence):
1323+
* assert len(sequence):
1324+
* assert not len(sequence):
12831325
"""
12841326

12851327
__implements__ = (interfaces.IAstroidChecker,)
@@ -1288,12 +1330,13 @@ class LenChecker(checkers.BaseChecker):
12881330
name = "refactoring"
12891331
msgs = {
12901332
"C1801": (
1291-
"Do not use `len(SEQUENCE)` to determine if a sequence is empty",
1333+
"Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty",
12921334
"len-as-condition",
1293-
"Used when Pylint detects that len(sequence) is being used inside "
1294-
"a condition to determine if a sequence is empty. Instead of "
1295-
"comparing the length to 0, rely on the fact that empty sequences "
1296-
"are false.",
1335+
"Used when Pylint detects that len(sequence) is being used "
1336+
"without explicit comparison inside a condition to determine if a sequence is empty. "
1337+
"Instead of coercing the length to a boolean, either "
1338+
"rely on the fact that empty sequences are false or "
1339+
"compare the length against a scalar.",
12971340
)
12981341
}
12991342

@@ -1332,109 +1375,6 @@ def visit_unaryop(self, node):
13321375
):
13331376
self.add_message("len-as-condition", node=node)
13341377

1335-
@utils.check_messages("len-as-condition")
1336-
def visit_compare(self, node):
1337-
# compare nodes are trickier because the len(S) expression
1338-
# may be somewhere in the middle of the node
1339-
1340-
# note: astroid.Compare has the left most operand in node.left
1341-
# while the rest are a list of tuples in node.ops
1342-
# the format of the tuple is ('compare operator sign', node)
1343-
# here we squash everything into `ops` to make it easier for processing later
1344-
ops = [("", node.left)]
1345-
ops.extend(node.ops)
1346-
ops = list(itertools.chain(*ops))
1347-
1348-
for ops_idx in range(len(ops) - 2):
1349-
op_1 = ops[ops_idx]
1350-
op_2 = ops[ops_idx + 1]
1351-
op_3 = ops[ops_idx + 2]
1352-
error_detected = False
1353-
1354-
# 0 ?? len()
1355-
if (
1356-
_is_constant_zero(op_1)
1357-
and op_2 in ["==", "!=", "<", ">="]
1358-
and _is_len_call(op_3)
1359-
):
1360-
error_detected = True
1361-
# len() ?? 0
1362-
elif (
1363-
_is_len_call(op_1)
1364-
and op_2 in ["==", "!=", ">", "<="]
1365-
and _is_constant_zero(op_3)
1366-
):
1367-
error_detected = True
1368-
elif (
1369-
_has_constant_value(op_1, value=1)
1370-
and op_2 == ">"
1371-
and _is_len_call(op_3)
1372-
):
1373-
error_detected = True
1374-
elif (
1375-
_is_len_call(op_1)
1376-
and op_2 == "<"
1377-
and _has_constant_value(op_3, value=1)
1378-
):
1379-
error_detected = True
1380-
1381-
if error_detected:
1382-
parent = node.parent
1383-
# traverse the AST to figure out if this comparison was part of
1384-
# a test condition
1385-
while parent and not _node_is_test_condition(parent):
1386-
parent = parent.parent
1387-
1388-
# report only if this len() comparison is part of a test condition
1389-
# for example: return len() > 0 should not report anything
1390-
if _node_is_test_condition(parent):
1391-
self.add_message("len-as-condition", node=node)
1392-
1393-
1394-
def is_trailing_comma(tokens, index):
1395-
"""Check if the given token is a trailing comma
1396-
1397-
:param tokens: Sequence of modules tokens
1398-
:type tokens: list[tokenize.TokenInfo]
1399-
:param int index: Index of token under check in tokens
1400-
:returns: True if the token is a comma which trails an expression
1401-
:rtype: bool
1402-
"""
1403-
token = tokens[index]
1404-
if token.exact_type != tokenize.COMMA:
1405-
return False
1406-
# Must have remaining tokens on the same line such as NEWLINE
1407-
left_tokens = itertools.islice(tokens, index + 1, None)
1408-
same_line_remaining_tokens = list(
1409-
itertools.takewhile(
1410-
lambda other_token, _token=token: other_token.start[0] == _token.start[0],
1411-
left_tokens,
1412-
)
1413-
)
1414-
# Note: If the newline is tokenize.NEWLINE and not tokenize.NL
1415-
# then the newline denotes the end of expression
1416-
is_last_element = all(
1417-
other_token.type in (tokenize.NEWLINE, tokenize.COMMENT)
1418-
for other_token in same_line_remaining_tokens
1419-
)
1420-
if not same_line_remaining_tokens or not is_last_element:
1421-
return False
1422-
1423-
def get_curline_index_start():
1424-
"""Get the index denoting the start of the current line"""
1425-
for subindex, token in enumerate(reversed(tokens[:index])):
1426-
# See Lib/tokenize.py and Lib/token.py in cpython for more info
1427-
if token.type in (tokenize.NEWLINE, tokenize.NL):
1428-
return index - subindex
1429-
return 0
1430-
1431-
curline_start = get_curline_index_start()
1432-
expected_tokens = {"return", "yield"}
1433-
for prevtoken in tokens[curline_start:index]:
1434-
if "=" in prevtoken.string or prevtoken.string in expected_tokens:
1435-
return True
1436-
return False
1437-
14381378

14391379
def register(linter):
14401380
"""Required method to auto register this checker."""

0 commit comments

Comments
 (0)