Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check syntax errors in "# type: ignore[code]" comments #7460

Merged
merged 11 commits into from Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 28 additions & 8 deletions mypy/fastparse.py
Expand Up @@ -124,7 +124,9 @@ def ast3_parse(source: Union[str, bytes], filename: str, mode: str,

TYPE_COMMENT_SYNTAX_ERROR = 'syntax error in type comment' # type: Final

TYPE_IGNORE_PATTERN = re.compile(r'[^#]*#\s*type:\s*ignore\s*(\[[^[#]*\]\s*)?($|#)')
INVALID_TYPE_IGNORE = 'Invalid "type: ignore" comment' # type: Final

TYPE_IGNORE_PATTERN = re.compile(r'[^#]*#\s*type:\s*ignore\s*(.*)')


def parse(source: Union[str, bytes],
Expand Down Expand Up @@ -170,13 +172,21 @@ def parse(source: Union[str, bytes],
return tree


def parse_type_ignore_tag(tag: Optional[str]) -> List[str]:
# TODO: Implement proper parsing and error checking
if not tag:
def parse_type_ignore_tag(tag: Optional[str]) -> Optional[List[str]]:
"""Parse optional "[code, ...]" tag after "# type: ignore".

Return:
* [] if no tag was found (ignore all errors)
* list of ignored error codes if a tag was found
* None if the tag was invalid.
"""
if not tag or tag.strip() == '' or tag.strip().startswith('#'):
# No tag -- ignore all errors.
return []
m = re.match(r'\s*\[([^#]*)\]', tag)
m = re.match(r'\s*\[([^]#]*)\]\s*(#.*)?$', tag)
if m is None:
return []
# Invalid "# type: ignore" comment.
return None
return [code.strip() for code in m.group(1).split(',')]


Expand Down Expand Up @@ -206,6 +216,11 @@ def parse_type_comment(type_comment: str,
# Typeshed has a non-optional return type for group!
tag = cast(Any, extra_ignore).group(1) # type: Optional[str]
ignored = parse_type_ignore_tag(tag) # type: Optional[List[str]]
if ignored is None:
if errors is not None:
errors.report(line, column, INVALID_TYPE_IGNORE, code=codes.SYNTAX)
else:
raise SyntaxError
else:
ignored = None
assert isinstance(typ, ast3_Expression)
Expand Down Expand Up @@ -451,8 +466,13 @@ def translate_module_id(self, id: str) -> str:
return id

def visit_Module(self, mod: ast3.Module) -> MypyFile:
self.type_ignores = {ti.lineno: parse_type_ignore_tag(ti.tag) # type: ignore[attr-defined]
for ti in mod.type_ignores}
self.type_ignores = {}
for ti in mod.type_ignores:
parsed = parse_type_ignore_tag(ti.tag) # type: ignore[attr-defined]
if parsed is not None:
self.type_ignores[ti.lineno] = parsed
else:
self.fail(INVALID_TYPE_IGNORE, ti.lineno, -1)
body = self.fix_function_overloads(self.translate_stmt_list(mod.body, ismodule=True))
return MypyFile(body,
self.imports,
Expand Down
16 changes: 12 additions & 4 deletions mypy/fastparse2.py
Expand Up @@ -47,7 +47,7 @@
from mypy.errors import Errors
from mypy.fastparse import (
TypeConverter, parse_type_comment, bytes_to_human_readable_repr, parse_type_ignore_tag,
TYPE_IGNORE_PATTERN
TYPE_IGNORE_PATTERN, INVALID_TYPE_IGNORE
)
from mypy.options import Options
from mypy.reachability import mark_block_unreachable
Expand Down Expand Up @@ -342,8 +342,13 @@ def translate_module_id(self, id: str) -> str:
return id

def visit_Module(self, mod: ast27.Module) -> MypyFile:
self.type_ignores = {ti.lineno: parse_type_ignore_tag(ti.tag) # type: ignore[attr-defined]
for ti in mod.type_ignores}
self.type_ignores = {}
for ti in mod.type_ignores:
parsed = parse_type_ignore_tag(ti.tag) # type: ignore[attr-defined]
if parsed is not None:
self.type_ignores[ti.lineno] = parsed
else:
self.fail(INVALID_TYPE_IGNORE, ti.lineno, -1)
body = self.fix_function_overloads(self.translate_stmt_list(mod.body))
return MypyFile(body,
self.imports,
Expand Down Expand Up @@ -553,7 +558,10 @@ def get_type(self,
if extra_ignore:
tag = cast(Any, extra_ignore).group(1) # type: Optional[str]
ignored = parse_type_ignore_tag(tag)
self.type_ignores[converter.line] = ignored
if ignored is None:
self.fail(INVALID_TYPE_IGNORE, converter.line, -1)
else:
self.type_ignores[converter.line] = ignored
return typ
return None

Expand Down
65 changes: 65 additions & 0 deletions test-data/unit/check-errorcodes.test
Expand Up @@ -163,6 +163,55 @@ from defusedxml import xyz # type: ignore[import]
import nostub # type: ignore[import]
from defusedxml import xyz # type: ignore[import]

[case testErrorCodeBadIgnore]
import nostub # type: ignore xyz # E: Invalid "type: ignore" comment [syntax]
import nostub # type: ignore[ # E: Invalid "type: ignore" comment [syntax]
import nostub # type: ignore[foo # E: Invalid "type: ignore" comment [syntax]
import nostub # type: ignore[foo, # E: Invalid "type: ignore" comment [syntax]
import nostub # type: ignore[foo]] # E: Invalid "type: ignore" comment [syntax]
import nostub # type: ignore[foo][bar] # E: Invalid "type: ignore" comment [syntax]
import nostub # type: ignore[foo] [bar] # E: Invalid "type: ignore" comment [syntax]

x = 0 # type: ignore[ # E: Invalid "type: ignore" comment [syntax]

def f(x, # type: int # type: ignore[ # E: Invalid "type: ignore" comment [syntax]
):
# type: (...) -> None
pass

[case testErrorCodeBadIgnoreNoExtraComment]
# Omit the E: ... comments, as they affect parsing
import nostub # type: ignore xyz
import nostub # type: ignore[xyz
import nostub # type: ignore[xyz][xyz]
x = 0 # type: ignore[
def f(x, # type: int # type: ignore[
):
# type: (...) -> None
pass
[out]
main:2: error: Invalid "type: ignore" comment [syntax]
main:3: error: Invalid "type: ignore" comment [syntax]
main:4: error: Invalid "type: ignore" comment [syntax]
main:5: error: Invalid "type: ignore" comment [syntax]
main:6: error: Invalid "type: ignore" comment [syntax]

[case testErrorCodeBadIgnore_python2]
import nostub # type: ignore xyz
import nostub # type: ignore[xyz # Comment [x]
import nostub # type: ignore[xyz][xyz]
x = 0 # type: ignore[
def f(x, # type: int # type: ignore[
):
# type: (...) -> None
pass
[out]
main:1: error: Invalid "type: ignore" comment [syntax]
main:2: error: Invalid "type: ignore" comment [syntax]
main:3: error: Invalid "type: ignore" comment [syntax]
main:4: error: Invalid "type: ignore" comment [syntax]
main:5: error: Invalid "type: ignore" comment [syntax]

[case testErrorCodeArgKindAndCount]
def f(x: int) -> None: pass # N: "f" defined here
f() # E: Too few arguments for "f" [call-arg]
Expand Down Expand Up @@ -593,3 +642,19 @@ class A:
def g(self: A) -> None: pass

A.f = g # E: Cannot assign to a method [assignment]

[case testErrorCodeTypeIgnoreMisspelled1]
x = y # type: ignored[foo]
xx = y # type: ignored [foo]
[out]
main:1: error: Name 'ignored' is not defined [name-defined]
main:1: error: Name 'y' is not defined [name-defined]
main:2: error: Name 'ignored' is not defined [name-defined]
main:2: error: Name 'y' is not defined [name-defined]

[case testErrorCodeTypeIgnoreMisspelled2]
x = y # type: int # type: ignored[foo]
x = y # type: int # type: ignored [foo]
[out]
main:1: error: syntax error in type comment 'int' [syntax]
main:2: error: syntax error in type comment 'int' [syntax]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH this error is confusing. Is it hard to fix this?