From 229e4a6cb90d7c21cf91306df7aa9111ec99a295 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 20 May 2025 06:39:16 +0200 Subject: [PATCH 1/7] Avoid false `unreachable` and `redundant-expr` warnings in loops more robustly and efficiently, and avoid multiple `revealed type` notes for the same line. This change is an improvement over 9685171. Besides fixing the regression reported in #18606 and removing the duplicates reported in #18511, it should significantly reduce the performance regression reported in #18991. At least running `Measure-command {python runtests.py self}` on my computer (with removed cache) is 10 % faster. --- mypy/checker.py | 39 ++++++++++++++------------ mypy/errors.py | 43 +++++++++++++++++++++++++++-- test-data/unit/check-narrowing.test | 28 +++++++++++++++++-- test-data/unit/check-redefine2.test | 6 ++-- 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2d82d74cc197..eb2b503390e2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -24,7 +24,7 @@ from mypy.constraints import SUPERTYPE_OF from mypy.erasetype import erase_type, erase_typevars, remove_instance_last_known_values from mypy.errorcodes import TYPE_VAR, UNUSED_AWAITABLE, UNUSED_COROUTINE, ErrorCode -from mypy.errors import Errors, ErrorWatcher, report_internal_error +from mypy.errors import Errors, ErrorWatcher, LoopErrorWatcher, report_internal_error from mypy.expandtype import expand_type from mypy.literals import Key, extract_var_from_literal_hash, literal, literal_hash from mypy.maptype import map_instance_to_supertype @@ -600,19 +600,23 @@ def accept_loop( # on without bound otherwise) widened_old = len(self.widened_vars) - # Disable error types that we cannot safely identify in intermediate iteration steps: - warn_unreachable = self.options.warn_unreachable - warn_redundant = codes.REDUNDANT_EXPR in self.options.enabled_error_codes - self.options.warn_unreachable = False - self.options.enabled_error_codes.discard(codes.REDUNDANT_EXPR) - iter = 1 while True: with self.binder.frame_context(can_skip=True, break_frame=2, continue_frame=1): if on_enter_body is not None: on_enter_body() - self.accept(body) + # We collect errors that indicate unreachability or redundant expressions + # in the first iteration and remove them in subsequent iterations if the + # related statement becomes reachable or non-redundant due to changes in + # partial types: + with LoopErrorWatcher(self.msg.errors) as watcher: + self.accept(body) + if iter == 1: + uselessness_errors = watcher.useless_statements + else: + uselessness_errors.intersection_update(watcher.useless_statements) + partials_new = sum(len(pts.map) for pts in self.partial_types) widened_new = len(self.widened_vars) # Perform multiple iterations if something changed that might affect @@ -633,16 +637,15 @@ def accept_loop( if iter == 20: raise RuntimeError("Too many iterations when checking a loop") - # If necessary, reset the modified options and make up for the postponed error checks: - self.options.warn_unreachable = warn_unreachable - if warn_redundant: - self.options.enabled_error_codes.add(codes.REDUNDANT_EXPR) - if warn_unreachable or warn_redundant: - with self.binder.frame_context(can_skip=True, break_frame=2, continue_frame=1): - if on_enter_body is not None: - on_enter_body() - - self.accept(body) + # Report those unreachable and redundant expression errors that have been + # identified in all iteration steps, as well as the revealed types identified + # in the last iteration step: + for notes_and_errors in itertools.chain(uselessness_errors, watcher.revealed_types): + context = Context(line=notes_and_errors[2], column=notes_and_errors[3]) + context.end_line = notes_and_errors[4] + context.end_column = notes_and_errors[5] + reporter = self.msg.note if notes_and_errors[0] == codes.MISC else self.msg.fail + reporter(notes_and_errors[1], context, code=notes_and_errors[0]) # If exit_condition is set, assume it must be False on exit from the loop: if exit_condition: diff --git a/mypy/errors.py b/mypy/errors.py index c9510ae5f1eb..f2035e35dc1d 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -6,7 +6,7 @@ from collections import defaultdict from collections.abc import Iterable from typing import Callable, Final, NoReturn, Optional, TextIO, TypeVar -from typing_extensions import Literal, TypeAlias as _TypeAlias +from typing_extensions import Literal, Self, TypeAlias as _TypeAlias from mypy import errorcodes as codes from mypy.error_formatter import ErrorFormatter @@ -179,7 +179,7 @@ def __init__( self._filter_deprecated = filter_deprecated self._filtered: list[ErrorInfo] | None = [] if save_filtered_errors else None - def __enter__(self) -> ErrorWatcher: + def __enter__(self) -> Self: self.errors._watchers.append(self) return self @@ -220,6 +220,45 @@ def filtered_errors(self) -> list[ErrorInfo]: return self._filtered +class LoopErrorWatcher(ErrorWatcher): + """Error watcher that filters and separately collects unreachable errors, redundant + expression errors, and revealed type notes when analysing loops iteratively to help + avoid making too-hasty reports.""" + + # Meaning of the entries: ErrorCode, message, line, column, end_line, end_column: + useless_statements: set[tuple[ErrorCode, str, int, int, int, int]] + revealed_types: set[tuple[ErrorCode, str, int, int, int, int]] + + def __init__( + self, + errors: Errors, + *, + filter_errors: bool | Callable[[str, ErrorInfo], bool] = False, + save_filtered_errors: bool = False, + filter_deprecated: bool = False, + ) -> None: + super().__init__( + errors, + filter_errors=filter_errors, + save_filtered_errors=save_filtered_errors, + filter_deprecated=filter_deprecated, + ) + self.useless_statements = set() + self.revealed_types = set() + + def on_error(self, file: str, info: ErrorInfo) -> bool: + if info.code in (codes.UNREACHABLE, codes.REDUNDANT_EXPR): + self.useless_statements.add( + (info.code, info.message, info.line, info.column, info.end_line, info.end_column) + ) + return True + if info.code == codes.MISC and info.message.startswith("Revealed type is "): + self.revealed_types.add( + (info.code, info.message, info.line, info.column, info.end_line, info.end_column) + ) + return True + return super().on_error(file, info) + class Errors: """Container for compile errors. diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index dc2cfd46d9ad..2e86758914ff 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2346,8 +2346,7 @@ def f() -> bool: ... y = None while f(): - reveal_type(y) # N: Revealed type is "None" \ - # N: Revealed type is "Union[builtins.int, None]" + reveal_type(y) # N: Revealed type is "Union[builtins.int, None]" y = 1 reveal_type(y) # N: Revealed type is "Union[builtins.int, None]" @@ -2370,7 +2369,7 @@ class A: [builtins fixtures/primitives.pyi] -[case testAvoidFalseUnreachableInLoop] +[case testAvoidFalseUnreachableInLoop1] # flags: --warn-unreachable --python-version 3.11 def f() -> int | None: ... @@ -2383,6 +2382,29 @@ while x is not None or b(): [builtins fixtures/bool.pyi] +[case testAvoidFalseUnreachableInLoop2] +# flags: --warn-unreachable --python-version 3.11 + +y = None +while y is None: + if y is None: + y = [] + y.append(1) + +[builtins fixtures/list.pyi] + +[case testAvoidFalseUnreachableInLoop3] +# flags: --warn-unreachable --python-version 3.11 + +xs: list[int | None] +y = None +for x in xs: + if x is not None: + if y is None: + y = {} # E: Need type annotation for "y" (hint: "y: Dict[, ] = ...") + +[builtins fixtures/list.pyi] + [case testAvoidFalseRedundantExprInLoop] # flags: --enable-error-code redundant-expr --python-version 3.11 diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index 238b64399ce4..180a0d879207 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -628,8 +628,7 @@ def f1() -> None: def f2() -> None: x = None while int(): - reveal_type(x) # N: Revealed type is "None" \ - # N: Revealed type is "Union[None, builtins.str]" + reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" if int(): x = "" reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" @@ -709,8 +708,7 @@ def b() -> None: def c() -> None: x = 0 while int(): - reveal_type(x) # N: Revealed type is "builtins.int" \ - # N: Revealed type is "Union[builtins.int, builtins.str, None]" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]" if int(): x = "" continue From c4bea4e7c9abb8af01892811c5fcd8765a2f7410 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 05:19:57 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/errors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/errors.py b/mypy/errors.py index f2035e35dc1d..78b8a59503b3 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -259,6 +259,7 @@ def on_error(self, file: str, info: ErrorInfo) -> bool: return True return super().on_error(file, info) + class Errors: """Container for compile errors. From c24c950341ed2dd8b91a5ac044d87a2c23c8e223 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 22 May 2025 08:07:16 +0200 Subject: [PATCH 3/7] report all revealed types, not only those of the last iterations step --- mypy/checker.py | 24 +++++++++++++++--------- mypy/errors.py | 20 ++++++++++++-------- test-data/unit/check-inference.test | 4 ++-- test-data/unit/check-redefine2.test | 7 +++---- test-data/unit/check-typevar-tuple.test | 2 +- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index eb2b503390e2..c575631c2bf1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -637,15 +637,21 @@ def accept_loop( if iter == 20: raise RuntimeError("Too many iterations when checking a loop") - # Report those unreachable and redundant expression errors that have been - # identified in all iteration steps, as well as the revealed types identified - # in the last iteration step: - for notes_and_errors in itertools.chain(uselessness_errors, watcher.revealed_types): - context = Context(line=notes_and_errors[2], column=notes_and_errors[3]) - context.end_line = notes_and_errors[4] - context.end_column = notes_and_errors[5] - reporter = self.msg.note if notes_and_errors[0] == codes.MISC else self.msg.fail - reporter(notes_and_errors[1], context, code=notes_and_errors[0]) + # Report those unreachable and redundant expression errors identified in all + # iteration steps: + for error_info in uselessness_errors: + context = Context(line=error_info[2], column=error_info[3]) + context.end_line = error_info[4] + context.end_column = error_info[5] + self.msg.fail(error_info[1], context, code=error_info[0]) + # Report all types revealed in at least one iteration step: + for note_info, types in watcher.revealed_types.items(): + sorted_ = sorted(types, key=lambda typ: typ.lower()) + revealed = sorted_[0] if len(types) == 1 else 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] + self.note(f'Revealed type is "{revealed}"', context) # If exit_condition is set, assume it must be False on exit from the loop: if exit_condition: diff --git a/mypy/errors.py b/mypy/errors.py index 78b8a59503b3..c11570ca15e3 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -222,12 +222,13 @@ def filtered_errors(self) -> list[ErrorInfo]: class LoopErrorWatcher(ErrorWatcher): """Error watcher that filters and separately collects unreachable errors, redundant - expression errors, and revealed type notes when analysing loops iteratively to help - avoid making too-hasty reports.""" + expression errors, and revealed types when analysing loops iteratively to help avoid + making too-hasty reports.""" - # Meaning of the entries: ErrorCode, message, line, column, end_line, end_column: + # Meaning of the tuple items: ErrorCode, message, line, column, end_line, end_column: useless_statements: set[tuple[ErrorCode, str, int, int, int, int]] - revealed_types: set[tuple[ErrorCode, str, int, int, int, int]] + # 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]] def __init__( self, @@ -244,7 +245,7 @@ def __init__( filter_deprecated=filter_deprecated, ) self.useless_statements = set() - self.revealed_types = set() + self.revealed_types = defaultdict(set) def on_error(self, file: str, info: ErrorInfo) -> bool: if info.code in (codes.UNREACHABLE, codes.REDUNDANT_EXPR): @@ -253,9 +254,12 @@ def on_error(self, file: str, info: ErrorInfo) -> bool: ) return True if info.code == codes.MISC and info.message.startswith("Revealed type is "): - self.revealed_types.add( - (info.code, info.message, info.line, info.column, info.end_line, info.end_column) - ) + 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(", ")) + else: + self.revealed_types[key].add(types) return True return super().on_error(file, info) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 25565946158e..6f8ebb6e010e 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -343,7 +343,7 @@ for var2 in [g, h, i, j, k, l]: reveal_type(var2) # N: Revealed type is "Union[builtins.int, builtins.str]" for var3 in [m, n, o, p, q, r]: - reveal_type(var3) # N: Revealed type is "Union[builtins.int, Any]" + reveal_type(var3) # N: Revealed type is "Union[Any, builtins.int]" T = TypeVar("T", bound=Type[Foo]) @@ -1247,7 +1247,7 @@ class X(TypedDict): x: X for a in ("hourly", "daily"): - reveal_type(a) # N: Revealed type is "Union[Literal['hourly']?, Literal['daily']?]" + reveal_type(a) # N: Revealed type is "Union[Literal['daily']?, Literal['hourly']?]" reveal_type(x[a]) # N: Revealed type is "builtins.int" reveal_type(a.upper()) # N: Revealed type is "builtins.str" c = a diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index 180a0d879207..cc7d18049e84 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -628,7 +628,7 @@ def f1() -> None: def f2() -> None: x = None while int(): - reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" + reveal_type(x) # N: Revealed type is "Union[builtins.str, None]" if int(): x = "" reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" @@ -808,8 +808,7 @@ def f4() -> None: x = None break finally: - reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]" \ - # N: Revealed type is "Union[builtins.int, None]" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]" reveal_type(x) # N: Revealed type is "Union[builtins.int, None]" [builtins fixtures/exception.pyi] @@ -925,7 +924,7 @@ class X(TypedDict): x: X for a in ("hourly", "daily"): - reveal_type(a) # N: Revealed type is "Union[Literal['hourly']?, Literal['daily']?]" + reveal_type(a) # N: Revealed type is "Union[Literal['daily']?, Literal['hourly']?]" reveal_type(x[a]) # N: Revealed type is "builtins.int" reveal_type(a.upper()) # N: Revealed type is "builtins.str" c = a diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index d364439f22e9..53d378648051 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -989,7 +989,7 @@ from typing_extensions import Unpack def pipeline(*xs: Unpack[Tuple[int, Unpack[Tuple[float, ...]], bool]]) -> None: for x in xs: - reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.float]" + reveal_type(x) # N: Revealed type is "Union[builtins.float, builtins.int]" [builtins fixtures/tuple.pyi] [case testFixedUnpackItemInInstanceArguments] From b6930e736a3bc7604a3aab7bfe4b750e0694c51d Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 22 May 2025 11:28:32 +0200 Subject: [PATCH 4/7] restart CI --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index c575631c2bf1..b0eb9cfa730a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -636,7 +636,7 @@ def accept_loop( iter += 1 if iter == 20: raise RuntimeError("Too many iterations when checking a loop") - +asdf # Report those unreachable and redundant expression errors identified in all # iteration steps: for error_info in uselessness_errors: From 01b6140208158ec9d2b74cd0b8b729edcb6cd84a Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 22 May 2025 11:28:38 +0200 Subject: [PATCH 5/7] Revert "restart CI" This reverts commit b6930e736a3bc7604a3aab7bfe4b750e0694c51d. --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index b0eb9cfa730a..c575631c2bf1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -636,7 +636,7 @@ def accept_loop( iter += 1 if iter == 20: raise RuntimeError("Too many iterations when checking a loop") -asdf + # Report those unreachable and redundant expression errors identified in all # iteration steps: for error_info in uselessness_errors: From 15f5740f4f055e154bfe3e17530afc5f8d126237 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 24 May 2025 02:09:55 +0200 Subject: [PATCH 6/7] Add the test case `testPersistentUnreachableLinesNestedInInpersistentUnreachableLines` provided by @A5rocks and extend the current logic to fix it. --- mypy/checker.py | 35 +++++++++++++++++++---------- mypy/errors.py | 22 +++++++++++++----- test-data/unit/check-narrowing.test | 13 +++++++++++ 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c575631c2bf1..94754200ac1f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -600,22 +600,25 @@ def accept_loop( # on without bound otherwise) widened_old = len(self.widened_vars) + # one set of `unreachable` and `redundant-expr`errors per iteration step: + uselessness_errors = [] + # one set of unreachable line numbers per iteration step: + unreachable_lines = [] + # one set of revealed types per line where `reveal_type` is used (each + # created set can grow during the iteration): + revealed_types = defaultdict(set) iter = 1 while True: with self.binder.frame_context(can_skip=True, break_frame=2, continue_frame=1): if on_enter_body is not None: on_enter_body() - # We collect errors that indicate unreachability or redundant expressions - # in the first iteration and remove them in subsequent iterations if the - # related statement becomes reachable or non-redundant due to changes in - # partial types: with LoopErrorWatcher(self.msg.errors) as watcher: self.accept(body) - if iter == 1: - uselessness_errors = watcher.useless_statements - else: - uselessness_errors.intersection_update(watcher.useless_statements) + uselessness_errors.append(watcher.uselessness_errors) + unreachable_lines.append(watcher.unreachable_lines) + for key, values in watcher.revealed_types.items(): + revealed_types[key].update(values) partials_new = sum(len(pts.map) for pts in self.partial_types) widened_new = len(self.widened_vars) @@ -637,15 +640,23 @@ def accept_loop( if iter == 20: raise RuntimeError("Too many iterations when checking a loop") - # Report those unreachable and redundant expression errors identified in all - # iteration steps: - for error_info in uselessness_errors: + # Report only those `unreachable` and `redundant-expr` errors that could not + # be ruled out in any iteration step: + persistent_uselessness_errors = set() + for candidate in set(itertools.chain(*uselessness_errors)): + if all( + (candidate in errors) or (candidate[2] in lines) + for errors, lines in zip(uselessness_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] self.msg.fail(error_info[1], context, code=error_info[0]) + # Report all types revealed in at least one iteration step: - for note_info, types in watcher.revealed_types.items(): + for note_info, types in revealed_types.items(): sorted_ = sorted(types, key=lambda typ: typ.lower()) revealed = sorted_[0] if len(types) == 1 else f"Union[{', '.join(sorted_)}]" context = Context(line=note_info[1], column=note_info[2]) diff --git a/mypy/errors.py b/mypy/errors.py index c11570ca15e3..5b671ea0dd12 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -221,15 +221,19 @@ def filtered_errors(self) -> list[ErrorInfo]: class LoopErrorWatcher(ErrorWatcher): - """Error watcher that filters and separately collects unreachable errors, redundant - expression errors, and revealed types when analysing loops iteratively to help avoid - making too-hasty reports.""" + """Error watcher that filters and separately collects `unreachable` errors, + `redundant-expr` errors, and revealed types when analysing loops iteratively + to help avoid making too-hasty reports.""" # Meaning of the tuple items: ErrorCode, message, line, column, end_line, end_column: - useless_statements: set[tuple[ErrorCode, str, int, int, int, int]] + uselessness_errors: set[tuple[ErrorCode, str, int, int, int, int]] + # 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, errors: Errors, @@ -244,15 +248,20 @@ def __init__( save_filtered_errors=save_filtered_errors, filter_deprecated=filter_deprecated, ) - self.useless_statements = set() + self.uselessness_errors = set() + self.unreachable_lines = set() self.revealed_types = defaultdict(set) def on_error(self, file: str, info: ErrorInfo) -> bool: + if info.code in (codes.UNREACHABLE, codes.REDUNDANT_EXPR): - self.useless_statements.add( + self.uselessness_errors.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)) 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] @@ -261,6 +270,7 @@ def on_error(self, file: str, info: ErrorInfo) -> bool: else: self.revealed_types[key].add(types) return True + return super().on_error(file, info) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 2e86758914ff..894bd2060662 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2369,6 +2369,19 @@ class A: [builtins fixtures/primitives.pyi] +[case testPersistentUnreachableLinesNestedInInpersistentUnreachableLines] +# flags: --warn-unreachable --python-version 3.11 + +x = None +y = None +while True: + if x is not None: + if y is not None: + reveal_type(y) # E: Statement is unreachable + x = 1 + +[builtins fixtures/bool.pyi] + [case testAvoidFalseUnreachableInLoop1] # flags: --warn-unreachable --python-version 3.11 From 129f1cb8762e3672563bfc3b029f2328c1357ccb Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 30 May 2025 12:48:12 +0200 Subject: [PATCH 7/7] consider `redundant-casts`, too test case from @auntsaninja (slightly simplified) --- mypy/checker.py | 7 ++++--- mypy/errors.py | 6 +++--- test-data/unit/check-narrowing.test | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 94754200ac1f..d786bcc7e78f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -600,7 +600,8 @@ def accept_loop( # on without bound otherwise) widened_old = len(self.widened_vars) - # one set of `unreachable` and `redundant-expr`errors per iteration step: + # one set of `unreachable`, `redundant-expr`, and `redundant-casts` errors + # per iteration step: uselessness_errors = [] # one set of unreachable line numbers per iteration step: unreachable_lines = [] @@ -640,8 +641,8 @@ def accept_loop( if iter == 20: raise RuntimeError("Too many iterations when checking a loop") - # Report only those `unreachable` and `redundant-expr` errors that could not - # be ruled out in any iteration step: + # Report only those `unreachable`, `redundant-expr`, and `redundant-casts` + # errors that could not be ruled out in any iteration step: persistent_uselessness_errors = set() for candidate in set(itertools.chain(*uselessness_errors)): if all( diff --git a/mypy/errors.py b/mypy/errors.py index 5b671ea0dd12..6aa19ed7c5a0 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -222,8 +222,8 @@ def filtered_errors(self) -> list[ErrorInfo]: class LoopErrorWatcher(ErrorWatcher): """Error watcher that filters and separately collects `unreachable` errors, - `redundant-expr` errors, and revealed types when analysing loops iteratively - to help avoid making too-hasty reports.""" + `redundant-expr` and `redundant-casts` errors, and revealed types when analysing + loops iteratively to help avoid making too-hasty reports.""" # Meaning of the tuple items: ErrorCode, message, line, column, end_line, end_column: uselessness_errors: set[tuple[ErrorCode, str, int, int, int, int]] @@ -254,7 +254,7 @@ def __init__( def on_error(self, file: str, info: ErrorInfo) -> bool: - if info.code in (codes.UNREACHABLE, codes.REDUNDANT_EXPR): + if info.code in (codes.UNREACHABLE, codes.REDUNDANT_EXPR, codes.REDUNDANT_CAST): self.uselessness_errors.add( (info.code, info.message, info.line, info.column, info.end_line, info.end_column) ) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 894bd2060662..d2d9d0615f73 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2382,6 +2382,28 @@ while True: [builtins fixtures/bool.pyi] +[case testAvoidFalseRedundantCastInLoops] +# flags: --warn-redundant-casts + +from typing import Callable, cast, Union + +ProcessorReturnValue = Union[str, int] +Processor = Callable[[str], ProcessorReturnValue] + +def main_cast(p: Processor) -> None: + ed: ProcessorReturnValue + ed = cast(str, ...) + while True: + ed = p(cast(str, ed)) + +def main_no_cast(p: Processor) -> None: + ed: ProcessorReturnValue + ed = cast(str, ...) + while True: + ed = p(ed) # E: Argument 1 has incompatible type "Union[str, int]"; expected "str" + +[builtins fixtures/bool.pyi] + [case testAvoidFalseUnreachableInLoop1] # flags: --warn-unreachable --python-version 3.11