Repository: PyCQA/flake8-bugbear

Issue:
B014 recommends `except ():`
In the following code:

```
    try:
        with open(fn) as f:
            return f.read().split('\n')
    except (OSError, IOError):
        pass
```

The new B014 warning shows this:

> B014 Redundant exception types in `except (OSError, IOError):`.  Write `except ():`, which catches exactly the same exceptions.

Doesn't seem to make sense to me. Is this a bug :bear:?

Discussion:

Patch:
diff --git a/bugbear.py b/bugbear.py
index e45d059..eb96788 100644
--- a/bugbear.py
+++ b/bugbear.py
@@ -174,6 +174,7 @@ class BugBearVisitor(ast.NodeVisitor):
                 #    (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 @@ class BugBearVisitor(ast.NodeVisitor):
                 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 @@ B014 = Error(
         "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.


Test patch:
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 @@ try:
 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 @@ class BugbearTestCase(unittest.TestCase):
             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)