Skip to content

Commit 229e4a6

Browse files
committed
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 python#18606 and removing the duplicates reported in python#18511, it should significantly reduce the performance regression reported in python#18991. At least running `Measure-command {python runtests.py self}` on my computer (with removed cache) is 10 % faster.
1 parent 64b0a57 commit 229e4a6

File tree

4 files changed

+89
-27
lines changed

4 files changed

+89
-27
lines changed

mypy/checker.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from mypy.constraints import SUPERTYPE_OF
2525
from mypy.erasetype import erase_type, erase_typevars, remove_instance_last_known_values
2626
from mypy.errorcodes import TYPE_VAR, UNUSED_AWAITABLE, UNUSED_COROUTINE, ErrorCode
27-
from mypy.errors import Errors, ErrorWatcher, report_internal_error
27+
from mypy.errors import Errors, ErrorWatcher, LoopErrorWatcher, report_internal_error
2828
from mypy.expandtype import expand_type
2929
from mypy.literals import Key, extract_var_from_literal_hash, literal, literal_hash
3030
from mypy.maptype import map_instance_to_supertype
@@ -600,19 +600,23 @@ def accept_loop(
600600
# on without bound otherwise)
601601
widened_old = len(self.widened_vars)
602602

603-
# Disable error types that we cannot safely identify in intermediate iteration steps:
604-
warn_unreachable = self.options.warn_unreachable
605-
warn_redundant = codes.REDUNDANT_EXPR in self.options.enabled_error_codes
606-
self.options.warn_unreachable = False
607-
self.options.enabled_error_codes.discard(codes.REDUNDANT_EXPR)
608-
609603
iter = 1
610604
while True:
611605
with self.binder.frame_context(can_skip=True, break_frame=2, continue_frame=1):
612606
if on_enter_body is not None:
613607
on_enter_body()
614608

615-
self.accept(body)
609+
# We collect errors that indicate unreachability or redundant expressions
610+
# in the first iteration and remove them in subsequent iterations if the
611+
# related statement becomes reachable or non-redundant due to changes in
612+
# partial types:
613+
with LoopErrorWatcher(self.msg.errors) as watcher:
614+
self.accept(body)
615+
if iter == 1:
616+
uselessness_errors = watcher.useless_statements
617+
else:
618+
uselessness_errors.intersection_update(watcher.useless_statements)
619+
616620
partials_new = sum(len(pts.map) for pts in self.partial_types)
617621
widened_new = len(self.widened_vars)
618622
# Perform multiple iterations if something changed that might affect
@@ -633,16 +637,15 @@ def accept_loop(
633637
if iter == 20:
634638
raise RuntimeError("Too many iterations when checking a loop")
635639

636-
# If necessary, reset the modified options and make up for the postponed error checks:
637-
self.options.warn_unreachable = warn_unreachable
638-
if warn_redundant:
639-
self.options.enabled_error_codes.add(codes.REDUNDANT_EXPR)
640-
if warn_unreachable or warn_redundant:
641-
with self.binder.frame_context(can_skip=True, break_frame=2, continue_frame=1):
642-
if on_enter_body is not None:
643-
on_enter_body()
644-
645-
self.accept(body)
640+
# Report those unreachable and redundant expression errors that have been
641+
# identified in all iteration steps, as well as the revealed types identified
642+
# in the last iteration step:
643+
for notes_and_errors in itertools.chain(uselessness_errors, watcher.revealed_types):
644+
context = Context(line=notes_and_errors[2], column=notes_and_errors[3])
645+
context.end_line = notes_and_errors[4]
646+
context.end_column = notes_and_errors[5]
647+
reporter = self.msg.note if notes_and_errors[0] == codes.MISC else self.msg.fail
648+
reporter(notes_and_errors[1], context, code=notes_and_errors[0])
646649

647650
# If exit_condition is set, assume it must be False on exit from the loop:
648651
if exit_condition:

mypy/errors.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections import defaultdict
77
from collections.abc import Iterable
88
from typing import Callable, Final, NoReturn, Optional, TextIO, TypeVar
9-
from typing_extensions import Literal, TypeAlias as _TypeAlias
9+
from typing_extensions import Literal, Self, TypeAlias as _TypeAlias
1010

1111
from mypy import errorcodes as codes
1212
from mypy.error_formatter import ErrorFormatter
@@ -179,7 +179,7 @@ def __init__(
179179
self._filter_deprecated = filter_deprecated
180180
self._filtered: list[ErrorInfo] | None = [] if save_filtered_errors else None
181181

182-
def __enter__(self) -> ErrorWatcher:
182+
def __enter__(self) -> Self:
183183
self.errors._watchers.append(self)
184184
return self
185185

@@ -220,6 +220,45 @@ def filtered_errors(self) -> list[ErrorInfo]:
220220
return self._filtered
221221

222222

223+
class LoopErrorWatcher(ErrorWatcher):
224+
"""Error watcher that filters and separately collects unreachable errors, redundant
225+
expression errors, and revealed type notes when analysing loops iteratively to help
226+
avoid making too-hasty reports."""
227+
228+
# Meaning of the entries: ErrorCode, message, line, column, end_line, end_column:
229+
useless_statements: set[tuple[ErrorCode, str, int, int, int, int]]
230+
revealed_types: set[tuple[ErrorCode, str, int, int, int, int]]
231+
232+
def __init__(
233+
self,
234+
errors: Errors,
235+
*,
236+
filter_errors: bool | Callable[[str, ErrorInfo], bool] = False,
237+
save_filtered_errors: bool = False,
238+
filter_deprecated: bool = False,
239+
) -> None:
240+
super().__init__(
241+
errors,
242+
filter_errors=filter_errors,
243+
save_filtered_errors=save_filtered_errors,
244+
filter_deprecated=filter_deprecated,
245+
)
246+
self.useless_statements = set()
247+
self.revealed_types = set()
248+
249+
def on_error(self, file: str, info: ErrorInfo) -> bool:
250+
if info.code in (codes.UNREACHABLE, codes.REDUNDANT_EXPR):
251+
self.useless_statements.add(
252+
(info.code, info.message, info.line, info.column, info.end_line, info.end_column)
253+
)
254+
return True
255+
if info.code == codes.MISC and info.message.startswith("Revealed type is "):
256+
self.revealed_types.add(
257+
(info.code, info.message, info.line, info.column, info.end_line, info.end_column)
258+
)
259+
return True
260+
return super().on_error(file, info)
261+
223262
class Errors:
224263
"""Container for compile errors.
225264

test-data/unit/check-narrowing.test

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2346,8 +2346,7 @@ def f() -> bool: ...
23462346

23472347
y = None
23482348
while f():
2349-
reveal_type(y) # N: Revealed type is "None" \
2350-
# N: Revealed type is "Union[builtins.int, None]"
2349+
reveal_type(y) # N: Revealed type is "Union[builtins.int, None]"
23512350
y = 1
23522351
reveal_type(y) # N: Revealed type is "Union[builtins.int, None]"
23532352

@@ -2370,7 +2369,7 @@ class A:
23702369

23712370
[builtins fixtures/primitives.pyi]
23722371

2373-
[case testAvoidFalseUnreachableInLoop]
2372+
[case testAvoidFalseUnreachableInLoop1]
23742373
# flags: --warn-unreachable --python-version 3.11
23752374

23762375
def f() -> int | None: ...
@@ -2383,6 +2382,29 @@ while x is not None or b():
23832382

23842383
[builtins fixtures/bool.pyi]
23852384

2385+
[case testAvoidFalseUnreachableInLoop2]
2386+
# flags: --warn-unreachable --python-version 3.11
2387+
2388+
y = None
2389+
while y is None:
2390+
if y is None:
2391+
y = []
2392+
y.append(1)
2393+
2394+
[builtins fixtures/list.pyi]
2395+
2396+
[case testAvoidFalseUnreachableInLoop3]
2397+
# flags: --warn-unreachable --python-version 3.11
2398+
2399+
xs: list[int | None]
2400+
y = None
2401+
for x in xs:
2402+
if x is not None:
2403+
if y is None:
2404+
y = {} # E: Need type annotation for "y" (hint: "y: Dict[<type>, <type>] = ...")
2405+
2406+
[builtins fixtures/list.pyi]
2407+
23862408
[case testAvoidFalseRedundantExprInLoop]
23872409
# flags: --enable-error-code redundant-expr --python-version 3.11
23882410

test-data/unit/check-redefine2.test

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -628,8 +628,7 @@ def f1() -> None:
628628
def f2() -> None:
629629
x = None
630630
while int():
631-
reveal_type(x) # N: Revealed type is "None" \
632-
# N: Revealed type is "Union[None, builtins.str]"
631+
reveal_type(x) # N: Revealed type is "Union[None, builtins.str]"
633632
if int():
634633
x = ""
635634
reveal_type(x) # N: Revealed type is "Union[None, builtins.str]"
@@ -709,8 +708,7 @@ def b() -> None:
709708
def c() -> None:
710709
x = 0
711710
while int():
712-
reveal_type(x) # N: Revealed type is "builtins.int" \
713-
# N: Revealed type is "Union[builtins.int, builtins.str, None]"
711+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]"
714712
if int():
715713
x = ""
716714
continue

0 commit comments

Comments
 (0)