@@ -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+
50113class 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-
12641305class 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
14391379def register (linter ):
14401380 """Required method to auto register this checker."""
0 commit comments