Skip to content

Commit ed351a8

Browse files
comments and tests
1 parent 7980683 commit ed351a8

File tree

3 files changed

+58
-3
lines changed

3 files changed

+58
-3
lines changed

crates/ty_python_semantic/resources/mdtest/terminal_statements.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,8 @@ def f():
572572

573573
## Calls to functions returning `Never` / `NoReturn`
574574

575+
These calls should be treated as terminal statements.
576+
575577
### No implicit return
576578

577579
If we see a call to a function returning `Never`, we should be able to understand that the function
@@ -614,9 +616,42 @@ def g(x: int | None):
614616
sys.exit(1)
615617

616618
# TODO: should be just int, not int | None
619+
# See https://github.com/astral-sh/ty/issues/685
617620
reveal_type(x) # revealed: int | None
618621
```
619622

623+
### Possibly unresolved diagnostics
624+
625+
If the codepath on which a variable is not defined eventually returns `Never`, use of the variable
626+
should not give any diagnostics.
627+
628+
```py
629+
import sys
630+
631+
def _(flag: bool):
632+
if flag:
633+
x = 3
634+
else:
635+
sys.exit()
636+
637+
x # No possibly-unresolved-references diagnostic here.
638+
```
639+
640+
Similarly, there shouldn't be any diagnostics if the `except` block of a `try/except` construct has
641+
a call with `NoReturn`.
642+
643+
```py
644+
import sys
645+
646+
def _():
647+
try:
648+
x = 3
649+
except:
650+
sys.exit()
651+
652+
x # No possibly-unresolved-references diagnostic here.
653+
```
654+
620655
### Bindings after call
621656

622657
These should be understood to be unreachable.
@@ -651,6 +686,7 @@ def f(x): ...
651686
def _() -> NoReturn:
652687
f(3)
653688

689+
# This should be an error because of implicitly returning `None`
654690
# error: [invalid-return-type]
655691
def _() -> NoReturn:
656692
f("")

crates/ty_python_semantic/src/semantic_index/builder.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1910,9 +1910,21 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
19101910

19111911
self.visit_expr(value);
19121912

1913-
// These constraints are expensive
1914-
if !self.source_type.is_stub() && self.in_function_scope() {
1915-
if let ast::Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
1913+
// If the statement is a call, it could possibly be a call to a function
1914+
// marked with `NoReturn` (for example, `sys.exit()`). In this case, we use a special
1915+
// kind of constraint to mark the following code as unreachable.
1916+
//
1917+
// Ideally, these constraints should be added for every call expression, even those in
1918+
// sub-expressions and in the module-level scope. But doing so makes the number of
1919+
// such constraints so high that it significantly degrades performance. We thus cut
1920+
// scope here and add these constraints only at statement level function calls,
1921+
// like `sys.exit()`, and not within sub-expression like `3 + sys.exit()` etc.
1922+
//
1923+
// We also only add these inside function scopes, since considering module-level
1924+
// constraints can affect the the type of imported symbols, leading to a lot more
1925+
// work in third-party code.
1926+
if let ast::Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
1927+
if !self.source_type.is_stub() && self.in_function_scope() {
19161928
let callable = self.add_standalone_expression(func);
19171929
let call_expr = self.add_standalone_expression(value.as_ref());
19181930

crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,13 @@ impl ReachabilityConstraints {
689689
callable,
690690
call_expr,
691691
}) => {
692+
// We first infer just the type of the callable. In the most likely case that the
693+
// function is not marked with `NoReturn`, or that it always returns `NoReturn`,
694+
// doing so allows us to avoid the more expensive work of inferring the entire call
695+
// expression (which could involve inferring argument types to possibly run the overload
696+
// selection algorithm).
697+
// Avoiding this on the happy-path is important because these constraints can be
698+
// very large in number, since we add them on all statement level function calls.
692699
let ty = infer_expression_type(db, callable);
693700

694701
let overloads_iterator =

0 commit comments

Comments
 (0)