diff --git a/bugbear.py b/bugbear.py index e45d059..eb96788 100644 --- a/bugbear.py +++ b/bugbear.py @@ -174,6 +174,7 @@ def visit_ExceptHandler(self, node): # (MyError, MyError) # duplicate names # (MyError, BaseException) # everything derives from the Base # (Exception, TypeError) # builtins where one subclasses another + # (IOError, OSError) # IOError is an alias of OSError since Python3.3 # but note that other cases are impractical to hande from the AST. # We expect this is mostly useful for users who do not have the # builtin exception hierarchy memorised, and include a 'shadowed' @@ -181,6 +182,14 @@ def visit_ExceptHandler(self, node): good = sorted(set(names), key=names.index) if "BaseException" in good: good = ["BaseException"] + # Find and remove aliases exceptions and only leave the primary alone + primaries = filter( + lambda primary: primary in good, B014.exception_aliases.keys() + ) + for primary in primaries: + aliases = B014.exception_aliases[primary] + good = list(filter(lambda e: e not in aliases, good)) + for name, other in itertools.permutations(tuple(good), 2): if issubclass( getattr(builtins, name, type), getattr(builtins, other, ()) @@ -639,6 +648,16 @@ def visit(self, node): "Write `except {2}{1}:`, which catches exactly the same exceptions." ) ) +B014.exception_aliases = { + "OSError": { + "IOError", + "EnvironmentError", + "WindowsError", + "mmap.error", + "socket.error", + "select.error", + } +} # Those could be false positives but it's more dangerous to let them slip # through if they're not. diff --git a/tests/b014.py b/tests/b014.py index 4c26794..a3e64e6 100644 --- a/tests/b014.py +++ b/tests/b014.py @@ -1,6 +1,6 @@ """ Should emit: -B014 - on lines 10, 16, 27, 41, and 48 +B014 - on lines 10, 16, 27, 41, 48, and 55 """ import re @@ -48,3 +48,14 @@ class MyError(Exception): except (re.error, re.error): # Duplicate exception types as attributes pass + + +try: + pass +except (IOError, EnvironmentError, OSError): + # Detect if a primary exception and any its aliases are present. + # + # Since Python 3.3, IOError, EnvironmentError, WindowsError, mmap.error, + # socket.error and select.error are aliases of OSError. See PEP 3151 for + # more info. + pass diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index bc4c61a..d6161af 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -178,6 +178,7 @@ def test_b014(self): B014(27, 0, vars=("MyError, MyError", "", "MyError")), B014(41, 0, vars=("MyError, BaseException", " as e", "BaseException")), B014(48, 0, vars=("re.error, re.error", "", "re.error")), + B014(55, 0, vars=("IOError, EnvironmentError, OSError", "", "OSError"),), ) self.assertEqual(errors, expected)