Skip to content

Commit 84696ce

Browse files
authored
Add an option to require ignore comments have error codes (#11633)
Fixes #11509
1 parent 6ff8091 commit 84696ce

File tree

7 files changed

+106
-2
lines changed

7 files changed

+106
-2
lines changed

docs/source/error_code_list2.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,34 @@ no error in practice. In such case, it might be prudent to annotate ``items: Seq
255255
This is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``Sized``),
256256
except that attempting to invoke an undefined method (e.g. ``__len__``) results in an error,
257257
while attempting to evaluate an object in boolean context without a concrete implementation results in a truthy value.
258+
259+
260+
Check that ``# type: ignore`` include an error code [ignore-without-code]
261+
-------------------------------------------------------------------------
262+
263+
Warn when a ``# type: ignore`` comment does not specify any error codes.
264+
This clarifies the intent of the ignore and ensures that only the
265+
expected errors are silenced.
266+
267+
Example:
268+
269+
.. code-block:: python
270+
271+
# mypy: enable-error-code ignore-without-code
272+
273+
class Foo:
274+
def __init__(self, name: str) -> None:
275+
self.name = name
276+
277+
f = Foo('foo')
278+
279+
# This line has a typo that mypy can't help with as both:
280+
# - the expected error 'assignment', and
281+
# - the unexpected error 'attr-defined'
282+
# are silenced.
283+
# Error: "type: ignore" comment without error code (currently ignored: [attr-defined])
284+
f.nme = 42 # type: ignore
285+
286+
# This line warns correctly about the typo in the attribute name
287+
# Error: "Foo" has no attribute "nme"; maybe "name"?
288+
f.nme = 42 # type: ignore[assignment]

mypy/build.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2369,6 +2369,13 @@ def generate_unused_ignore_notes(self) -> None:
23692369
self.verify_dependencies(suppressed_only=True)
23702370
self.manager.errors.generate_unused_ignore_errors(self.xpath)
23712371

2372+
def generate_ignore_without_code_notes(self) -> None:
2373+
if self.manager.errors.is_error_code_enabled(codes.IGNORE_WITHOUT_CODE):
2374+
self.manager.errors.generate_ignore_without_code_errors(
2375+
self.xpath,
2376+
self.options.warn_unused_ignores,
2377+
)
2378+
23722379

23732380
# Module import and diagnostic glue
23742381

@@ -3168,6 +3175,7 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
31683175
graph[id].finish_passes()
31693176
for id in stale:
31703177
graph[id].generate_unused_ignore_notes()
3178+
graph[id].generate_ignore_without_code_notes()
31713179
if any(manager.errors.is_errors_for_file(graph[id].xpath) for id in stale):
31723180
for id in stale:
31733181
graph[id].transitive_error = True

mypy/errorcodes.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,21 @@ def __str__(self) -> str:
141141
"Check that overloaded functions outside stub files have an implementation",
142142
"General",
143143
)
144+
IGNORE_WITHOUT_CODE: Final = ErrorCode(
145+
"ignore-without-code",
146+
"Warn about '# type: ignore' comments which do not have error codes",
147+
"General",
148+
default_enabled=False,
149+
)
144150

145151

146152
# Syntax errors are often blocking.
147153
SYNTAX: Final = ErrorCode("syntax", "Report syntax errors", "General")
148154

155+
# This is an internal marker code for a whole-file ignore. It is not intended to
156+
# be user-visible.
157+
FILE: Final = ErrorCode("file", "Internal marker for a whole file being ignored", "General")
158+
del error_codes[FILE.code]
159+
149160
# This is a catch-all for remaining uncategorized errors.
150161
MISC: Final = ErrorCode("misc", "Miscellaneous other checks", "General")

mypy/errors.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,41 @@ def generate_unused_ignore_errors(self, file: str) -> None:
508508
None, False, False, False)
509509
self._add_error_info(file, info)
510510

511+
def generate_ignore_without_code_errors(self,
512+
file: str,
513+
is_warning_unused_ignores: bool) -> None:
514+
if is_typeshed_file(file) or file in self.ignored_files:
515+
return
516+
517+
used_ignored_lines = self.used_ignored_lines[file]
518+
519+
# If the whole file is ignored, ignore it.
520+
if used_ignored_lines:
521+
_, used_codes = min(used_ignored_lines.items())
522+
if codes.FILE.code in used_codes:
523+
return
524+
525+
for line, ignored_codes in self.ignored_lines[file].items():
526+
if ignored_codes:
527+
continue
528+
529+
# If the ignore is itself unused and that would be warned about, let
530+
# that error stand alone
531+
if is_warning_unused_ignores and not used_ignored_lines[line]:
532+
continue
533+
534+
codes_hint = ''
535+
ignored_codes = used_ignored_lines[line]
536+
if ignored_codes:
537+
codes_hint = f' (currently ignored: [{", ".join(ignored_codes)}])'
538+
539+
message = f'"type: ignore" comment without error code{codes_hint}'
540+
# Don't use report since add_error_info will ignore the error!
541+
info = ErrorInfo(self.import_context(), file, self.current_module(), None,
542+
None, line, -1, 'error', message, codes.IGNORE_WITHOUT_CODE,
543+
False, False, False)
544+
self._add_error_info(file, info)
545+
511546
def num_messages(self) -> int:
512547
"""Return the number of generated messages."""
513548
return sum(len(x) for x in self.error_info_map.values())

mypy/fastparse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ def translate_stmt_list(self,
389389
if (ismodule and stmts and self.type_ignores
390390
and min(self.type_ignores) < self.get_lineno(stmts[0])):
391391
self.errors.used_ignored_lines[self.errors.file][min(self.type_ignores)].append(
392-
codes.MISC.code)
392+
codes.FILE.code)
393393
block = Block(self.fix_function_overloads(self.translate_stmt_list(stmts)))
394394
mark_block_unreachable(block)
395395
return [block]

mypy/fastparse2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def translate_stmt_list(self,
217217
if (module and stmts and self.type_ignores
218218
and min(self.type_ignores) < self.get_lineno(stmts[0])):
219219
self.errors.used_ignored_lines[self.errors.file][min(self.type_ignores)].append(
220-
codes.MISC.code)
220+
codes.FILE.code)
221221
block = Block(self.fix_function_overloads(self.translate_stmt_list(stmts)))
222222
mark_block_unreachable(block)
223223
return [block]

test-data/unit/check-errorcodes.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,25 @@ x # type: ignore[name-defined, attr-defined] # E: Unused "type: ignore[attr-defi
145145
# flags: --warn-unused-ignores
146146
"x" # type: ignore[name-defined] # E: Unused "type: ignore" comment
147147

148+
[case testErrorCodeMissingWhenRequired]
149+
# flags: --enable-error-code ignore-without-code
150+
"x" # type: ignore # E: "type: ignore" comment without error code [ignore-without-code]
151+
y # type: ignore # E: "type: ignore" comment without error code (currently ignored: [name-defined]) [ignore-without-code]
152+
z # type: ignore[name-defined]
153+
"a" # type: ignore[ignore-without-code]
154+
155+
[case testErrorCodeMissingDoesntTrampleUnusedIgnoresWarning]
156+
# flags: --enable-error-code ignore-without-code --warn-unused-ignores
157+
"x" # type: ignore # E: Unused "type: ignore" comment
158+
"y" # type: ignore[ignore-without-code] # E: Unused "type: ignore" comment
159+
z # type: ignore[ignore-without-code] # E: Unused "type: ignore" comment # E: Name "z" is not defined [name-defined] # N: Error code "name-defined" not covered by "type: ignore" comment
160+
161+
[case testErrorCodeMissingWholeFileIgnores]
162+
# flags: --enable-error-code ignore-without-code
163+
# type: ignore # whole file ignore
164+
x
165+
y # type: ignore # ignore the lack of error code since we're ignore the whole file
166+
148167
[case testErrorCodeIgnoreWithExtraSpace]
149168
x # type: ignore [name-defined]
150169
x2 # type: ignore [ name-defined ]

0 commit comments

Comments
 (0)