From 62bb243d161239a6bcdda56a00a137c16649fb2e Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Sun, 25 May 2025 01:13:52 +0000 Subject: [PATCH 01/11] [ty] Add partial support for `TypeIs` --- .../annotations/unsupported_special_forms.md | 1 - .../resources/mdtest/narrow/type_guards.md | 244 ++++++++++++++++++ .../mdtest/type_properties/is_subtype_of.md | 27 ++ crates/ty_python_semantic/src/types.rs | 124 +++++++-- .../src/types/class_base.rs | 3 +- .../src/types/diagnostic.rs | 59 +++++ .../ty_python_semantic/src/types/display.rs | 9 + crates/ty_python_semantic/src/types/infer.rs | 99 +++++-- crates/ty_python_semantic/src/types/narrow.rs | 66 ++++- .../src/types/type_ordering.rs | 30 ++- 10 files changed, 608 insertions(+), 54 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 3887d9bb8455e..254ed90eea873 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -19,7 +19,6 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`) def g() -> TypeGuard[int]: ... -def h() -> TypeIs[int]: ... def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co: reveal_type(args) # revealed: tuple[@Todo(Support for `typing.ParamSpec`), ...] reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)] diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md new file mode 100644 index 0000000000000..433f87526910a --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -0,0 +1,244 @@ +# User-defined type guards + +User-defined type guards are functions of which the return type is either `TypeGuard[...]` or +`TypeIs[...]`. + +## Display + +```py +from ty_extensions import Intersection, Not, TypeOf +from typing_extensions import TypeGuard, TypeIs + +def _( + a: TypeGuard[str], + b: TypeIs[str | int], + c: TypeGuard[Intersection[complex, Not[int], Not[float]]], + d: TypeIs[tuple[TypeOf[bytes]]], +): + # TODO: Should be `TypeGuard[str]` + reveal_type(a) # revealed: @Todo(`TypeGuard[]` special form) + reveal_type(b) # revealed: TypeIs[str | int] + # TODO: Should be `TypeGuard[complex & ~int & ~float]` + reveal_type(c) # revealed: @Todo(`TypeGuard[]` special form) + reveal_type(d) # revealed: TypeIs[tuple[]] + +# TODO: error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeGuard[str]`" +def _(a) -> TypeGuard[str]: ... +# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`" +def _(a) -> TypeIs[str]: ... + +def f(a) -> TypeGuard[str]: return True +def g(a) -> TypeIs[str]: return True +def _(a: object): + # TODO: Should be `TypeGuard[a, str]` + reveal_type(f(a)) # revealed: @Todo(`TypeGuard[]` special form) + reveal_type(g(a)) # revealed: TypeIs[a, str] +``` + +## Parameters + +A user-defined type guard must accept at least one positional argument (in addition to `self`/`cls` +for non-static methods). + +```pyi +from typing_extensions import TypeGuard, TypeIs + +# TODO: error: [invalid-type-guard-definition] +def _() -> TypeGuard[str]: ... + +# TODO: error: [invalid-type-guard-definition] +def _(**kwargs) -> TypeIs[str]: ... + +class _: + # fine + def _(self, /, a) -> TypeGuard[str]: ... + @classmethod + def _(cls, a) -> TypeGuard[str]: ... + @staticmethod + def _(a) -> TypeIs[str]: ... + + # errors + def _(self) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition] + def _(self, /, *, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition] + @classmethod + def _(cls) -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition] + @classmethod + def _() -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition] + @staticmethod + def _(*, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition] +``` + +For `TypeIs` functions, the narrowed type must be assignable to the declared type of that parameter, +if any. + +```pyi +from typing import Any +from typing_extensions import TypeIs + +def _(a: object) -> TypeIs[str]: ... +def _(a: Any) -> TypeIs[str]: ... +def _(a: tuple[object]) -> TypeIs[tuple[str]]: ... +def _(a: str | Any) -> TypeIs[str]: ... +def _(a) -> TypeIs[str]: ... + +# TODO: error: [invalid-type-guard-definition] +def _(a: int) -> TypeIs[str]: ... + +# TODO: error: [invalid-type-guard-definition] +def _(a: bool | str) -> TypeIs[int]: ... +``` + +## Arguments to special forms + +`TypeGuard` and `TypeIs` accept exactly one type argument. + +```py +from typing_extensions import TypeGuard, TypeIs + +a = 123 + +# TODO: error: [invalid-type-form] +def f(_) -> TypeGuard[int, str]: ... + +# error: [invalid-type-form] +def g(_) -> TypeIs[a, str]: ... + +# TODO: Should be `Unknown` +reveal_type(f(0)) # revealed: @Todo(`TypeGuard[]` special form) +reveal_type(g(0)) # revealed: Unknown +``` + +## Return types + +All code paths in a type guard function must return booleans. + +```py +from typing_extensions import Literal, TypeGuard, TypeIs, assert_never + +def _(a: object, flag: bool) -> TypeGuard[str]: + if flag: + return 0 + + return "foo" + +# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`" +def f(a: object, flag: bool) -> TypeIs[str]: + if flag: + # error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `float`" + return 1.2 + +def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]: + if a == "foo": + # Logically wrong, but allowed regardless + return False + + return False +``` + +## Invalid calls + +```pyi +from typing import Any +from typing_extensions import TypeGuard, TypeIs + +def f(a: object) -> TypeGuard[str]: ... +def g(a: object) -> TypeIs[int]: ... +def _(d: Any): + if f(): # error: [missing-argument] + ... + + # TODO: Is this error correct? + if g(*d): # error: [missing-argument] + ... + + if f("foo"): # TODO: error: [invalid-type-guard-call] + ... + + if g(a=d): # error: [invalid-type-guard-call] + ... + +def _(a: tuple[str, int] | tuple[int, str]): + if g(a[0]): # error: [invalid-type-guard-call] + # TODO: Should be `tuple[str, int]` + reveal_type(a) # revealed: tuple[str, int] | tuple[int, str] +``` + +## Narrowing + +```py +from typing import Any +from typing_extensions import TypeGuard, TypeIs + +def guard_str(a: object) -> TypeGuard[str]: return True +def is_int(a: object) -> TypeIs[int]: return True +def _(a: str | int): + if guard_str(a): + # TODO: Should be `str` + reveal_type(a) # revealed: str | int + else: + reveal_type(a) # revealed: str | int + + if is_int(a): + reveal_type(a) # revealed: int + else: + reveal_type(a) # revealed: str & ~int + +def _(a: str | int): + b = guard_str(a) + c = is_int(a) + + reveal_type(a) # revealed: str | int + # TODO: Should be `TypeGuard[a, str]` + reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form) + reveal_type(c) # revealed: TypeIs[a, int] + + if b: + # TODO: Should be `str` + reveal_type(a) # revealed: str | int + else: + reveal_type(a) # revealed: str | int + + if c: + reveal_type(a) # revealed: int + else: + reveal_type(a) # revealed: str & ~int + +def _(x: str | int, flag: bool) -> None: + b = is_int(x) + reveal_type(b) # revealed: TypeIs[x, int] + + if flag: + x = "" + + if b: + # TODO: Should be `str | int` + reveal_type(x) # revealed: int +``` + +## `TypeGuard` special cases + +```py +from typing import Any +from typing_extensions import TypeGuard + +def guard_int(a: object) -> TypeGuard[int]: return True +def is_int(a: object) -> TypeGuard[int]: return True +def does_not_narrow_in_negative_case(a: str | int): + if not guard_int(a): + # TODO: Should be `str` + reveal_type(a) # revealed: str | int + else: + reveal_type(a) # revealed: str | int + +def narrowed_type_must_be_exact(a: object, b: bool): + if guard_int(b): + # TODO: Should be `int` + reveal_type(b) # revealed: bool + + if isinstance(a, bool) and is_int(a): + reveal_type(a) # revealed: bool + + if isinstance(a, bool) and guard_int(a): + # TODO: Should be `int` + reveal_type(a) # revealed: bool +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index ffa1e0fad04d7..50895a1be31c5 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -342,6 +342,33 @@ static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[A static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy])) ``` +### `TypeGuard` and `TypeIs` + +`TypeGuard[...]` and `TypeIs[...]` are subtypes of `bool`. + +```py +from ty_extensions import is_subtype_of, static_assert +from typing_extensions import TypeGuard, TypeIs + +# TODO: TypeGuard +# static_assert(is_subtype_of(TypeGuard[int], bool)) +# static_assert(is_subtype_of(TypeIs[str], bool)) +``` + +`TypeIs` is invariant. `TypeGuard` is covariant. + +```py +from ty_extensions import is_subtype_of, static_assert +from typing_extensions import TypeGuard, TypeIs + +# TODO: TypeGuard +# static_assert(is_subtype_of(TypeGuard[bool], TypeGuard[int])) + +static_assert(not is_subtype_of(TypeGuard[int], TypeGuard[bool])) +static_assert(not is_subtype_of(TypeIs[bool], TypeIs[int])) +static_assert(not is_subtype_of(TypeIs[int], TypeIs[bool])) +``` + ### Module literals ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index c8f3afa6256c6..f45d02c714004 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -35,7 +35,7 @@ use crate::module_name::ModuleName; use crate::module_resolver::{KnownModule, file_to_module, resolve_module}; use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId}; use crate::semantic_index::definition::Definition; -use crate::semantic_index::symbol::ScopeId; +use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId}; use crate::semantic_index::{imported_modules, semantic_index}; use crate::suppression::check_suppressions; use crate::symbol::{ @@ -546,10 +546,12 @@ pub enum Type<'db> { /// An instance of a typevar in a generic class or function. When the generic class or function /// is specialized, we will replace this typevar with its specialization. TypeVar(TypeVarInstance<'db>), - // A bound super object like `super()` or `super(A, A())` - // This type doesn't handle an unbound super object like `super(A)`; for that we just use - // a `Type::NominalInstance` of `builtins.super`. + /// A bound super object like `super()` or `super(A, A())` + /// This type doesn't handle an unbound super object like `super(A)`; for that we just use + /// a `Type::NominalInstance` of `builtins.super`. BoundSuper(BoundSuperType<'db>), + /// A subtype of `bool` that allows narrowing in both positive and negative cases. + TypeIs(TypeIsType<'db>), // TODO protocols, overloads, generics } @@ -656,6 +658,10 @@ impl<'db> Type<'db> { *self } + Self::TypeIs(type_is) => { + type_is.with_ty(db, type_is.ty(db).replace_self_reference(db, class)) + } + Self::Dynamic(_) | Self::AlwaysFalsy | Self::AlwaysTruthy @@ -789,6 +795,8 @@ impl<'db> Type<'db> { .iter() .any(|ty| ty.any_over_type(db, type_fn)), }, + + Self::TypeIs(type_is) => type_is.ty(db).any_over_type(db, type_fn), } } @@ -1024,6 +1032,7 @@ impl<'db> Type<'db> { Type::KnownInstance(known_instance) => { Type::KnownInstance(known_instance.normalized(db)) } + Type::TypeIs(type_is) => type_is.ty(db).normalized(db), Type::LiteralString | Type::AlwaysFalsy | Type::AlwaysTruthy @@ -1230,6 +1239,9 @@ impl<'db> Type<'db> { ) => (self.literal_fallback_instance(db)) .is_some_and(|instance| instance.is_subtype_of(db, target)), + // `TypeIs[T]` is a subtype of `bool`. + (Type::TypeIs(_), _) => KnownClass::Bool.to_instance(db).is_subtype_of(db, target), + // Function-like callables are subtypes of `FunctionType` (Type::Callable(callable), Type::NominalInstance(target)) if callable.is_function_like(db) @@ -2086,14 +2098,15 @@ impl<'db> Type<'db> { known_instance_ty @ (Type::SpecialForm(_) | Type::KnownInstance(_)), ) => known_instance_ty.is_disjoint_from(db, tuple.homogeneous_supertype(db)), - (Type::BooleanLiteral(..), Type::NominalInstance(instance)) - | (Type::NominalInstance(instance), Type::BooleanLiteral(..)) => { + (Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => { // A `Type::BooleanLiteral()` must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) !KnownClass::Bool.is_subclass_of(db, instance.class) } - (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, + (Type::BooleanLiteral(..) | Type::TypeIs(_), _) + | (_, Type::BooleanLiteral(..) | Type::TypeIs(_)) => true, (Type::IntLiteral(..), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::IntLiteral(..)) => { @@ -2315,6 +2328,8 @@ impl<'db> Type<'db> { .iter() .all(|elem| elem.is_fully_static(db)), Type::Callable(callable) => callable.is_fully_static(db), + + Type::TypeIs(type_is) => type_is.ty(db).is_fully_static(db), } } @@ -2329,7 +2344,8 @@ impl<'db> Type<'db> { | Type::IntLiteral(..) | Type::StringLiteral(..) | Type::BytesLiteral(..) - | Type::LiteralString => { + | Type::LiteralString + | Type::TypeIs(_) => { // Note: The literal types included in this pattern are not true singletons. // There can be multiple Python objects (at different memory locations) that // are both of type Literal[345], for example. @@ -2506,7 +2522,8 @@ impl<'db> Type<'db> { | Type::Callable(_) | Type::PropertyInstance(_) | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) => false, + | Type::DataclassTransformer(_) + | Type::TypeIs(_) => false, } } @@ -2624,7 +2641,8 @@ impl<'db> Type<'db> { | Type::TypeVar(_) | Type::NominalInstance(_) | Type::ProtocolInstance(_) - | Type::PropertyInstance(_) => None, + | Type::PropertyInstance(_) + | Type::TypeIs(_) => None, } } @@ -2724,7 +2742,9 @@ impl<'db> Type<'db> { }, Type::IntLiteral(_) => KnownClass::Int.to_instance(db).instance_member(db, name), - Type::BooleanLiteral(_) => KnownClass::Bool.to_instance(db).instance_member(db, name), + Type::BooleanLiteral(_) | Type::TypeIs(_) => { + KnownClass::Bool.to_instance(db).instance_member(db, name) + } Type::StringLiteral(_) | Type::LiteralString => { KnownClass::Str.to_instance(db).instance_member(db, name) } @@ -3214,7 +3234,8 @@ impl<'db> Type<'db> { | Type::SpecialForm(..) | Type::KnownInstance(..) | Type::PropertyInstance(..) - | Type::FunctionLiteral(..) => { + | Type::FunctionLiteral(..) + | Type::TypeIs(..) => { let fallback = self.instance_member(db, name_str); let result = self.invoke_descriptor_protocol( @@ -3479,9 +3500,11 @@ impl<'db> Type<'db> { }; let truthiness = match self { - Type::Dynamic(_) | Type::Never | Type::Callable(_) | Type::LiteralString => { - Truthiness::Ambiguous - } + Type::Dynamic(_) + | Type::Never + | Type::Callable(_) + | Type::LiteralString + | Type::TypeIs(_) => Truthiness::Ambiguous, Type::FunctionLiteral(_) | Type::BoundMethod(_) @@ -4423,7 +4446,8 @@ impl<'db> Type<'db> { | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) - | Type::ModuleLiteral(_) => CallableBinding::not_callable(self).into(), + | Type::ModuleLiteral(_) + | Type::TypeIs(_) => CallableBinding::not_callable(self).into(), } } @@ -4911,7 +4935,8 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BoundSuper(_) | Type::AlwaysTruthy - | Type::AlwaysFalsy => None, + | Type::AlwaysFalsy + | Type::TypeIs(_) => None, } } @@ -4974,7 +4999,8 @@ impl<'db> Type<'db> { | Type::FunctionLiteral(_) | Type::BoundSuper(_) | Type::ProtocolInstance(_) - | Type::PropertyInstance(_) => Err(InvalidTypeExpressionError { + | Type::PropertyInstance(_) + | Type::TypeIs(_) => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType( *self, scope_id )], @@ -5212,7 +5238,7 @@ impl<'db> Type<'db> { Type::SpecialForm(special_form) => special_form.to_meta_type(db), Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db), Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), - Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db), + Type::BooleanLiteral(_) | Type::TypeIs(_) => KnownClass::Bool.to_class_literal(db), Type::BytesLiteral(_) => KnownClass::Bytes.to_class_literal(db), Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), @@ -5412,7 +5438,8 @@ impl<'db> Type<'db> { | Type::ClassLiteral(_) | Type::BoundSuper(_) | Type::SpecialForm(_) - | Type::KnownInstance(_) => self, + | Type::KnownInstance(_) + | Type::TypeIs(_) => self, } } @@ -5495,6 +5522,10 @@ impl<'db> Type<'db> { subclass_of.find_legacy_typevars(db, typevars); } + Type::TypeIs(type_is) => { + type_is.ty(db).find_legacy_typevars(db, typevars); + } + Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy @@ -5625,7 +5656,8 @@ impl<'db> Type<'db> { | Self::Callable(_) | Self::AlwaysTruthy | Self::SpecialForm(_) - | Self::AlwaysFalsy => None, + | Self::AlwaysFalsy + | Self::TypeIs(_) => None, } } @@ -9186,6 +9218,56 @@ impl<'db> BoundSuperType<'db> { } } +#[salsa::interned(debug)] +pub struct TypeIsType<'db> { + #[return_ref] + ty: Type<'db>, + /// The ID of the scope to which the symbol belongs, + /// the ID of the symbol itself within that scope, + /// and the symbol's name. + symbol_info: Option<(ScopeId<'db>, ScopedSymbolId, String)>, +} + +impl<'db> TypeIsType<'db> { + pub fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + Type::TypeIs(Self::new(db, ty, None)) + } + + pub fn bound( + db: &'db dyn Db, + ty: Type<'db>, + scope: ScopeId<'db>, + symbol: ScopedSymbolId, + name: String, + ) -> Type<'db> { + Type::TypeIs(Self::new(db, ty, Some((scope, symbol, name)))) + } + + #[must_use] + pub fn bind( + self, + db: &'db dyn Db, + scope: ScopeId<'db>, + symbol: ScopedSymbolId, + name: String, + ) -> Type<'db> { + Self::bound(db, self.ty(db), scope, symbol, name) + } + + #[must_use] + pub fn with_ty(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + Type::TypeIs(Self::new(db, ty, self.symbol_info(db))) + } + + pub fn is_bound(&self, db: &'db dyn Db) -> bool { + self.symbol_info(db).is_some() + } + + pub fn is_unbound(&self, db: &'db dyn Db) -> bool { + self.symbol_info(db).is_none() + } +} + // Make sure that the `Type` enum does not grow unexpectedly. #[cfg(not(debug_assertions))] #[cfg(target_pointer_width = "64")] diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index cf13ee5b38453..dd18e6edca4bf 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -146,7 +146,8 @@ impl<'db> ClassBase<'db> { | Type::BoundSuper(_) | Type::ProtocolInstance(_) | Type::AlwaysFalsy - | Type::AlwaysTruthy => None, + | Type::AlwaysTruthy + | Type::TypeIs(_) => None, Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic), diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index e93101375599e..78bc3e22ac41d 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -54,6 +54,8 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_SUPER_ARGUMENT); registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT); registry.register_lint(&INVALID_TYPE_FORM); + registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION); + registry.register_lint(&INVALID_TYPE_GUARD_CALL); registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS); registry.register_lint(&MISSING_ARGUMENT); registry.register_lint(&NO_MATCHING_OVERLOAD); @@ -893,6 +895,63 @@ declare_lint! { } } + +declare_lint! { + /// ## What it does + /// Checks for type guard functions without + /// a first non-self-like non-keyword-only non-variadic parameter. + /// + /// ## Why is this bad? + /// Type narrowing functions must accept at least one positional argument + /// (non-static methods must accept another in addition to `self`/`cls`). + /// + /// Extra parameters/arguments are allowed but do not affect narrowing. + /// + /// ## Examples + /// ```python + /// from typing import TypeIs + /// + /// def f() -> TypeIs[int]: ... # Error, no parameter + /// def f(*, v: object) -> TypeIs[int]: ... # Error, no positional arguments allowed + /// def f(*args: object) -> TypeIs[int]: ... # Error, expect variadic arguments + /// class C: + /// def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self` + /// ``` + pub(crate) static INVALID_TYPE_GUARD_DEFINITION = { + summary: "detects malformed type guard functions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for type guard function calls without a valid target. + /// + /// ## Why is this bad? + /// The first non-keyword non-variadic argument to a type guard function + /// is its target and must map to a symbol. + /// + /// Starred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like + /// expressions are invalid as narrowing targets. + /// + /// ## Examples + /// ```python + /// from typing import TypeIs + /// + /// def f(v: object) -> TypeIs[int]: ... + /// + /// f() # Error + /// f(*a) # Error + /// f(10) # Error + /// ``` + pub(crate) static INVALID_TYPE_GUARD_CALL = { + summary: "detects type guard function calls that has no narrowing effect", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for constrained [type variables] with only one constraint. diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 6c4dd480d03e7..3caf611573b09 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -237,6 +237,15 @@ impl Display for DisplayRepresentation<'_> { owner = bound_super.owner(self.db).into_type().display(self.db) ) } + Type::TypeIs(type_is) => { + f.write_str("TypeIs[")?; + if let Some((_, _, name)) = type_is.symbol_info(self.db) { + f.write_str(&name)?; + f.write_str(", ")?; + } + type_is.ty(self.db).display(self.db).fmt(f)?; + f.write_str("]") + } } } } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 50964ded3daaf..5a0ebeb7f1883 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -58,7 +58,7 @@ use crate::semantic_index::narrowing_constraints::ConstraintKey; use crate::semantic_index::symbol::{ FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind, ScopedSymbolId, }; -use crate::semantic_index::{EagerSnapshotResult, SemanticIndex, semantic_index}; +use crate::semantic_index::{EagerSnapshotResult, SemanticIndex, semantic_index, symbol_table}; use crate::symbol::{ Boundness, LookupError, builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol, module_type_implicit_global_declaration, module_type_implicit_global_symbol, @@ -101,13 +101,13 @@ use crate::{Db, FxOrderSet, Program}; use super::context::{InNoTypeCheck, InferContext}; use super::diagnostic::{ - INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, REDUNDANT_CAST, STATIC_ASSERT_ERROR, - SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, report_attempted_protocol_instantiation, - report_bad_argument_to_get_protocol_members, report_duplicate_bases, - report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, - report_invalid_exception_raised, report_invalid_or_unsupported_base, - report_invalid_type_checking_constant, report_non_subscriptable, - report_possibly_unresolved_reference, + INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, INVALID_TYPE_GUARD_CALL, REDUNDANT_CAST, + STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, + report_attempted_protocol_instantiation, report_bad_argument_to_get_protocol_members, + report_duplicate_bases, report_index_out_of_bounds, report_invalid_exception_caught, + report_invalid_exception_cause, report_invalid_exception_raised, + report_invalid_or_unsupported_base, report_invalid_type_checking_constant, + report_non_subscriptable, report_possibly_unresolved_reference, report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero, report_unresolved_reference, }; @@ -118,7 +118,7 @@ use super::string_annotation::{ }; use super::subclass_of::SubclassOfInner; use super::{ - BoundSuperError, BoundSuperType, ClassBase, add_inferred_python_version_hint_to_diagnostic, + BoundSuperError, BoundSuperType, ClassBase, TypeIsType, add_inferred_python_version_hint_to_diagnostic, }; /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. @@ -1815,6 +1815,10 @@ impl<'db> TypeInferenceBuilder<'db> { } let declared_ty = self.file_expression_type(returns); + let expected_ty = match declared_ty { + Type::TypeIs(_) => KnownClass::Bool.to_instance(self.db()), + ty => ty, + }; let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function)); if scope_id.is_generator_function(self.index) { @@ -1832,7 +1836,7 @@ impl<'db> TypeInferenceBuilder<'db> { if !inferred_return .to_instance(self.db()) - .is_assignable_to(self.db(), declared_ty) + .is_assignable_to(self.db(), expected_ty) { report_invalid_generator_function_return_type( &self.context, @@ -1858,7 +1862,7 @@ impl<'db> TypeInferenceBuilder<'db> { ty if ty.is_notimplemented(self.db()) => None, _ => Some(ty_range), }) - .filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), declared_ty)) + .filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), expected_ty)) { report_invalid_return_type( &self.context, @@ -1870,7 +1874,7 @@ impl<'db> TypeInferenceBuilder<'db> { } let use_def = self.index.use_def_map(scope_id); if use_def.can_implicit_return(self.db()) - && !Type::none(self.db()).is_assignable_to(self.db(), declared_ty) + && !Type::none(self.db()).is_assignable_to(self.db(), expected_ty) { let no_return = self.return_types_and_ranges.is_empty(); report_implicit_return_type( @@ -3098,7 +3102,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::DataclassTransformer(_) | Type::TypeVar(..) | Type::AlwaysTruthy - | Type::AlwaysFalsy => { + | Type::AlwaysFalsy + | Type::TypeIs(_) => { let is_read_only = || { let dataclass_params = match object_ty { Type::NominalInstance(instance) => match instance.class { @@ -5500,7 +5505,47 @@ impl<'db> TypeInferenceBuilder<'db> { } } } - bindings.return_type(self.db()) + + let db = self.db(); + let scope = self.scope(); + let return_ty = bindings.return_type(db); + + let find_narrowed_symbol = || match arguments.args.first() { + None => { + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_GUARD_CALL, arguments) + { + builder.into_diagnostic("Type guard call does not have a target"); + }; + None + } + Some(ast::Expr::Name(ast::ExprName { id, .. })) => { + let name = id.as_str(); + let symbol = symbol_table(db, scope).symbol_id_by_name(name)?; + + Some((symbol, name.to_string())) + } + // TODO: Attribute and subscript narrowing + Some(expr) => { + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_GUARD_CALL, expr) + { + builder.into_diagnostic("Type guard call target is not a symbol"); + }; + None + } + }; + + // TODO: Handle unions/intersections + match return_ty { + // TODO: TypeGuard + Type::TypeIs(type_is) => match find_narrowed_symbol() { + Some((symbol, name)) => type_is.bind(db, scope, symbol, name), + None => return_ty, + }, + _ => return_ty, + } } Err(CallError(_, bindings)) => { @@ -5973,7 +6018,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::BytesLiteral(_) | Type::Tuple(_) | Type::BoundSuper(_) - | Type::TypeVar(_), + | Type::TypeVar(_) + | Type::TypeIs(_), ) => { let unary_dunder_method = match op { ast::UnaryOp::Invert => "__invert__", @@ -6303,7 +6349,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::BytesLiteral(_) | Type::Tuple(_) | Type::BoundSuper(_) - | Type::TypeVar(_), + | Type::TypeVar(_) + | Type::TypeIs(_), Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) @@ -6329,7 +6376,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::BytesLiteral(_) | Type::Tuple(_) | Type::BoundSuper(_) - | Type::TypeVar(_), + | Type::TypeVar(_) + | Type::TypeIs(_), op, ) => { // We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from @@ -8810,10 +8858,19 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_expression(arguments_slice); todo_type!("`Required[]` type qualifier") } - SpecialFormType::TypeIs => { - self.infer_type_expression(arguments_slice); - todo_type!("`TypeIs[]` special form") - } + SpecialFormType::TypeIs => match arguments_slice { + ast::Expr::Tuple(_) => { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let diag = builder.into_diagnostic(format_args!( + "Special form `{}` expected exactly one type parameter", + known_instance.repr(self.db()) + )); + diagnostic::add_type_expression_reference_link(diag); + } + Type::unknown() + } + _ => TypeIsType::unbound(self.db(), self.infer_type_expression(arguments_slice)), + }, SpecialFormType::TypeGuard => { self.infer_type_expression(arguments_slice); todo_type!("`TypeGuard[]` special form") diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 303885c32049a..1f2f043dc6b7c 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -299,7 +299,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> { is_positive: bool, ) -> Option> { match expression_node { - ast::Expr::Name(name) => Some(self.evaluate_expr_name(name, is_positive)), + ast::Expr::Name(name) => Some(self.evaluate_expr_name(name, expression, is_positive)), ast::Expr::Compare(expr_compare) => { self.evaluate_expr_compare(expr_compare, expression, is_positive) } @@ -310,7 +310,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> { self.evaluate_expression_node_predicate(&unary_op.operand, expression, !is_positive) } ast::Expr::BoolOp(bool_op) => self.evaluate_bool_op(bool_op, expression, is_positive), - ast::Expr::Named(expr_named) => self.evaluate_expr_named(expr_named, is_positive), + ast::Expr::Named(expr_named) => { + self.evaluate_expr_named(expr_named, expression, is_positive) + } _ => None, } } @@ -368,28 +370,54 @@ impl<'db> NarrowingConstraintsBuilder<'db> { fn evaluate_expr_name( &mut self, expr_name: &ast::ExprName, + expression: Expression<'db>, is_positive: bool, ) -> NarrowingConstraints<'db> { let ast::ExprName { id, .. } = expr_name; + let inference = infer_expression_types(self.db, expression); let symbol = self.expect_expr_name_symbol(id); + let ty = inference.expression_type(expr_name.scoped_expression_id(self.db, self.scope())); - let ty = if is_positive { - Type::AlwaysFalsy.negate(self.db) - } else { - Type::AlwaysTruthy.negate(self.db) + let mut constraints = NarrowingConstraints::default(); + + // TODO: Handle unions and intersections + let mut narrow_by_typeguards = || match ty { + // TODO: TypeGuard + Type::TypeIs(type_is) => { + let (_, guarded_symbol, _) = type_is.symbol_info(self.db)?; + + constraints.insert( + guarded_symbol, + type_is.ty(self.db).negate_if(self.db, !is_positive), + ); + + Some(()) + } + _ => None, }; + narrow_by_typeguards(); - NarrowingConstraints::from_iter([(symbol, ty)]) + constraints.insert( + symbol, + if is_positive { + Type::AlwaysFalsy.negate(self.db) + } else { + Type::AlwaysTruthy.negate(self.db) + }, + ); + + constraints } fn evaluate_expr_named( &mut self, expr_named: &ast::ExprNamed, + expression: Expression<'db>, is_positive: bool, ) -> Option> { if let ast::Expr::Name(expr_name) = expr_named.target.as_ref() { - Some(self.evaluate_expr_name(expr_name, is_positive)) + Some(self.evaluate_expr_name(expr_name, expression, is_positive)) } else { None } @@ -710,6 +738,28 @@ impl<'db> NarrowingConstraintsBuilder<'db> { // TODO: add support for PEP 604 union types on the right hand side of `isinstance` // and `issubclass`, for example `isinstance(x, str | (int | float))`. match callable_ty { + Type::FunctionLiteral(function_type) if function_type.known(self.db).is_none() => { + let return_ty = + inference.expression_type(expr_call.scoped_expression_id(self.db, scope)); + + // TODO: Handle unions and intersections + let (guarded_ty, symbol) = match return_ty { + // TODO: TypeGuard + Type::TypeIs(type_is) => { + let (_, symbol, _) = type_is.symbol_info(self.db)?; + (type_is.ty(self.db), symbol) + } + _ => return None, + }; + + let mut constraints = NarrowingConstraints::default(); + + if is_positive { + constraints.insert(symbol, guarded_ty.negate_if(self.db, !is_positive)); + } + + Some(constraints) + } Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => { let [first_arg, second_arg] = &*expr_call.arguments.args else { return None; diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 669d116fd9fc8..c250b9777190d 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -3,8 +3,8 @@ use std::cmp::Ordering; use crate::db::Db; use super::{ - DynamicType, SuperOwnerKind, TodoType, Type, class_base::ClassBase, - subclass_of::SubclassOfInner, + DynamicType, SuperOwnerKind, TodoType, Type, TypeIsType, + class_base::ClassBase, subclass_of::SubclassOfInner, }; /// Return an [`Ordering`] that describes the canonical order in which two types should appear @@ -126,6 +126,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::SubclassOf(_), _) => Ordering::Less, (_, Type::SubclassOf(_)) => Ordering::Greater, + (Type::TypeIs(left), Type::TypeIs(right)) => typeis_ordering(db, left, right), + (Type::TypeIs(_), _) => Ordering::Less, + (_, Type::TypeIs(_)) => Ordering::Greater, + (Type::NominalInstance(left), Type::NominalInstance(right)) => left.class.cmp(&right.class), (Type::NominalInstance(_), _) => Ordering::Less, (_, Type::NominalInstance(_)) => Ordering::Greater, @@ -248,3 +252,25 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering (_, DynamicType::TodoPEP695ParamSpec) => Ordering::Greater, } } + +/// Determine a canonical order for two instances of [`TypeIsType`]. +/// +/// The following criteria are considered, in order: +/// * Boundness: Unbound precedes bound +/// * Symbol name: String comparison +/// * Guarded type: [`union_or_intersection_elements_ordering`] +fn typeis_ordering(db: &dyn Db, left: &TypeIsType, right: &TypeIsType) -> Ordering { + let (left_ty, right_ty) = (left.ty(db), right.ty(db)); + + match (left.symbol_info(db), right.symbol_info(db)) { + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + + (None, None) => union_or_intersection_elements_ordering(db, &left_ty, &right_ty), + + (Some((_, _, left_name)), Some((_, _, right_name))) => match left_name.cmp(&right_name) { + Ordering::Equal => union_or_intersection_elements_ordering(db, &left_ty, &right_ty), + ordering => ordering, + }, + } +} From b7408158dbc0799305c97291afb9fbc5cd643e29 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Sun, 25 May 2025 21:33:55 +0000 Subject: [PATCH 02/11] Formatting --- crates/ty_python_semantic/src/types/diagnostic.rs | 1 - crates/ty_python_semantic/src/types/infer.rs | 4 ++-- crates/ty_python_semantic/src/types/type_ordering.rs | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 78bc3e22ac41d..6b0ba09029e7a 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -895,7 +895,6 @@ declare_lint! { } } - declare_lint! { /// ## What it does /// Checks for type guard functions without diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 5a0ebeb7f1883..63b94ed69ae88 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -5517,7 +5517,7 @@ impl<'db> TypeInferenceBuilder<'db> { .report_lint(&INVALID_TYPE_GUARD_CALL, arguments) { builder.into_diagnostic("Type guard call does not have a target"); - }; + } None } Some(ast::Expr::Name(ast::ExprName { id, .. })) => { @@ -5532,7 +5532,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.context.report_lint(&INVALID_TYPE_GUARD_CALL, expr) { builder.into_diagnostic("Type guard call target is not a symbol"); - }; + } None } }; diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index c250b9777190d..6048ba8f37fd8 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -126,7 +126,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::SubclassOf(_), _) => Ordering::Less, (_, Type::SubclassOf(_)) => Ordering::Greater, - (Type::TypeIs(left), Type::TypeIs(right)) => typeis_ordering(db, left, right), + (Type::TypeIs(left), Type::TypeIs(right)) => typeis_ordering(db, *left, *right), (Type::TypeIs(_), _) => Ordering::Less, (_, Type::TypeIs(_)) => Ordering::Greater, @@ -259,7 +259,7 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering /// * Boundness: Unbound precedes bound /// * Symbol name: String comparison /// * Guarded type: [`union_or_intersection_elements_ordering`] -fn typeis_ordering(db: &dyn Db, left: &TypeIsType, right: &TypeIsType) -> Ordering { +fn typeis_ordering(db: &dyn Db, left: TypeIsType, right: TypeIsType) -> Ordering { let (left_ty, right_ty) = (left.ty(db), right.ty(db)); match (left.symbol_info(db), right.symbol_info(db)) { From 874818b60f98b15de9dd898ebdec6f22395ab220 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Sun, 25 May 2025 21:54:06 +0000 Subject: [PATCH 03/11] Do not report calls with unrecognized targets --- crates/ty_python_semantic/src/types/infer.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 63b94ed69ae88..1646239460586 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -5512,6 +5512,14 @@ impl<'db> TypeInferenceBuilder<'db> { let find_narrowed_symbol = || match arguments.args.first() { None => { + // This branch looks extraneous, especially in the face of + // `missing-arguments`. However, that lint won't be able to catch this: + // + // ```python + // def f(v: object = object()) -> TypeIs[int]: ... + // + // if f(): ... + // ``` if let Some(builder) = self .context .report_lint(&INVALID_TYPE_GUARD_CALL, arguments) @@ -5528,11 +5536,6 @@ impl<'db> TypeInferenceBuilder<'db> { } // TODO: Attribute and subscript narrowing Some(expr) => { - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_GUARD_CALL, expr) - { - builder.into_diagnostic("Type guard call target is not a symbol"); - } None } }; From a5da083073b974634cfca9898475d9b651a49f36 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Wed, 28 May 2025 23:57:07 +0000 Subject: [PATCH 04/11] Address failing tests --- .../resources/mdtest/narrow/type_guards.md | 16 ++++++++++------ .../type_properties/is_disjoint_from.md | 11 +++++++++++ .../mdtest/type_properties/is_subtype_of.md | 4 +++- crates/ty_python_semantic/src/types/infer.rs | 7 +++---- crates/ty_python_semantic/src/types/narrow.rs | 19 ++++++++++++------- .../src/types/type_ordering.rs | 4 ++-- 6 files changed, 41 insertions(+), 20 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index 433f87526910a..d483d233f430d 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -14,6 +14,8 @@ def _( b: TypeIs[str | int], c: TypeGuard[Intersection[complex, Not[int], Not[float]]], d: TypeIs[tuple[TypeOf[bytes]]], + e: TypeGuard, # error: [invalid-type-form] + f: TypeIs, # error: [invalid-type-form] ): # TODO: Should be `TypeGuard[str]` reveal_type(a) # revealed: @Todo(`TypeGuard[]` special form) @@ -21,6 +23,8 @@ def _( # TODO: Should be `TypeGuard[complex & ~int & ~float]` reveal_type(c) # revealed: @Todo(`TypeGuard[]` special form) reveal_type(d) # revealed: TypeIs[tuple[]] + reveal_type(e) # revealed: Unknown + reveal_type(f) # revealed: Unknown # TODO: error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeGuard[str]`" def _(a) -> TypeGuard[str]: ... @@ -137,12 +141,12 @@ def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]: ## Invalid calls -```pyi +```py from typing import Any from typing_extensions import TypeGuard, TypeIs -def f(a: object) -> TypeGuard[str]: ... -def g(a: object) -> TypeIs[int]: ... +def f(a: object) -> TypeGuard[str]: return True +def g(a: object) -> TypeIs[int]: return True def _(d: Any): if f(): # error: [missing-argument] ... @@ -158,7 +162,7 @@ def _(d: Any): ... def _(a: tuple[str, int] | tuple[int, str]): - if g(a[0]): # error: [invalid-type-guard-call] + if g(a[0]): # TODO: Should be `tuple[str, int]` reveal_type(a) # revealed: tuple[str, int] | tuple[int, str] ``` @@ -219,10 +223,10 @@ def _(x: str | int, flag: bool) -> None: ```py from typing import Any -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, TypeIs def guard_int(a: object) -> TypeGuard[int]: return True -def is_int(a: object) -> TypeGuard[int]: return True +def is_int(a: object) -> TypeIs[int]: return True def does_not_narrow_in_negative_case(a: str | int): if not guard_int(a): # TODO: Should be `str` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index 46cf9c907483e..cd0cf752cc9e0 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -402,6 +402,17 @@ static_assert(is_disjoint_from(TypeOf[C.prop], D)) static_assert(is_disjoint_from(D, TypeOf[C.prop])) ``` +### `TypeGuard` and `TypeIs` + +```py +from ty_extensions import static_assert, is_disjoint_from +from typing_extensions import TypeIs + +# TODO: TypeGuard +# static_assert(not is_disjoint_from(bool, TypeGuard[str])) +static_assert(not is_disjoint_from(bool, TypeIs[str])) +``` + ## Callables No two callable types are disjoint because there exists a non-empty callable type diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 50895a1be31c5..8b24e56628a6f 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -358,11 +358,13 @@ from typing_extensions import TypeGuard, TypeIs `TypeIs` is invariant. `TypeGuard` is covariant. ```py -from ty_extensions import is_subtype_of, static_assert +from ty_extensions import is_equivalent_to, is_subtype_of, static_assert from typing_extensions import TypeGuard, TypeIs # TODO: TypeGuard +# static_assert(is_subtype_of(TypeGuard[int], TypeGuard[int])) # static_assert(is_subtype_of(TypeGuard[bool], TypeGuard[int])) +static_assert(is_subtype_of(TypeIs[int], TypeIs[int])) static_assert(not is_subtype_of(TypeGuard[int], TypeGuard[bool])) static_assert(not is_subtype_of(TypeIs[bool], TypeIs[int])) diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 1646239460586..7e59a722d02de 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -118,7 +118,8 @@ use super::string_annotation::{ }; use super::subclass_of::SubclassOfInner; use super::{ - BoundSuperError, BoundSuperType, ClassBase, TypeIsType, add_inferred_python_version_hint_to_diagnostic, + BoundSuperError, BoundSuperType, ClassBase, TypeIsType, + add_inferred_python_version_hint_to_diagnostic, }; /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. @@ -5535,9 +5536,7 @@ impl<'db> TypeInferenceBuilder<'db> { Some((symbol, name.to_string())) } // TODO: Attribute and subscript narrowing - Some(expr) => { - None - } + Some(_) => None, }; // TODO: Handle unions/intersections diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 1f2f043dc6b7c..39bf0813c2f0e 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -742,23 +742,28 @@ impl<'db> NarrowingConstraintsBuilder<'db> { let return_ty = inference.expression_type(expr_call.scoped_expression_id(self.db, scope)); + if let Some(name) = expr_call.func.as_name_expr() { + dbg!(&name.id); + dbg!(return_ty); + } + // TODO: Handle unions and intersections let (guarded_ty, symbol) = match return_ty { // TODO: TypeGuard Type::TypeIs(type_is) => { let (_, symbol, _) = type_is.symbol_info(self.db)?; + if let Some(name) = expr_call.func.as_name_expr() { + dbg!(symbol, name); + } (type_is.ty(self.db), symbol) } _ => return None, }; - let mut constraints = NarrowingConstraints::default(); - - if is_positive { - constraints.insert(symbol, guarded_ty.negate_if(self.db, !is_positive)); - } - - Some(constraints) + Some(NarrowingConstraints::from_iter([( + symbol, + guarded_ty.negate_if(self.db, !is_positive), + )])) } Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => { let [first_arg, second_arg] = &*expr_call.arguments.args else { diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 6048ba8f37fd8..0ff6b2122842b 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -3,8 +3,8 @@ use std::cmp::Ordering; use crate::db::Db; use super::{ - DynamicType, SuperOwnerKind, TodoType, Type, TypeIsType, - class_base::ClassBase, subclass_of::SubclassOfInner, + DynamicType, SuperOwnerKind, TodoType, Type, TypeIsType, class_base::ClassBase, + subclass_of::SubclassOfInner, }; /// Return an [`Ordering`] that describes the canonical order in which two types should appear From bc172560bde22199c9c08808760eb1aebac134fe Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Thu, 29 May 2025 00:02:26 +0000 Subject: [PATCH 05/11] Remove `dbg!()` --- crates/ty_python_semantic/src/types/narrow.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 39bf0813c2f0e..07ce7c5c27c69 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -742,19 +742,11 @@ impl<'db> NarrowingConstraintsBuilder<'db> { let return_ty = inference.expression_type(expr_call.scoped_expression_id(self.db, scope)); - if let Some(name) = expr_call.func.as_name_expr() { - dbg!(&name.id); - dbg!(return_ty); - } - // TODO: Handle unions and intersections let (guarded_ty, symbol) = match return_ty { // TODO: TypeGuard Type::TypeIs(type_is) => { let (_, symbol, _) = type_is.symbol_info(self.db)?; - if let Some(name) = expr_call.func.as_name_expr() { - dbg!(symbol, name); - } (type_is.ty(self.db), symbol) } _ => return None, From a451a16007928c8182ebd21b587d963d82071dae Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Thu, 29 May 2025 00:18:08 +0000 Subject: [PATCH 06/11] Update docs/schema --- crates/ty/docs/rules.md | 174 +++++++++++++------ crates/ty_python_semantic/src/types/infer.rs | 2 +- ty.schema.json | 20 +++ 3 files changed, 141 insertions(+), 55 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 30fe995231433..b36ccae6576be 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -52,7 +52,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L92) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L94) ## `conflicting-argument-forms` @@ -83,7 +83,7 @@ f(int) # error ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L136) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L138) ## `conflicting-declarations` @@ -113,7 +113,7 @@ a = 1 ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L162) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L164) ## `conflicting-metaclass` @@ -144,7 +144,7 @@ class C(A, B): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L187) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L189) ## `cyclic-class-definition` @@ -175,7 +175,7 @@ class B(A): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L213) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L215) ## `duplicate-base` @@ -201,7 +201,7 @@ class B(A, A): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L257) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L259) ## `escape-character-in-forward-annotation` @@ -338,7 +338,7 @@ TypeError: multiple bases have instance lay-out conflict ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20incompatible-slots) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L278) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L280) ## `inconsistent-mro` @@ -367,7 +367,7 @@ class C(A, B): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L364) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L366) ## `index-out-of-bounds` @@ -392,7 +392,7 @@ t[3] # IndexError: tuple index out of range ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L388) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L390) ## `invalid-argument-type` @@ -418,7 +418,7 @@ func("foo") # error: [invalid-argument-type] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L408) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L410) ## `invalid-assignment` @@ -445,7 +445,7 @@ a: int = '' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L448) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L450) ## `invalid-attribute-access` @@ -478,7 +478,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1396) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1454) ## `invalid-base` @@ -501,7 +501,7 @@ class A(42): ... # error: [invalid-base] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L470) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L472) ## `invalid-context-manager` @@ -527,7 +527,7 @@ with 1: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L521) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523) ## `invalid-declaration` @@ -555,7 +555,7 @@ a: str ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L542) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L544) ## `invalid-exception-caught` @@ -596,7 +596,7 @@ except ZeroDivisionError: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L565) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567) ## `invalid-generic-class` @@ -627,7 +627,7 @@ class C[U](Generic[T]): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L601) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L603) ## `invalid-legacy-type-variable` @@ -660,7 +660,7 @@ def f(t: TypeVar("U")): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L629) ## `invalid-metaclass` @@ -692,7 +692,7 @@ class B(metaclass=f): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L676) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L678) ## `invalid-overload` @@ -740,7 +740,7 @@ def foo(x: int) -> int: ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L703) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L705) ## `invalid-parameter-default` @@ -765,7 +765,7 @@ def f(a: int = ''): ... ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L746) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L748) ## `invalid-protocol` @@ -798,7 +798,7 @@ TypeError: Protocols can only inherit from other protocols, got ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L336) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L338) ## `invalid-raise` @@ -846,7 +846,7 @@ def g(): ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L768) ## `invalid-return-type` @@ -870,7 +870,7 @@ def func() -> int: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L429) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L431) ## `invalid-super-argument` @@ -914,7 +914,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)` ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811) ## `invalid-syntax-in-forward-annotation` @@ -954,7 +954,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L655) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L657) ## `invalid-type-checking-constant` @@ -983,7 +983,7 @@ TYPE_CHECKING = '' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L848) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L850) ## `invalid-type-form` @@ -1012,7 +1012,73 @@ b: Annotated[int] # `Annotated` expects at least two arguments ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L872) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L874) + + +## `invalid-type-guard-call` + +**Default level**: error + +
+detects type guard function calls that has no narrowing effect + +### What it does +Checks for type guard function calls without a valid target. + +### Why is this bad? +The first non-keyword non-variadic argument to a type guard function +is its target and must map to a symbol. + +Starred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like +expressions are invalid as narrowing targets. + +### Examples +```python +from typing import TypeIs + +def f(v: object) -> TypeIs[int]: ... + +f() # Error +f(*a) # Error +f(10) # Error +``` + +### Links +* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L926) +
+ +## `invalid-type-guard-definition` + +**Default level**: error + +
+detects malformed type guard functions + +### What it does +Checks for type guard functions without +a first non-self-like non-keyword-only non-variadic parameter. + +### Why is this bad? +Type narrowing functions must accept at least one positional argument +(non-static methods must accept another in addition to `self`/`cls`). + +Extra parameters/arguments are allowed but do not affect narrowing. + +### Examples +```python +from typing import TypeIs + +def f() -> TypeIs[int]: ... # Error, no parameter +def f(*, v: object) -> TypeIs[int]: ... # Error, no positional arguments allowed +def f(*args: object) -> TypeIs[int]: ... # Error, expect variadic arguments +class C: + def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self` +``` + +### Links +* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L898)
## `invalid-type-variable-constraints` @@ -1046,7 +1112,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L896) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L954) ## `missing-argument` @@ -1070,7 +1136,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L925) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L983) ## `no-matching-overload` @@ -1098,7 +1164,7 @@ func("string") # error: [no-matching-overload] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L944) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1002) ## `non-subscriptable` @@ -1121,7 +1187,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L967) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1025) ## `not-iterable` @@ -1146,7 +1212,7 @@ for i in 34: # TypeError: 'int' object is not iterable ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L985) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1043) ## `parameter-already-assigned` @@ -1172,7 +1238,7 @@ f(1, x=2) # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1036) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1094) ## `raw-string-type-annotation` @@ -1231,7 +1297,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1372) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1430) ## `subclass-of-final-class` @@ -1259,7 +1325,7 @@ class B(A): ... # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1127) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1185) ## `too-many-positional-arguments` @@ -1285,7 +1351,7 @@ f("foo") # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1172) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1230) ## `type-assertion-failure` @@ -1312,7 +1378,7 @@ def _(x: int): ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1150) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1208) ## `unavailable-implicit-super-arguments` @@ -1356,7 +1422,7 @@ class A: ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1193) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1251) ## `unknown-argument` @@ -1382,7 +1448,7 @@ f(x=1, y=2) # Error raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1250) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1308) ## `unresolved-attribute` @@ -1409,7 +1475,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1271) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1329) ## `unresolved-import` @@ -1433,7 +1499,7 @@ import foo # ModuleNotFoundError: No module named 'foo' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1293) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1351) ## `unresolved-reference` @@ -1457,7 +1523,7 @@ print(x) # NameError: name 'x' is not defined ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1370) ## `unsupported-bool-conversion` @@ -1493,7 +1559,7 @@ b1 < b2 < b1 # exception raised here ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1005) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063) ## `unsupported-operator` @@ -1520,7 +1586,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1331) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1389) ## `zero-stepsize-in-slice` @@ -1544,7 +1610,7 @@ l[1:10:0] # ValueError: slice step cannot be zero ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1353) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411) ## `invalid-ignore-comment` @@ -1600,7 +1666,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1057) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1115) ## `possibly-unbound-implicit-call` @@ -1631,7 +1697,7 @@ A()[0] # TypeError: 'A' object is not subscriptable ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L110) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L112) ## `possibly-unbound-import` @@ -1662,7 +1728,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1079) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1137) ## `redundant-cast` @@ -1688,7 +1754,7 @@ cast(int, f()) # Redundant ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1424) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1482) ## `undefined-reveal` @@ -1711,7 +1777,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1232) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290) ## `unknown-rule` @@ -1779,7 +1845,7 @@ class D(C): ... # error: [unsupported-base] ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L488) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L490) ## `division-by-zero` @@ -1802,7 +1868,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L239) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L241) ## `possibly-unresolved-reference` @@ -1829,7 +1895,7 @@ print(x) # NameError: name 'x' is not defined ### Links * [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) -* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1105) +* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1163) ## `unused-ignore-comment` diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 7e59a722d02de..8cb3660948448 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -8865,7 +8865,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let diag = builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", - known_instance.repr(self.db()) + special_form.repr() )); diagnostic::add_type_expression_reference_link(diag); } diff --git a/ty.schema.json b/ty.schema.json index f4ae8d241babf..13ec923dec26d 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -563,6 +563,26 @@ } ] }, + "invalid-type-guard-call": { + "title": "detects type guard function calls that has no narrowing effect", + "description": "## What it does\nChecks for type guard function calls without a valid target.\n\n## Why is this bad?\nThe first non-keyword non-variadic argument to a type guard function\nis its target and must map to a symbol.\n\nStarred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like\nexpressions are invalid as narrowing targets.\n\n## Examples\n```python\nfrom typing import TypeIs\n\ndef f(v: object) -> TypeIs[int]: ...\n\nf() # Error\nf(*a) # Error\nf(10) # Error\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-type-guard-definition": { + "title": "detects malformed type guard functions", + "description": "## What it does\nChecks for type guard functions without\na first non-self-like non-keyword-only non-variadic parameter.\n\n## Why is this bad?\nType narrowing functions must accept at least one positional argument\n(non-static methods must accept another in addition to `self`/`cls`).\n\nExtra parameters/arguments are allowed but do not affect narrowing.\n\n## Examples\n```python\nfrom typing import TypeIs\n\ndef f() -> TypeIs[int]: ... # Error, no parameter\ndef f(*, v: object) -> TypeIs[int]: ... # Error, no positional arguments allowed\ndef f(*args: object) -> TypeIs[int]: ... # Error, expect variadic arguments\nclass C:\n def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self`\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-type-variable-constraints": { "title": "detects invalid type variable constraints", "description": "## What it does\nChecks for constrained [type variables] with only one constraint.\n\n## Why is this bad?\nA constrained type variable must have at least two constraints.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar('T', str) # invalid constrained TypeVar\n```\n\nUse instead:\n```python\nT = TypeVar('T', str, int) # valid constrained TypeVar\n# or\nT = TypeVar('T', bound=str) # valid bound TypeVar\n```\n\n[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar", From 4379dd436528b496653aed4f3eaf35b3db1df9c6 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Thu, 29 May 2025 00:26:39 +0000 Subject: [PATCH 07/11] Formatting --- .../resources/mdtest/narrow/type_guards.md | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index d483d233f430d..5ae6775915bf9 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -31,8 +31,12 @@ def _(a) -> TypeGuard[str]: ... # error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`" def _(a) -> TypeIs[str]: ... -def f(a) -> TypeGuard[str]: return True -def g(a) -> TypeIs[str]: return True +def f(a) -> TypeGuard[str]: + return True + +def g(a) -> TypeIs[str]: + return True + def _(a: object): # TODO: Should be `TypeGuard[a, str]` reveal_type(f(a)) # revealed: @Todo(`TypeGuard[]` special form) @@ -145,8 +149,12 @@ def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]: from typing import Any from typing_extensions import TypeGuard, TypeIs -def f(a: object) -> TypeGuard[str]: return True -def g(a: object) -> TypeIs[int]: return True +def f(a: object) -> TypeGuard[str]: + return True + +def g(a: object) -> TypeIs[int]: + return True + def _(d: Any): if f(): # error: [missing-argument] ... @@ -173,8 +181,12 @@ def _(a: tuple[str, int] | tuple[int, str]): from typing import Any from typing_extensions import TypeGuard, TypeIs -def guard_str(a: object) -> TypeGuard[str]: return True -def is_int(a: object) -> TypeIs[int]: return True +def guard_str(a: object) -> TypeGuard[str]: + return True + +def is_int(a: object) -> TypeIs[int]: + return True + def _(a: str | int): if guard_str(a): # TODO: Should be `str` @@ -225,8 +237,12 @@ def _(x: str | int, flag: bool) -> None: from typing import Any from typing_extensions import TypeGuard, TypeIs -def guard_int(a: object) -> TypeGuard[int]: return True -def is_int(a: object) -> TypeIs[int]: return True +def guard_int(a: object) -> TypeGuard[int]: + return True + +def is_int(a: object) -> TypeIs[int]: + return True + def does_not_narrow_in_negative_case(a: str | int): if not guard_int(a): # TODO: Should be `str` From 62c14df3a99c874f75734ad55806c270801d43d3 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Thu, 29 May 2025 01:00:30 +0000 Subject: [PATCH 08/11] Formatting --- .../ty_python_semantic/resources/mdtest/narrow/type_guards.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index 5ae6775915bf9..d3201208100c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -28,9 +28,9 @@ def _( # TODO: error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeGuard[str]`" def _(a) -> TypeGuard[str]: ... + # error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`" def _(a) -> TypeIs[str]: ... - def f(a) -> TypeGuard[str]: return True From f894e8dde057412347e6ca506975a3221d739c24 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Thu, 29 May 2025 04:01:20 +0000 Subject: [PATCH 09/11] Bound `TypeIs` types are singleton/single-valued --- .../resources/mdtest/type_properties/is_subtype_of.md | 2 +- crates/ty_python_semantic/src/types.rs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 8b24e56628a6f..40a4420b8d1d6 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -352,7 +352,7 @@ from typing_extensions import TypeGuard, TypeIs # TODO: TypeGuard # static_assert(is_subtype_of(TypeGuard[int], bool)) -# static_assert(is_subtype_of(TypeIs[str], bool)) +static_assert(is_subtype_of(TypeIs[str], bool)) ``` `TypeIs` is invariant. `TypeGuard` is covariant. diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f45d02c714004..027c961bde830 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2344,8 +2344,7 @@ impl<'db> Type<'db> { | Type::IntLiteral(..) | Type::StringLiteral(..) | Type::BytesLiteral(..) - | Type::LiteralString - | Type::TypeIs(_) => { + | Type::LiteralString => { // Note: The literal types included in this pattern are not true singletons. // There can be multiple Python objects (at different memory locations) that // are both of type Literal[345], for example. @@ -2455,6 +2454,7 @@ impl<'db> Type<'db> { false } Type::AlwaysTruthy | Type::AlwaysFalsy => false, + Type::TypeIs(type_is) => type_is.is_bound(db), } } @@ -2512,6 +2512,8 @@ impl<'db> Type<'db> { false } + Type::TypeIs(type_is) => type_is.is_bound(db), + Type::Dynamic(_) | Type::Never | Type::Union(..) @@ -2522,8 +2524,7 @@ impl<'db> Type<'db> { | Type::Callable(_) | Type::PropertyInstance(_) | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::TypeIs(_) => false, + | Type::DataclassTransformer(_) => false, } } From ef3d3ebb99391f60b5a6bc314df47541dbb3360b Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Thu, 29 May 2025 06:08:27 +0000 Subject: [PATCH 10/11] Account for non-fully-static `TypeIs` types in `is_assignable_to()` --- .../mdtest/type_properties/is_assignable_to.md | 13 +++++++++++++ crates/ty_python_semantic/src/types.rs | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 111e5ef9dc138..21f47c83108c7 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -789,4 +789,17 @@ def g3(obj: Foo[tuple[A]]): f3(obj) ``` +## `TypeGuard` and `TypeIs` + +`TypeGuard[...]` and `TypeIs[...]` are always assignable to `bool`. + +```py +from ty_extensions import Unknown, is_assignable_to, static_assert +from typing_extensions import Any, TypeGuard, TypeIs + +# TODO: TypeGuard +# static_assert(is_assignable_to(TypeGuard[Unknown], bool)) +static_assert(is_assignable_to(TypeIs[Any], bool)) +``` + [typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 027c961bde830..bbab7ab7ba85e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1696,6 +1696,10 @@ impl<'db> Type<'db> { (Type::ProtocolInstance(_), _) => false, (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol), + (Type::TypeIs(_), _) => KnownClass::Bool + .to_instance(db) + .is_assignable_to(db, target), + // TODO other types containing gradual forms _ => self.is_subtype_of(db, target), } From 90f93c74fa61c53eccca42ea9f8fcffdbfd8e8b2 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Fri, 30 May 2025 04:29:14 +0000 Subject: [PATCH 11/11] Fix --- .../ty_python_semantic/resources/mdtest/narrow/type_guards.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index d3201208100c6..bd25b11a1db8d 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -26,10 +26,10 @@ def _( reveal_type(e) # revealed: Unknown reveal_type(f) # revealed: Unknown -# TODO: error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeGuard[str]`" +# TODO: error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeGuard[str]`" def _(a) -> TypeGuard[str]: ... -# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`" +# error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeIs[str]`" def _(a) -> TypeIs[str]: ... def f(a) -> TypeGuard[str]: return True