Skip to content

Commit 3157b89

Browse files
authored
add b040: exception with note added not reraised or used (#477)
* add b040: exception with note added not reraised or used * handle two more cases, with temp ugly code * add test case, clean up stray debug prints * break out b013,b029,b303 handler, clean up test file * simplify and clean up implementation. Replace `attr.ib(default=attr.Factory(...))` with `attr.ib(factory=...)`
1 parent 188eab8 commit 3157b89

File tree

4 files changed

+325
-43
lines changed

4 files changed

+325
-43
lines changed

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ second usage. Save the result to a list if the result is needed multiple times.
203203

204204
**B039**: ``ContextVar`` with mutable literal or function call as default. This is only evaluated once, and all subsequent calls to `.get()` would return the same instance of the default. This uses the same logic as B006 and B008, including ignoring values in ``extend-immutable-calls``.
205205

206+
**B040**: Caught exception with call to ``add_note`` not used. Did you forget to ``raise`` it?
207+
206208
Opinionated warnings
207209
~~~~~~~~~~~~~~~~~~~~
208210

@@ -357,6 +359,7 @@ FUTURE
357359
~~~~~~
358360

359361
* Add B039, ``ContextVar`` with mutable literal or function call as default.
362+
* Add B040: Exception with added note not reraised. (#474)
360363

361364
24.4.26
362365
~~~~~~~

bugbear.py

Lines changed: 123 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from contextlib import suppress
1313
from functools import lru_cache, partial
1414
from keyword import iskeyword
15-
from typing import Dict, List, Set, Union
15+
from typing import Dict, Iterable, Iterator, List, Set, Union
1616

1717
import attr
1818
import pycodestyle
@@ -55,7 +55,7 @@ class BugBearChecker:
5555
filename = attr.ib(default="(none)")
5656
lines = attr.ib(default=None)
5757
max_line_length = attr.ib(default=79)
58-
visitor = attr.ib(init=False, default=attr.Factory(lambda: BugBearVisitor))
58+
visitor = attr.ib(init=False, factory=lambda: BugBearVisitor)
5959
options = attr.ib(default=None)
6060

6161
def run(self):
@@ -227,7 +227,7 @@ def _is_identifier(arg):
227227
return re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", arg.value) is not None
228228

229229

230-
def _flatten_excepthandler(node):
230+
def _flatten_excepthandler(node: ast.expr | None) -> Iterator[ast.expr | None]:
231231
if not isinstance(node, ast.Tuple):
232232
yield node
233233
return
@@ -356,16 +356,23 @@ def visit_ExceptHandler(self, node: ast.ExceptHandler):
356356
return super().generic_visit(node)
357357

358358

359+
@attr.define
360+
class B040CaughtException:
361+
name: str
362+
has_note: bool
363+
364+
359365
@attr.s
360366
class BugBearVisitor(ast.NodeVisitor):
361367
filename = attr.ib()
362368
lines = attr.ib()
363-
b008_b039_extend_immutable_calls = attr.ib(default=attr.Factory(set))
364-
b902_classmethod_decorators = attr.ib(default=attr.Factory(set))
365-
node_window = attr.ib(default=attr.Factory(list))
366-
errors = attr.ib(default=attr.Factory(list))
367-
futures = attr.ib(default=attr.Factory(set))
368-
contexts = attr.ib(default=attr.Factory(list))
369+
b008_b039_extend_immutable_calls = attr.ib(factory=set)
370+
b902_classmethod_decorators = attr.ib(factory=set)
371+
node_window = attr.ib(factory=list)
372+
errors = attr.ib(factory=list)
373+
futures = attr.ib(factory=set)
374+
contexts = attr.ib(factory=list)
375+
b040_caught_exception: B040CaughtException | None = attr.ib(default=None)
369376

370377
NODE_WINDOW_SIZE = 4
371378
_b023_seen = attr.ib(factory=set, init=False)
@@ -428,41 +435,20 @@ def visit(self, node):
428435

429436
self.check_for_b018(node)
430437

431-
def visit_ExceptHandler(self, node):
438+
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
432439
if node.type is None:
433440
self.errors.append(B001(node.lineno, node.col_offset))
434441
self.generic_visit(node)
435442
return
436-
handlers = _flatten_excepthandler(node.type)
437-
names = []
438-
bad_handlers = []
439-
ignored_handlers = []
440-
for handler in handlers:
441-
if isinstance(handler, (ast.Name, ast.Attribute)):
442-
name = _to_name_str(handler)
443-
if name is None:
444-
ignored_handlers.append(handler)
445-
else:
446-
names.append(name)
447-
elif isinstance(handler, (ast.Call, ast.Starred)):
448-
ignored_handlers.append(handler)
449-
else:
450-
bad_handlers.append(handler)
451-
if bad_handlers:
452-
self.errors.append(B030(node.lineno, node.col_offset))
453-
if len(names) == 0 and not bad_handlers and not ignored_handlers:
454-
self.errors.append(B029(node.lineno, node.col_offset))
455-
elif (
456-
len(names) == 1
457-
and not bad_handlers
458-
and not ignored_handlers
459-
and isinstance(node.type, ast.Tuple)
460-
):
461-
self.errors.append(B013(node.lineno, node.col_offset, vars=names))
443+
444+
old_b040_caught_exception = self.b040_caught_exception
445+
if node.name is None:
446+
self.b040_caught_exception = None
462447
else:
463-
maybe_error = _check_redundant_excepthandlers(names, node)
464-
if maybe_error is not None:
465-
self.errors.append(maybe_error)
448+
self.b040_caught_exception = B040CaughtException(node.name, False)
449+
450+
names = self.check_for_b013_b029_b030(node)
451+
466452
if (
467453
"BaseException" in names
468454
and not ExceptBaseExceptionVisitor(node).re_raised()
@@ -471,6 +457,13 @@ def visit_ExceptHandler(self, node):
471457

472458
self.generic_visit(node)
473459

460+
if (
461+
self.b040_caught_exception is not None
462+
and self.b040_caught_exception.has_note
463+
):
464+
self.errors.append(B040(node.lineno, node.col_offset))
465+
self.b040_caught_exception = old_b040_caught_exception
466+
474467
def visit_UAdd(self, node):
475468
trailing_nodes = list(map(type, self.node_window[-4:]))
476469
if trailing_nodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]:
@@ -479,8 +472,10 @@ def visit_UAdd(self, node):
479472
self.generic_visit(node)
480473

481474
def visit_Call(self, node):
475+
is_b040_add_note = False
482476
if isinstance(node.func, ast.Attribute):
483477
self.check_for_b005(node)
478+
is_b040_add_note = self.check_for_b040_add_note(node.func)
484479
else:
485480
with suppress(AttributeError, IndexError):
486481
if (
@@ -509,14 +504,28 @@ def visit_Call(self, node):
509504
self.check_for_b034(node)
510505
self.check_for_b039(node)
511506
self.check_for_b905(node)
507+
508+
# no need for copying, if used in nested calls it will be set to None
509+
current_b040_caught_exception = self.b040_caught_exception
510+
if not is_b040_add_note:
511+
self.check_for_b040_usage(node.args)
512+
self.check_for_b040_usage(node.keywords)
513+
512514
self.generic_visit(node)
513515

516+
if is_b040_add_note:
517+
# Avoid nested calls within the parameter list using the variable itself.
518+
# e.g. `e.add_note(str(e))`
519+
self.b040_caught_exception = current_b040_caught_exception
520+
514521
def visit_Module(self, node):
515522
self.generic_visit(node)
516523

517-
def visit_Assign(self, node):
524+
def visit_Assign(self, node: ast.Assign) -> None:
525+
self.check_for_b040_usage(node.value)
518526
if len(node.targets) == 1:
519527
t = node.targets[0]
528+
520529
if isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name):
521530
if (t.value.id, t.attr) == ("os", "environ"):
522531
self.errors.append(B003(node.lineno, node.col_offset))
@@ -588,7 +597,12 @@ def visit_Compare(self, node):
588597
self.check_for_b015(node)
589598
self.generic_visit(node)
590599

591-
def visit_Raise(self, node):
600+
def visit_Raise(self, node: ast.Raise):
601+
if node.exc is None:
602+
self.b040_caught_exception = None
603+
else:
604+
self.check_for_b040_usage(node.exc)
605+
self.check_for_b040_usage(node.cause)
592606
self.check_for_b016(node)
593607
self.check_for_b904(node)
594608
self.generic_visit(node)
@@ -605,6 +619,7 @@ def visit_JoinedStr(self, node):
605619

606620
def visit_AnnAssign(self, node):
607621
self.check_for_b032(node)
622+
self.check_for_b040_usage(node.value)
608623
self.generic_visit(node)
609624

610625
def visit_Import(self, node):
@@ -719,6 +734,40 @@ def _loop(node, bad_node_types):
719734
for child in node.finalbody:
720735
_loop(child, (ast.Return, ast.Continue, ast.Break))
721736

737+
def check_for_b013_b029_b030(self, node: ast.ExceptHandler) -> list[str]:
738+
handlers: Iterable[ast.expr | None] = _flatten_excepthandler(node.type)
739+
names: list[str] = []
740+
bad_handlers: list[object] = []
741+
ignored_handlers: list[ast.Name | ast.Attribute | ast.Call | ast.Starred] = []
742+
743+
for handler in handlers:
744+
if isinstance(handler, (ast.Name, ast.Attribute)):
745+
name = _to_name_str(handler)
746+
if name is None:
747+
ignored_handlers.append(handler)
748+
else:
749+
names.append(name)
750+
elif isinstance(handler, (ast.Call, ast.Starred)):
751+
ignored_handlers.append(handler)
752+
else:
753+
bad_handlers.append(handler)
754+
if bad_handlers:
755+
self.errors.append(B030(node.lineno, node.col_offset))
756+
if len(names) == 0 and not bad_handlers and not ignored_handlers:
757+
self.errors.append(B029(node.lineno, node.col_offset))
758+
elif (
759+
len(names) == 1
760+
and not bad_handlers
761+
and not ignored_handlers
762+
and isinstance(node.type, ast.Tuple)
763+
):
764+
self.errors.append(B013(node.lineno, node.col_offset, vars=names))
765+
else:
766+
maybe_error = _check_redundant_excepthandlers(names, node)
767+
if maybe_error is not None:
768+
self.errors.append(maybe_error)
769+
return names
770+
722771
def check_for_b015(self, node):
723772
if isinstance(self.node_stack[-2], ast.Expr):
724773
self.errors.append(B015(node.lineno, node.col_offset))
@@ -1081,6 +1130,33 @@ def check_for_b035(self, node: ast.DictComp):
10811130
B035(node.key.lineno, node.key.col_offset, vars=(node.key.id,))
10821131
)
10831132

1133+
def check_for_b040_add_note(self, node: ast.Attribute) -> bool:
1134+
if (
1135+
node.attr == "add_note"
1136+
and isinstance(node.value, ast.Name)
1137+
and self.b040_caught_exception
1138+
and node.value.id == self.b040_caught_exception.name
1139+
):
1140+
self.b040_caught_exception.has_note = True
1141+
return True
1142+
return False
1143+
1144+
def check_for_b040_usage(self, node: ast.expr | None) -> None:
1145+
def superwalk(node: ast.AST | list[ast.AST]):
1146+
if isinstance(node, list):
1147+
for n in node:
1148+
yield from ast.walk(n)
1149+
else:
1150+
yield from ast.walk(node)
1151+
1152+
if not self.b040_caught_exception or node is None:
1153+
return
1154+
1155+
for n in superwalk(node):
1156+
if isinstance(n, ast.Name) and n.id == self.b040_caught_exception.name:
1157+
self.b040_caught_exception = None
1158+
break
1159+
10841160
def _get_assigned_names(self, loop_node):
10851161
loop_targets = (ast.For, ast.AsyncFor, ast.comprehension)
10861162
for node in children_in_scope(loop_node):
@@ -1766,7 +1842,7 @@ class NameFinder(ast.NodeVisitor):
17661842
key is name string, value is the node (useful for location purposes).
17671843
"""
17681844

1769-
names: Dict[str, List[ast.Name]] = attr.ib(default=attr.Factory(dict))
1845+
names: Dict[str, List[ast.Name]] = attr.ib(factory=dict)
17701846

17711847
def visit_Name( # noqa: B906 # names don't contain other names
17721848
self, node: ast.Name
@@ -1791,7 +1867,7 @@ class NamedExprFinder(ast.NodeVisitor):
17911867
key is name string, value is the node (useful for location purposes).
17921868
"""
17931869

1794-
names: Dict[str, List[ast.Name]] = attr.ib(default=attr.Factory(dict))
1870+
names: Dict[str, List[ast.Name]] = attr.ib(factory=dict)
17951871

17961872
def visit_NamedExpr(self, node: ast.NamedExpr):
17971873
self.names.setdefault(node.target.id, []).append(node.target)
@@ -2218,6 +2294,10 @@ def visit_Lambda(self, node):
22182294
)
22192295
)
22202296

2297+
B040 = Error(
2298+
message="B040 Exception with added note not used. Did you forget to raise it?"
2299+
)
2300+
22212301
# Warnings disabled by default.
22222302
B901 = Error(
22232303
message=(

0 commit comments

Comments
 (0)