[ty] Add partial support for TypeIs (#18589)

## Summary

Part of [#117](https://github.com/astral-sh/ty/issues/117).

`TypeIs[]` is a special form that allows users to define their own
narrowing functions. Despite the syntax, `TypeIs` is not a generic and,
on its own, it is meaningless as a type.
[Officially](https://typing.python.org/en/latest/spec/narrowing.html#typeis),
a function annotated as returning a `TypeIs[T]` is a <i>type narrowing
function</i>, where `T` is called the <i>`TypeIs` return type</i>.

A `TypeIs[T]` may or may not be bound to a symbol. Only bound types have
narrowing effect:

```python
def f(v: object = object()) -> TypeIs[int]: ...

a: str = returns_str()

if reveal_type(f()):   # Unbound: TypeIs[int]
	reveal_type(a)     # str

if reveal_type(f(a)):  # Bound:   TypeIs[a, int]
	reveal_type(a)     # str & int
```

Delayed usages of a bound type has no effect, however:

```python
b = f(a)

if b:
	reveal_type(a)     # str
```

A `TypeIs[T]` type:

* Is fully static when `T` is fully static.
* Is a singleton/single-valued when it is bound.
* Has exactly two runtime inhabitants when it is unbound: `True` and
`False`.
  In other words, an unbound type have ambiguous truthiness.
It is possible to infer more precise truthiness for bound types;
however, that is not part of this change.

`TypeIs[T]` is a subtype of or otherwise assignable to `bool`. `TypeIs`
is invariant with respect to the `TypeIs` return type: `TypeIs[int]` is
neither a subtype nor a supertype of `TypeIs[bool]`. When ty sees a
function marked as returning `TypeIs[T]`, its `return`s will be checked
against `bool` instead. ty will also report such functions if they don't
accept a positional argument. Addtionally, a type narrowing function
call with no positional arguments (e.g., `f()` in the example above)
will be considered invalid.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
InSync 2025-06-14 05:27:45 +07:00 committed by GitHub
parent 89d915a1e3
commit 6d56ee803e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 841 additions and 97 deletions

View file

@ -35,8 +35,8 @@ use crate::module_resolver::{KnownModule, resolve_module};
use crate::place::{Boundness, Place, PlaceAndQualifiers, imported_symbol};
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::place::ScopeId;
use crate::semantic_index::{imported_modules, semantic_index};
use crate::semantic_index::place::{ScopeId, ScopedPlaceId};
use crate::semantic_index::{imported_modules, place_table, semantic_index};
use crate::suppression::check_suppressions;
use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallableBinding};
pub(crate) use crate::types::class_base::ClassBase;
@ -553,6 +553,8 @@ pub enum Type<'db> {
// 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
}
@ -726,6 +728,9 @@ impl<'db> Type<'db> {
.map(|ty| ty.materialize(db, variance)),
),
Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)),
Type::TypeIs(type_is) => {
type_is.with_type(db, type_is.return_type(db).materialize(db, variance))
}
}
}
@ -777,6 +782,11 @@ impl<'db> Type<'db> {
*self
}
Self::TypeIs(type_is) => type_is.with_type(
db,
type_is.return_type(db).replace_self_reference(db, class),
),
Self::Dynamic(_)
| Self::AlwaysFalsy
| Self::AlwaysTruthy
@ -910,6 +920,8 @@ impl<'db> Type<'db> {
.iter()
.any(|ty| ty.any_over_type(db, type_fn)),
},
Self::TypeIs(type_is) => type_is.return_type(db).any_over_type(db, type_fn),
}
}
@ -1145,6 +1157,7 @@ impl<'db> Type<'db> {
Type::KnownInstance(known_instance) => {
Type::KnownInstance(known_instance.normalized(db))
}
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).normalized(db)),
Type::LiteralString
| Type::AlwaysFalsy
| Type::AlwaysTruthy
@ -1404,6 +1417,11 @@ impl<'db> Type<'db> {
false
}
// `TypeIs[T]` is a subtype of `bool`.
(Type::TypeIs(_), _) => KnownClass::Bool
.to_instance(db)
.has_relation_to(db, target, relation),
// Function-like callables are subtypes of `FunctionType`
(Type::Callable(callable), _)
if callable.is_function_like(db)
@ -1949,14 +1967,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(..)) => {
@ -2186,6 +2205,7 @@ 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.return_type(db).is_fully_static(db),
}
}
@ -2310,6 +2330,7 @@ impl<'db> Type<'db> {
false
}
Type::AlwaysTruthy | Type::AlwaysFalsy => false,
Type::TypeIs(type_is) => type_is.is_bound(db),
}
}
@ -2367,6 +2388,8 @@ impl<'db> Type<'db> {
false
}
Type::TypeIs(type_is) => type_is.is_bound(db),
Type::Dynamic(_)
| Type::Never
| Type::Union(..)
@ -2495,7 +2518,8 @@ impl<'db> Type<'db> {
| Type::TypeVar(_)
| Type::NominalInstance(_)
| Type::ProtocolInstance(_)
| Type::PropertyInstance(_) => None,
| Type::PropertyInstance(_)
| Type::TypeIs(_) => None,
}
}
@ -2595,7 +2619,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)
}
@ -3116,7 +3142,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(
@ -3381,9 +3408,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(_)
@ -4348,7 +4377,8 @@ impl<'db> Type<'db> {
| Type::LiteralString
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::ModuleLiteral(_) => CallableBinding::not_callable(self).into(),
| Type::ModuleLiteral(_)
| Type::TypeIs(_) => CallableBinding::not_callable(self).into(),
}
}
@ -4836,7 +4866,8 @@ impl<'db> Type<'db> {
| Type::LiteralString
| Type::BoundSuper(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy => None,
| Type::AlwaysFalsy
| Type::TypeIs(_) => None,
}
}
@ -4902,7 +4933,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
)],
@ -5141,7 +5173,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),
@ -5315,6 +5347,8 @@ impl<'db> Type<'db> {
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
),
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)),
Type::ModuleLiteral(_)
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
@ -5424,6 +5458,10 @@ impl<'db> Type<'db> {
subclass_of.find_legacy_typevars(db, typevars);
}
Type::TypeIs(type_is) => {
type_is.return_type(db).find_legacy_typevars(db, typevars);
}
Type::Dynamic(_)
| Type::Never
| Type::AlwaysTruthy
@ -5553,8 +5591,9 @@ impl<'db> Type<'db> {
| Self::Never
| Self::Callable(_)
| Self::AlwaysTruthy
| Self::AlwaysFalsy
| Self::SpecialForm(_)
| Self::AlwaysFalsy => None,
| Self::TypeIs(_) => None,
}
}
@ -8476,6 +8515,54 @@ impl<'db> BoundSuperType<'db> {
}
}
#[salsa::interned(debug)]
pub struct TypeIsType<'db> {
return_type: Type<'db>,
/// The ID of the scope to which the place belongs
/// and the ID of the place itself within that scope.
place_info: Option<(ScopeId<'db>, ScopedPlaceId)>,
}
impl<'db> TypeIsType<'db> {
pub fn place_name(self, db: &'db dyn Db) -> Option<String> {
let (scope, place) = self.place_info(db)?;
let table = place_table(db, scope);
Some(format!("{}", table.place_expr(place)))
}
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,
return_type: Type<'db>,
scope: ScopeId<'db>,
place: ScopedPlaceId,
) -> Type<'db> {
Type::TypeIs(Self::new(db, return_type, Some((scope, place))))
}
#[must_use]
pub fn bind(self, db: &'db dyn Db, scope: ScopeId<'db>, place: ScopedPlaceId) -> Type<'db> {
Self::bound(db, self.return_type(db), scope, place)
}
#[must_use]
pub fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> {
Type::TypeIs(Self::new(db, ty, self.place_info(db)))
}
pub fn is_bound(&self, db: &'db dyn Db) -> bool {
self.place_info(db).is_some()
}
pub fn is_unbound(&self, db: &'db dyn Db) -> bool {
self.place_info(db).is_none()
}
}
// Make sure that the `Type` enum does not grow unexpectedly.
#[cfg(not(debug_assertions))]
#[cfg(target_pointer_width = "64")]

View file

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

View file

@ -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,62 @@ 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.

View file

@ -211,6 +211,15 @@ impl Display for DisplayRepresentation<'_> {
owner = bound_super.owner(self.db).into_type().display(self.db)
)
}
Type::TypeIs(type_is) => {
f.write_str("TypeIs[")?;
type_is.return_type(self.db).display(self.db).fmt(f)?;
if let Some(name) = type_is.place_name(self.db) {
f.write_str(" @ ")?;
f.write_str(&name)?;
}
f.write_str("]")
}
}
}
}

View file

@ -116,7 +116,8 @@ impl AllMembers {
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::TypeVar(_)
| Type::BoundSuper(_) => {
| Type::BoundSuper(_)
| Type::TypeIs(_) => {
if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) {
self.extend_with_class_members(db, class_literal);
}

View file

@ -68,7 +68,7 @@ use crate::semantic_index::narrowing_constraints::ConstraintKey;
use crate::semantic_index::place::{
FileScopeId, NodeWithScopeKind, NodeWithScopeRef, PlaceExpr, ScopeId, ScopeKind, ScopedPlaceId,
};
use crate::semantic_index::{EagerSnapshotResult, SemanticIndex, semantic_index};
use crate::semantic_index::{EagerSnapshotResult, SemanticIndex, place_table, semantic_index};
use crate::types::call::{
Argument, Binding, Bindings, CallArgumentTypes, CallArguments, CallError,
};
@ -78,13 +78,14 @@ use crate::types::diagnostic::{
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE,
INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION,
INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE, INVALID_PARAMETER_DEFAULT,
INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS,
POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
UNSUPPORTED_OPERATOR, report_implicit_return_type, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
report_invalid_return_type, report_possibly_unbound_attribute,
INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT,
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_generator_function_return_type, report_invalid_return_type,
report_possibly_unbound_attribute,
};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
@ -99,8 +100,8 @@ use crate::types::{
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, StringLiteralType,
SubclassOfType, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
TypeArrayDisplay, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
};
use crate::unpack::{Unpack, UnpackPosition};
use crate::util::subscript::{PyIndex, PySlice};
@ -672,6 +673,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.types.expressions.extend(inference.expressions.iter());
self.types.deferred.extend(inference.deferred.iter());
self.context.extend(inference.diagnostics());
self.types.cycle_fallback_type = self
.types
.cycle_fallback_type
.or(inference.cycle_fallback_type);
}
fn file(&self) -> File {
@ -1904,6 +1909,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
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) {
@ -1921,7 +1930,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
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,
@ -1947,7 +1956,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
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,
@ -1959,7 +1968,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
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(
@ -3213,7 +3222,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| 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 {
@ -5800,7 +5810,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
}
bindings.return_type(self.db())
let db = self.db();
let scope = self.scope();
let return_ty = bindings.return_type(db);
let find_narrowed_place = || 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(): ...
// ```
//
// TODO: Will this report things that is actually fine?
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(expr) => match PlaceExpr::try_from(expr) {
Ok(place_expr) => place_table(db, scope).place_id_by_expr(&place_expr),
Err(()) => None,
},
};
match return_ty {
// TODO: TypeGuard
Type::TypeIs(type_is) => match find_narrowed_place() {
Some(place) => type_is.bind(db, scope, place),
None => return_ty,
},
_ => return_ty,
}
}
Err(CallError(_, bindings)) => {
@ -6428,7 +6476,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| Type::BytesLiteral(_)
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::TypeVar(_),
| Type::TypeVar(_)
| Type::TypeIs(_),
) => {
let unary_dunder_method = match op {
ast::UnaryOp::Invert => "__invert__",
@ -6759,7 +6808,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| Type::BytesLiteral(_)
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::TypeVar(_),
| Type::TypeVar(_)
| Type::TypeIs(_),
Type::FunctionLiteral(_)
| Type::Callable(..)
| Type::BoundMethod(_)
@ -6785,7 +6835,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| 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
@ -9552,10 +9603,22 @@ 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(_) => {
self.infer_type_expression(arguments_slice);
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",
special_form.repr()
));
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")

View file

@ -388,7 +388,6 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
let ast::ExprName { id, .. } = expr_name;
let symbol = self.expect_expr_name_symbol(id);
let ty = if is_positive {
Type::AlwaysFalsy.negate(self.db)
} else {
@ -728,6 +727,29 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
// 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 matches!(
function_type.known(self.db),
None | Some(KnownFunction::RevealType)
) =>
{
let return_ty =
inference.expression_type(expr_call.scoped_expression_id(self.db, scope));
let (guarded_ty, place) = match return_ty {
// TODO: TypeGuard
Type::TypeIs(type_is) => {
let (_, place) = type_is.place_info(self.db)?;
(type_is.return_type(self.db), place)
}
_ => return None,
};
Some(NarrowingConstraints::from_iter([(
place,
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 {
return None;

View file

@ -3,7 +3,7 @@ use std::cmp::Ordering;
use crate::db::Db;
use super::{
DynamicType, SuperOwnerKind, TodoType, Type, class_base::ClassBase,
DynamicType, SuperOwnerKind, TodoType, Type, TypeIsType, class_base::ClassBase,
subclass_of::SubclassOfInner,
};
@ -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.return_type(db), right.return_type(db));
match (left.place_info(db), right.place_info(db)) {
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(None, None) => union_or_intersection_elements_ordering(db, &left_ty, &right_ty),
(Some(_), Some(_)) => match left.place_name(db).cmp(&right.place_name(db)) {
Ordering::Equal => union_or_intersection_elements_ordering(db, &left_ty, &right_ty),
ordering => ordering,
},
}
}