From 557d583e327f0d7023f5e753fb09a1605dc0cd99 Mon Sep 17 00:00:00 2001 From: Shaygan Hooshyari Date: Mon, 25 Nov 2024 22:37:55 +0100 Subject: [PATCH] Support `typing.NoReturn` and `typing.Never` (#14559) Fix #14558 ## Summary - Add `typing.NoReturn` and `typing.Never` to known instances and infer them as `Type::Never` - Add `is_assignable_to` cases for `Type::Never` I skipped emitting diagnostic for when a function is annotated as `NoReturn` but it actually returns. ## Test Plan Added tests from https://github.com/python/typing/blob/main/conformance/tests/specialtypes_never.py except from generics and checking if the return value of the function and the annotations match. --------- Co-authored-by: Alex Waygood Co-authored-by: Carl Meyer --- .../resources/mdtest/annotations/never.md | 62 +++++++++++++++++++ .../resources/mdtest/expression/if.md | 2 +- crates/red_knot_python_semantic/src/types.rs | 27 ++++++-- .../src/types/infer.rs | 16 ++++- .../red_knot_python_semantic/src/types/mro.rs | 2 + 5 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/annotations/never.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/never.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/never.md new file mode 100644 index 0000000000000..f571eaffef1fc --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/never.md @@ -0,0 +1,62 @@ +# NoReturn & Never + +`NoReturn` is used to annotate the return type for functions that never return. `Never` is the +bottom type, representing the empty set of Python objects. These two annotations can be used +interchangeably. + +## Function Return Type Annotation + +```py +from typing import NoReturn + +def stop() -> NoReturn: + raise RuntimeError("no way") + +# revealed: Never +reveal_type(stop()) +``` + +## Assignment + +```py +from typing import NoReturn, Never, Any + +# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter" +x: Never[int] +a1: NoReturn +# TODO: Test `Never` is only available in python >= 3.11 +a2: Never +b1: Any +b2: int + +def f(): + # revealed: Never + reveal_type(a1) + # revealed: Never + reveal_type(a2) + + # Never is assignable to all types. + v1: int = a1 + v2: str = a1 + # Other types are not assignable to Never except for Never (and Any). + v3: Never = b1 + v4: Never = a2 + v5: Any = b2 + # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Never`" + v6: Never = 1 +``` + +## Typing Extensions + +```py +from typing_extensions import NoReturn, Never + +x: NoReturn +y: Never + +def f(): + # revealed: Never + reveal_type(x) + # revealed: Never + reveal_type(y) +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/if.md b/crates/red_knot_python_semantic/resources/mdtest/expression/if.md index 68d5ef444fc7a..d3f9eef48f124 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/if.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/if.md @@ -27,7 +27,7 @@ reveal_type(1 if 0 else 2) # revealed: Literal[2] (issue #14588) -The test inside an if expression should not affect code outside of the block. +The test inside an if expression should not affect code outside of the expression. ```py def bool_instance() -> bool: diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 5edc68a469152..64bfd3ec79660 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1553,6 +1553,9 @@ impl<'db> Type<'db> { // TODO map this to a new `Type::TypeVar` variant Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self, Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => alias.value_ty(db), + Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => { + Type::Never + } _ => todo_type!(), } } @@ -1889,6 +1892,10 @@ pub enum KnownInstanceType<'db> { Optional, /// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`) Union, + /// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`) + NoReturn, + /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) + Never, /// A single instance of `typing.TypeVar` TypeVar(TypeVarInstance<'db>), /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) @@ -1899,11 +1906,13 @@ pub enum KnownInstanceType<'db> { impl<'db> KnownInstanceType<'db> { pub const fn as_str(self) -> &'static str { match self { - KnownInstanceType::Literal => "Literal", - KnownInstanceType::Optional => "Optional", - KnownInstanceType::Union => "Union", - KnownInstanceType::TypeVar(_) => "TypeVar", - KnownInstanceType::TypeAliasType(_) => "TypeAliasType", + Self::Literal => "Literal", + Self::Optional => "Optional", + Self::Union => "Union", + Self::TypeVar(_) => "TypeVar", + Self::NoReturn => "NoReturn", + Self::Never => "Never", + Self::TypeAliasType(_) => "TypeAliasType", } } @@ -1914,6 +1923,8 @@ impl<'db> KnownInstanceType<'db> { | Self::Optional | Self::TypeVar(_) | Self::Union + | Self::NoReturn + | Self::Never | Self::TypeAliasType(_) => Truthiness::AlwaysTrue, } } @@ -1924,6 +1935,8 @@ impl<'db> KnownInstanceType<'db> { Self::Literal => "typing.Literal", Self::Optional => "typing.Optional", Self::Union => "typing.Union", + Self::NoReturn => "typing.NoReturn", + Self::Never => "typing.Never", Self::TypeVar(typevar) => typevar.name(db), Self::TypeAliasType(_) => "typing.TypeAliasType", } @@ -1935,6 +1948,8 @@ impl<'db> KnownInstanceType<'db> { Self::Literal => KnownClass::SpecialForm, Self::Optional => KnownClass::SpecialForm, Self::Union => KnownClass::SpecialForm, + Self::NoReturn => KnownClass::SpecialForm, + Self::Never => KnownClass::SpecialForm, Self::TypeVar(_) => KnownClass::TypeVar, Self::TypeAliasType(_) => KnownClass::TypeAliasType, } @@ -1957,6 +1972,8 @@ impl<'db> KnownInstanceType<'db> { ("typing" | "typing_extensions", "Literal") => Some(Self::Literal), ("typing" | "typing_extensions", "Optional") => Some(Self::Optional), ("typing" | "typing_extensions", "Union") => Some(Self::Union), + ("typing" | "typing_extensions", "NoReturn") => Some(Self::NoReturn), + ("typing" | "typing_extensions", "Never") => Some(Self::Never), _ => None, } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 6337d66f8d69d..dbf6c0bbef531 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -4575,7 +4575,7 @@ impl<'db> TypeInferenceBuilder<'db> { match value_ty { Type::KnownInstance(known_instance) => { - self.infer_parameterized_known_instance_type_expression(known_instance, slice) + self.infer_parameterized_known_instance_type_expression(subscript, known_instance) } _ => { self.infer_type_expression(slice); @@ -4586,9 +4586,10 @@ impl<'db> TypeInferenceBuilder<'db> { fn infer_parameterized_known_instance_type_expression( &mut self, + subscript: &ast::ExprSubscript, known_instance: KnownInstanceType, - parameters: &ast::Expr, ) -> Type<'db> { + let parameters = &*subscript.slice; match known_instance { KnownInstanceType::Literal => match self.infer_literal_parameter_type(parameters) { Ok(ty) => ty, @@ -4629,6 +4630,17 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_expression(parameters); todo_type!("generic type alias") } + KnownInstanceType::NoReturn | KnownInstanceType::Never => { + self.diagnostics.add( + subscript.into(), + "invalid-type-parameter", + format_args!( + "Type `{}` expected no type parameter", + known_instance.repr(self.db) + ), + ); + Type::Unknown + } } } diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index e31d487bdfeb6..e576d7c634647 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -375,6 +375,8 @@ impl<'db> ClassBase<'db> { | KnownInstanceType::TypeAliasType(_) | KnownInstanceType::Literal | KnownInstanceType::Union + | KnownInstanceType::NoReturn + | KnownInstanceType::Never | KnownInstanceType::Optional => None, }, }