44
55from __future__ import annotations
66
7+ import itertools
8+ from collections .abc import Iterable
9+ from typing import Any
10+
711import astroid
812from astroid import bases , nodes
913
1014from pylint import checkers
1115from pylint .checkers import utils
1216
1317
18+ def _is_constant_zero (node ):
19+ return isinstance (node , astroid .Const ) and node .value == 0
20+
21+
1422class ImplicitBooleanessChecker (checkers .BaseChecker ):
1523 """Checks for incorrect usage of comparisons or len() inside conditions.
1624
@@ -50,7 +58,6 @@ class ImplicitBooleanessChecker(checkers.BaseChecker):
5058 * comparison such as variable != empty_literal:
5159 """
5260
53- # configuration section name
5461 name = "refactoring"
5562 msgs = {
5663 "C1802" : (
@@ -64,11 +71,17 @@ class ImplicitBooleanessChecker(checkers.BaseChecker):
6471 {"old_names" : [("C1801" , "len-as-condition" )]},
6572 ),
6673 "C1803" : (
67- "'%s' can be simplified to '%s' as an empty sequence is falsey" ,
74+ "'%s' can be simplified to '%s' as %s is falsey" ,
6875 "use-implicit-booleaness-not-comparison" ,
6976 "Used when Pylint detects that collection literal comparison is being "
7077 "used to check for emptiness; Use implicit booleaness instead"
7178 "of a collection classes; empty collections are considered as false" ,
79+ {
80+ "old_names" : [
81+ ("C1901" , "compare-to-empty-string" ),
82+ ("C2001" , "compare-to-zero" ),
83+ ]
84+ },
7285 ),
7386 }
7487
@@ -139,6 +152,80 @@ def visit_unaryop(self, node: nodes.UnaryOp) -> None:
139152 @utils .only_required_for_messages ("use-implicit-booleaness-not-comparison" )
140153 def visit_compare (self , node : nodes .Compare ) -> None :
141154 self ._check_use_implicit_booleaness_not_comparison (node )
155+ self ._check_compare_to_empty_string (node )
156+ self ._check_compare_to_zero (node )
157+
158+ def _check_compare_to_zero (self , node : nodes .Compare ) -> None :
159+ """Checks for comparisons to zero.
160+
161+ Most of the time you should use the fact that integers with a value of 0 are false.
162+ An exception to this rule is when 0 is allowed in the program and has a
163+ different meaning than None!
164+ """
165+ # pylint: disable=duplicate-code
166+ _operators = ["!=" , "==" , "is not" , "is" ]
167+ # note: astroid.Compare has the left most operand in node.left
168+ # while the rest are a list of tuples in node.ops
169+ # the format of the tuple is ('compare operator sign', node)
170+ # here we squash everything into `ops` to make it easier for processing later
171+ ops = [("" , node .left )]
172+ ops .extend (node .ops )
173+ iter_ops : Iterable [Any ] = iter (ops )
174+ ops = list (itertools .chain (* iter_ops ))
175+ for ops_idx in range (len (ops ) - 2 ):
176+ op_1 = ops [ops_idx ]
177+ op_2 = ops [ops_idx + 1 ]
178+ op_3 = ops [ops_idx + 2 ]
179+ error_detected = False
180+
181+ # 0 ?? X
182+ if _is_constant_zero (op_1 ) and op_2 in _operators :
183+ error_detected = True
184+ # X ?? 0
185+ elif op_2 in _operators and _is_constant_zero (op_3 ):
186+ error_detected = True
187+
188+ if error_detected :
189+ self .add_message (
190+ "compare-to-zero" , args = ("TODO" , "TODO" , "'0'" ), node = node
191+ )
192+
193+ def _check_compare_to_empty_string (self , node : nodes .Compare ) -> None :
194+ """Checks for comparisons to empty string.
195+
196+ Most of the time you should use the fact that empty strings are false.
197+ An exception to this rule is when an empty string value is allowed in the program
198+ and has a different meaning than None!
199+ """
200+ _operators = ["!=" , "==" , "is not" , "is" ]
201+ # note: astroid.Compare has the left most operand in node.left
202+ # while the rest are a list of tuples in node.ops
203+ # the format of the tuple is ('compare operator sign', node)
204+ # here we squash everything into `ops` to make it easier for processing later
205+ ops = [("" , node .left )]
206+ ops .extend (node .ops )
207+ iter_ops : Iterable [Any ] = iter (ops )
208+ ops = list (itertools .chain (* iter_ops ))
209+ for ops_idx in range (len (ops ) - 2 ):
210+ op_1 = ops [ops_idx ]
211+ op_2 = ops [ops_idx + 1 ]
212+ op_3 = ops [ops_idx + 2 ]
213+ error_detected = False
214+
215+ # x ?? ""
216+ if utils .is_empty_str_literal (op_1 ) and op_2 in _operators :
217+ error_detected = True
218+ suggestion = op_3 .name
219+ # '' ?? X
220+ elif op_2 in _operators and utils .is_empty_str_literal (op_3 ):
221+ error_detected = True
222+ suggestion = op_1 .name
223+ if error_detected :
224+ self .add_message (
225+ "compare-to-empty-string" ,
226+ args = (node .as_string (), suggestion , "an empty string" ),
227+ node = node ,
228+ )
142229
143230 def _check_use_implicit_booleaness_not_comparison (
144231 self , node : nodes .Compare
@@ -199,10 +286,7 @@ def _check_use_implicit_booleaness_not_comparison(
199286 )
200287 self .add_message (
201288 "use-implicit-booleaness-not-comparison" ,
202- args = (
203- original_comparison ,
204- suggestion ,
205- ),
289+ args = (original_comparison , suggestion , "an empty sequence" ),
206290 node = node ,
207291 )
208292
0 commit comments