Skip to content

Commit b6ec270

Browse files
authored
Handle reachability consistently in two-phase checking (#21322)
We have this (somewhat questionable) behaviour of not checking unreachable code. Surprisingly many repos rely on this. Thus we should preserve this behaviour in two-phase checking, which means skip top-level definitions that were found unreachable. I tried few approaches, and using line numbers seems to be the simplest one.
1 parent 680c1ac commit b6ec270

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

mypy/checker.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ def __init__(
493493
self.binder = ConditionalTypeBinder(options)
494494
self.globals_binder = self.binder
495495
self.globals = tree.names
496+
self.globals_unreachable: set[int] = set()
496497
self.return_types = []
497498
self.dynamic_funcs = []
498499
self.partial_types = []
@@ -577,6 +578,12 @@ def reset(self) -> None:
577578
self.partial_types = []
578579
self.inferred_attribute_types = None
579580
self.scope = CheckerScope(self.tree)
581+
self.globals_unreachable.clear()
582+
583+
def mark_unreachable(self, block: list[Statement], after: Statement) -> None:
584+
"""Marks all statements in block after a given one (inclusive) as unreachable."""
585+
last_line = (last := block[-1]).end_line or last.line
586+
self.globals_unreachable.update(range(after.line, last_line + 1))
580587

581588
def check_first_pass(self, recurse_into_functions: bool = True) -> None:
582589
"""Type check the entire file, but defer functions with unresolved references.
@@ -595,8 +602,12 @@ def check_first_pass(self, recurse_into_functions: bool = True) -> None:
595602
)
596603
with self.tscope.module_scope(self.tree.fullname):
597604
with self.enter_partial_types(), self.binder.top_frame_context():
605+
marked_unreachable = False
598606
for d in self.tree.defs:
599607
if self.binder.is_unreachable():
608+
if not marked_unreachable:
609+
self.mark_unreachable(self.tree.defs, after=d)
610+
marked_unreachable = True
600611
if not self.should_report_unreachable_issues():
601612
break
602613
if not self.is_noop_for_reachability(d):
@@ -676,6 +687,8 @@ def check_partial(
676687
if not impl_only:
677688
self.accept(node)
678689
return
690+
if node.line in self.globals_unreachable:
691+
return
679692
if isinstance(node, (FuncDef, Decorator)):
680693
self.check_partial_impl(node)
681694
else:
@@ -3246,8 +3259,12 @@ def visit_block(self, b: Block) -> None:
32463259
# as unreachable -- so we don't display an error.
32473260
self.binder.unreachable()
32483261
return
3262+
marked_unreachable = False
32493263
for s in b.body:
32503264
if self.binder.is_unreachable():
3265+
if self.scope.top_level_function() is None and not marked_unreachable:
3266+
self.mark_unreachable(b.body, after=s)
3267+
marked_unreachable = True
32513268
if not self.should_report_unreachable_issues():
32523269
break
32533270
if not self.is_noop_for_reachability(s):

test-data/unit/check-unreachable-code.test

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,3 +1701,37 @@ def main(contents: Any, commit: str | None) -> None:
17011701

17021702
main({"commit": None}, None)
17031703
[builtins fixtures/tuple.pyi]
1704+
1705+
[case testUnreachableFunctionAfterTopLevelAssert]
1706+
def foo() -> int:
1707+
...
1708+
1709+
raise Exception
1710+
1711+
x = foo()
1712+
1713+
def test() -> None:
1714+
# It is questionable to allow this, this test exists to check 1:1
1715+
# behavior match of parallel checking with sequential checking.
1716+
x + "hm..."
1717+
1718+
class C:
1719+
def test(self) -> None:
1720+
x + "same here..."
1721+
[builtins fixtures/exception.pyi]
1722+
1723+
[case testUnreachableFunctionAfterClassLevelAssert]
1724+
def foo() -> int:
1725+
...
1726+
1727+
class C:
1728+
x = foo()
1729+
raise Exception
1730+
def test(self) -> None:
1731+
# It is questionable to allow this, this test exists to check 1:1
1732+
# behavior match of parallel checking with sequential checking.
1733+
C.x + "hm..."
1734+
1735+
def test_outer() -> None:
1736+
C.x + "no way" # E: Unsupported left operand type for + ("int")
1737+
[builtins fixtures/exception.pyi]

0 commit comments

Comments
 (0)