Skip to content

Commit 6deff50

Browse files
committed
B044: add check for using str.find()/rfind() result as a boolean (#170)
1 parent 81f82a1 commit 6deff50

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

README.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,15 @@ If you define `__str__/__reduce__` in super classes this check is unable to dete
306306
**B043**: Do not call ``delattr(x, 'attr')``, instead use ``del x.attr``.
307307
There is no additional safety in using ``delattr`` if you know the attribute name ahead of time.
308308

309+
.. _B044:
310+
311+
**B044**: Do not use the result of ``str.find()`` or ``str.rfind()`` directly as a boolean.
312+
They return ``-1`` when the substring is missing, which is truthy, and ``0`` when it is found at the
313+
start of the string, which is falsy, so ``if s.find(x):`` reads backwards from what it does. Compare
314+
the returned index explicitly instead, e.g. ``if s.find(x) != -1:`` or ``if s.find(x) == 0:``.
315+
This is a name-based check, so it can fire on unrelated objects that also define a ``find`` method
316+
(such as BeautifulSoup); add a ``# noqa: B044`` there if needed.
317+
309318

310319
Opinionated warnings
311320
~~~~~~~~~~~~~~~~~~~~
@@ -508,6 +517,7 @@ UNRELEASED
508517
* B018: handle also useless calls such as `isinstance(x, int)` without assigning or using the result
509518
* B031: don't count a store-context reference (e.g. an annotation target like `group: T`) as a use of the `groupby` generator (#465)
510519
* B902: don't raise a false positive on a metaclass defined with a dotted base such as `abc.ABCMeta` or `enum.EnumMeta` (#411)
520+
* B044: Add new check for using the result of `str.find()`/`str.rfind()` directly as a boolean (#170)
511521

512522
25.11.29
513523
~~~~~~~~

bugbear.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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=(

tests/eval_files/b044.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
haystack = "hello world"
2+
needle = "world"
3+
4+
5+
# Bad: the index is used directly as a boolean.
6+
if haystack.find(needle): # B044: 3
7+
pass
8+
9+
if not haystack.find(needle): # B044: 7
10+
pass
11+
12+
while haystack.find(needle): # B044: 6
13+
pass
14+
15+
assert haystack.find(needle) # B044: 7
16+
17+
found = "yes" if haystack.find(needle) else "no" # B044: 17
18+
19+
if haystack.find(needle) and needle: # B044: 3
20+
pass
21+
22+
if needle or haystack.find(needle): # B044: 13
23+
pass
24+
25+
if not (haystack.find(needle) or needle): # B044: 8
26+
pass
27+
28+
matches = [c for c in haystack if haystack.find(c)] # B044: 34
29+
30+
if haystack.rfind(needle): # B044: 3
31+
pass
32+
33+
if b"data".find(b"a"): # B044: 3
34+
pass
35+
36+
37+
# OK: the returned index is compared explicitly.
38+
if haystack.find(needle) == 0:
39+
pass
40+
41+
if haystack.find(needle) != -1:
42+
pass
43+
44+
if haystack.find(needle) >= 0:
45+
pass
46+
47+
index = haystack.find(needle)
48+
if index:
49+
pass
50+
51+
52+
def get_index():
53+
return haystack.find(needle)
54+
55+
56+
haystack.find(needle)
57+
print(haystack.find(needle))
58+
59+
# OK: `.index()` raises instead of returning -1, and unrelated `.find()` users
60+
# such as BeautifulSoup are not our concern here, but a plain attribute is.
61+
if haystack.index(needle):
62+
pass

0 commit comments

Comments
 (0)