diff --git a/mypy/checker.py b/mypy/checker.py index fc03dd80b73e..cd1c9e321094 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -493,6 +493,7 @@ def __init__( self.binder = ConditionalTypeBinder(options) self.globals_binder = self.binder self.globals = tree.names + self.globals_unreachable: set[int] = set() self.return_types = [] self.dynamic_funcs = [] self.partial_types = [] @@ -577,6 +578,12 @@ def reset(self) -> None: self.partial_types = [] self.inferred_attribute_types = None self.scope = CheckerScope(self.tree) + self.globals_unreachable.clear() + + def mark_unreachable(self, block: list[Statement], after: Statement) -> None: + """Marks all statements in block after a given one (inclusive) as unreachable.""" + last_line = (last := block[-1]).end_line or last.line + self.globals_unreachable.update(range(after.line, last_line + 1)) def check_first_pass(self, recurse_into_functions: bool = True) -> None: """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: ) with self.tscope.module_scope(self.tree.fullname): with self.enter_partial_types(), self.binder.top_frame_context(): + marked_unreachable = False for d in self.tree.defs: if self.binder.is_unreachable(): + if not marked_unreachable: + self.mark_unreachable(self.tree.defs, after=d) + marked_unreachable = True if not self.should_report_unreachable_issues(): break if not self.is_noop_for_reachability(d): @@ -676,6 +687,8 @@ def check_partial( if not impl_only: self.accept(node) return + if node.line in self.globals_unreachable: + return if isinstance(node, (FuncDef, Decorator)): self.check_partial_impl(node) else: @@ -3244,8 +3257,12 @@ def visit_block(self, b: Block) -> None: # as unreachable -- so we don't display an error. self.binder.unreachable() return + marked_unreachable = False for s in b.body: if self.binder.is_unreachable(): + if self.scope.top_level_function() is None and not marked_unreachable: + self.mark_unreachable(b.body, after=s) + marked_unreachable = True if not self.should_report_unreachable_issues(): break if not self.is_noop_for_reachability(s): diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 257478c517f3..8d67de4edd83 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -1701,3 +1701,37 @@ def main(contents: Any, commit: str | None) -> None: main({"commit": None}, None) [builtins fixtures/tuple.pyi] + +[case testUnreachableFunctionAfterTopLevelAssert] +def foo() -> int: + ... + +raise Exception + +x = foo() + +def test() -> None: + # It is questionable to allow this, this test exists to check 1:1 + # behavior match of parallel checking with sequential checking. + x + "hm..." + +class C: + def test(self) -> None: + x + "same here..." +[builtins fixtures/exception.pyi] + +[case testUnreachableFunctionAfterClassLevelAssert] +def foo() -> int: + ... + +class C: + x = foo() + raise Exception + def test(self) -> None: + # It is questionable to allow this, this test exists to check 1:1 + # behavior match of parallel checking with sequential checking. + C.x + "hm..." + +def test_outer() -> None: + C.x + "no way" # E: Unsupported left operand type for + ("int") +[builtins fixtures/exception.pyi]