Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
34 changes: 34 additions & 0 deletions test-data/unit/check-unreachable-code.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading