1212from contextlib import suppress
1313from functools import lru_cache , partial
1414from keyword import iskeyword
15- from typing import Dict , List , Set , Union
15+ from typing import Dict , Iterable , Iterator , List , Set , Union
1616
1717import attr
1818import 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
360366class 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.
22222302B901 = Error (
22232303 message = (
0 commit comments