From f4bd74ab6af8981ccd384ad0c791ea6028e21a94 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas <55339528+abhijeetbodas2001@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:22:52 +0530 Subject: [PATCH] [ty] Correctly handle calls to functions marked as returning `Never` / `NoReturn` (#18333) ## Summary `ty` does not understand that calls to functions which have been annotated as having a return type of `Never` / `NoReturn` are terminal. This PR fixes that, by adding new reachability constraints when call expressions are seen. If the call expression evaluates to `Never`, the code following it will be considered to be unreachable. Note that, for adding these constraints, we only consider call expressions at the statement level, and that too only inside function scopes. This is because otherwise, the number of such constraints becomes too high, and evaluating them later on during type inference results in a major performance degradation. Fixes https://github.com/astral-sh/ty/issues/180 ## Test Plan New mdtests. ## Ecosystem changes This PR removes the following false-positives: - "Function can implicitly return `None`, which is not assignable to ...". - "Name `foo` used when possibly not defind" - because the branch in which it is not defined has a `NoReturn` call, or when `foo` was imported in a `try`, and the except had a `NoReturn` call. --------- Co-authored-by: David Peter --- .../mdtest/directives/assert_never.md | 34 ++- ...ity_-_Diagnostics_(be8f5d8b0718ee54).snap} | 124 +++++----- .../resources/mdtest/terminal_statements.md | 223 ++++++++++++++++++ .../src/semantic_index/builder.rs | 44 +++- .../src/semantic_index/predicate.rs | 7 + .../reachability_constraints.rs | 50 +++- .../src/semantic_index/use_def.rs | 11 + crates/ty_python_semantic/src/types.rs | 2 +- crates/ty_python_semantic/src/types/infer.rs | 7 +- crates/ty_python_semantic/src/types/narrow.rs | 7 +- 10 files changed, 437 insertions(+), 72 deletions(-) rename crates/ty_python_semantic/resources/mdtest/snapshots/{assert_never.md_-_`assert_never`_-_Basic_functionality_(78e7b52096b8d36c).snap => assert_never.md_-_`assert_never`_-_Basic_functionality_-_Diagnostics_(be8f5d8b0718ee54).snap} (53%) diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md index 958f2c04f9..abb5117564 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md @@ -2,27 +2,53 @@ ## Basic functionality - +`assert_never` makes sure that the type of the argument is `Never`. -`assert_never` makes sure that the type of the argument is `Never`. If it is not, a -`type-assertion-failure` diagnostic is emitted. +### Correct usage ```py from typing_extensions import assert_never, Never, Any from ty_extensions import Unknown -def _(never: Never, any_: Any, unknown: Unknown, flag: bool): +def _(never: Never): assert_never(never) # fine +``` +### Diagnostics + + + +If it is not, a `type-assertion-failure` diagnostic is emitted. + +```py +from typing_extensions import assert_never, Never, Any +from ty_extensions import Unknown + +def _(): assert_never(0) # error: [type-assertion-failure] + +def _(): assert_never("") # error: [type-assertion-failure] + +def _(): assert_never(None) # error: [type-assertion-failure] + +def _(): assert_never([]) # error: [type-assertion-failure] + +def _(): assert_never({}) # error: [type-assertion-failure] + +def _(): assert_never(()) # error: [type-assertion-failure] + +def _(flag: bool, never: Never): assert_never(1 if flag else never) # error: [type-assertion-failure] +def _(any_: Any): assert_never(any_) # error: [type-assertion-failure] + +def _(unknown: Unknown): assert_never(unknown) # error: [type-assertion-failure] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality_(78e7b52096b8d36c).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality_-_Diagnostics_(be8f5d8b0718ee54).snap similarity index 53% rename from crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality_(78e7b52096b8d36c).snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality_-_Diagnostics_(be8f5d8b0718ee54).snap index 6b4c6d3625..8c2ae5522a 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality_(78e7b52096b8d36c).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality_-_Diagnostics_(be8f5d8b0718ee54).snap @@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs expression: snapshot --- --- -mdtest name: assert_never.md - `assert_never` - Basic functionality +mdtest name: assert_never.md - `assert_never` - Basic functionality - Diagnostics mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.md --- @@ -15,35 +15,47 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never. 1 | from typing_extensions import assert_never, Never, Any 2 | from ty_extensions import Unknown 3 | - 4 | def _(never: Never, any_: Any, unknown: Unknown, flag: bool): - 5 | assert_never(never) # fine + 4 | def _(): + 5 | assert_never(0) # error: [type-assertion-failure] 6 | - 7 | assert_never(0) # error: [type-assertion-failure] + 7 | def _(): 8 | assert_never("") # error: [type-assertion-failure] - 9 | assert_never(None) # error: [type-assertion-failure] -10 | assert_never([]) # error: [type-assertion-failure] -11 | assert_never({}) # error: [type-assertion-failure] -12 | assert_never(()) # error: [type-assertion-failure] -13 | assert_never(1 if flag else never) # error: [type-assertion-failure] -14 | -15 | assert_never(any_) # error: [type-assertion-failure] -16 | assert_never(unknown) # error: [type-assertion-failure] + 9 | +10 | def _(): +11 | assert_never(None) # error: [type-assertion-failure] +12 | +13 | def _(): +14 | assert_never([]) # error: [type-assertion-failure] +15 | +16 | def _(): +17 | assert_never({}) # error: [type-assertion-failure] +18 | +19 | def _(): +20 | assert_never(()) # error: [type-assertion-failure] +21 | +22 | def _(flag: bool, never: Never): +23 | assert_never(1 if flag else never) # error: [type-assertion-failure] +24 | +25 | def _(any_: Any): +26 | assert_never(any_) # error: [type-assertion-failure] +27 | +28 | def _(unknown: Unknown): +29 | assert_never(unknown) # error: [type-assertion-failure] ``` # Diagnostics ``` error[type-assertion-failure]: Argument does not have asserted type `Never` - --> src/mdtest_snippet.py:7:5 + --> src/mdtest_snippet.py:5:5 | -5 | assert_never(never) # fine -6 | -7 | assert_never(0) # error: [type-assertion-failure] +4 | def _(): +5 | assert_never(0) # error: [type-assertion-failure] | ^^^^^^^^^^^^^-^ | | | Inferred type of argument is `Literal[0]` -8 | assert_never("") # error: [type-assertion-failure] -9 | assert_never(None) # error: [type-assertion-failure] +6 | +7 | def _(): | info: `Never` and `Literal[0]` are not equivalent types info: rule `type-assertion-failure` is enabled by default @@ -54,13 +66,13 @@ info: rule `type-assertion-failure` is enabled by default error[type-assertion-failure]: Argument does not have asserted type `Never` --> src/mdtest_snippet.py:8:5 | - 7 | assert_never(0) # error: [type-assertion-failure] + 7 | def _(): 8 | assert_never("") # error: [type-assertion-failure] | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is `Literal[""]` - 9 | assert_never(None) # error: [type-assertion-failure] -10 | assert_never([]) # error: [type-assertion-failure] + 9 | +10 | def _(): | info: `Never` and `Literal[""]` are not equivalent types info: rule `type-assertion-failure` is enabled by default @@ -69,16 +81,15 @@ info: rule `type-assertion-failure` is enabled by default ``` error[type-assertion-failure]: Argument does not have asserted type `Never` - --> src/mdtest_snippet.py:9:5 + --> src/mdtest_snippet.py:11:5 | - 7 | assert_never(0) # error: [type-assertion-failure] - 8 | assert_never("") # error: [type-assertion-failure] - 9 | assert_never(None) # error: [type-assertion-failure] +10 | def _(): +11 | assert_never(None) # error: [type-assertion-failure] | ^^^^^^^^^^^^^----^ | | | Inferred type of argument is `None` -10 | assert_never([]) # error: [type-assertion-failure] -11 | assert_never({}) # error: [type-assertion-failure] +12 | +13 | def _(): | info: `Never` and `None` are not equivalent types info: rule `type-assertion-failure` is enabled by default @@ -87,16 +98,15 @@ info: rule `type-assertion-failure` is enabled by default ``` error[type-assertion-failure]: Argument does not have asserted type `Never` - --> src/mdtest_snippet.py:10:5 + --> src/mdtest_snippet.py:14:5 | - 8 | assert_never("") # error: [type-assertion-failure] - 9 | assert_never(None) # error: [type-assertion-failure] -10 | assert_never([]) # error: [type-assertion-failure] +13 | def _(): +14 | assert_never([]) # error: [type-assertion-failure] | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is `list[Unknown]` -11 | assert_never({}) # error: [type-assertion-failure] -12 | assert_never(()) # error: [type-assertion-failure] +15 | +16 | def _(): | info: `Never` and `list[Unknown]` are not equivalent types info: rule `type-assertion-failure` is enabled by default @@ -105,16 +115,15 @@ info: rule `type-assertion-failure` is enabled by default ``` error[type-assertion-failure]: Argument does not have asserted type `Never` - --> src/mdtest_snippet.py:11:5 + --> src/mdtest_snippet.py:17:5 | - 9 | assert_never(None) # error: [type-assertion-failure] -10 | assert_never([]) # error: [type-assertion-failure] -11 | assert_never({}) # error: [type-assertion-failure] +16 | def _(): +17 | assert_never({}) # error: [type-assertion-failure] | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is `dict[Unknown, Unknown]` -12 | assert_never(()) # error: [type-assertion-failure] -13 | assert_never(1 if flag else never) # error: [type-assertion-failure] +18 | +19 | def _(): | info: `Never` and `dict[Unknown, Unknown]` are not equivalent types info: rule `type-assertion-failure` is enabled by default @@ -123,15 +132,15 @@ info: rule `type-assertion-failure` is enabled by default ``` error[type-assertion-failure]: Argument does not have asserted type `Never` - --> src/mdtest_snippet.py:12:5 + --> src/mdtest_snippet.py:20:5 | -10 | assert_never([]) # error: [type-assertion-failure] -11 | assert_never({}) # error: [type-assertion-failure] -12 | assert_never(()) # error: [type-assertion-failure] +19 | def _(): +20 | assert_never(()) # error: [type-assertion-failure] | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is `tuple[()]` -13 | assert_never(1 if flag else never) # error: [type-assertion-failure] +21 | +22 | def _(flag: bool, never: Never): | info: `Never` and `tuple[()]` are not equivalent types info: rule `type-assertion-failure` is enabled by default @@ -140,16 +149,15 @@ info: rule `type-assertion-failure` is enabled by default ``` error[type-assertion-failure]: Argument does not have asserted type `Never` - --> src/mdtest_snippet.py:13:5 + --> src/mdtest_snippet.py:23:5 | -11 | assert_never({}) # error: [type-assertion-failure] -12 | assert_never(()) # error: [type-assertion-failure] -13 | assert_never(1 if flag else never) # error: [type-assertion-failure] +22 | def _(flag: bool, never: Never): +23 | assert_never(1 if flag else never) # error: [type-assertion-failure] | ^^^^^^^^^^^^^--------------------^ | | | Inferred type of argument is `Literal[1]` -14 | -15 | assert_never(any_) # error: [type-assertion-failure] +24 | +25 | def _(any_: Any): | info: `Never` and `Literal[1]` are not equivalent types info: rule `type-assertion-failure` is enabled by default @@ -158,15 +166,15 @@ info: rule `type-assertion-failure` is enabled by default ``` error[type-assertion-failure]: Argument does not have asserted type `Never` - --> src/mdtest_snippet.py:15:5 + --> src/mdtest_snippet.py:26:5 | -13 | assert_never(1 if flag else never) # error: [type-assertion-failure] -14 | -15 | assert_never(any_) # error: [type-assertion-failure] +25 | def _(any_: Any): +26 | assert_never(any_) # error: [type-assertion-failure] | ^^^^^^^^^^^^^----^ | | | Inferred type of argument is `Any` -16 | assert_never(unknown) # error: [type-assertion-failure] +27 | +28 | def _(unknown: Unknown): | info: `Never` and `Any` are not equivalent types info: rule `type-assertion-failure` is enabled by default @@ -175,10 +183,10 @@ info: rule `type-assertion-failure` is enabled by default ``` error[type-assertion-failure]: Argument does not have asserted type `Never` - --> src/mdtest_snippet.py:16:5 + --> src/mdtest_snippet.py:29:5 | -15 | assert_never(any_) # error: [type-assertion-failure] -16 | assert_never(unknown) # error: [type-assertion-failure] +28 | def _(unknown: Unknown): +29 | assert_never(unknown) # error: [type-assertion-failure] | ^^^^^^^^^^^^^-------^ | | | Inferred type of argument is `Unknown` diff --git a/crates/ty_python_semantic/resources/mdtest/terminal_statements.md b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md index baec20573d..02b97f2a1d 100644 --- a/crates/ty_python_semantic/resources/mdtest/terminal_statements.md +++ b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md @@ -570,6 +570,229 @@ def f(): reveal_type(x) # revealed: Literal[1] ``` +## Calls to functions returning `Never` / `NoReturn` + +These calls should be treated as terminal statements. + +### No implicit return + +If we see a call to a function returning `Never`, we should be able to understand that the function +cannot implicitly return `None`. In the below examples, verify that there are no errors emitted for +invalid return type. + +```py +from typing import NoReturn +import sys + +def f() -> NoReturn: + sys.exit(1) +``` + +Let's try cases where the function annotated with `NoReturn` is some sub-expression. + +```py +from typing import NoReturn +import sys + +# TODO: this is currently not yet supported +# error: [invalid-return-type] +def _() -> NoReturn: + 3 + sys.exit(1) + +# TODO: this is currently not yet supported +# error: [invalid-return-type] +def _() -> NoReturn: + 3 if sys.exit(1) else 4 +``` + +### Type narrowing + +If a variable's type is a union, and some types in the union result in a function marked with +`NoReturn` being called, then we should correctly narrow the variable's type. + +```py +from typing import NoReturn +import sys + +def g(x: int | None): + if x is None: + sys.exit(1) + + # TODO: should be just `int`, not `int | None` + # See https://github.com/astral-sh/ty/issues/685 + reveal_type(x) # revealed: int | None +``` + +### Possibly unresolved diagnostics + +If the codepath on which a variable is not defined eventually returns `Never`, use of the variable +should not give any diagnostics. + +```py +import sys + +def _(flag: bool): + if flag: + x = 3 + else: + sys.exit() + + x # No possibly-unresolved-references diagnostic here. +``` + +Similarly, there shouldn't be any diagnostics if the `except` block of a `try/except` construct has +a call with `NoReturn`. + +```py +import sys + +def _(): + try: + x = 3 + except: + sys.exit() + + x # No possibly-unresolved-references diagnostic here. +``` + +### Bindings in branches + +In case of a `NoReturn` call being present in conditionals, the revealed type of the end of the +branch should reflect the path which did not hit any of the `NoReturn` calls. These tests are +similar to the ones for `return` above. + +```py +import sys + +def call_in_then_branch(cond: bool): + if cond: + x = "terminal" + reveal_type(x) # revealed: Literal["terminal"] + sys.exit() + else: + x = "test" + reveal_type(x) # revealed: Literal["test"] + reveal_type(x) # revealed: Literal["test"] + +def call_in_else_branch(cond: bool): + if cond: + x = "test" + reveal_type(x) # revealed: Literal["test"] + else: + x = "terminal" + reveal_type(x) # revealed: Literal["terminal"] + sys.exit() + reveal_type(x) # revealed: Literal["test"] + +def call_in_both_branches(cond: bool): + if cond: + x = "terminal1" + reveal_type(x) # revealed: Literal["terminal1"] + sys.exit() + else: + x = "terminal2" + reveal_type(x) # revealed: Literal["terminal2"] + sys.exit() + + reveal_type(x) # revealed: Never + +def call_in_nested_then_branch(cond1: bool, cond2: bool): + if cond1: + x = "test1" + reveal_type(x) # revealed: Literal["test1"] + else: + if cond2: + x = "terminal" + reveal_type(x) # revealed: Literal["terminal"] + sys.exit() + else: + x = "test2" + reveal_type(x) # revealed: Literal["test2"] + reveal_type(x) # revealed: Literal["test2"] + reveal_type(x) # revealed: Literal["test1", "test2"] + +def call_in_nested_else_branch(cond1: bool, cond2: bool): + if cond1: + x = "test1" + reveal_type(x) # revealed: Literal["test1"] + else: + if cond2: + x = "test2" + reveal_type(x) # revealed: Literal["test2"] + else: + x = "terminal" + reveal_type(x) # revealed: Literal["terminal"] + sys.exit() + reveal_type(x) # revealed: Literal["test2"] + reveal_type(x) # revealed: Literal["test1", "test2"] + +def call_in_both_nested_branches(cond1: bool, cond2: bool): + if cond1: + x = "test" + reveal_type(x) # revealed: Literal["test"] + else: + x = "terminal0" + if cond2: + x = "terminal1" + reveal_type(x) # revealed: Literal["terminal1"] + sys.exit() + else: + x = "terminal2" + reveal_type(x) # revealed: Literal["terminal2"] + sys.exit() + reveal_type(x) # revealed: Literal["test"] +``` + +### Overloads + +If only some overloads of a function are marked with `NoReturn`, we should run the overload +evaluation algorithm when evaluating the constraints. + +```py +from typing import NoReturn, overload + +@overload +def f(x: int) -> NoReturn: ... +@overload +def f(x: str) -> int: ... +def f(x): ... + +# No errors +def _() -> NoReturn: + f(3) + +# This should be an error because of implicitly returning `None` +# error: [invalid-return-type] +def _() -> NoReturn: + f("") +``` + +### Other callables + +If other types of callables are annotated with `NoReturn`, we should still be ablt to infer correct +reachability. + +```py +import sys + +from typing import NoReturn + +class C: + def __call__(self) -> NoReturn: + sys.exit() + + def die(self) -> NoReturn: + sys.exit() + +# No "implicitly returns `None`" diagnostic +def _() -> NoReturn: + C()() + +# No "implicitly returns `None`" diagnostic +def _() -> NoReturn: + C().die() +``` + ## Nested functions Free references inside of a function body refer to variables defined in the containing scope. diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 9ea9f8f058..f735b4f785 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -35,8 +35,8 @@ use crate::semantic_index::place::{ PlaceExprWithFlags, PlaceTableBuilder, Scope, ScopeId, ScopeKind, ScopedPlaceId, }; use crate::semantic_index::predicate::{ - PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, PredicateOrLiteral, - ScopedPredicateId, StarImportPlaceholderPredicate, + CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, + PredicateOrLiteral, ScopedPredicateId, StarImportPlaceholderPredicate, }; use crate::semantic_index::re_exports::exported_names; use crate::semantic_index::reachability_constraints::{ @@ -1901,11 +1901,45 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { value, range: _, node_index: _, - }) if self.in_module_scope() => { - if let Some(expr) = dunder_all_extend_argument(value) { - self.add_standalone_expression(expr); + }) => { + if self.in_module_scope() { + if let Some(expr) = dunder_all_extend_argument(value) { + self.add_standalone_expression(expr); + } } + self.visit_expr(value); + + // If the statement is a call, it could possibly be a call to a function + // marked with `NoReturn` (for example, `sys.exit()`). In this case, we use a special + // kind of constraint to mark the following code as unreachable. + // + // Ideally, these constraints should be added for every call expression, even those in + // sub-expressions and in the module-level scope. But doing so makes the number of + // such constraints so high that it significantly degrades performance. We thus cut + // scope here and add these constraints only at statement level function calls, + // like `sys.exit()`, and not within sub-expression like `3 + sys.exit()` etc. + // + // We also only add these inside function scopes, since considering module-level + // constraints can affect the the type of imported symbols, leading to a lot more + // work in third-party code. + if let ast::Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { + if !self.source_type.is_stub() && self.in_function_scope() { + let callable = self.add_standalone_expression(func); + let call_expr = self.add_standalone_expression(value.as_ref()); + + let predicate = Predicate { + node: PredicateNode::ReturnsNever(CallableAndCallExpr { + callable, + call_expr, + }), + is_positive: false, + }; + self.record_reachability_constraint(PredicateOrLiteral::Predicate( + predicate, + )); + } + } } _ => { walk_stmt(self, stmt); diff --git a/crates/ty_python_semantic/src/semantic_index/predicate.rs b/crates/ty_python_semantic/src/semantic_index/predicate.rs index 80096841e1..499fc9fac6 100644 --- a/crates/ty_python_semantic/src/semantic_index/predicate.rs +++ b/crates/ty_python_semantic/src/semantic_index/predicate.rs @@ -102,9 +102,16 @@ impl PredicateOrLiteral<'_> { } } +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) struct CallableAndCallExpr<'db> { + pub(crate) callable: Expression<'db>, + pub(crate) call_expr: Expression<'db>, +} + #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(crate) enum PredicateNode<'db> { Expression(Expression<'db>), + ReturnsNever(CallableAndCallExpr<'db>), Pattern(PatternPredicate<'db>), StarImportPlaceholder(StarImportPlaceholderPredicate<'db>), } diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index e34ba33096..9781ebc8e0 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -204,7 +204,8 @@ use crate::place::{RequiresExplicitReExport, imported_symbol}; use crate::semantic_index::expression::Expression; use crate::semantic_index::place_table; use crate::semantic_index::predicate::{ - PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId, + CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, + Predicates, ScopedPredicateId, }; use crate::types::{Truthiness, Type, infer_expression_type}; @@ -684,6 +685,53 @@ impl ReachabilityConstraints { let ty = infer_expression_type(db, test_expr); ty.bool(db).negate_if(!predicate.is_positive) } + PredicateNode::ReturnsNever(CallableAndCallExpr { + callable, + call_expr, + }) => { + // We first infer just the type of the callable. In the most likely case that the + // function is not marked with `NoReturn`, or that it always returns `NoReturn`, + // doing so allows us to avoid the more expensive work of inferring the entire call + // expression (which could involve inferring argument types to possibly run the overload + // selection algorithm). + // Avoiding this on the happy-path is important because these constraints can be + // very large in number, since we add them on all statement level function calls. + let ty = infer_expression_type(db, callable); + + let overloads_iterator = + if let Some(Type::Callable(callable)) = ty.into_callable(db) { + callable.signatures(db).overloads.iter() + } else { + return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive); + }; + + let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator + .fold((true, true), |(none, all), overload| { + let overload_returns_never = + overload.return_ty.is_some_and(|return_type| { + return_type.is_equivalent_to(db, Type::Never) + }); + + ( + none && !overload_returns_never, + all && overload_returns_never, + ) + }); + + if no_overloads_return_never { + Truthiness::AlwaysFalse + } else if all_overloads_return_never { + Truthiness::AlwaysTrue + } else { + let call_expr_ty = infer_expression_type(db, call_expr); + if call_expr_ty.is_equivalent_to(db, Type::Never) { + Truthiness::AlwaysTrue + } else { + Truthiness::AlwaysFalse + } + } + .negate_if(!predicate.is_positive) + } PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner), PredicateNode::StarImportPlaceholder(star_import) => { let place_table = place_table(db, star_import.scope(db)); diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index f975c4bf85..3abdd3e192 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -192,6 +192,17 @@ //! for that place that we need for that use or definition. When we reach the end of the scope, it //! records the state for each place as the public definitions of that place. //! +//! ```python +//! x = 1 +//! x = 2 +//! y = x +//! if flag: +//! x = 3 +//! else: +//! x = 4 +//! z = x +//! ``` +//! //! Let's walk through the above example. Initially we do not have any record of `x`. When we add //! the new place (before we process the first binding), we create a new undefined `PlaceState` //! which has a single live binding (the "unbound" definition) and a single live declaration (the diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 0a1ce8112a..978608211e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7226,7 +7226,7 @@ impl<'db> BoundMethodType<'db> { #[derive(PartialOrd, Ord)] pub struct CallableType<'db> { #[returns(ref)] - signatures: CallableSignature<'db>, + pub(crate) signatures: CallableSignature<'db>, /// We use `CallableType` to represent function-like objects, like the synthesized methods /// of dataclasses or NamedTuples. These callables act like real functions when accessed diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 3cdb5d05c2..4b23562d20 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -2122,7 +2122,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { node_index: _, value, }) => { - self.infer_expression(value); + // If this is a call expression, we would have added a `ReturnsNever` constraint, + // meaning this will be a standalone expression. + self.infer_maybe_standalone_expression(value); } ast::Stmt::If(if_statement) => self.infer_if_statement(if_statement), ast::Stmt::Try(try_statement) => self.infer_try_statement(try_statement), @@ -5263,7 +5265,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // arguments after matching them to parameters, but before checking that the argument types // are assignable to any parameter annotations. let call_arguments = Self::parse_arguments(arguments); - let callable_type = self.infer_expression(func); + + let callable_type = self.infer_maybe_standalone_expression(func); if let Type::FunctionLiteral(function) = callable_type { // Make sure that the `function.definition` is only called when the function is defined diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 87f33354e7..20a912c72d 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -3,7 +3,7 @@ use crate::semantic_index::expression::Expression; use crate::semantic_index::place::{PlaceExpr, PlaceTable, ScopeId, ScopedPlaceId}; use crate::semantic_index::place_table; use crate::semantic_index::predicate::{ - PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, + CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, }; use crate::types::function::KnownFunction; use crate::types::infer::infer_same_file_expression_type; @@ -59,6 +59,7 @@ pub(crate) fn infer_narrowing_constraint<'db>( all_negative_narrowing_constraints_for_pattern(db, pattern) } } + PredicateNode::ReturnsNever(_) => return None, PredicateNode::StarImportPlaceholder(_) => return None, }; if let Some(constraints) = constraints { @@ -346,6 +347,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { PredicateNode::Pattern(pattern) => { self.evaluate_pattern_predicate(pattern, self.is_positive) } + PredicateNode::ReturnsNever(_) => return None, PredicateNode::StarImportPlaceholder(_) => return None, }; if let Some(mut constraints) = constraints { @@ -429,6 +431,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { match self.predicate { PredicateNode::Expression(expression) => expression.scope(self.db), PredicateNode::Pattern(pattern) => pattern.scope(self.db), + PredicateNode::ReturnsNever(CallableAndCallExpr { callable, .. }) => { + callable.scope(self.db) + } PredicateNode::StarImportPlaceholder(definition) => definition.scope(self.db), } }