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), } }