From 9c3349d8e3b79962879db9c5991f473a4eed5560 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 25 Jan 2022 15:20:53 +0000 Subject: [PATCH 1/3] Add note about changed error codes, such as literal-required 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 to suggest changing the ignored error code. --- mypy/errors.py | 19 +++++++++++++++++++ test-data/unit/check-errorcodes.test | 16 ++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/mypy/errors.py b/mypy/errors.py index c711456468ed4..6d8c58aa62f71 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,20 @@ def add_error_info(self, info: ErrorInfo) -> None: info.hidden = True self.report_hidden_errors(info) self._add_error_info(file, info) + if info.code in original_error_codes: + # If there seems to be a "type: ignore" with a stale error + # code, report a helpful note. + old_code = original_error_codes[info.code].code + if old_code in self.ignored_lines.get(file, {}).get(info.line, []): + msg = 'Error code changed to {}; "type: ignore" comment may be out of date'.format( + info.code.code) + 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 568d3a9522f92..4c99415d3edd3 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -944,3 +944,19 @@ 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] + 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] From 2168c97c5b1af85aaa9f3b2cc8214582ee80e456 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 25 Jan 2022 16:10:46 +0000 Subject: [PATCH 2/3] Also add note if we don't know the original error code --- mypy/errors.py | 32 ++++++++++++++++------------ test-data/unit/check-errorcodes.test | 7 +++++- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 6d8c58aa62f71..324e980d2ac2e 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -393,20 +393,24 @@ def add_error_info(self, info: ErrorInfo) -> None: info.hidden = True self.report_hidden_errors(info) self._add_error_info(file, info) - if info.code in original_error_codes: - # If there seems to be a "type: ignore" with a stale error - # code, report a helpful note. - old_code = original_error_codes[info.code].code - if old_code in self.ignored_lines.get(file, {}).get(info.line, []): - msg = 'Error code changed to {}; "type: ignore" comment may be out of date'.format( - info.code.code) - 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) + ignored_codes = self.ignored_lines.get(file, {}).get(info.line, []) + if ignored_codes: + # Something is ignored on the line, but not this error, so maybe the error + # code is incorrect. + msg = 'Error code in "type: ignore" comment may be incorrect or out-of-date' + 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 4c99415d3edd3..5e989a3bd4650 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -952,7 +952,8 @@ class D(TypedDict): 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] + # E: TypedDict key must be a string literal; expected one of ("x") [literal-required] \ + # N: Error code in "type: ignore" comment may be incorrect or out-of-date 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] \ @@ -960,3 +961,7 @@ def f(d: D, s: str) -> None: d[s] # type: ignore[literal-required] [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testNoteAboutInvalidErrorCodeIgnored] +1() # type: ignore[xyz] # E: "int" not callable [operator] \ + # N: Error code in "type: ignore" comment may be incorrect or out-of-date From eec63a6a0af27c016039cb4ef78ceeaf0ca32bf1 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 25 Jan 2022 16:26:14 +0000 Subject: [PATCH 3/3] Fixes and tweaks; update tests --- mypy/errors.py | 4 +- test-data/unit/check-errorcodes.test | 60 +++++++++++++++++----------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 324e980d2ac2e..ec49541a15a51 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -394,10 +394,10 @@ def add_error_info(self, info: ErrorInfo) -> None: self.report_hidden_errors(info) self._add_error_info(file, info) ignored_codes = self.ignored_lines.get(file, {}).get(info.line, []) - if ignored_codes: + if ignored_codes and info.code: # Something is ignored on the line, but not this error, so maybe the error # code is incorrect. - msg = 'Error code in "type: ignore" comment may be incorrect or out-of-date' + 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. diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 5e989a3bd4650..177612959354f 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 @@ -953,7 +971,7 @@ class D(TypedDict): 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 in "type: ignore" comment may be incorrect or out-of-date + # 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] \ @@ -961,7 +979,3 @@ def f(d: D, s: str) -> None: d[s] # type: ignore[literal-required] [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] - -[case testNoteAboutInvalidErrorCodeIgnored] -1() # type: ignore[xyz] # E: "int" not callable [operator] \ - # N: Error code in "type: ignore" comment may be incorrect or out-of-date