diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 8816e1bf87..b923e62e35 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -222,7 +222,6 @@ impl<'db> Completion<'db> { // "struct" here as a more general "object." ---AG Type::NominalInstance(_) | Type::PropertyInstance(_) - | Type::Tuple(_) | Type::BoundSuper(_) => CompletionKind::Struct, Type::IntLiteral(_) | Type::BooleanLiteral(_) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 76cb7f52c7..b5a95f3d79 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -592,11 +592,6 @@ pub enum Type<'db> { LiteralString, /// A bytes literal BytesLiteral(BytesLiteralType<'db>), - /// An instance of the builtin `tuple` class. - /// TODO: Consider removing this in favor of `NominalInstance`. This is currently stored as a - /// separate variant partly for historical reasons, and partly to allow us to easily - /// distinguish tuples since they occur so often. - Tuple(TupleType<'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>), @@ -681,6 +676,30 @@ impl<'db> Type<'db> { self.materialize(db, TypeVarVariance::Contravariant) } + /// If this type is an instance type where the class has a tuple spec, returns the tuple spec. + /// + /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. + /// For a subclass of `tuple[int, str]`, it will return the same tuple spec. + fn tuple_instance_spec(&self, db: &'db dyn Db) -> Option>> { + self.into_nominal_instance() + .and_then(|instance| instance.class.tuple_spec(db)) + } + + /// If this type is an *exact* tuple type (*not* a subclass of `tuple`), returns the + /// tuple spec. + /// + /// You usually don't want to use this method, as you usually want to consider a subclass + /// of a tuple type in the same way as the `tuple` type itself. Only use this method if you + /// are certain that a *literal tuple* is required, and that a subclass of tuple will not + /// do. + /// + /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. + /// But for a subclass of `tuple[int, str]`, it will return `None`. + fn exact_tuple_instance_spec(&self, db: &'db dyn Db) -> Option>> { + self.into_nominal_instance() + .and_then(|instance| instance.class.own_tuple_spec(db)) + } + /// Returns the materialization of this type depending on the given `variance`. /// /// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of @@ -774,7 +793,6 @@ impl<'db> Type<'db> { .map(|ty| ty.materialize(db, variance.flip())), ) .build(), - Type::Tuple(tuple_type) => Type::tuple(tuple_type.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)) @@ -1023,9 +1041,6 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => visitor.visit(self, |v| { Type::Intersection(intersection.normalized_impl(db, v)) }), - Type::Tuple(tuple) => { - visitor.visit(self, |v| Type::tuple(tuple.normalized_impl(db, v))) - } Type::Callable(callable) => { visitor.visit(self, |v| Type::Callable(callable.normalized_impl(db, v))) } @@ -1129,7 +1144,6 @@ impl<'db> Type<'db> { | Type::Union(_) | Type::Intersection(_) | Type::Callable(_) - | Type::Tuple(_) | Type::TypeVar(_) | Type::BoundSuper(_) | Type::TypeIs(_) => false, @@ -1194,7 +1208,6 @@ impl<'db> Type<'db> { | Type::StringLiteral(_) | Type::LiteralString | Type::BytesLiteral(_) - | Type::Tuple(_) | Type::TypeIs(_) => None, // TODO @@ -1528,24 +1541,6 @@ impl<'db> Type<'db> { (Type::Callable(_), _) => false, - (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { - self_tuple.has_relation_to(db, target_tuple, relation) - } - - (Type::Tuple(self_tuple), Type::NominalInstance(target_instance)) => { - self_tuple.to_class_type(db).is_some_and(|self_class| { - self_class.has_relation_to(db, target_instance.class, relation) - }) - } - (Type::NominalInstance(self_instance), Type::Tuple(target_tuple)) => { - target_tuple.to_class_type(db).is_some_and(|target_class| { - self_instance - .class - .has_relation_to(db, target_class, relation) - }) - } - (Type::Tuple(_), _) => false, - (Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target), (Type::BoundSuper(_), _) => KnownClass::Super .to_instance(db) @@ -1675,8 +1670,6 @@ impl<'db> Type<'db> { first.is_equivalent_to(db, second) } - (Type::Tuple(first), Type::Tuple(second)) => first.is_equivalent_to(db, second), - (Type::Union(first), Type::Union(second)) => first.is_equivalent_to(db, second), (Type::Intersection(first), Type::Intersection(second)) => { @@ -1852,45 +1845,6 @@ impl<'db> Type<'db> { | Type::KnownInstance(..)), ) => left != right, - // One tuple type can be a subtype of another tuple type, - // but we know for sure that any given tuple type is disjoint from all single-valued types - ( - Type::Tuple(..), - Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::ModuleLiteral(..) - | Type::BooleanLiteral(..) - | Type::BytesLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::DataclassDecorator(..) - | Type::DataclassTransformer(..) - | Type::IntLiteral(..) - | Type::EnumLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString, - ) - | ( - Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::ModuleLiteral(..) - | Type::BooleanLiteral(..) - | Type::BytesLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::DataclassDecorator(..) - | Type::DataclassTransformer(..) - | Type::IntLiteral(..) - | Type::EnumLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString, - Type::Tuple(..), - ) => true, - ( Type::SubclassOf(_), Type::BooleanLiteral(..) @@ -2061,16 +2015,6 @@ impl<'db> Type<'db> { !known_instance.is_instance_of(db, instance.class) } - (Type::SpecialForm(special_form), Type::Tuple(tuple)) - | (Type::Tuple(tuple), Type::SpecialForm(special_form)) => tuple - .to_class_type(db) - .is_some_and(|tuple_class| !special_form.is_instance_of(db, tuple_class)), - - (Type::KnownInstance(known_instance), Type::Tuple(tuple)) - | (Type::Tuple(tuple), Type::KnownInstance(known_instance)) => tuple - .to_class_type(db) - .is_some_and(|tuple_class| !known_instance.is_instance_of(db, tuple_class)), - (Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => { // A `Type::BooleanLiteral()` must be an instance of exactly `bool` @@ -2219,17 +2163,6 @@ impl<'db> Type<'db> { left.is_disjoint_from_impl(db, right) } - (Type::Tuple(tuple), Type::Tuple(other_tuple)) => { - tuple.is_disjoint_from_impl(db, other_tuple, visitor) - } - - (Type::Tuple(tuple), Type::NominalInstance(instance)) - | (Type::NominalInstance(instance), Type::Tuple(tuple)) => { - tuple.to_class_type(db).is_some_and(|tuple_class| { - !instance.class.could_coexist_in_mro_with(db, tuple_class) - }) - } - (Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => { KnownClass::Property .to_instance(db) @@ -2340,14 +2273,6 @@ impl<'db> Type<'db> { Type::DataclassDecorator(_) | Type::DataclassTransformer(_) => false, Type::NominalInstance(instance) => instance.is_singleton(db), Type::PropertyInstance(_) => false, - Type::Tuple(..) => { - // The empty tuple is a singleton on CPython and PyPy, but not on other Python - // implementations such as GraalPy. Its *use* as a singleton is discouraged and - // should not be relied on for type narrowing, so we do not treat it as one. - // See: - // https://docs.python.org/3/reference/expressions.html#parenthesized-forms - false - } Type::Union(..) => { // A single-element union, where the sole element was a singleton, would itself // be a singleton type. However, unions with length < 2 should never appear in @@ -2434,7 +2359,6 @@ impl<'db> Type<'db> { false } - Type::Tuple(tuple) => tuple.is_single_valued(db), Type::NominalInstance(instance) => instance.is_single_valued(db), Type::BoundSuper(_) => { @@ -2569,7 +2493,6 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(_) | Type::EnumLiteral(_) - | Type::Tuple(_) | Type::TypeVar(_) | Type::NominalInstance(_) | Type::ProtocolInstance(_) @@ -2701,10 +2624,6 @@ impl<'db> Type<'db> { Type::EnumLiteral(enum_literal) => enum_literal .enum_class_instance(db) .instance_member(db, name), - Type::Tuple(tuple) => tuple - .to_class_type(db) - .map(|class| class.instance_member(db, name)) - .unwrap_or(Place::Unbound.into()), Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name), Type::ModuleLiteral(_) => KnownClass::ModuleType @@ -3212,7 +3131,6 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::EnumLiteral(..) | Type::LiteralString - | Type::Tuple(..) | Type::TypeVar(..) | Type::SpecialForm(..) | Type::KnownInstance(..) @@ -3569,7 +3487,6 @@ impl<'db> Type<'db> { Type::BooleanLiteral(bool) => Truthiness::from(*bool), Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()), - Type::Tuple(tuple) => tuple.truthiness(db), }; Ok(truthiness) @@ -3596,13 +3513,6 @@ impl<'db> Type<'db> { let usize_len = match self { Type::BytesLiteral(bytes) => Some(bytes.python_len(db)), Type::StringLiteral(string) => Some(string.python_len(db)), - - // N.B. This is strictly-speaking redundant, since the `__len__` method on tuples - // is special-cased in `ClassType::own_class_member`. However, it's probably more - // efficient to short-circuit here and check against the tuple spec directly, - // rather than going through the `__len__` method. - Type::Tuple(tuple) => tuple.tuple(db).len().into_fixed_length(), - _ => None, }; @@ -4541,7 +4451,6 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::BooleanLiteral(_) | Type::LiteralString - | Type::Tuple(_) | Type::BoundSuper(_) | Type::ModuleLiteral(_) | Type::TypeIs(_) => CallableBinding::not_callable(self).into(), @@ -4698,7 +4607,11 @@ impl<'db> Type<'db> { } match self { - Type::Tuple(tuple_type) => return Ok(Cow::Borrowed(tuple_type.tuple(db))), + Type::NominalInstance(NominalInstanceType { class, .. }) => { + if let Some(spec) = class.tuple_spec(db) { + return Ok(spec); + } + } Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => { return Ok(Cow::Owned(TupleSpec::homogeneous(todo_type!( "*tuple[] annotations" @@ -5153,7 +5066,6 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) - | Type::Tuple(_) | Type::LiteralString | Type::BoundSuper(_) | Type::AlwaysTruthy @@ -5213,7 +5125,6 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::ModuleLiteral(_) | Type::StringLiteral(_) - | Type::Tuple(_) | Type::TypeVar(_) | Type::Callable(_) | Type::BoundMethod(_) @@ -5432,40 +5343,6 @@ impl<'db> Type<'db> { KnownClass::NoneType.to_instance(db) } - /// Return the type of `tuple(sys.version_info)`. - /// - /// This is not exactly the type that `sys.version_info` has at runtime, - /// but it's a useful fallback for us in order to infer `Literal` types from `sys.version_info` comparisons. - fn version_info_tuple(db: &'db dyn Db) -> Self { - let python_version = Program::get(db).python_version(db); - let int_instance_ty = KnownClass::Int.to_instance(db); - - // TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there) - let release_level_ty = { - let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"] - .iter() - .map(|level| Type::string_literal(db, level)) - .collect(); - - // For most unions, it's better to go via `UnionType::from_elements` or use `UnionBuilder`; - // those techniques ensure that union elements are deduplicated and unions are eagerly simplified - // into other types where necessary. Here, however, we know that there are no duplicates - // in this union, so it's probably more efficient to use `UnionType::new()` directly. - Type::Union(UnionType::new(db, elements)) - }; - - TupleType::from_elements( - db, - [ - Type::IntLiteral(python_version.major.into()), - Type::IntLiteral(python_version.minor.into()), - int_instance_ty, - release_level_ty, - int_instance_ty, - ], - ) - } - /// Given a type that is assumed to represent an instance of a class, /// return a type that represents that class itself. #[must_use] @@ -5491,10 +5368,6 @@ impl<'db> Type<'db> { } Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), - Type::Tuple(tuple) => tuple - .to_class_type(db) - .map(Type::from) - .unwrap_or_else(Type::unknown), Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { None => KnownClass::Type.to_instance(db), @@ -5648,7 +5521,6 @@ impl<'db> Type<'db> { } builder.build() } - Type::Tuple(tuple) => Type::tuple(tuple.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)), @@ -5741,10 +5613,6 @@ impl<'db> Type<'db> { } } - Type::Tuple(tuple) => { - tuple.find_legacy_typevars(db, typevars); - } - Type::GenericAlias(alias) => { alias.find_legacy_typevars(db, typevars); } @@ -5888,8 +5756,7 @@ impl<'db> Type<'db> { | Self::DataclassDecorator(_) | Self::DataclassTransformer(_) | Self::PropertyInstance(_) - | Self::BoundSuper(_) - | Self::Tuple(_) => self.to_meta_type(db).definition(db), + | Self::BoundSuper(_) => self.to_meta_type(db).definition(db), Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db)?)), diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 5259872902..d41e8a30b2 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -6,7 +6,7 @@ use ruff_python_ast as ast; use crate::Db; use crate::types::KnownClass; use crate::types::enums::enum_member_literals; -use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; +use crate::types::tuple::{Tuple, TupleLength, TupleType}; use super::Type; @@ -221,6 +221,34 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { ]); } + // If the class is a fixed-length tuple subtype, we expand it to its elements. + if let Some(spec) = instance.class.tuple_spec(db) { + return match &*spec { + Tuple::Fixed(fixed_length_tuple) => { + let expanded = fixed_length_tuple + .all_elements() + .map(|element| { + if let Some(expanded) = expand_type(db, *element) { + Either::Left(expanded.into_iter()) + } else { + Either::Right(std::iter::once(*element)) + } + }) + .multi_cartesian_product() + .map(|types| TupleType::from_elements(db, types)) + .collect::>(); + + if expanded.len() == 1 { + // There are no elements in the tuple type that can be expanded. + None + } else { + Some(expanded) + } + } + Tuple::Variable(_) => None, + }; + } + let class_literal = instance.class.class_literal(db).0; if let Some(enum_members) = enum_member_literals(db, class_literal, None) { @@ -229,32 +257,6 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { None } - Type::Tuple(tuple_type) => { - // Note: This should only account for tuples of known length, i.e., `tuple[bool, ...]` - // should not be expanded here. - let tuple = tuple_type.tuple(db); - if !matches!(tuple, TupleSpec::Fixed(_)) { - return None; - } - let expanded = tuple - .all_elements() - .map(|element| { - if let Some(expanded) = expand_type(db, *element) { - Either::Left(expanded.into_iter()) - } else { - Either::Right(std::iter::once(*element)) - } - }) - .multi_cartesian_product() - .map(|types| TupleType::from_elements(db, types)) - .collect::>(); - if expanded.len() == 1 { - // There are no elements in the tuple type that can be expanded. - None - } else { - Some(expanded) - } - } Type::Union(union) => Some(union.iter(db).copied().collect()), // We don't handle `type[A | B]` here because it's already stored in the expanded form // i.e., `type[A] | type[B]` which is handled by the `Type::Union` case. diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 32dc8ad258..045f0adc07 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1034,7 +1034,7 @@ impl<'db> Bindings<'db> { ); continue; }; - overload.set_return_type(Type::tuple(TupleType::new( + overload.set_return_type(Type::tuple(db, TupleType::new( db, tuple_spec.as_ref(), ))); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index e32a174603..ce096e0a8c 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::hash::BuildHasherDefault; use std::sync::{LazyLock, Mutex}; @@ -22,9 +23,9 @@ use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signatu use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::{ BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, - DeprecatedInstance, DynamicType, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, - TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type, - infer_definition_types, + DeprecatedInstance, DynamicType, KnownInstanceType, NominalInstanceType, TypeAliasType, + TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, + TypeVarKind, declaration_type, infer_definition_types, }; use crate::{ Db, FxIndexMap, FxOrderSet, KnownModule, Program, @@ -1013,6 +1014,59 @@ impl<'db> ClassType<'db> { } } } + + /// If this class is `tuple`, a specialization of `tuple` (`tuple[int, str]`, etc.), + /// *or a subclass of `tuple`, or a subclass of a specialization of `tuple`*, + /// return its tuple specification. + pub(super) fn tuple_spec(self, db: &'db dyn Db) -> Option>> { + self.iter_mro(db) + .filter_map(ClassBase::into_class) + .find_map(|class| class.own_tuple_spec(db)) + } + + /// If this class is `tuple` or a specialization of `tuple` (`tuple[int, str]`, etc.), + /// return its tuple specification. + /// + /// You usually don't want to use this method directly, because you usually want to consider + /// any subclass of a certain tuple type in the same way as that tuple type itself. + /// Use [`ClassType::tuple_spec`] instead. + pub(super) fn own_tuple_spec(self, db: &'db dyn Db) -> Option>> { + let (class_literal, specialization) = self.class_literal(db); + match class_literal.known(db)? { + KnownClass::Tuple => Some( + specialization + .map(|spec| Cow::Borrowed(spec.tuple(db))) + .unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown()))), + ), + KnownClass::VersionInfo => { + let python_version = Program::get(db).python_version(db); + let int_instance_ty = KnownClass::Int.to_instance(db); + + // TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there) + let release_level_ty = { + let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"] + .iter() + .map(|level| Type::string_literal(db, level)) + .collect(); + + // For most unions, it's better to go via `UnionType::from_elements` or use `UnionBuilder`; + // those techniques ensure that union elements are deduplicated and unions are eagerly simplified + // into other types where necessary. Here, however, we know that there are no duplicates + // in this union, so it's probably more efficient to use `UnionType::new()` directly. + Type::Union(UnionType::new(db, elements)) + }; + + Some(Cow::Owned(TupleSpec::from_elements([ + Type::IntLiteral(python_version.major.into()), + Type::IntLiteral(python_version.minor.into()), + int_instance_ty, + release_level_ty, + int_instance_ty, + ]))) + } + _ => None, + } + } } impl<'db> From> for ClassType<'db> { @@ -4313,16 +4367,14 @@ impl SlotsKind { match slots_ty { // __slots__ = ("a", "b") - Type::Tuple(tuple) => { - let tuple = tuple.tuple(db); - if tuple.is_variadic() { - Self::Dynamic - } else if tuple.is_empty() { - Self::Empty - } else { - Self::NotEmpty - } - } + Type::NominalInstance(NominalInstanceType { class, .. }) => match class + .tuple_spec(db) + .and_then(|spec| spec.len().into_fixed_length()) + { + Some(0) => Self::Empty, + Some(_) => Self::NotEmpty, + None => Self::Dynamic, + }, // __slots__ = "abc" # Same as `("abc",)` Type::StringLiteral(_) => Self::NotEmpty, diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 8a359e3a15..fa4efd81ad 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -151,7 +151,6 @@ impl<'db> ClassBase<'db> { | Type::EnumLiteral(_) | Type::StringLiteral(_) | Type::LiteralString - | Type::Tuple(_) | Type::ModuleLiteral(_) | Type::TypeVar(_) | Type::BoundSuper(_) diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 1495cfc0bd..6000dc3aaa 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -76,6 +76,11 @@ impl Display for DisplayRepresentation<'_> { match (instance.class, instance.class.known(self.db)) { (_, Some(KnownClass::NoneType)) => f.write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), + (ClassType::Generic(alias), Some(KnownClass::Tuple)) => alias + .specialization(self.db) + .tuple(self.db) + .display(self.db) + .fmt(f), (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)), (ClassType::Generic(alias), _) => alias.display(self.db).fmt(f), } @@ -201,7 +206,6 @@ impl Display for DisplayRepresentation<'_> { name = enum_literal.name(self.db), ) } - Type::Tuple(specialization) => specialization.tuple(self.db).display(self.db).fmt(f), Type::TypeVar(typevar) => f.write_str(typevar.name(self.db)), Type::AlwaysTruthy => f.write_str("AlwaysTruthy"), Type::AlwaysFalsy => f.write_str("AlwaysFalsy"), diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 2b284830c9..1048ae6d0f 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -944,8 +944,6 @@ fn is_instance_truthiness<'db>( .is_some_and(is_instance), ), - Type::Tuple(..) => always_true_if(class.is_known(db, KnownClass::Tuple)), - Type::FunctionLiteral(..) => { always_true_if(is_instance(&KnownClass::FunctionType.to_instance(db))) } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index f765bfdc66..a09cf6a039 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -758,22 +758,25 @@ impl<'db> SpecializationBuilder<'db> { } } - (Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => { - let formal_tuple = formal_tuple.tuple(self.db); - let actual_tuple = actual_tuple.tuple(self.db); - let Some(most_precise_length) = formal_tuple.len().most_precise(actual_tuple.len()) else { - return Ok(()); - }; - let Ok(formal_tuple) = formal_tuple.resize(self.db, most_precise_length) else { - return Ok(()); - }; - let Ok(actual_tuple) = actual_tuple.resize(self.db, most_precise_length) else { - return Ok(()); - }; - for (formal_element, actual_element) in - formal_tuple.all_elements().zip(actual_tuple.all_elements()) - { - self.infer(*formal_element, *actual_element)?; + ( + Type::NominalInstance(NominalInstanceType { class: class1, .. }), + Type::NominalInstance(NominalInstanceType { class: class2, .. }) + ) if class1.tuple_spec(self.db).is_some() => { + if let (Some(formal_tuple), Some(actual_tuple)) = (class1.tuple_spec(self.db), class2.tuple_spec(self.db)) { + let Some(most_precise_length) = formal_tuple.len().most_precise(actual_tuple.len()) else { + return Ok(()); + }; + let Ok(formal_tuple) = formal_tuple.resize(self.db, most_precise_length) else { + return Ok(()); + }; + let Ok(actual_tuple) = actual_tuple.resize(self.db, most_precise_length) else { + return Ok(()); + }; + for (formal_element, actual_element) in + formal_tuple.all_elements().zip(actual_tuple.all_elements()) + { + self.infer(*formal_element, *actual_element)?; + } } } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index efbe151702..836c9d0bce 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -125,7 +125,6 @@ impl<'db> AllMembers<'db> { | Type::BytesLiteral(_) | Type::EnumLiteral(_) | Type::LiteralString - | Type::Tuple(_) | Type::PropertyInstance(_) | Type::FunctionLiteral(_) | Type::BoundMethod(_) diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 05d0d817ce..0225a5987b 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -3240,9 +3240,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If it's an `except*` handler, this won't actually be the type of the bound symbol; // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. - let symbol_ty = if let Type::Tuple(tuple) = node_ty { + let symbol_ty = if let Some(tuple_spec) = node_ty.tuple_instance_spec(self.db()) { let mut builder = UnionBuilder::new(self.db()); - for element in tuple.tuple(self.db()).all_elements().copied() { + for element in tuple_spec.all_elements().copied() { builder = builder.add( if element.is_assignable_to(self.db(), type_base_exception) { element.to_instance(self.db()).expect( @@ -3748,7 +3748,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::BytesLiteral(..) | Type::EnumLiteral(..) | Type::LiteralString - | Type::Tuple(..) | Type::SpecialForm(..) | Type::KnownInstance(..) | Type::PropertyInstance(..) @@ -4158,15 +4157,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::Expr::Name(name) => self.infer_definition(name), ast::Expr::List(ast::ExprList { elts, .. }) | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { - let mut assigned_tys = match assigned_ty { - Some(Type::Tuple(tuple)) => { - Either::Left(tuple.tuple(self.db()).all_elements().copied()) + if let Some(tuple_spec) = + assigned_ty.and_then(|ty| ty.tuple_instance_spec(self.db())) + { + let mut assigned_tys = tuple_spec.all_elements(); + for element in elts { + self.infer_target_impl(element, assigned_tys.next().copied()); + } + } else { + for element in elts { + self.infer_target_impl(element, None); } - Some(_) | None => Either::Right(std::iter::empty()), - }; - - for element in elts { - self.infer_target_impl(element, assigned_tys.next()); } } ast::Expr::Attribute( @@ -6942,7 +6943,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::LiteralString | Type::BytesLiteral(_) | Type::EnumLiteral(_) - | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) | Type::TypeIs(_), @@ -7267,7 +7267,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::LiteralString | Type::BytesLiteral(_) | Type::EnumLiteral(_) - | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) | Type::TypeIs(_), @@ -7296,7 +7295,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::LiteralString | Type::BytesLiteral(_) | Type::EnumLiteral(_) - | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) | Type::TypeIs(_), @@ -7904,96 +7902,77 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Ok(Type::BooleanLiteral(literal_1 != literal_2)) } - (Type::Tuple(_), Type::NominalInstance(instance)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => - { - self.infer_binary_type_comparison( - left, - op, - Type::version_info_tuple(self.db()), - range, - ) - } - (Type::NominalInstance(instance), Type::Tuple(_)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => - { - self.infer_binary_type_comparison( - Type::version_info_tuple(self.db()), - op, - right, - range, - ) - } - (Type::Tuple(lhs), Type::Tuple(rhs)) => { - let lhs_tuple = lhs.tuple(self.db()); - let rhs_tuple = rhs.tuple(self.db()); + _ => { + // Special-casing for comparisons between tuple instance types + if let (Some(lhs_tuple), Some(rhs_tuple)) = ( + left.tuple_instance_spec(self.db()), + right.tuple_instance_spec(self.db()), + ) { + let mut tuple_rich_comparison = + |op| self.infer_tuple_rich_comparison(&lhs_tuple, op, &rhs_tuple, range); - let mut tuple_rich_comparison = - |op| self.infer_tuple_rich_comparison(lhs_tuple, op, rhs_tuple, range); + return match op { + ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), + ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne), + ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt), + ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le), + ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), + ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), + ast::CmpOp::In | ast::CmpOp::NotIn => { + let mut any_eq = false; + let mut any_ambiguous = false; - match op { - ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), - ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne), - ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt), - ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le), - ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), - ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), - ast::CmpOp::In | ast::CmpOp::NotIn => { - let mut any_eq = false; - let mut any_ambiguous = false; - - for ty in rhs_tuple.all_elements().copied() { - let eq_result = self.infer_binary_type_comparison( - Type::Tuple(lhs), + for ty in rhs_tuple.all_elements().copied() { + let eq_result = self.infer_binary_type_comparison( + left, ast::CmpOp::Eq, ty, range, ).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); - match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for different union variants. Instead, this is just for us to - // evaluate a possibly truthy value to `false` or `true`. - ty => match ty.bool(self.db()) { - Truthiness::AlwaysTrue => any_eq = true, - Truthiness::AlwaysFalse => (), - Truthiness::Ambiguous => any_ambiguous = true, - }, + match eq_result { + todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for different union variants. Instead, this is just for us to + // evaluate a possibly truthy value to `false` or `true`. + ty => match ty.bool(self.db()) { + Truthiness::AlwaysTrue => any_eq = true, + Truthiness::AlwaysFalse => (), + Truthiness::Ambiguous => any_ambiguous = true, + }, + } + } + + if any_eq { + Ok(Type::BooleanLiteral(op.is_in())) + } else if !any_ambiguous { + Ok(Type::BooleanLiteral(op.is_not_in())) + } else { + Ok(KnownClass::Bool.to_instance(self.db())) } } - - if any_eq { - Ok(Type::BooleanLiteral(op.is_in())) - } else if !any_ambiguous { - Ok(Type::BooleanLiteral(op.is_not_in())) - } else { - Ok(KnownClass::Bool.to_instance(self.db())) - } - } - ast::CmpOp::Is | ast::CmpOp::IsNot => { - // - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool` - // - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool` - let eq_result = tuple_rich_comparison(RichCompareOperator::Eq).expect( + ast::CmpOp::Is | ast::CmpOp::IsNot => { + // - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool` + // - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool` + let eq_result = tuple_rich_comparison(RichCompareOperator::Eq).expect( "infer_binary_type_comparison should never return None for `CmpOp::Eq`", ); - Ok(match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => todo, - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for `is` and `is not` comparisons. This is an implementation detail - // for how we determine the truthiness of a type. - ty => match ty.bool(self.db()) { - Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()), - _ => KnownClass::Bool.to_instance(self.db()), - }, - }) - } + Ok(match eq_result { + todo @ Type::Dynamic(DynamicType::Todo(_)) => todo, + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for `is` and `is not` comparisons. This is an implementation detail + // for how we determine the truthiness of a type. + ty => match ty.bool(self.db()) { + Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()), + _ => KnownClass::Bool.to_instance(self.db()), + }, + }) + } + }; } - } - // Lookup the rich comparison `__dunder__` methods - _ => { + // Final generalized fallback: lookup the rich comparison `__dunder__` methods let rich_comparison = |op| self.infer_rich_comparison(left, right, op); let membership_test_comparison = |op, range: TextRange| { self.infer_membership_test_comparison(left, right, op, range) @@ -8369,17 +8348,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let context = &self.context; let inferred = match (value_ty, slice_ty) { - (Type::NominalInstance(instance), _) - if instance.class.is_known(db, KnownClass::VersionInfo) => - { - Some(self.infer_subscript_expression_types( - value_node, - Type::version_info_tuple(db), - slice_ty, - expr_context, - )) - } - (Type::Union(union), _) => Some(union.map(db, |element| { self.infer_subscript_expression_types(value_node, *element, slice_ty, expr_context) })), @@ -8393,9 +8361,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` - (Type::Tuple(tuple_ty), Type::IntLiteral(i64_int)) => { - i32::try_from(i64_int).ok().map(|i32_int| { - let tuple = tuple_ty.tuple(db); + ( + Type::NominalInstance(NominalInstanceType { class, .. }), + Type::IntLiteral(i64_int), + ) => class + .tuple_spec(db) + .and_then(|tuple| Some((tuple, i32::try_from(i64_int).ok()?))) + .map(|(tuple, i32_int)| { tuple.py_index(db, i32_int).unwrap_or_else(|_| { report_index_out_of_bounds( context, @@ -8407,26 +8379,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); Type::unknown() }) - }) - } + }), // Ex) Given `("a", 1, Null)[0:2]`, return `("a", 1)` - (Type::Tuple(tuple_ty), Type::NominalInstance(NominalInstanceType { class, .. })) => { - class - .slice_literal(db) - .map(|SliceLiteral { start, stop, step }| { - let TupleSpec::Fixed(tuple) = tuple_ty.tuple(db) else { - return todo_type!("slice into variable-length tuple"); - }; - + ( + Type::NominalInstance(NominalInstanceType { + class: maybe_tuple, .. + }), + Type::NominalInstance(NominalInstanceType { + class: maybe_slice, .. + }), + ) => maybe_tuple + .tuple_spec(db) + .as_deref() + .and_then(|tuple_spec| Some((tuple_spec, maybe_slice.slice_literal(db)?))) + .map(|(tuple, SliceLiteral { start, stop, step })| match tuple { + TupleSpec::Fixed(tuple) => { if let Ok(new_elements) = tuple.py_slice(db, start, stop, step) { TupleType::from_elements(db, new_elements.copied()) } else { report_slice_step_size_zero(context, value_node.into()); Type::unknown() } - }) - } + } + TupleSpec::Variable(_) => { + todo_type!("slice into variable-length tuple") + } + }), // Ex) Given `"value"[1]`, return `"a"` (Type::StringLiteral(literal_ty), Type::IntLiteral(i64_int)) => { @@ -8508,18 +8487,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }), // Ex) Given `"value"[True]`, return `"a"` + (Type::StringLiteral(_) | Type::BytesLiteral(_), Type::BooleanLiteral(bool)) => { + Some(self.infer_subscript_expression_types( + value_node, + value_ty, + Type::IntLiteral(i64::from(bool)), + expr_context, + )) + } + ( - Type::Tuple(_) | Type::StringLiteral(_) | Type::BytesLiteral(_), + Type::NominalInstance(NominalInstanceType { class, .. }), Type::BooleanLiteral(bool), - ) => Some(self.infer_subscript_expression_types( + ) if class.tuple_spec(db).is_some() => Some(self.infer_subscript_expression_types( value_node, value_ty, Type::IntLiteral(i64::from(bool)), expr_context, )), - (Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars)) => { - Some(match typevars.tuple(db) { + ( + Type::SpecialForm(SpecialFormType::Protocol), + Type::NominalInstance(NominalInstanceType { class, .. }), + ) => { + class.tuple_spec(db).map(|typevars| match &*typevars { TupleSpec::Fixed(typevars) => self .legacy_generic_class_context( value_node, @@ -8550,8 +8541,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(todo_type!("doubly-specialized typing.Protocol")) } - (Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars)) => { - Some(match typevars.tuple(db) { + ( + Type::SpecialForm(SpecialFormType::Generic), + Type::NominalInstance(NominalInstanceType { class, .. }), + ) => { + class.tuple_spec(db).map(|typevars| match &*typevars { TupleSpec::Fixed(typevars) => self .legacy_generic_class_context( value_node, @@ -9654,7 +9648,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } = starred; let starred_type = self.infer_type_expression(value); - if let Type::Tuple(_) = starred_type { + if starred_type.exact_tuple_instance_spec(self.db()).is_some() { starred_type } else { todo_type!("PEP 646") @@ -9712,7 +9706,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } match element { - ast::Expr::Starred(_) => !matches!(element_ty, Type::Tuple(_)), + ast::Expr::Starred(_) => !element_ty.exact_tuple_instance_spec(builder.db()).is_some(), ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => { let value_ty = if builder.deferred_state.in_string_annotation() { // Using `.expression_type` does not work in string annotations, because @@ -9749,10 +9743,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let element_ty = self.infer_type_expression(element); return_todo |= element_could_alter_type_of_whole_tuple(element, element_ty, self); + if let ast::Expr::Starred(_) = element { - if let Type::Tuple(inner_tuple) = element_ty { - element_types = - element_types.concat(self.db(), inner_tuple.tuple(self.db())); + if let Some(inner_tuple) = element_ty.exact_tuple_instance_spec(self.db()) { + element_types = element_types.concat(self.db(), &inner_tuple); } else { // TODO: emit a diagnostic } @@ -9764,7 +9758,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let ty = if return_todo { todo_type!("PEP 646") } else { - Type::tuple(TupleType::new(self.db(), element_types)) + Type::tuple(self.db(), TupleType::new(self.db(), element_types)) }; // Here, we store the type for the inner `int, str` tuple-expression, diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 3959b041e1..2c8872ceb1 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -7,9 +7,12 @@ use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance}; use crate::place::PlaceAndQualifiers; use crate::types::cyclic::PairVisitor; use crate::types::enums::is_single_member_enum; +use crate::types::generics::{GenericContext, Specialization}; use crate::types::protocol_class::walk_protocol_interface; use crate::types::tuple::TupleType; -use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance}; +use crate::types::{ + DynamicType, GenericAlias, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, +}; use crate::{Db, FxOrderSet}; pub(super) use synthesized_protocol::SynthesizedProtocolType; @@ -18,12 +21,6 @@ impl<'db> Type<'db> { pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { match (class, class.known(db)) { (_, Some(KnownClass::Any)) => Self::Dynamic(DynamicType::Any), - (ClassType::NonGeneric(_), Some(KnownClass::Tuple)) => { - TupleType::homogeneous(db, Type::unknown()) - } - (ClassType::Generic(alias), Some(KnownClass::Tuple)) => { - Self::tuple(TupleType::new(db, alias.specialization(db).tuple(db))) - } _ if class.class_literal(db).0.is_protocol(db) => { Self::ProtocolInstance(ProtocolInstanceType::from_class(class)) } @@ -31,6 +28,26 @@ impl<'db> Type<'db> { } } + pub(crate) fn tuple(db: &'db dyn Db, tuple: Option>) -> Self { + let Some(tuple) = tuple else { + return Type::Never; + }; + let Some(tuple_class) = KnownClass::Tuple.try_to_class_literal(db) else { + return Type::unknown(); + }; + let specialization = Specialization::new( + db, + GenericContext::new(db, FxOrderSet::default()), + Box::default(), + Some(tuple), + ); + let alias = GenericAlias::new(db, tuple_class, specialization); + Type::NominalInstance(NominalInstanceType { + class: ClassType::Generic(alias), + _phantom: PhantomData, + }) + } + pub(crate) const fn into_nominal_instance(self) -> Option> { match self { Type::NominalInstance(instance_type) => Some(instance_type), diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index be02679fe7..d02940459d 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -11,8 +11,9 @@ use crate::types::enums::{enum_member_literals, enum_metadata}; use crate::types::function::KnownFunction; use crate::types::infer::infer_same_file_expression_type; use crate::types::{ - ClassLiteral, ClassType, IntersectionBuilder, KnownClass, SubclassOfInner, SubclassOfType, - Truthiness, Type, TypeVarBoundOrConstraints, UnionBuilder, infer_expression_types, + ClassLiteral, ClassType, IntersectionBuilder, KnownClass, NominalInstanceType, SubclassOfInner, + SubclassOfType, Truthiness, Type, TypeVarBoundOrConstraints, UnionBuilder, + infer_expression_types, }; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -182,14 +183,6 @@ impl ClassInfoConstraintFunction { }; match classinfo { - Type::Tuple(tuple) => UnionType::try_from_elements( - db, - tuple - .tuple(db) - .all_elements() - .copied() - .map(|element| self.generate_constraint(db, element)), - ), Type::ClassLiteral(class_literal) => { // At runtime (on Python 3.11+), this will return `True` for classes that actually // do inherit `typing.Any` and `False` otherwise. We could accurately model that? @@ -233,6 +226,18 @@ impl ClassInfoConstraintFunction { // e.g. `isinstance(x, list[int])` fails at runtime. Type::GenericAlias(_) => None, + Type::NominalInstance(NominalInstanceType { class, .. }) => { + class.tuple_spec(db).and_then(|tuple| { + UnionType::try_from_elements( + db, + tuple + .all_elements() + .copied() + .map(|element| self.generate_constraint(db, element)), + ) + }) + } + Type::AlwaysFalsy | Type::AlwaysTruthy | Type::BooleanLiteral(_) @@ -249,7 +254,6 @@ impl ClassInfoConstraintFunction { | Type::ProtocolInstance(_) | Type::PropertyInstance(_) | Type::SpecialForm(_) - | Type::NominalInstance(_) | Type::LiteralString | Type::StringLiteral(_) | Type::IntLiteral(_) @@ -616,20 +620,21 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) { - match rhs_ty { - Type::Tuple(rhs_tuple) => Some(UnionType::from_elements( - self.db, - rhs_tuple.tuple(self.db).all_elements(), - )), - - Type::StringLiteral(string_literal) => Some(UnionType::from_elements( + if let Type::StringLiteral(string_literal) = rhs_ty { + Some(UnionType::from_elements( self.db, string_literal .iter_each_char(self.db) .map(Type::StringLiteral), - )), - - _ => None, + )) + } else if let Some(tuple_spec) = rhs_ty.tuple_instance_spec(self.db) { + // N.B. Strictly speaking this is unsound, since a tuple subclass might override `__contains__` + // but we'd still apply the narrowing here. This seems unlikely, however, and narrowing is + // generally unsound in numerous ways anyway (attribute narrowing, subscript, narrowing, + // narrowing of globals, etc.). So this doesn't seem worth worrying about too much. + Some(UnionType::from_elements(self.db, tuple_spec.all_elements())) + } else { + None } } else { None diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index cd2f1eb026..243a83b11c 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -144,15 +144,6 @@ pub(super) fn walk_tuple_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized> // The Salsa heap is tracked separately. impl get_size2::GetSize for TupleType<'_> {} -impl<'db> Type<'db> { - pub(crate) fn tuple(tuple: Option>) -> Self { - let Some(tuple) = tuple else { - return Type::Never; - }; - Self::Tuple(tuple) - } -} - impl<'db> TupleType<'db> { pub(crate) fn new(db: &'db dyn Db, tuple_key: T) -> Option where @@ -181,17 +172,17 @@ impl<'db> TupleType<'db> { } pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> { - Type::tuple(TupleType::new( + Type::tuple( db, - TupleSpec::from(FixedLengthTuple::empty()), - )) + TupleType::new(db, TupleSpec::from(FixedLengthTuple::empty())), + ) } pub(crate) fn from_elements( db: &'db dyn Db, types: impl IntoIterator>, ) -> Type<'db> { - Type::tuple(TupleType::new(db, TupleSpec::from_elements(types))) + Type::tuple(db, TupleType::new(db, TupleSpec::from_elements(types))) } #[cfg(test)] @@ -201,14 +192,14 @@ impl<'db> TupleType<'db> { variable: Type<'db>, suffix: impl IntoIterator>, ) -> Type<'db> { - Type::tuple(TupleType::new( + Type::tuple( db, - VariableLengthTuple::mixed(prefix, variable, suffix), - )) + TupleType::new(db, VariableLengthTuple::mixed(prefix, variable, suffix)), + ) } pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Type<'db> { - Type::tuple(TupleType::new(db, TupleSpec::homogeneous(element))) + Type::tuple(db, TupleType::new(db, TupleSpec::homogeneous(element))) } pub(crate) fn to_class_type(self, db: &'db dyn Db) -> Option> { @@ -970,10 +961,9 @@ impl Tuple { VariableLengthTuple::homogeneous(element) } - pub(crate) fn from_elements(elements: impl IntoIterator) -> Self { + pub(crate) fn from_elements(elements: impl IntoIterator) -> Tuple { FixedLengthTuple::from_elements(elements).into() } - pub(crate) fn with_capacity(capacity: usize) -> Self { Tuple::Fixed(FixedLengthTuple::with_capacity(capacity)) } diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 58b62af789..76d7b5b34c 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -100,10 +100,6 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::Callable(_), _) => Ordering::Less, (_, Type::Callable(_)) => Ordering::Greater, - (Type::Tuple(left), Type::Tuple(right)) => left.cmp(right), - (Type::Tuple(_), _) => Ordering::Less, - (_, Type::Tuple(_)) => Ordering::Greater, - (Type::ModuleLiteral(left), Type::ModuleLiteral(right)) => left.cmp(right), (Type::ModuleLiteral(_), _) => Ordering::Less, (_, Type::ModuleLiteral(_)) => Ordering::Greater, diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 3c9bb00a14..0f2b1ff156 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -163,7 +163,6 @@ impl<'db> From> for TypeKind<'db> { TypeKind::NonAtomic(NonAtomicType::Intersection(intersection)) } Type::Union(union) => TypeKind::NonAtomic(NonAtomicType::Union(union)), - Type::Tuple(tuple) => TypeKind::NonAtomic(NonAtomicType::Tuple(tuple)), Type::BoundMethod(method) => TypeKind::NonAtomic(NonAtomicType::BoundMethod(method)), Type::BoundSuper(bound_super) => { TypeKind::NonAtomic(NonAtomicType::BoundSuper(bound_super))