diff --git a/mypy/errors.py b/mypy/errors.py index 6e90c28d9c03..e9233cb4e2a4 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -498,7 +498,33 @@ def add_error_info(self, info: ErrorInfo) -> None: # Check each line in this context for "type: ignore" comments. # line == end_line for most nodes, so we only loop once. for scope_line in lines: - if self.is_ignored_error(scope_line, info, self.ignored_lines[file]): + ignores = self.ignored_lines[file] + if info.code and not self.is_error_code_enabled(info.code): + is_ignored_error = True + record_ignored_line = False + elif scope_line not in ignores: + is_ignored_error = False + record_ignored_line = False + elif not ignores[scope_line]: + # Empty list means that we ignore all errors + is_ignored_error = True + record_ignored_line = True + elif info.code and self.is_error_code_enabled(info.code): + is_ignored_error = ( + info.code.code in ignores[scope_line] + or info.code.sub_code_of is not None + and info.code.sub_code_of.code in ignores[scope_line] + ) + record_ignored_line = is_ignored_error + else: + is_ignored_error = False + record_ignored_line = False + + if record_ignored_line and file not in self.ignored_files: + info.hidden = True + self._add_error_info(file, info) + + if is_ignored_error: # Annotation requests us to ignore all errors on this line. self.used_ignored_lines[file][scope_line].append( (info.code or codes.MISC).code @@ -627,25 +653,6 @@ def report_hidden_errors(self, info: ErrorInfo) -> None: ) self._add_error_info(info.origin[0], new_info) - def is_ignored_error(self, line: int, info: ErrorInfo, ignores: dict[int, list[str]]) -> bool: - if info.blocker: - # Blocking errors can never be ignored - return False - if info.code and not self.is_error_code_enabled(info.code): - return True - if line not in ignores: - return False - if not ignores[line]: - # Empty list means that we ignore all errors - return True - if info.code and self.is_error_code_enabled(info.code): - return ( - info.code.code in ignores[line] - or info.code.sub_code_of is not None - and info.code.sub_code_of.code in ignores[line] - ) - return False - def is_error_code_enabled(self, error_code: ErrorCode) -> bool: if self.options: current_mod_disabled = self.options.disabled_error_codes @@ -672,7 +679,7 @@ def clear_errors_in_targets(self, path: str, targets: set[str]) -> None: if info.target not in targets: new_errors.append(info) has_blocker |= info.blocker - elif info.only_once: + elif info.only_once and not info.hidden: self.only_once_messages.remove(info.message) self.error_info_map[path] = new_errors if not has_blocker and path in self.has_blockers: @@ -725,6 +732,8 @@ def generate_unused_ignore_errors(self, file: str) -> None: blocker=False, only_once=False, allow_dups=False, + origin=(self.file, [line]), + target=self.target_module, ) self._add_error_info(file, info) @@ -777,6 +786,8 @@ def generate_ignore_without_code_errors( blocker=False, only_once=False, allow_dups=False, + origin=(self.file, [line]), + target=self.target_module, ) self._add_error_info(file, info) @@ -801,8 +812,9 @@ def blocker_module(self) -> str | None: return None def is_errors_for_file(self, file: str) -> bool: - """Are there any errors for the given file?""" - return file in self.error_info_map + """Are there any visible errors for the given file?""" + errors = self.error_info_map.get(file, ()) + return any(error.hidden is False for error in errors) def prefer_simple_messages(self) -> bool: """Should we generate simple/fast error messages? diff --git a/mypy/server/update.py b/mypy/server/update.py index 0cc7a2229514..0371d5d40266 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -667,6 +667,8 @@ def restore(ids: list[str]) -> None: state.type_check_first_pass() state.type_check_second_pass() state.detect_possibly_undefined_vars() + state.generate_unused_ignore_notes() + state.generate_ignore_without_code_notes() t2 = time.time() state.finish_passes() t3 = time.time() @@ -1028,6 +1030,10 @@ def key(node: FineGrainedDeferredNode) -> int: if graph[module_id].type_checker().check_second_pass(): more = True + graph[module_id].detect_possibly_undefined_vars() + graph[module_id].generate_unused_ignore_notes() + graph[module_id].generate_ignore_without_code_notes() + if manager.options.export_types: manager.all_types.update(graph[module_id].type_map()) diff --git a/test-data/unit/daemon.test b/test-data/unit/daemon.test index ca2c969d2f5e..40ae67862998 100644 --- a/test-data/unit/daemon.test +++ b/test-data/unit/daemon.test @@ -615,3 +615,77 @@ b: str from demo.test import a [file demo/test.py] a: int + +[case testUnusedTypeIgnorePreservedOnRerun] +-- Regression test for https://github.com/python/mypy/issues/9655 +$ dmypy start -- --warn-unused-ignores --no-error-summary --hide-error-codes +Daemon started +$ dmypy check -- bar.py +bar.py:2: error: Unused "type: ignore" comment +== Return code: 1 +$ dmypy check -- bar.py +bar.py:2: error: Unused "type: ignore" comment +== Return code: 1 + +[file foo/__init__.py] +[file foo/empty.py] +[file bar.py] +from foo.empty import * +a = 1 # type: ignore + +[case testTypeIgnoreWithoutCodePreservedOnRerun] +-- Regression test for https://github.com/python/mypy/issues/9655 +$ dmypy start -- --enable-error-code ignore-without-code --no-error-summary +Daemon started +$ dmypy check -- bar.py +bar.py:2: error: "type: ignore" comment without error code [ignore-without-code] +== Return code: 1 +$ dmypy check -- bar.py +bar.py:2: error: "type: ignore" comment without error code [ignore-without-code] +== Return code: 1 + +[file foo/__init__.py] +[file foo/empty.py] +[file bar.py] +from foo.empty import * +a = 1 # type: ignore + +[case testPossiblyUndefinedVarsPreservedAfterUpdate] +-- Regression test for https://github.com/python/mypy/issues/9655 +$ dmypy start -- --enable-error-code possibly-undefined --no-error-summary +Daemon started +$ dmypy check -- bar.py +bar.py:4: error: Name "a" may be undefined [possibly-undefined] +== Return code: 1 +$ dmypy check -- bar.py +bar.py:4: error: Name "a" may be undefined [possibly-undefined] +== Return code: 1 + +[file foo/__init__.py] +[file foo/empty.py] +[file bar.py] +from foo.empty import * +if False: + a = 1 +a + +[case testReturnTypeIgnoreAfterUnknownImport] +-- Return type ignores after unknown imports and unused modules are respected on the second pass. +$ dmypy start -- --warn-unused-ignores --no-error-summary +Daemon started +$ dmypy check -- foo.py +foo.py:2: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found] +foo.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports +== Return code: 1 +$ dmypy check -- foo.py +foo.py:2: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found] +foo.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports +== Return code: 1 + +[file unused/__init__.py] +[file unused/empty.py] +[file foo.py] +from unused.empty import * +import a_module_which_does_not_exist +def is_foo() -> str: + return True # type: ignore diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 165a2089b466..e1d35098d206 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10505,3 +10505,63 @@ from pkg.sub import modb [out] == + +[case testUnusedTypeIgnorePreservedAfterChange] +# flags: --warn-unused-ignores --no-error-summary +[file main.py] +a = 1 # type: ignore +[file main.py.2] +a = 1 # type: ignore +# Comment to trigger reload. +[out] +main.py:1: error: Unused "type: ignore" comment +== +main.py:1: error: Unused "type: ignore" comment + +[case testTypeIgnoreWithoutCodePreservedAfterChange] +# flags: --enable-error-code ignore-without-code --no-error-summary +[file main.py] +a = 1 # type: ignore +[file main.py.2] +a = 1 # type: ignore +# Comment to trigger reload. +[out] +main.py:1: error: "type: ignore" comment without error code +== +main.py:1: error: "type: ignore" comment without error code + + +[case testUnusedTypeIgnorePreservedOnRerunWithIgnoredMissingImports] +# flags: --no-error-summary --ignore-missing-imports --warn-unused-ignores +import foo +[file foo/__init__.py] +from bar import * +[file foo/__init__.py.2] +from bar import * +# Comment to trigger reload. +[file foo/empty.py] +[file bar.py] +from foo import empty +a = 1 # type: ignore +[out] +bar.py:2: error: Unused "type: ignore" comment +== +bar.py:2: error: Unused "type: ignore" comment + + +[case testModuleDoesNotExistPreservedOnRerun] +# flags: --no-error-summary --ignore-missing-imports +import foo +[file foo/__init__.py] +from bar import * +[file bar.py] +from foo import does_not_exist +from unused.submodule import * +[file bar.py.2] +from foo import does_not_exist +from unused.submodule import * +# Comment to trigger reload. +[out] +bar.py:1: error: Module "foo" has no attribute "does_not_exist" [attr-defined] +== +bar.py:1: error: Module "foo" has no attribute "does_not_exist" [attr-defined]