diff --git a/mypy/errors.py b/mypy/errors.py index cc66653aeca3..3a0e0e14d8b3 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -145,7 +145,7 @@ class Errors: ignored_lines: Dict[str, Dict[int, List[str]]] # Lines on which an error was actually ignored. - used_ignored_lines: Dict[str, Set[int]] + used_ignored_lines: Dict[str, Dict[int, List[str]]] # Files where all errors should be ignored. ignored_files: Set[str] @@ -200,7 +200,7 @@ def initialize(self) -> None: self.import_ctx = [] self.function_or_member = [None] self.ignored_lines = OrderedDict() - self.used_ignored_lines = defaultdict(set) + self.used_ignored_lines = defaultdict(lambda: defaultdict(list)) self.ignored_files = set() self.only_once_messages = set() self.scope = None @@ -369,7 +369,8 @@ def add_error_info(self, info: ErrorInfo) -> None: for scope_line in range(line, end_line + 1): if self.is_ignored_error(scope_line, info, self.ignored_lines[file]): # Annotation requests us to ignore all errors on this line. - self.used_ignored_lines[file].add(scope_line) + self.used_ignored_lines[file][scope_line].append( + (info.code or codes.MISC).code) return if file in self.ignored_files: return @@ -461,10 +462,25 @@ def clear_errors_in_targets(self, path: str, targets: Set[str]) -> None: def generate_unused_ignore_errors(self, file: str) -> None: ignored_lines = self.ignored_lines[file] if not is_typeshed_file(file) and file not in self.ignored_files: - for line in set(ignored_lines) - self.used_ignored_lines[file]: + ignored_lines = self.ignored_lines[file] + used_ignored_lines = self.used_ignored_lines[file] + for line, ignored_codes in ignored_lines.items(): + used_ignored_codes = used_ignored_lines[line] + unused_ignored_codes = set(ignored_codes) - set(used_ignored_codes) + # `ignore` is used + if len(ignored_codes) == 0 and len(used_ignored_codes) > 0: + continue + # All codes appearing in `ignore[...]` are used + if len(ignored_codes) > 0 and len(unused_ignored_codes) == 0: + continue + # Display detail only when `ignore[...]` specifies more than one error code + unused_codes_message = "" + if len(ignored_codes) > 1 and len(unused_ignored_codes) > 0: + unused_codes_message = f"[{', '.join(sorted(unused_ignored_codes))}]" + message = f'unused "type: ignore{unused_codes_message}" comment' # Don't use report since add_error_info will ignore the error! info = ErrorInfo(self.import_context(), file, self.current_module(), None, - None, line, -1, 'error', 'unused "type: ignore" comment', + None, line, -1, 'error', message, None, False, False, False) self._add_error_info(file, info) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 3108d4602a8a..2f4122bf3bfa 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -358,7 +358,8 @@ def translate_stmt_list(self, # ignores the whole module: if (ismodule and stmts and self.type_ignores and min(self.type_ignores) < self.get_lineno(stmts[0])): - self.errors.used_ignored_lines[self.errors.file].add(min(self.type_ignores)) + self.errors.used_ignored_lines[self.errors.file][min(self.type_ignores)].append( + codes.MISC.code) block = Block(self.fix_function_overloads(self.translate_stmt_list(stmts))) mark_block_unreachable(block) return [block] diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index bf5ec9663a78..2d288bf158e5 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -215,7 +215,8 @@ def translate_stmt_list(self, # ignores the whole module: if (module and stmts and self.type_ignores and min(self.type_ignores) < self.get_lineno(stmts[0])): - self.errors.used_ignored_lines[self.errors.file].add(min(self.type_ignores)) + self.errors.used_ignored_lines[self.errors.file][min(self.type_ignores)].append( + codes.MISC.code) block = Block(self.fix_function_overloads(self.translate_stmt_list(stmts))) mark_block_unreachable(block) return [block] diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index c7dbf81d909f..1e7c7898f9ac 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -111,6 +111,30 @@ a = 'x'.foobar(b) # type: ignore[xyz, w, attr-defined] # E: Name "b" is not de 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] +[case testErrorCodeWarnUnusedIgnores1] +# flags: --warn-unused-ignores +x # type: ignore[name-defined, attr-defined] # E: unused "type: ignore[attr-defined]" comment + +[case testErrorCodeWarnUnusedIgnores2] +# flags: --warn-unused-ignores +"x".foobar(y) # type: ignore[name-defined, attr-defined] + +[case testErrorCodeWarnUnusedIgnores3] +# flags: --warn-unused-ignores +"x".foobar(y) # type: ignore[name-defined, attr-defined, xyz] # E: unused "type: ignore[xyz]" comment + +[case testErrorCodeWarnUnusedIgnores4] +# flags: --warn-unused-ignores +"x".foobar(y) # type: ignore[name-defined, attr-defined, valid-type] # E: unused "type: ignore[valid-type]" comment + +[case testErrorCodeWarnUnusedIgnores5] +# flags: --warn-unused-ignores +"x".foobar(y) # type: ignore[name-defined, attr-defined, valid-type, xyz] # E: unused "type: ignore[valid-type, xyz]" comment + +[case testErrorCodeWarnUnusedIgnores6_NoDetailWhenSingleErrorCode] +# flags: --warn-unused-ignores +"x" # type: ignore[name-defined] # E: unused "type: ignore" comment + [case testErrorCodeIgnoreWithExtraSpace] x # type: ignore [name-defined] x2 # type: ignore [ name-defined ]