Skip to content

Commit 322e94e

Browse files
authored
Check syntax errors in "# type: ignore[code]" comments (python#7460)
1 parent e88ff4c commit 322e94e

File tree

3 files changed

+105
-12
lines changed

3 files changed

+105
-12
lines changed

mypy/fastparse.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ def ast3_parse(source: Union[str, bytes], filename: str, mode: str,
124124

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

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

129131

130132
def parse(source: Union[str, bytes],
@@ -170,13 +172,21 @@ def parse(source: Union[str, bytes],
170172
return tree
171173

172174

173-
def parse_type_ignore_tag(tag: Optional[str]) -> List[str]:
174-
# TODO: Implement proper parsing and error checking
175-
if not tag:
175+
def parse_type_ignore_tag(tag: Optional[str]) -> Optional[List[str]]:
176+
"""Parse optional "[code, ...]" tag after "# type: ignore".
177+
178+
Return:
179+
* [] if no tag was found (ignore all errors)
180+
* list of ignored error codes if a tag was found
181+
* None if the tag was invalid.
182+
"""
183+
if not tag or tag.strip() == '' or tag.strip().startswith('#'):
184+
# No tag -- ignore all errors.
176185
return []
177-
m = re.match(r'\s*\[([^#]*)\]', tag)
186+
m = re.match(r'\s*\[([^]#]*)\]\s*(#.*)?$', tag)
178187
if m is None:
179-
return []
188+
# Invalid "# type: ignore" comment.
189+
return None
180190
return [code.strip() for code in m.group(1).split(',')]
181191

182192

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

453468
def visit_Module(self, mod: ast3.Module) -> MypyFile:
454-
self.type_ignores = {ti.lineno: parse_type_ignore_tag(ti.tag) # type: ignore[attr-defined]
455-
for ti in mod.type_ignores}
469+
self.type_ignores = {}
470+
for ti in mod.type_ignores:
471+
parsed = parse_type_ignore_tag(ti.tag) # type: ignore[attr-defined]
472+
if parsed is not None:
473+
self.type_ignores[ti.lineno] = parsed
474+
else:
475+
self.fail(INVALID_TYPE_IGNORE, ti.lineno, -1)
456476
body = self.fix_function_overloads(self.translate_stmt_list(mod.body, ismodule=True))
457477
return MypyFile(body,
458478
self.imports,

mypy/fastparse2.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from mypy.errors import Errors
4848
from mypy.fastparse import (
4949
TypeConverter, parse_type_comment, bytes_to_human_readable_repr, parse_type_ignore_tag,
50-
TYPE_IGNORE_PATTERN
50+
TYPE_IGNORE_PATTERN, INVALID_TYPE_IGNORE
5151
)
5252
from mypy.options import Options
5353
from mypy.reachability import mark_block_unreachable
@@ -342,8 +342,13 @@ def translate_module_id(self, id: str) -> str:
342342
return id
343343

344344
def visit_Module(self, mod: ast27.Module) -> MypyFile:
345-
self.type_ignores = {ti.lineno: parse_type_ignore_tag(ti.tag) # type: ignore[attr-defined]
346-
for ti in mod.type_ignores}
345+
self.type_ignores = {}
346+
for ti in mod.type_ignores:
347+
parsed = parse_type_ignore_tag(ti.tag) # type: ignore[attr-defined]
348+
if parsed is not None:
349+
self.type_ignores[ti.lineno] = parsed
350+
else:
351+
self.fail(INVALID_TYPE_IGNORE, ti.lineno, -1)
347352
body = self.fix_function_overloads(self.translate_stmt_list(mod.body))
348353
return MypyFile(body,
349354
self.imports,
@@ -553,7 +558,10 @@ def get_type(self,
553558
if extra_ignore:
554559
tag = cast(Any, extra_ignore).group(1) # type: Optional[str]
555560
ignored = parse_type_ignore_tag(tag)
556-
self.type_ignores[converter.line] = ignored
561+
if ignored is None:
562+
self.fail(INVALID_TYPE_IGNORE, converter.line, -1)
563+
else:
564+
self.type_ignores[converter.line] = ignored
557565
return typ
558566
return None
559567

test-data/unit/check-errorcodes.test

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,55 @@ from defusedxml import xyz # type: ignore[import]
163163
import nostub # type: ignore[import]
164164
from defusedxml import xyz # type: ignore[import]
165165

166+
[case testErrorCodeBadIgnore]
167+
import nostub # type: ignore xyz # E: Invalid "type: ignore" comment [syntax]
168+
import nostub # type: ignore[ # E: Invalid "type: ignore" comment [syntax]
169+
import nostub # type: ignore[foo # E: Invalid "type: ignore" comment [syntax]
170+
import nostub # type: ignore[foo, # E: Invalid "type: ignore" comment [syntax]
171+
import nostub # type: ignore[foo]] # E: Invalid "type: ignore" comment [syntax]
172+
import nostub # type: ignore[foo][bar] # E: Invalid "type: ignore" comment [syntax]
173+
import nostub # type: ignore[foo] [bar] # E: Invalid "type: ignore" comment [syntax]
174+
175+
x = 0 # type: ignore[ # E: Invalid "type: ignore" comment [syntax]
176+
177+
def f(x, # type: int # type: ignore[ # E: Invalid "type: ignore" comment [syntax]
178+
):
179+
# type: (...) -> None
180+
pass
181+
182+
[case testErrorCodeBadIgnoreNoExtraComment]
183+
# Omit the E: ... comments, as they affect parsing
184+
import nostub # type: ignore xyz
185+
import nostub # type: ignore[xyz
186+
import nostub # type: ignore[xyz][xyz]
187+
x = 0 # type: ignore[
188+
def f(x, # type: int # type: ignore[
189+
):
190+
# type: (...) -> None
191+
pass
192+
[out]
193+
main:2: error: Invalid "type: ignore" comment [syntax]
194+
main:3: error: Invalid "type: ignore" comment [syntax]
195+
main:4: error: Invalid "type: ignore" comment [syntax]
196+
main:5: error: Invalid "type: ignore" comment [syntax]
197+
main:6: error: Invalid "type: ignore" comment [syntax]
198+
199+
[case testErrorCodeBadIgnore_python2]
200+
import nostub # type: ignore xyz
201+
import nostub # type: ignore[xyz # Comment [x]
202+
import nostub # type: ignore[xyz][xyz]
203+
x = 0 # type: ignore[
204+
def f(x, # type: int # type: ignore[
205+
):
206+
# type: (...) -> None
207+
pass
208+
[out]
209+
main:1: error: Invalid "type: ignore" comment [syntax]
210+
main:2: error: Invalid "type: ignore" comment [syntax]
211+
main:3: error: Invalid "type: ignore" comment [syntax]
212+
main:4: error: Invalid "type: ignore" comment [syntax]
213+
main:5: error: Invalid "type: ignore" comment [syntax]
214+
166215
[case testErrorCodeArgKindAndCount]
167216
def f(x: int) -> None: pass # N: "f" defined here
168217
f() # E: Too few arguments for "f" [call-arg]
@@ -607,3 +656,19 @@ class A:
607656
def g(self: A) -> None: pass
608657

609658
A.f = g # E: Cannot assign to a method [assignment]
659+
660+
[case testErrorCodeTypeIgnoreMisspelled1]
661+
x = y # type: ignored[foo]
662+
xx = y # type: ignored [foo]
663+
[out]
664+
main:1: error: Name 'ignored' is not defined [name-defined]
665+
main:1: error: Name 'y' is not defined [name-defined]
666+
main:2: error: Name 'ignored' is not defined [name-defined]
667+
main:2: error: Name 'y' is not defined [name-defined]
668+
669+
[case testErrorCodeTypeIgnoreMisspelled2]
670+
x = y # type: int # type: ignored[foo]
671+
x = y # type: int # type: ignored [foo]
672+
[out]
673+
main:1: error: syntax error in type comment 'int' [syntax]
674+
main:2: error: syntax error in type comment 'int' [syntax]

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy