-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Improve the handling of "iteration dependent" errors and notes in finally clauses. #19270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
71a3b28
fc1c790
84de3bb
2ddab26
0b6f6b9
435b1f5
af8b672
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,15 @@ | |
import sys | ||
import traceback | ||
from collections import defaultdict | ||
from collections.abc import Iterable | ||
from collections.abc import Iterable, Iterator | ||
from itertools import chain | ||
from typing import Callable, Final, NoReturn, Optional, TextIO, TypeVar | ||
from typing_extensions import Literal, Self, TypeAlias as _TypeAlias | ||
|
||
from mypy import errorcodes as codes | ||
from mypy.error_formatter import ErrorFormatter | ||
from mypy.errorcodes import IMPORT, IMPORT_NOT_FOUND, IMPORT_UNTYPED, ErrorCode, mypy_error_codes | ||
from mypy.nodes import Context | ||
from mypy.options import Options | ||
from mypy.scope import Scope | ||
from mypy.util import DEFAULT_SOURCE_OFFSET, is_typeshed_file | ||
|
@@ -219,23 +221,43 @@ def filtered_errors(self) -> list[ErrorInfo]: | |
return self._filtered | ||
|
||
|
||
class LoopErrorWatcher(ErrorWatcher): | ||
"""Error watcher that filters and separately collects `unreachable` errors, | ||
`redundant-expr` and `redundant-casts` errors, and revealed types when analysing | ||
loops iteratively to help avoid making too-hasty reports.""" | ||
class IterationDependentErrors: | ||
"""An `IterationDependentErrors` instance serves to collect the `unreachable`, | ||
`redundant-expr`, and `redundant-casts` errors, as well as the revealed types, | ||
handled by the individual `IterationErrorWatcher` instances sequentially applied to | ||
the same code section.""" | ||
|
||
# Meaning of the tuple items: ErrorCode, message, line, column, end_line, end_column: | ||
uselessness_errors: set[tuple[ErrorCode, str, int, int, int, int]] | ||
# One set of `unreachable`, `redundant-expr`, and `redundant-casts` errors per | ||
# iteration step. Meaning of the tuple items: ErrorCode, message, line, column, | ||
# end_line, end_column. | ||
uselessness_errors: list[set[tuple[ErrorCode, str, int, int, int, int]]] | ||
|
||
# Meaning of the tuple items: function_or_member, line, column, end_line, end_column: | ||
# One set of unreachable line numbers per iteration step. Not only the lines where | ||
# the error report occurs but really all unreachable lines. | ||
unreachable_lines: list[set[int]] | ||
|
||
# One set of revealed types for each `reveal_type` statement. Each created set can | ||
# grow during the iteration. Meaning of the tuple items: function_or_member, line, | ||
# column, end_line, end_column: | ||
revealed_types: dict[tuple[str | None, int, int, int, int], set[str]] | ||
|
||
# Not only the lines where the error report occurs but really all unreachable lines: | ||
unreachable_lines: set[int] | ||
def __init__(self) -> None: | ||
self.uselessness_errors = [] | ||
self.unreachable_lines = [] | ||
self.revealed_types = defaultdict(set) | ||
|
||
|
||
class IterationErrorWatcher(ErrorWatcher): | ||
"""Error watcher that filters and separately collects `unreachable` errors, | ||
`redundant-expr` and `redundant-casts` errors, and revealed types when analysing | ||
code sections iteratively to help avoid making too-hasty reports.""" | ||
|
||
iteration_dependent_errors: IterationDependentErrors | ||
|
||
def __init__( | ||
self, | ||
errors: Errors, | ||
iteration_dependent_errors: IterationDependentErrors, | ||
*, | ||
filter_errors: bool | Callable[[str, ErrorInfo], bool] = False, | ||
save_filtered_errors: bool = False, | ||
|
@@ -247,31 +269,71 @@ def __init__( | |
save_filtered_errors=save_filtered_errors, | ||
filter_deprecated=filter_deprecated, | ||
) | ||
self.uselessness_errors = set() | ||
self.unreachable_lines = set() | ||
self.revealed_types = defaultdict(set) | ||
self.iteration_dependent_errors = iteration_dependent_errors | ||
iteration_dependent_errors.uselessness_errors.append(set()) | ||
iteration_dependent_errors.unreachable_lines.append(set()) | ||
|
||
def on_error(self, file: str, info: ErrorInfo) -> bool: | ||
"""Filter out the "iteration-dependent" errors and notes and store their | ||
information to handle them after iteration is completed.""" | ||
|
||
iter_errors = self.iteration_dependent_errors | ||
|
||
if info.code in (codes.UNREACHABLE, codes.REDUNDANT_EXPR, codes.REDUNDANT_CAST): | ||
self.uselessness_errors.add( | ||
iter_errors.uselessness_errors[-1].add( | ||
(info.code, info.message, info.line, info.column, info.end_line, info.end_column) | ||
) | ||
if info.code == codes.UNREACHABLE: | ||
self.unreachable_lines.update(range(info.line, info.end_line + 1)) | ||
iter_errors.unreachable_lines[-1].update(range(info.line, info.end_line + 1)) | ||
return True | ||
|
||
if info.code == codes.MISC and info.message.startswith("Revealed type is "): | ||
key = info.function_or_member, info.line, info.column, info.end_line, info.end_column | ||
types = info.message.split('"')[1] | ||
if types.startswith("Union["): | ||
self.revealed_types[key].update(types[6:-1].split(", ")) | ||
iter_errors.revealed_types[key].update(types[6:-1].split(", ")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's another issue, but since it's pre-existing, it's better to fix in a separate PR: I don't think that the |
||
else: | ||
self.revealed_types[key].add(types) | ||
iter_errors.revealed_types[key].add(types) | ||
return True | ||
|
||
return super().on_error(file, info) | ||
|
||
def yield_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]: | ||
"""Report only those `unreachable`, `redundant-expr`, and `redundant-casts` | ||
errors that could not be ruled out in any iteration step.""" | ||
|
||
persistent_uselessness_errors = set() | ||
iter_errors = self.iteration_dependent_errors | ||
for candidate in set(chain(*iter_errors.uselessness_errors)): | ||
if all( | ||
(candidate in errors) or (candidate[2] in lines) | ||
for errors, lines in zip( | ||
iter_errors.uselessness_errors, iter_errors.unreachable_lines | ||
) | ||
): | ||
persistent_uselessness_errors.add(candidate) | ||
for error_info in persistent_uselessness_errors: | ||
context = Context(line=error_info[2], column=error_info[3]) | ||
context.end_line = error_info[4] | ||
context.end_column = error_info[5] | ||
yield error_info[1], context, error_info[0] | ||
|
||
def yield_note_infos(self, options: Options) -> Iterator[tuple[str, Context]]: | ||
"""Yield all types revealed in at least one iteration step.""" | ||
|
||
for note_info, types in self.iteration_dependent_errors.revealed_types.items(): | ||
sorted_ = sorted(types, key=lambda typ: typ.lower()) | ||
if len(types) == 1: | ||
revealed = sorted_[0] | ||
elif options.use_or_syntax(): | ||
revealed = " | ".join(sorted_) | ||
else: | ||
revealed = f"Union[{', '.join(sorted_)}]" | ||
context = Context(line=note_info[1], column=note_info[2]) | ||
context.end_line = note_info[3] | ||
context.end_column = note_info[4] | ||
yield f'Revealed type is "{revealed}"', context | ||
|
||
|
||
class Errors: | ||
"""Container for compile errors. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic seems unreliable -- consider code like
reveal_type('"\'')
. A better approach would be to keep anything between the first and last double quotes. Since this issue is pre-existing, it's best to fix it in a separate PR.