Skip to content

Commit 269e7ee

Browse files
authored
Avoid false possibly-undefined errors due to omitted unrequired else statements. (#20149)
Fixes #14771 The mentioned issue's `except: assert_never` examples are already fixed by #15995. This PR only targets the "no unreachable else" examples. Introducing the special-purpose attribute `IfStmt.else_irrelevant_for_possibly_undefined` is not super nice, but it appears more straightforward and less risky than starting to introduce empty else blocks with `is_unreachable = True` (as discussed [here](#13926 (comment))).
1 parent 041aa5d commit 269e7ee

File tree

4 files changed

+41
-6
lines changed

4 files changed

+41
-6
lines changed

mypy/checker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5070,6 +5070,8 @@ def visit_if_stmt(self, s: IfStmt) -> None:
50705070

50715071
if_map, else_map = self.find_isinstance_check(e)
50725072

5073+
s.unreachable_else = else_map is None
5074+
50735075
# XXX Issue a warning if condition is always False?
50745076
with self.binder.frame_context(can_skip=True, fall_through=2):
50755077
self.push_type_map(if_map, from_assignment=False)

mypy/nodes.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,19 +1832,26 @@ def accept(self, visitor: StatementVisitor[T]) -> T:
18321832

18331833

18341834
class IfStmt(Statement):
1835-
__slots__ = ("expr", "body", "else_body")
1835+
__slots__ = ("expr", "body", "else_body", "unreachable_else")
18361836

1837-
__match_args__ = ("expr", "body", "else_body")
1837+
__match_args__ = ("expr", "body", "else_body", "unreachable_else")
18381838

18391839
expr: list[Expression]
18401840
body: list[Block]
18411841
else_body: Block | None
1842+
# (If there is actually no else statement, semantic analysis may nevertheless create an
1843+
# empty else block and mark it permanently as unreachable to tell that the control flow
1844+
# must always go through the if block.)
1845+
unreachable_else: bool
1846+
# (Type checking may modify this flag repeatedly to indicate whether an actually available
1847+
# or unavailable else block is unreachable, considering the current type information.)
18421848

18431849
def __init__(self, expr: list[Expression], body: list[Block], else_body: Block | None) -> None:
18441850
super().__init__()
18451851
self.expr = expr
18461852
self.body = body
18471853
self.else_body = else_body
1854+
self.unreachable_else = False
18481855

18491856
def accept(self, visitor: StatementVisitor[T]) -> T:
18501857
return visitor.visit_if_stmt(self)

mypy/partially_defined.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,13 @@ def visit_if_stmt(self, o: IfStmt) -> None:
397397
continue
398398
b.accept(self)
399399
self.tracker.next_branch()
400-
if o.else_body:
401-
if not o.else_body.is_unreachable:
402-
o.else_body.accept(self)
403-
else:
400+
if o.unreachable_else:
401+
self.tracker.skip_branch()
402+
elif o.else_body:
403+
if o.else_body.is_unreachable:
404404
self.tracker.skip_branch()
405+
else:
406+
o.else_body.accept(self)
405407
self.tracker.end_branch_statement()
406408

407409
def visit_match_stmt(self, o: MatchStmt) -> None:

test-data/unit/check-possibly-undefined.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,3 +1054,27 @@ def foo(x: Union[int, str]) -> None:
10541054
assert_never(x)
10551055
f # OK
10561056
[builtins fixtures/tuple.pyi]
1057+
1058+
[case testOmittedUnrequiredElse]
1059+
# flags: --enable-error-code possibly-undefined
1060+
from typing import Literal
1061+
1062+
a: Literal[True]
1063+
if a:
1064+
w = 1
1065+
w + 1
1066+
1067+
b: bool
1068+
if b:
1069+
x = 1
1070+
elif not b:
1071+
x = 2
1072+
x + 1
1073+
1074+
if b:
1075+
y = 1
1076+
elif not b:
1077+
if a:
1078+
z = 2
1079+
y = z
1080+
y + 1

0 commit comments

Comments
 (0)