Repository: asottile/dead

Issue:
Crash caused by certain # type: ignore statements
It appears that `dead` can't handle code that uses MyPy error codes in `type: ignore` directives.

To reproduce:

```py
def foo() -> int:
    return None  # type: ignore[no-any-return]
```

Run `dead` with no arguments:

```
dargueta@tux ~/blah (master)$ dead           
Traceback (most recent call last):
  File "/Users/dargueta/.pyenv/versions/3.8.6/bin/dead", line 8, in <module>
    sys.exit(main())
  File "/Users/dargueta/.pyenv/versions/3.8.6/lib/python3.8/site-packages/dead.py", line 314, in main
    visitor.visit_comment(lineno, s)
  File "/Users/dargueta/.pyenv/versions/3.8.6/lib/python3.8/site-packages/dead.py", line 195, in visit_comment
    ast_obj = ast.parse(part, f'<{self.filename}:{lineno}: comment>')
  File "/Users/dargueta/.pyenv/versions/3.8.6/lib/python3.8/ast.py", line 47, in parse
    return compile(source, filename, mode, flags,
  File "<blah.py:2: comment>", line 1
    ignore[no-any-return]
                  ^
SyntaxError: invalid syntax
```

The problem is in the `TYPE_IGNORE_RE` regex. The visit_comment() function ignores these `type: ignore` comments, but the regex doesn't match `type: ignore[xyz]`, only `type: ignore`. I recommend changing the regex to:

```py
TYPE_COMMENT_RE.pattern + r'ignore\s*(\[[\w-]+\]\s*)?(#|$)'
#                                    ^^^^^^^^^^^^^^^^
#                                    insert this
```

**Environment:**

* Python: 3.7.6, 3.8.6
* Operating System: MacOS 11.1 (Big Sur)
* `dead` version: 1.3.0

Discussion:

Patch:
diff --git a/dead.py b/dead.py
index a2e28fa..2ab24d7 100644
--- a/dead.py
+++ b/dead.py
@@ -25,8 +25,14 @@ UsageMap = DefaultDict[str, Set[FileLine]]
 FunctionDef = Union[ast.AsyncFunctionDef, ast.FunctionDef]
 # https://github.com/python/typed_ast/blob/55420396/ast27/Parser/tokenizer.c#L102-L104
 TYPE_COMMENT_RE = re.compile(r'^#\s*type:\s*')
+# The following regex largely conforms to:
 # https://github.com/python/typed_ast/blob/55420396/ast27/Parser/tokenizer.c#L1400
-TYPE_IGNORE_RE = re.compile(TYPE_COMMENT_RE.pattern + r'ignore\s*(#|$)')
+# However, it also supports MyPy's extended ignore syntax:
+# https://github.com/python/mypy/issues/7239
+TYPE_IGNORE_RE = re.compile(
+    TYPE_COMMENT_RE.pattern +
+    r'ignore\s*(?:\[[\w-]+(?:\s*,\s*[\w-]+)*\s*\]\s*)?(#|$)',
+)
 # https://github.com/python/typed_ast/blob/55420396/ast27/Grammar/Grammar#L147
 TYPE_FUNC_RE = re.compile(r'^(\(.*?\))\s*->\s*(.*)$')
 DISABLE_COMMENT_RE = re.compile(r'\bdead\s*:\s*disable')


Test patch:
diff --git a/tests/dead_test.py b/tests/dead_test.py
index 9de3213..fb3b6c5 100644
--- a/tests/dead_test.py
+++ b/tests/dead_test.py
@@ -18,6 +18,57 @@ def test_dead_disable_regex_not_matching(s):
     assert not dead.DISABLE_COMMENT_RE.search(s)
 
 
+@pytest.mark.parametrize(
+    'string',
+    (
+        '# type: (int, int) -> float',
+        '# type: ignore',
+        '# type: ClassVar[str]',
+        '# type: (int, int) -> float # dead: disable',
+        '# type: ignore # dead: disable',
+        '# type: ClassVar[str] # dead: disable',
+    ),
+)
+def test_type_regex_matching(string):
+    assert dead.TYPE_COMMENT_RE.search(string)
+
+
+@pytest.mark.parametrize(
+    'string',
+    (
+        '#type:ignore',
+        '# type: ignore',
+        '# type: ignore[no-any-return]',
+        '# type: ignore   [no-any-return]',
+        '# type: ignore[no-any-return,attribute]',
+        '# type: ignore[no-any-return  ,attribute ]',
+        '#type:ignore # dead: disable',
+        '# type: ignore # dead: disable',
+        '# type: ignore[no-any-return] # dead: disable',
+        '# type: ignore   [no-any-return] # dead: disable',
+        '# type: ignore[no-any-return,attribute] # dead: disable',
+        '# type: ignore[no-any-return  ,attribute ] # dead: disable',
+    ),
+)
+def test_type_ignore_regex_matching(string):
+    assert dead.TYPE_IGNORE_RE.search(string)
+
+
+@pytest.mark.parametrize(
+    'string',
+    (
+        '# type: ignore[$omething]',
+        '# type: ignore{no-any-return}',
+        '# type: ignore[]',
+        '# type: ignore[$omething] # dead: disable',
+        '# type: ignore{no-any-return} # dead: disable',
+        '# type: ignore[] # dead: disable',
+    ),
+)
+def test_type_ignore_regex_not_matching(string):
+    assert not dead.TYPE_IGNORE_RE.search(string)
+
+
 @pytest.fixture
 def git_dir(tmpdir):
     with tmpdir.as_cwd():