44
55from __future__ import annotations
66
7+ import itertools
8+
79import astroid
810from astroid import bases , nodes
911
@@ -50,7 +52,6 @@ class ImplicitBooleanessChecker(checkers.BaseChecker):
5052 * comparison such as variable != empty_literal:
5153 """
5254
53- # configuration section name
5455 name = "refactoring"
5556 msgs = {
5657 "C1802" : (
@@ -64,11 +65,16 @@ class ImplicitBooleanessChecker(checkers.BaseChecker):
6465 {"old_names" : [("C1801" , "len-as-condition" )]},
6566 ),
6667 "C1803" : (
67- "'%s' can be simplified to '%s' as an empty sequence is falsey" ,
68+ "'%s' can be simplified to '%s' as %s is falsey" ,
6869 "use-implicit-booleaness-not-comparison" ,
6970 "Used when Pylint detects that collection literal comparison is being "
7071 "used to check for emptiness; Use implicit booleaness instead "
7172 "of a collection classes; empty collections are considered as false" ,
73+ {
74+ "old_names" : [
75+ ("C1901" , "compare-to-empty-string" ),
76+ ]
77+ },
7278 ),
7379 }
7480
@@ -139,6 +145,52 @@ def visit_unaryop(self, node: nodes.UnaryOp) -> None:
139145 @utils .only_required_for_messages ("use-implicit-booleaness-not-comparison" )
140146 def visit_compare (self , node : nodes .Compare ) -> None :
141147 self ._check_use_implicit_booleaness_not_comparison (node )
148+ self ._check_compare_to_empty_string (node )
149+
150+ def _check_compare_to_empty_string (self , node : nodes .Compare ) -> None :
151+ """Checks for comparisons to empty string.
152+
153+ Most of the time you should use the fact that empty strings are false.
154+ An exception to this rule is when an empty string value is allowed in the program
155+ and has a different meaning than None!
156+ """
157+ _operators = {"!=" , "==" , "is not" , "is" }
158+ # note: astroid.Compare has the left most operand in node.left
159+ # while the rest are a list of tuples in node.ops
160+ # the format of the tuple is ('compare operator sign', node)
161+ # here we squash everything into `ops` to make it easier for processing later
162+ ops : list [tuple [str , nodes .NodeNG | None ]] = [("" , node .left )]
163+ ops .extend (node .ops )
164+ iter_ops = iter (ops )
165+ ops = list (itertools .chain (* iter_ops )) # type: ignore[arg-type]
166+ for ops_idx in range (len (ops ) - 2 ):
167+ op_1 : nodes .NodeNG | None = ops [ops_idx ]
168+ op_2 : str = ops [ops_idx + 1 ] # type: ignore[assignment]
169+ op_3 : nodes .NodeNG | None = ops [ops_idx + 2 ]
170+ error_detected = False
171+ if op_1 is None or op_3 is None or op_2 not in _operators :
172+ continue
173+ if isinstance (node .parent , nodes .Assert ) and op_2 == "==" :
174+ # Comparing equality to an empty string directly make more
175+ # sense in tests where you want to make sure you don't just
176+ # have a falsey value but an empty string
177+ continue
178+ # x ?? ""
179+ if utils .is_empty_str_literal (op_1 ):
180+ error_detected = True
181+ node_name = op_3 .as_string ()
182+ # '' ?? X
183+ elif utils .is_empty_str_literal (op_3 ):
184+ error_detected = True
185+ node_name = op_1 .as_string ()
186+ if error_detected :
187+ suggestion = f"not { node_name } " if op_2 in {"==" , "is" } else node_name
188+
189+ self .add_message (
190+ "compare-to-empty-string" ,
191+ args = (node .as_string (), suggestion , "an empty string" ),
192+ node = node ,
193+ )
142194
143195 def _check_use_implicit_booleaness_not_comparison (
144196 self , node : nodes .Compare
@@ -177,11 +229,11 @@ def _check_use_implicit_booleaness_not_comparison(
177229
178230 # No need to check for operator when visiting compare node
179231 if operator in {"==" , "!=" , ">=" , ">" , "<=" , "<" }:
180- collection_literal = "{}"
232+ collection_literal , sequence_type = "{}" , "dict "
181233 if isinstance (literal_node , nodes .List ):
182- collection_literal = "[]"
234+ collection_literal , sequence_type = "[]" , "list "
183235 if isinstance (literal_node , nodes .Tuple ):
184- collection_literal = "()"
236+ collection_literal , sequence_type = "()" , "tuple "
185237
186238 instance_name = "x"
187239 if isinstance (target_node , nodes .Call ) and target_node .func :
@@ -202,6 +254,7 @@ def _check_use_implicit_booleaness_not_comparison(
202254 args = (
203255 original_comparison ,
204256 suggestion ,
257+ f"an empty { sequence_type } " ,
205258 ),
206259 node = node ,
207260 )
0 commit comments