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..be592dc284 100644 --- a/crates/ty_python_semantic/resources/mdtest/terminal_statements.md +++ b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md @@ -570,6 +570,65 @@ def f(): reveal_type(x) # revealed: Literal[1] ``` +## Calls to functions returning `Never` / `NoReturn` + +### 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 + +def _() -> NoReturn: + 3 + sys.exit(1) + +def _() -> NoReturn: + 3 if sys.exit(1) else 4 +``` + +### Type narrowing + +```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 + reveal_type(x) # revealed: int | None +``` + +### Bindings after call + +These should be understood to be unreachable. + +```py +import sys + +def _(): + x = 3 + + sys.exit(1) + + x = 4 + reveal_type(x) # revealed: Never +``` + ## Nested functions Free references inside of a function body refer to variables defined in the containing scope. diff --git a/crates/ty_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt index 213e5c4dca..63a8fba437 100644 --- a/crates/ty_python_semantic/resources/primer/bad.txt +++ b/crates/ty_python_semantic/resources/primer/bad.txt @@ -8,6 +8,7 @@ hydpy # too many iterations ibis # too many iterations jax # too many iterations mypy # too many iterations (self-recursive type alias) +nox # too many iterations (because of packaging) packaging # too many iterations pandas # slow (9s) pandera # too many iterations @@ -19,4 +20,6 @@ setuptools # vendors packaging, see above spack # slow, success, but mypy-primer hangs processing the output spark # too many iterations steam.py # hangs (single threaded) +streamlit # too many iterations (because of packaging) +tornado # bad use-def map (https://github.com/astral-sh/ty/issues/365) xarray # too many iterations diff --git a/crates/ty_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt index 9a036f3c12..40835e442c 100644 --- a/crates/ty_python_semantic/resources/primer/good.txt +++ b/crates/ty_python_semantic/resources/primer/good.txt @@ -63,7 +63,6 @@ more-itertools mypy-protobuf mypy_primer nionutils -nox openlibrary operator optuna @@ -107,7 +106,6 @@ starlette static-frame stone strawberry -streamlit svcs sympy tornado diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 14451b3ed2..a64243afe8 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -2225,6 +2225,18 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { } walk_expr(self, expr); } + ast::Expr::Call(ast::ExprCall { func, .. }) if !self.source_type.is_stub() => { + let expression = self.add_standalone_expression(func); + + let predicate = Predicate { + node: PredicateNode::ReturnsNever(expression), + is_positive: false, + }; + + walk_expr(self, expr); + + self.record_reachability_constraint(PredicateOrLiteral::Predicate(predicate)); + } _ => { walk_expr(self, expr); } diff --git a/crates/ty_python_semantic/src/semantic_index/predicate.rs b/crates/ty_python_semantic/src/semantic_index/predicate.rs index 80096841e1..bd487a2d85 100644 --- a/crates/ty_python_semantic/src/semantic_index/predicate.rs +++ b/crates/ty_python_semantic/src/semantic_index/predicate.rs @@ -105,6 +105,7 @@ impl PredicateOrLiteral<'_> { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(crate) enum PredicateNode<'db> { Expression(Expression<'db>), + ReturnsNever(Expression<'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..c2ba7799a7 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -684,6 +684,35 @@ impl ReachabilityConstraints { let ty = infer_expression_type(db, test_expr); ty.bool(db).negate_if(!predicate.is_positive) } + PredicateNode::ReturnsNever(test_expr) => { + let ty = infer_expression_type(db, test_expr); + if let Type::FunctionLiteral(function_literal) = ty { + let returns_never = + if function_literal + .signature(db) + .overloads + .iter() + .all(|overload| { + // HACK: for now, require that *all* overloads are annotated with + // returning `Never` + // Ideally, if only some overloads return `Never`, we should consider + // the types of the arguments. + overload.return_ty.is_some_and(|return_type| { + return_type.is_equivalent_to(db, Type::Never) + }) + }) + { + Truthiness::AlwaysTrue + } else { + Truthiness::AlwaysFalse + }; + returns_never.negate_if(!predicate.is_positive) + } else { + // Should I add a panic here? + // What about methods / other callables which are not functions? + Truthiness::AlwaysTrue + } + } 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/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 1635458fb5..7426c107aa 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -5285,7 +5285,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 e79ae39e3a..920bd5c2a9 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -60,6 +60,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 { @@ -347,6 +348,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 { @@ -430,6 +432,7 @@ 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(expression) => expression.scope(self.db), PredicateNode::StarImportPlaceholder(definition) => definition.scope(self.db), } }