@@ -604,6 +604,7 @@ def visit_Call(self, node: ast.Call) -> None:
604604 self .check_for_b910 (node )
605605 self .check_for_b911 (node )
606606 self .check_for_b912 (node )
607+ self .check_for_b044 (node )
607608
608609 # no need for copying, if used in nested calls it will be set to None
609610 current_b040_caught_exception = self .b040_caught_exception
@@ -1416,6 +1417,39 @@ def check_for_b031(self, loop_node: ast.For) -> None: # noqa: C901
14161417 if num_usages > 1 :
14171418 self .add_error ("B031" , node , node .id )
14181419
1420+ def check_for_b044 (self , node : ast .Call ) -> None :
1421+ # `str.find()`/`rfind()` return -1 when the substring is missing, which
1422+ # is truthy, and 0 when it is found at the start, which is falsy. Testing
1423+ # the result directly therefore inverts the intended logic, so require an
1424+ # explicit comparison against the returned index instead.
1425+ if not (
1426+ isinstance (node .func , ast .Attribute )
1427+ and node .func .attr in ("find" , "rfind" )
1428+ and node .args
1429+ ):
1430+ return
1431+ if self ._is_used_as_boolean (node ):
1432+ self .add_error ("B044" , node )
1433+
1434+ def _is_used_as_boolean (self , node : ast .expr ) -> bool :
1435+ # node is the node currently being visited, so it sits on top of the
1436+ # stack. Walk the ancestors, stepping through `not`/`and`/`or` wrappers
1437+ # that keep testing the value's truthiness, and report if we reach a
1438+ # place that uses it as a condition.
1439+ child : ast .AST = node
1440+ for parent in reversed (self .node_stack [:- 1 ]):
1441+ if isinstance (parent , ast .BoolOp ):
1442+ child = parent
1443+ elif isinstance (parent , ast .UnaryOp ) and isinstance (parent .op , ast .Not ):
1444+ child = parent
1445+ elif isinstance (parent , (ast .If , ast .IfExp , ast .While , ast .Assert )):
1446+ return parent .test is child
1447+ elif isinstance (parent , ast .comprehension ):
1448+ return child in parent .ifs
1449+ else :
1450+ return False
1451+ return False
1452+
14191453 def _get_names_from_tuple (self , node : ast .Tuple ) -> Iterator [str ]:
14201454 for dim in node .elts :
14211455 if isinstance (dim , ast .Name ):
@@ -2797,6 +2831,14 @@ def __call__(self, lineno: int, col: int, vars: tuple[object, ...] = ()) -> erro
27972831 "it is not any safer than normal property access."
27982832 )
27992833 ),
2834+ "B044" : Error (
2835+ message = (
2836+ "B044 Using the result of `.find()`/`.rfind()` as a boolean is "
2837+ "misleading: it returns -1 (truthy) when the substring is missing and "
2838+ "0 (falsy) when it is found at the start. Compare the returned index "
2839+ "explicitly instead."
2840+ )
2841+ ),
28002842 # Warnings disabled by default.
28012843 "B901" : Error (
28022844 message = (
0 commit comments