Skip to content

Commit 9685171

Browse files
authored
Avoid false unreachable and redundant-expr warnings in loops. (#18433)
Fixes #18348 Fixes #13973 Fixes #11612 Fixes #8721 Fixes #8865 Fixes #7204 I manually checked all the listed issues. Some of them were already partly fixed by #18180.
1 parent 9274a07 commit 9685171

File tree

2 files changed

+50
-5
lines changed

2 files changed

+50
-5
lines changed

mypy/checker.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -584,24 +584,43 @@ def accept_loop(
584584
*,
585585
exit_condition: Expression | None = None,
586586
) -> None:
587-
"""Repeatedly type check a loop body until the frame doesn't change.
588-
If exit_condition is set, assume it must be False on exit from the loop.
587+
"""Repeatedly type check a loop body until the frame doesn't change."""
589588

590-
Then check the else_body.
591-
"""
592-
# The outer frame accumulates the results of all iterations
589+
# The outer frame accumulates the results of all iterations:
593590
with self.binder.frame_context(can_skip=False, conditional_frame=True):
591+
592+
# Check for potential decreases in the number of partial types so as not to stop the
593+
# iteration too early:
594594
partials_old = sum(len(pts.map) for pts in self.partial_types)
595+
596+
# Disable error types that we cannot safely identify in intermediate iteration steps:
597+
warn_unreachable = self.options.warn_unreachable
598+
warn_redundant = codes.REDUNDANT_EXPR in self.options.enabled_error_codes
599+
self.options.warn_unreachable = False
600+
self.options.enabled_error_codes.discard(codes.REDUNDANT_EXPR)
601+
595602
while True:
596603
with self.binder.frame_context(can_skip=True, break_frame=2, continue_frame=1):
597604
self.accept(body)
598605
partials_new = sum(len(pts.map) for pts in self.partial_types)
599606
if (partials_new == partials_old) and not self.binder.last_pop_changed:
600607
break
601608
partials_old = partials_new
609+
610+
# If necessary, reset the modified options and make up for the postponed error checks:
611+
self.options.warn_unreachable = warn_unreachable
612+
if warn_redundant:
613+
self.options.enabled_error_codes.add(codes.REDUNDANT_EXPR)
614+
if warn_unreachable or warn_redundant:
615+
with self.binder.frame_context(can_skip=True, break_frame=2, continue_frame=1):
616+
self.accept(body)
617+
618+
# If exit_condition is set, assume it must be False on exit from the loop:
602619
if exit_condition:
603620
_, else_map = self.find_isinstance_check(exit_condition)
604621
self.push_type_map(else_map)
622+
623+
# Check the else body:
605624
if else_body:
606625
self.accept(else_body)
607626

test-data/unit/check-narrowing.test

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2390,3 +2390,29 @@ class A:
23902390
z.append(1)
23912391

23922392
[builtins fixtures/primitives.pyi]
2393+
2394+
[case testAvoidFalseUnreachableInLoop]
2395+
# flags: --warn-unreachable --python-version 3.11
2396+
2397+
def f() -> int | None: ...
2398+
def b() -> bool: ...
2399+
2400+
x: int | None
2401+
x = 1
2402+
while x is not None or b():
2403+
x = f()
2404+
2405+
[builtins fixtures/bool.pyi]
2406+
2407+
[case testAvoidFalseRedundantExprInLoop]
2408+
# flags: --enable-error-code redundant-expr --python-version 3.11
2409+
2410+
def f() -> int | None: ...
2411+
def b() -> bool: ...
2412+
2413+
x: int | None
2414+
x = 1
2415+
while x is not None and b():
2416+
x = f()
2417+
2418+
[builtins fixtures/primitives.pyi]

0 commit comments

Comments
 (0)