From 99f4d5af147d364eda1d4b99e79770c171896f13 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 25 Jan 2022 16:54:40 +0000 Subject: [PATCH] Add note about wrong error code in type: ignore (#12067) If we change the error code of a message, it can result in existing "type: ignore" comments being ignored. If this seems to be the case, add a note that hints about changing the ignored error code. Give a more specific message for a set of special cased error codes where we know the original error code. In other cases give a more vague message. --- mypy/errors.py | 23 +++++++++ test-data/unit/check-errorcodes.test | 71 +++++++++++++++++++++------- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index c711456468ed..ec49541a15a5 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -16,8 +16,13 @@ from mypy.util import DEFAULT_SOURCE_OFFSET, is_typeshed_file T = TypeVar("T") + allowed_duplicates: Final = ["@overload", "Got:", "Expected:"] +# Keep track of the original error code when the error code of a message is changed. +# This is used to give notes about out-of-date "type: ignore" comments. +original_error_codes: Final = {codes.LITERAL_REQ: codes.MISC} + class ErrorInfo: """Representation of a single error message.""" @@ -388,6 +393,24 @@ def add_error_info(self, info: ErrorInfo) -> None: info.hidden = True self.report_hidden_errors(info) self._add_error_info(file, info) + ignored_codes = self.ignored_lines.get(file, {}).get(info.line, []) + if ignored_codes and info.code: + # Something is ignored on the line, but not this error, so maybe the error + # code is incorrect. + msg = f'Error code "{info.code.code}" not covered by "type: ignore" comment' + if info.code in original_error_codes: + # If there seems to be a "type: ignore" with a stale error + # code, report a more specific note. + old_code = original_error_codes[info.code].code + if old_code in ignored_codes: + msg = (f'Error code changed to {info.code.code}; "type: ignore" comment ' + + 'may be out of date') + note = ErrorInfo( + info.import_ctx, info.file, info.module, info.type, info.function_or_member, + info.line, info.column, 'note', msg, + code=None, blocker=False, only_once=False, allow_dups=False + ) + self._add_error_info(file, note) def has_many_errors(self) -> bool: if self.many_errors_threshold < 0: diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 568d3a9522f9..177612959354 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -75,41 +75,51 @@ for v in f(): # type: int, int # E: Syntax error in type annotation [syntax] [case testErrorCodeIgnore1] 'x'.foobar # type: ignore[attr-defined] -'x'.foobar # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] +'x'.foobar # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] \ + # N: Error code "attr-defined" not covered by "type: ignore" comment 'x'.foobar # type: ignore [case testErrorCodeIgnore2] a = 'x'.foobar # type: int # type: ignore[attr-defined] -b = 'x'.foobar # type: int # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] +b = 'x'.foobar # type: int # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] \ + # N: Error code "attr-defined" not covered by "type: ignore" comment c = 'x'.foobar # type: int # type: ignore [case testErrorCodeIgnore1_python2] 'x'.foobar # type: ignore[attr-defined] -'x'.foobar # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] +'x'.foobar # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] \ + # N: Error code "attr-defined" not covered by "type: ignore" comment 'x'.foobar # type: ignore [case testErrorCodeIgnore2_python2] a = 'x'.foobar # type: int # type: ignore[attr-defined] -b = 'x'.foobar # type: int # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] +b = 'x'.foobar # type: int # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] \ + # N: Error code "attr-defined" not covered by "type: ignore" comment c = 'x'.foobar # type: int # type: ignore [case testErrorCodeIgnoreMultiple1] a = 'x'.foobar(b) # type: ignore[name-defined, attr-defined] -a = 'x'.foobar(b) # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] -a = 'x'.foobar(b) # type: ignore[xyz, w, attr-defined] # E: Name "b" is not defined [name-defined] +a = 'x'.foobar(b) # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] \ + # N: Error code "attr-defined" not covered by "type: ignore" comment +a = 'x'.foobar(b) # type: ignore[xyz, w, attr-defined] # E: Name "b" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment [case testErrorCodeIgnoreMultiple2] a = 'x'.foobar(b) # type: int # type: ignore[name-defined, attr-defined] -b = 'x'.foobar(b) # type: int # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] +b = 'x'.foobar(b) # type: int # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] \ + # N: Error code "attr-defined" not covered by "type: ignore" comment [case testErrorCodeIgnoreMultiple1_python2] a = 'x'.foobar(b) # type: ignore[name-defined, attr-defined] -a = 'x'.foobar(b) # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] -a = 'x'.foobar(b) # type: ignore[xyz, w, attr-defined] # E: Name "b" is not defined [name-defined] +a = 'x'.foobar(b) # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] \ + # N: Error code "attr-defined" not covered by "type: ignore" comment +a = 'x'.foobar(b) # type: ignore[xyz, w, attr-defined] # E: Name "b" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment [case testErrorCodeIgnoreMultiple2_python2] a = 'x'.foobar(b) # type: int # type: ignore[name-defined, attr-defined] -b = 'x'.foobar(b) # type: int # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] +b = 'x'.foobar(b) # type: int # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] \ + # N: Error code "attr-defined" not covered by "type: ignore" comment [case testErrorCodeWarnUnusedIgnores1] # flags: --warn-unused-ignores @@ -140,16 +150,22 @@ x # type: ignore [name-defined] x2 # type: ignore [ name-defined ] x3 # type: ignore [ xyz , name-defined ] x4 # type: ignore[xyz,name-defined] -y # type: ignore [xyz] # E: Name "y" is not defined [name-defined] -y # type: ignore[ xyz ] # E: Name "y" is not defined [name-defined] -y # type: ignore[ xyz , foo ] # E: Name "y" is not defined [name-defined] +y # type: ignore [xyz] # E: Name "y" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment +y # type: ignore[ xyz ] # E: Name "y" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment +y # type: ignore[ xyz , foo ] # E: Name "y" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment a = z # type: int # type: ignore [name-defined] b = z2 # type: int # type: ignore [ name-defined ] c = z2 # type: int # type: ignore [ name-defined , xyz ] -d = zz # type: int # type: ignore [xyz] # E: Name "zz" is not defined [name-defined] -e = zz # type: int # type: ignore [ xyz ] # E: Name "zz" is not defined [name-defined] -f = zz # type: int # type: ignore [ xyz,foo ] # E: Name "zz" is not defined [name-defined] +d = zz # type: int # type: ignore [xyz] # E: Name "zz" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment +e = zz # type: int # type: ignore [ xyz ] # E: Name "zz" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment +f = zz # type: int # type: ignore [ xyz,foo ] # E: Name "zz" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment [case testErrorCodeIgnoreAfterArgComment] def f(x # type: xyz # type: ignore[name-defined] # Comment @@ -162,7 +178,8 @@ def g(x # type: xyz # type: ignore # Comment # type () -> None pass -def h(x # type: xyz # type: ignore[foo] # E: Name "xyz" is not defined [name-defined] +def h(x # type: xyz # type: ignore[foo] # E: Name "xyz" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment ): # type () -> None pass @@ -178,7 +195,8 @@ def g(x # type: xyz # type: ignore # Comment # type () -> None pass -def h(x # type: xyz # type: ignore[foo] # E: Name "xyz" is not defined [name-defined] +def h(x # type: xyz # type: ignore[foo] # E: Name "xyz" is not defined [name-defined] \ + # N: Error code "name-defined" not covered by "type: ignore" comment ): # type () -> None pass @@ -944,3 +962,20 @@ class TensorType: ... t: TensorType["batch":..., float] # type: ignore reveal_type(t) # N: Revealed type is "__main__.TensorType" [builtins fixtures/tuple.pyi] + +[case testNoteAboutChangedTypedDictErrorCode] +from typing_extensions import TypedDict +class D(TypedDict): + x: int + +def f(d: D, s: str) -> None: + d[s] # type: ignore[xyz] \ + # E: TypedDict key must be a string literal; expected one of ("x") [literal-required] \ + # N: Error code "literal-required" not covered by "type: ignore" comment + d[s] # E: TypedDict key must be a string literal; expected one of ("x") [literal-required] + d[s] # type: ignore[misc] \ + # E: TypedDict key must be a string literal; expected one of ("x") [literal-required] \ + # N: Error code changed to literal-required; "type: ignore" comment may be out of date + d[s] # type: ignore[literal-required] +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi]