[ty] Add dedicated variant for NominalInstance(object) (#20340)

Previously, `Type::object` would find the definition of the `object`
class in typeshed, load that in (to produce a `ClassLiteral` and
`ClassType`), and then create a `NominalInstance` of that class.

It's possible that we are using a typeshed that doesn't define `object`.
We will not be able to do much useful work with that kind of typeshed,
but it's still a possibility that we have to support at least without
panicking. Previously, we would handle this situation by falling back on
`Unknown`.

In most cases, that's a perfectly fine fallback! But `object` is also
our top type — the type of all values. `Unknown` is _not_ an acceptable
stand-in for the top type.

This PR adds a new `NominalInstance` variant for "instances of
`object`". Unlike other nominal instances, we do not need to load in
`object`'s `ClassType` to instantiate this variant. We will use this new
variant even when the current typeshed does not define an `object`
class, ensuring that we have a fully static representation of our top
type at all times.

There are several operations that need access to a nominal instance's
class, and for this new `object` variant we load it lazily only when
it's needed. That means this operation is now fallible, since this is
where the "typeshed doesn't define `object`" failure shows up.

This new approach also has the benefit of avoiding some salsa cycles
that were cropping up while I was debugging #20093, since the new
constraint set representation was trying to instantiate `Type::object`
while in the middle of processing its definition in typeshed. Cycle
handling was kicking in correctly and returning the `Unknown` fallback
mentioned above. But the constraint set implementation depends on
`Type::object` being a distinct and fully static type, highlighting that
this is a correctness fix, not just an optimization fix.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Douglas Creager 2025-09-11 13:02:58 -04:00 committed by GitHub
parent 0e3697a643
commit abb705aa4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 196 additions and 155 deletions

View file

@ -344,7 +344,7 @@ fn pattern_kind_to_type<'db>(db: &'db dyn Db, kind: &PatternPredicateKind<'db>)
PatternPredicateKind::As(pattern, _) => pattern
.as_deref()
.map(|p| pattern_kind_to_type(db, p))
.unwrap_or_else(|| Type::object(db)),
.unwrap_or_else(Type::object),
PatternPredicateKind::Unsupported => Type::Never,
}
}

View file

@ -766,10 +766,6 @@ impl<'db> Type<'db> {
Self::Dynamic(DynamicType::Divergent(DivergentType { scope }))
}
pub(crate) fn object(db: &'db dyn Db) -> Self {
KnownClass::Object.to_instance(db)
}
pub const fn is_unknown(&self) -> bool {
matches!(self, Type::Dynamic(DynamicType::Unknown))
}
@ -785,18 +781,18 @@ impl<'db> Type<'db> {
fn is_none(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance()
.is_some_and(|instance| instance.class(db).is_known(db, KnownClass::NoneType))
.is_some_and(|instance| instance.has_known_class(db, KnownClass::NoneType))
}
fn is_bool(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance()
.is_some_and(|instance| instance.class(db).is_known(db, KnownClass::Bool))
.is_some_and(|instance| instance.has_known_class(db, KnownClass::Bool))
}
fn is_enum(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance().is_some_and(|instance| {
crate::types::enums::enum_metadata(db, instance.class(db).class_literal(db).0).is_some()
})
self.into_nominal_instance()
.and_then(|instance| crate::types::enums::enum_metadata(db, instance.class_literal(db)))
.is_some()
}
/// Return true if this type overrides __eq__ or __ne__ methods
@ -823,16 +819,8 @@ impl<'db> Type<'db> {
}
pub(crate) fn is_notimplemented(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance().is_some_and(|instance| {
instance
.class(db)
.is_known(db, KnownClass::NotImplementedType)
})
}
pub(crate) fn is_object(&self, db: &'db dyn Db) -> bool {
self.into_nominal_instance()
.is_some_and(|instance| instance.is_object(db))
.is_some_and(|instance| instance.has_known_class(db, KnownClass::NotImplementedType))
}
pub(crate) const fn is_todo(&self) -> bool {
@ -1455,7 +1443,7 @@ impl<'db> Type<'db> {
match (self, target) {
// Everything is a subtype of `object`.
(_, Type::NominalInstance(instance)) if instance.is_object(db) => {
(_, Type::NominalInstance(instance)) if instance.is_object() => {
C::always_satisfiable(db)
}
(_, Type::ProtocolInstance(target)) if target.is_equivalent_to_object(db) => {
@ -1627,7 +1615,7 @@ impl<'db> Type<'db> {
(left, Type::AlwaysTruthy) => C::from_bool(db, left.bool(db).is_always_true()),
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
target.when_equivalent_to(db, Type::object(db))
target.when_equivalent_to(db, Type::object())
}
// These clauses handle type variants that include function literals. A function
@ -1979,7 +1967,7 @@ impl<'db> Type<'db> {
}
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
C::from_bool(db, n.is_object(db) && protocol.normalized(db) == nominal)
C::from_bool(db, n.is_object() && protocol.normalized(db) == nominal)
}
// An instance of an enum class is equivalent to an enum literal of that class,
// if that enum has only has one member.
@ -1988,9 +1976,7 @@ impl<'db> Type<'db> {
if literal.enum_class_instance(db) != Type::NominalInstance(instance) {
return C::unsatisfiable(db);
}
let class_literal = instance.class(db).class_literal(db).0;
C::from_bool(db, is_single_member_enum(db, class_literal))
C::from_bool(db, is_single_member_enum(db, instance.class_literal(db)))
}
(Type::PropertyInstance(left), Type::PropertyInstance(right)) => {
@ -2840,9 +2826,7 @@ impl<'db> Type<'db> {
// i.e. Type::NominalInstance(type). So looking up a name in the MRO of
// `Type::NominalInstance(type)` is equivalent to looking up the name in the
// MRO of the class `object`.
Type::NominalInstance(instance)
if instance.class(db).is_known(db, KnownClass::Type) =>
{
Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Type) => {
if policy.mro_no_object_fallback() {
Some(Place::Unbound.into())
} else {
@ -2982,12 +2966,12 @@ impl<'db> Type<'db> {
.to_instance(db)
.instance_member(db, name),
Type::Callable(_) | Type::DataclassTransformer(_) => {
Type::object(db).instance_member(db, name)
Type::object().instance_member(db, name)
}
Type::NonInferableTypeVar(bound_typevar) => {
match bound_typevar.typevar(db).bound_or_constraints(db) {
None => Type::object(db).instance_member(db, name),
None => Type::object().instance_member(db, name),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.instance_member(db, name)
}
@ -3020,7 +3004,7 @@ impl<'db> Type<'db> {
.enum_class_instance(db)
.instance_member(db, name),
Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name),
Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object().instance_member(db, name),
Type::ModuleLiteral(_) => KnownClass::ModuleType
.to_instance(db)
.instance_member(db, name),
@ -3477,12 +3461,12 @@ impl<'db> Type<'db> {
.member_lookup_with_policy(db, name, policy),
Type::Callable(_) | Type::DataclassTransformer(_) => {
Type::object(db).member_lookup_with_policy(db, name, policy)
Type::object().member_lookup_with_policy(db, name, policy)
}
Type::NominalInstance(instance)
if matches!(name.as_str(), "major" | "minor")
&& instance.class(db).is_known(db, KnownClass::VersionInfo) =>
&& instance.has_known_class(db, KnownClass::VersionInfo) =>
{
let python_version = Program::get(db).python_version(db);
let segment = if name == "major" {
@ -3577,7 +3561,7 @@ impl<'db> Type<'db> {
// resolve the attribute.
if matches!(
self.into_nominal_instance()
.and_then(|instance| instance.class(db).known(db)),
.and_then(|instance| instance.known_class(db)),
Some(KnownClass::ModuleType | KnownClass::GenericAlias)
) {
return Place::Unbound.into();
@ -3906,8 +3890,7 @@ impl<'db> Type<'db> {
Type::TypeVar(_) => Truthiness::Ambiguous,
Type::NominalInstance(instance) => instance
.class(db)
.known(db)
.known_class(db)
.and_then(KnownClass::bool)
.map(Ok)
.unwrap_or_else(try_dunder_bool)?,
@ -4042,7 +4025,7 @@ impl<'db> Type<'db> {
self,
Signature::new(
Parameters::new([Parameter::positional_only(Some(Name::new_static("func")))
.with_annotated_type(Type::object(db))]),
.with_annotated_type(Type::object())]),
None,
),
)
@ -4307,7 +4290,7 @@ impl<'db> Type<'db> {
Parameters::new([Parameter::positional_or_keyword(
Name::new_static("object"),
)
.with_annotated_type(Type::object(db))
.with_annotated_type(Type::object())
.with_default_type(Type::string_literal(db, ""))]),
Some(KnownClass::Str.to_instance(db)),
),
@ -4390,7 +4373,7 @@ impl<'db> Type<'db> {
// ```
Binding::single(
self,
Signature::new(Parameters::empty(), Some(Type::object(db))),
Signature::new(Parameters::empty(), Some(Type::object())),
)
.into()
}
@ -4631,7 +4614,7 @@ impl<'db> Type<'db> {
}
Some(KnownClass::Tuple) => {
let object = Type::object(db);
let object = Type::object();
// ```py
// class tuple:
@ -5648,7 +5631,7 @@ impl<'db> Type<'db> {
// See conversation in https://github.com/astral-sh/ruff/pull/19915.
SpecialFormType::NamedTuple => Ok(IntersectionBuilder::new(db)
.positive_elements([
Type::homogeneous_tuple(db, Type::object(db)),
Type::homogeneous_tuple(db, Type::object()),
KnownClass::NamedTupleLike.to_instance(db),
])
.build()),
@ -5787,7 +5770,7 @@ impl<'db> Type<'db> {
Type::Dynamic(_) => Ok(*self),
Type::NominalInstance(instance) => match instance.class(db).known(db) {
Type::NominalInstance(instance) => match instance.known_class(db) {
Some(KnownClass::TypeVar) => Ok(todo_type!(
"Support for `typing.TypeVar` instances in type expressions"
)),
@ -5908,7 +5891,7 @@ impl<'db> Type<'db> {
pub(crate) fn dunder_class(self, db: &'db dyn Db) -> Type<'db> {
if self.is_typed_dict() {
return KnownClass::Dict
.to_specialized_class_type(db, [KnownClass::Str.to_instance(db), Type::object(db)])
.to_specialized_class_type(db, [KnownClass::Str.to_instance(db), Type::object()])
.map(Type::from)
// Guard against user-customized typesheds with a broken `dict` class
.unwrap_or_else(Type::unknown);
@ -6142,7 +6125,7 @@ impl<'db> Type<'db> {
TypeMapping::MarkTypeVarsInferable(_) |
TypeMapping::PromoteLiterals => self,
TypeMapping::Materialize(materialization_kind) => match materialization_kind {
MaterializationKind::Top => Type::object(db),
MaterializationKind::Top => Type::object(),
MaterializationKind::Bottom => Type::Never,
}
}
@ -9096,7 +9079,7 @@ impl<'db> CallableType<'db> {
/// `(*args: object, **kwargs: object) -> Never`.
#[cfg(test)]
pub(crate) fn bottom(db: &'db dyn Db) -> Type<'db> {
Self::single(db, Signature::bottom(db))
Self::single(db, Signature::bottom())
}
/// Return a "normalized" version of this `Callable` type.
@ -9380,7 +9363,7 @@ impl<'db> KnownBoundMethodType<'db> {
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("instance")))
.with_annotated_type(Type::object(db)),
.with_annotated_type(Type::object()),
Parameter::positional_only(Some(Name::new_static("owner")))
.with_annotated_type(UnionType::from_elements(
db,
@ -9400,9 +9383,9 @@ impl<'db> KnownBoundMethodType<'db> {
Either::Right(std::iter::once(Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("instance")))
.with_annotated_type(Type::object(db)),
.with_annotated_type(Type::object()),
Parameter::positional_only(Some(Name::new_static("value")))
.with_annotated_type(Type::object(db)),
.with_annotated_type(Type::object()),
]),
None,
)))
@ -9482,7 +9465,7 @@ impl WrapperDescriptorKind {
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(descriptor),
Parameter::positional_only(Some(Name::new_static("instance")))
.with_annotated_type(Type::object(db)),
.with_annotated_type(Type::object()),
Parameter::positional_only(Some(Name::new_static("owner")))
.with_annotated_type(UnionType::from_elements(
db,
@ -9503,7 +9486,7 @@ impl WrapperDescriptorKind {
Either::Left(dunder_get_signatures(db, KnownClass::Property).into_iter())
}
WrapperDescriptorKind::PropertyDunderSet => {
let object = Type::object(db);
let object = Type::object();
Either::Right(std::iter::once(Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
@ -10257,7 +10240,7 @@ impl<'db> IntersectionType<'db> {
/// there are no positive elements, returns a single `object` type.
fn positive_elements_or_object(self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
if self.positive(db).is_empty() {
Either::Left(std::iter::once(Type::object(db)))
Either::Left(std::iter::once(Type::object()))
} else {
Either::Right(self.positive(db).iter().copied())
}

View file

@ -242,8 +242,7 @@ impl<'db> UnionBuilder<'db> {
/// Collapse the union to a single type: `object`.
fn collapse_to_object(&mut self) {
self.elements.clear();
self.elements
.push(UnionElement::Type(Type::object(self.db)));
self.elements.push(UnionElement::Type(Type::object()));
}
/// Adds a type to this union.
@ -448,7 +447,7 @@ impl<'db> UnionBuilder<'db> {
}
}
// Adding `object` to a union results in `object`.
ty if ty.is_object(self.db) => {
ty if ty.is_object() => {
self.collapse_to_object();
}
_ => {
@ -648,8 +647,7 @@ impl<'db> IntersectionBuilder<'db> {
self
}
Type::NominalInstance(instance)
if enum_metadata(self.db, instance.class(self.db).class_literal(self.db).0)
.is_some() =>
if enum_metadata(self.db, instance.class_literal(self.db)).is_some() =>
{
let mut contains_enum_literal_as_negative_element = false;
for intersection in &self.intersections {
@ -674,7 +672,7 @@ impl<'db> IntersectionBuilder<'db> {
self.add_positive_impl(
Type::Union(UnionType::new(
db,
enum_member_literals(db, instance.class(db).class_literal(db).0, None)
enum_member_literals(db, instance.class_literal(db), None)
.expect("Calling `enum_member_literals` on an enum class")
.collect::<Box<[_]>>(),
)),
@ -860,7 +858,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
_ => {
let known_instance = new_positive
.into_nominal_instance()
.and_then(|instance| instance.class(db).known(db));
.and_then(|instance| instance.known_class(db));
if known_instance == Some(KnownClass::Object) {
// `object & T` -> `T`; it is always redundant to add `object` to an intersection
@ -880,7 +878,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
new_positive = Type::BooleanLiteral(false);
}
Type::NominalInstance(instance)
if instance.class(db).is_known(db, KnownClass::Bool) =>
if instance.has_known_class(db, KnownClass::Bool) =>
{
match new_positive {
// `bool & AlwaysTruthy` -> `Literal[True]`
@ -974,7 +972,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
self.positive
.iter()
.filter_map(|ty| ty.into_nominal_instance())
.filter_map(|instance| instance.class(db).known(db))
.filter_map(|instance| instance.known_class(db))
.any(KnownClass::is_bool)
};
@ -990,7 +988,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
Type::Never => {
// Adding ~Never to an intersection is a no-op.
}
Type::NominalInstance(instance) if instance.is_object(db) => {
Type::NominalInstance(instance) if instance.is_object() => {
// Adding ~object to an intersection results in Never.
*self = Self::default();
self.positive.insert(Type::Never);
@ -1152,7 +1150,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
self.simplify_constrained_typevars(db);
match (self.positive.len(), self.negative.len()) {
(0, 0) => Type::object(db),
(0, 0) => Type::object(),
(1, 0) => self.positive[0],
_ => {
self.positive.shrink_to_fit();
@ -1208,7 +1206,7 @@ mod tests {
let db = setup_db();
let intersection = IntersectionBuilder::new(&db).build();
assert_eq!(intersection, Type::object(&db));
assert_eq!(intersection, Type::object());
}
#[test_case(Type::BooleanLiteral(true))]
@ -1222,7 +1220,7 @@ mod tests {
// We add t_object in various orders (in first or second position) in
// the tests below to ensure that the boolean simplification eliminates
// everything from the intersection, not just `bool`.
let t_object = Type::object(&db);
let t_object = Type::object();
let t_bool = KnownClass::Bool.to_instance(&db);
let ty = IntersectionBuilder::new(&db)

View file

@ -1313,7 +1313,7 @@ impl<'db> Field<'db> {
pub(crate) fn is_kw_only_sentinel(&self, db: &'db dyn Db) -> bool {
self.declared_ty
.into_nominal_instance()
.is_some_and(|instance| instance.class(db).is_known(db, KnownClass::KwOnly))
.is_some_and(|instance| instance.has_known_class(db, KnownClass::KwOnly))
}
}

View file

@ -82,7 +82,7 @@ impl<'db> ClassBase<'db> {
Type::ClassLiteral(literal) => Some(Self::Class(literal.default_specialization(db))),
Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))),
Type::NominalInstance(instance)
if instance.class(db).is_known(db, KnownClass::GenericAlias) =>
if instance.has_known_class(db, KnownClass::GenericAlias) =>
{
Self::try_from_type(db, todo_type!("GenericAlias instance"), subclass)
}

View file

@ -1042,7 +1042,7 @@ impl<'db> Constraint<'db> {
// If the requested constraint is `Never ≤ T ≤ object`, then the typevar can be specialized
// to _any_ type, and the constraint does nothing.
if lower.is_never() && upper.is_object(db) {
if lower.is_never() && upper.is_object() {
return Satisfiable::Always;
}
@ -1189,7 +1189,7 @@ impl<'db> RangeConstraint<'db> {
ConstraintClause::from_constraints(
db,
[
Constraint::range(db, self.upper, Type::object(db)).constrain(typevar),
Constraint::range(db, self.upper, Type::object()).constrain(typevar),
Constraint::not_equivalent(db, self.upper).constrain(typevar),
],
),
@ -1214,7 +1214,7 @@ impl<'db> RangeConstraint<'db> {
write!(f, "{} ≤ ", self.constraint.lower.display(self.db))?;
}
self.typevar.fmt(f)?;
if !self.constraint.upper.is_object(self.db) {
if !self.constraint.upper.is_object() {
write!(f, " ≤ {}", self.constraint.upper.display(self.db))?;
}
f.write_str(")")
@ -1356,7 +1356,7 @@ impl<'db> Constraint<'db> {
debug_assert_eq!(ty, ty.top_materialization(db));
// Every type is comparable to Never and to object.
if ty.is_never() || ty.is_object(db) {
if ty.is_never() || ty.is_object() {
return Satisfiable::Never;
}
@ -1407,7 +1407,7 @@ impl<'db> IncomparableConstraint<'db> {
);
set.union_constraint(
db,
Constraint::range(db, self.ty, Type::object(db)).constrain(typevar),
Constraint::range(db, self.ty, Type::object()).constrain(typevar),
);
}

View file

@ -90,9 +90,7 @@ impl DisplaySettings {
fn type_to_class_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<ClassLiteral<'db>> {
match ty {
Type::ClassLiteral(class) => Some(class),
Type::NominalInstance(instance) => {
type_to_class_literal(db, Type::from(instance.class(db)))
}
Type::NominalInstance(instance) => Some(instance.class_literal(db)),
Type::EnumLiteral(enum_literal) => Some(enum_literal.enum_class(db)),
Type::GenericAlias(alias) => Some(alias.origin(db)),
Type::ProtocolInstance(ProtocolInstanceType {
@ -1648,7 +1646,7 @@ mod tests {
.with_annotated_type(KnownClass::Int.to_instance(&db))
.with_default_type(Type::IntLiteral(4)),
Parameter::variadic(Name::new_static("args"))
.with_annotated_type(Type::object(&db)),
.with_annotated_type(Type::object()),
Parameter::keyword_only(Name::new_static("g"))
.with_default_type(Type::IntLiteral(5)),
Parameter::keyword_only(Name::new_static("h"))
@ -1804,7 +1802,7 @@ mod tests {
.with_annotated_type(KnownClass::Int.to_instance(&db))
.with_default_type(Type::IntLiteral(4)),
Parameter::variadic(Name::new_static("args"))
.with_annotated_type(Type::object(&db)),
.with_annotated_type(Type::object()),
Parameter::keyword_only(Name::new_static("g"))
.with_default_type(Type::IntLiteral(5)),
Parameter::keyword_only(Name::new_static("h"))

View file

@ -130,7 +130,7 @@ pub(crate) fn enum_metadata<'db>(
// Some types are specifically disallowed for enum members.
return None;
}
Type::NominalInstance(instance) => match instance.class(db).known(db) {
Type::NominalInstance(instance) => match instance.known_class(db) {
// enum.nonmember
Some(KnownClass::Nonmember) => return None,
@ -208,7 +208,7 @@ pub(crate) fn enum_metadata<'db>(
PlaceAndQualifiers {
place: Place::Type(Type::NominalInstance(instance), _),
..
} if instance.class(db).is_known(db, KnownClass::Member) => {
} if instance.has_known_class(db, KnownClass::Member) => {
// If the attribute is specifically declared with `enum.member`, it is considered a member
}
_ => {

View file

@ -1133,10 +1133,10 @@ fn signature_cycle_recover<'db>(
}
fn signature_cycle_initial<'db>(
db: &'db dyn Db,
_db: &'db dyn Db,
_function: FunctionType<'db>,
) -> CallableSignature<'db> {
CallableSignature::single(Signature::bottom(db))
CallableSignature::single(Signature::bottom())
}
fn last_definition_signature_cycle_recover<'db>(
@ -1149,10 +1149,10 @@ fn last_definition_signature_cycle_recover<'db>(
}
fn last_definition_signature_cycle_initial<'db>(
db: &'db dyn Db,
_db: &'db dyn Db,
_function: FunctionType<'db>,
) -> Signature<'db> {
Signature::bottom(db)
Signature::bottom()
}
/// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might

View file

@ -95,8 +95,7 @@ impl<'db> AllMembers<'db> {
),
Type::NominalInstance(instance) => {
let (class_literal, _specialization) = instance.class(db).class_literal(db);
self.extend_with_instance_members(db, ty, class_literal);
self.extend_with_instance_members(db, ty, instance.class_literal(db));
}
Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => {
@ -211,7 +210,7 @@ impl<'db> AllMembers<'db> {
match ty {
Type::NominalInstance(instance)
if matches!(
instance.class(db).known(db),
instance.known_class(db),
Some(
KnownClass::TypeVar
| KnownClass::TypeVarTuple

View file

@ -1245,7 +1245,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::BooleanLiteral(_) | Type::IntLiteral(_) => {}
Type::NominalInstance(instance)
if matches!(
instance.class(self.db()).known(self.db()),
instance.known_class(self.db()),
Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool)
) => {}
_ => return false,
@ -3411,9 +3411,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
),
// Super instances do not allow attribute assignment
Type::NominalInstance(instance)
if instance.class(db).is_known(db, KnownClass::Super) =>
{
Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Super) => {
if emit_diagnostics {
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) {
builder.into_diagnostic(format_args!(
@ -7559,10 +7557,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// We didn't see any positive elements, check if the operation is supported on `object`:
match intersection_on {
IntersectionOn::Left => {
self.infer_binary_type_comparison(Type::object(self.db()), op, other, range)
self.infer_binary_type_comparison(Type::object(), op, other, range)
}
IntersectionOn::Right => {
self.infer_binary_type_comparison(other, op, Type::object(self.db()), range)
self.infer_binary_type_comparison(other, op, Type::object(), range)
}
}
}
@ -8733,7 +8731,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
} else if any_over_type(self.db(), *typevar, &|ty| match ty {
Type::Dynamic(DynamicType::TodoUnpack) => true,
Type::NominalInstance(nominal) => matches!(
nominal.class(self.db()).known(self.db()),
nominal.known_class(self.db()),
Some(KnownClass::TypeVarTuple | KnownClass::ParamSpec)
),
_ => false,
@ -8776,9 +8774,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let type_to_slice_argument = |ty: Option<Type<'db>>| match ty {
Some(ty @ (Type::IntLiteral(_) | Type::BooleanLiteral(_))) => SliceArg::Arg(ty),
Some(ty @ Type::NominalInstance(instance))
if instance
.class(self.db())
.is_known(self.db(), KnownClass::NoneType) =>
if instance.has_known_class(self.db(), KnownClass::NoneType) =>
{
SliceArg::Arg(ty)
}

View file

@ -1521,9 +1521,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
if any_over_type(self.db(), self.infer_name_load(name), &|ty| match ty {
Type::Dynamic(DynamicType::TodoPEP695ParamSpec) => true,
Type::NominalInstance(nominal) => nominal
.class(self.db())
.is_known(self.db(), KnownClass::ParamSpec),
Type::NominalInstance(nominal) => {
nominal.has_known_class(self.db(), KnownClass::ParamSpec)
}
_ => false,
}) {
return Some(Parameters::todo());

View file

@ -12,32 +12,44 @@ use crate::types::enums::is_single_member_enum;
use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::{
ApplyTypeMappingVisitor, ClassBase, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, TypeMapping, TypeRelation,
VarianceInferable,
ApplyTypeMappingVisitor, ClassBase, ClassLiteral, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, TypeMapping,
TypeRelation, VarianceInferable,
};
use crate::{Db, FxOrderSet};
pub(super) use synthesized_protocol::SynthesizedProtocolType;
impl<'db> Type<'db> {
pub(crate) const fn object() -> Self {
Type::NominalInstance(NominalInstanceType(NominalInstanceInner::Object))
}
pub(crate) const fn is_object(&self) -> bool {
matches!(
self,
Type::NominalInstance(NominalInstanceType(NominalInstanceInner::Object))
)
}
pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
let (class_literal, specialization) = class.class_literal(db);
if class_literal.is_known(db, KnownClass::Tuple) {
Type::tuple(TupleType::new(
match class_literal.known(db) {
Some(KnownClass::Tuple) => Type::tuple(TupleType::new(
db,
specialization
.and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?)))
.unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown())))
.as_ref(),
))
} else if class_literal.is_protocol(db) {
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
} else if class_literal.is_typed_dict(db) {
Type::typed_dict(class)
} else {
Type::non_tuple_instance(class)
)),
Some(KnownClass::Object) => Type::object(),
_ if class_literal.is_protocol(db) => {
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
}
_ if class_literal.is_typed_dict(db) => Type::typed_dict(class),
// We don't call non_tuple_instance here because we've already checked that the class
// is not `object`
_ => Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple(class))),
}
}
@ -74,8 +86,12 @@ impl<'db> Type<'db> {
/// **Private** helper function to create a `Type::NominalInstance` from a class that
/// is known not to be `Any`, a protocol class, or a typed dict class.
fn non_tuple_instance(class: ClassType<'db>) -> Self {
Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple(class)))
fn non_tuple_instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
if class.is_known(db, KnownClass::Object) {
Type::NominalInstance(NominalInstanceType(NominalInstanceInner::Object))
} else {
Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple(class)))
}
}
pub(crate) const fn into_nominal_instance(self) -> Option<NominalInstanceType<'db>> {
@ -132,7 +148,12 @@ impl<'db> Type<'db> {
// recognise `str` as a subtype of `Container[str]`.
structurally_satisfied.or(db, || {
if let Protocol::FromClass(class) = protocol.inner {
self.has_relation_to_impl(db, Type::non_tuple_instance(class), relation, visitor)
self.has_relation_to_impl(
db,
Type::non_tuple_instance(db, class),
relation,
visitor,
)
} else {
C::unsatisfiable(db)
}
@ -161,9 +182,42 @@ impl<'db> NominalInstanceType<'db> {
match self.0 {
NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db),
NominalInstanceInner::NonTuple(class) => class,
NominalInstanceInner::Object => KnownClass::Object
.try_to_class_literal(db)
.expect("Typeshed should always have a `object` class in `builtins.pyi`")
.default_specialization(db),
}
}
pub(super) fn class_literal(&self, db: &'db dyn Db) -> ClassLiteral<'db> {
let class = match self.0 {
NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db),
NominalInstanceInner::NonTuple(class) => class,
NominalInstanceInner::Object => {
return KnownClass::Object
.try_to_class_literal(db)
.expect("Typeshed should always have a `object` class in `builtins.pyi`");
}
};
let (class_literal, _) = class.class_literal(db);
class_literal
}
/// Returns the [`KnownClass`] that this is a nominal instance of, or `None` if it is not an
/// instance of a known class.
pub(super) fn known_class(&self, db: &'db dyn Db) -> Option<KnownClass> {
match self.0 {
NominalInstanceInner::ExactTuple(_) => Some(KnownClass::Tuple),
NominalInstanceInner::NonTuple(class) => class.known(db),
NominalInstanceInner::Object => Some(KnownClass::Object),
}
}
/// Returns whether this is a nominal instance of a particular [`KnownClass`].
pub(super) fn has_known_class(&self, db: &'db dyn Db, known_class: KnownClass) -> bool {
self.known_class(db) == Some(known_class)
}
/// If this 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]`.
@ -203,15 +257,13 @@ impl<'db> NominalInstanceType<'db> {
_ => None,
})
}
NominalInstanceInner::Object => None,
}
}
/// Return `true` if this type represents instances of the class `builtins.object`.
pub(super) fn is_object(self, db: &'db dyn Db) -> bool {
match self.0 {
NominalInstanceInner::ExactTuple(_) => false,
NominalInstanceInner::NonTuple(class) => class.is_object(db),
}
pub(super) const fn is_object(self) -> bool {
matches!(self.0, NominalInstanceInner::Object)
}
/// If this type is an *exact* tuple type (*not* a subclass of `tuple`), returns the
@ -227,7 +279,7 @@ impl<'db> NominalInstanceType<'db> {
pub(super) fn own_tuple_spec(&self, db: &'db dyn Db) -> Option<Cow<'db, TupleSpec<'db>>> {
match self.0 {
NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))),
NominalInstanceInner::NonTuple(_) => None,
NominalInstanceInner::NonTuple(_) | NominalInstanceInner::Object => None,
}
}
@ -238,7 +290,7 @@ impl<'db> NominalInstanceType<'db> {
/// integers or `None`.
pub(crate) fn slice_literal(self, db: &'db dyn Db) -> Option<SliceLiteral> {
let class = match self.0 {
NominalInstanceInner::ExactTuple(_) => return None,
NominalInstanceInner::ExactTuple(_) | NominalInstanceInner::Object => return None,
NominalInstanceInner::NonTuple(class) => class,
};
let (class, Some(specialization)) = class.class_literal(db) else {
@ -255,7 +307,7 @@ impl<'db> NominalInstanceType<'db> {
Type::IntLiteral(n) => i32::try_from(*n).map(Some).ok(),
Type::BooleanLiteral(b) => Some(Some(i32::from(*b))),
Type::NominalInstance(instance)
if instance.class(db).is_known(db, KnownClass::NoneType) =>
if instance.has_known_class(db, KnownClass::NoneType) =>
{
Some(None)
}
@ -278,8 +330,9 @@ impl<'db> NominalInstanceType<'db> {
Type::tuple(tuple.normalized_impl(db, visitor))
}
NominalInstanceInner::NonTuple(class) => {
Type::non_tuple_instance(class.normalized_impl(db, visitor))
Type::non_tuple_instance(db, class.normalized_impl(db, visitor))
}
NominalInstanceInner::Object => Type::object(),
}
}
@ -291,6 +344,7 @@ impl<'db> NominalInstanceType<'db> {
visitor: &HasRelationToVisitor<'db, C>,
) -> C {
match (self.0, other.0) {
(_, NominalInstanceInner::Object) => C::always_satisfiable(db),
(
NominalInstanceInner::ExactTuple(tuple1),
NominalInstanceInner::ExactTuple(tuple2),
@ -312,6 +366,9 @@ impl<'db> NominalInstanceType<'db> {
NominalInstanceInner::ExactTuple(tuple1),
NominalInstanceInner::ExactTuple(tuple2),
) => tuple1.is_equivalent_to_impl(db, tuple2, visitor),
(NominalInstanceInner::Object, NominalInstanceInner::Object) => {
C::always_satisfiable(db)
}
(NominalInstanceInner::NonTuple(class1), NominalInstanceInner::NonTuple(class2)) => {
class1.is_equivalent_to_impl(db, class2, visitor)
}
@ -325,6 +382,9 @@ impl<'db> NominalInstanceType<'db> {
other: Self,
visitor: &IsDisjointVisitor<'db, C>,
) -> C {
if self.is_object() || other.is_object() {
return C::unsatisfiable(db);
}
let mut result = C::unsatisfiable(db);
if let Some(self_spec) = self.tuple_spec(db) {
if let Some(other_spec) = other.tuple_spec(db) {
@ -349,7 +409,7 @@ impl<'db> NominalInstanceType<'db> {
// 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
NominalInstanceInner::ExactTuple(_) => false,
NominalInstanceInner::ExactTuple(_) | NominalInstanceInner::Object => false,
NominalInstanceInner::NonTuple(class) => class
.known(db)
.map(KnownClass::is_singleton)
@ -360,6 +420,7 @@ impl<'db> NominalInstanceType<'db> {
pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool {
match self.0 {
NominalInstanceInner::ExactTuple(tuple) => tuple.is_single_valued(db),
NominalInstanceInner::Object => false,
NominalInstanceInner::NonTuple(class) => class
.known(db)
.and_then(KnownClass::is_single_valued)
@ -382,9 +443,11 @@ impl<'db> NominalInstanceType<'db> {
NominalInstanceInner::ExactTuple(tuple) => {
Type::tuple(tuple.apply_type_mapping_impl(db, type_mapping, visitor))
}
NominalInstanceInner::NonTuple(class) => {
Type::non_tuple_instance(class.apply_type_mapping_impl(db, type_mapping, visitor))
}
NominalInstanceInner::NonTuple(class) => Type::non_tuple_instance(
db,
class.apply_type_mapping_impl(db, type_mapping, visitor),
),
NominalInstanceInner::Object => Type::object(),
}
}
@ -402,6 +465,7 @@ impl<'db> NominalInstanceType<'db> {
NominalInstanceInner::NonTuple(class) => {
class.find_legacy_typevars_impl(db, binding_context, typevars, visitor);
}
NominalInstanceInner::Object => {}
}
}
}
@ -417,6 +481,12 @@ impl<'db> From<NominalInstanceType<'db>> for Type<'db> {
/// instances where it would be unnecessary (this is somewhat expensive!).
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)]
enum NominalInstanceInner<'db> {
/// An instance of `object`.
///
/// We model it with a dedicated enum variant since its use as "the type of all values" is so
/// prevalent and foundational, and it's useful to be able to instantiate this without having
/// to load the definition of `object` from the typeshed.
Object,
/// A tuple type, e.g. `tuple[int, str]`.
///
/// Note that the type `tuple[int, str]` includes subtypes of `tuple[int, str]`,
@ -514,7 +584,7 @@ impl<'db> ProtocolInstanceType<'db> {
pub(super) fn is_equivalent_to_object(self, db: &'db dyn Db) -> bool {
#[salsa::tracked(cycle_fn=recover, cycle_initial=initial, heap_size=ruff_memory_usage::heap_size)]
fn inner<'db>(db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, _: ()) -> bool {
Type::object(db)
Type::object()
.satisfies_protocol(
db,
protocol,
@ -558,7 +628,7 @@ impl<'db> ProtocolInstanceType<'db> {
visitor: &NormalizedVisitor<'db>,
) -> Type<'db> {
if self.is_equivalent_to_object(db) {
return Type::object(db);
return Type::object();
}
match self.inner {
Protocol::FromClass(_) => Type::ProtocolInstance(Self::synthesized(

View file

@ -292,13 +292,13 @@ fn merge_constraints_or<'db>(
*entry.get_mut() = UnionBuilder::new(db).add(*entry.get()).add(*value).build();
}
Entry::Vacant(entry) => {
entry.insert(Type::object(db));
entry.insert(Type::object());
}
}
}
for (key, value) in into.iter_mut() {
if !from.contains_key(key) {
*value = Type::object(db);
*value = Type::object();
}
}
}
@ -554,7 +554,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
}
// Treat `bool` as `Literal[True, False]`.
Type::NominalInstance(instance)
if instance.class(db).is_known(db, KnownClass::Bool) =>
if instance.has_known_class(db, KnownClass::Bool) =>
{
UnionType::from_elements(
db,
@ -565,11 +565,11 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
}
// Treat enums as a union of their members.
Type::NominalInstance(instance)
if enum_metadata(db, instance.class(db).class_literal(db).0).is_some() =>
if enum_metadata(db, instance.class_literal(db)).is_some() =>
{
UnionType::from_elements(
db,
enum_member_literals(db, instance.class(db).class_literal(db).0, None)
enum_member_literals(db, instance.class_literal(db), None)
.expect("Calling `enum_member_literals` on an enum class")
.map(|ty| filter_to_cannot_be_equal(db, ty, rhs_ty)),
)
@ -596,7 +596,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
fn evaluate_expr_ne(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option<Type<'db>> {
match (lhs_ty, rhs_ty) {
(Type::NominalInstance(instance), Type::IntLiteral(i))
if instance.class(self.db).is_known(self.db, KnownClass::Bool) =>
if instance.has_known_class(self.db, KnownClass::Bool) =>
{
if i == 0 {
Some(Type::BooleanLiteral(false).negate(self.db))
@ -912,10 +912,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
// Since `hasattr` only checks if an attribute is readable,
// the type of the protocol member should be a read-only property that returns `object`.
let constraint = Type::protocol_with_readonly_members(
self.db,
[(attr, Type::object(self.db))],
);
let constraint =
Type::protocol_with_readonly_members(self.db, [(attr, Type::object())]);
return Some(NarrowingConstraints::from_iter([(
place,

View file

@ -133,13 +133,13 @@ mod stable {
// All types should be assignable to `object`
type_property_test!(
all_types_assignable_to_object, db,
forall types t. t.is_assignable_to(db, Type::object(db))
forall types t. t.is_assignable_to(db, Type::object())
);
// And all types should be subtypes of `object`
type_property_test!(
all_types_subtype_of_object, db,
forall types t. t.is_subtype_of(db, Type::object(db))
forall types t. t.is_subtype_of(db, Type::object())
);
// Never should be assignable to every type
@ -182,7 +182,7 @@ mod stable {
// Only `object` is a supertype of `Any`.
type_property_test!(
only_object_is_supertype_of_any, db,
forall types t. !t.is_equivalent_to(db, Type::object(db)) => !Type::any().is_subtype_of(db, t)
forall types t. !t.is_equivalent_to(db, Type::object()) => !Type::any().is_subtype_of(db, t)
);
// Equivalence is commutative.
@ -332,6 +332,6 @@ mod flaky {
// Currently flaky due to <https://github.com/astral-sh/ty/issues/889>
type_property_test!(
all_type_assignable_to_iterable_are_iterable, db,
forall types t. t.is_assignable_to(db, KnownClass::Iterable.to_specialized_instance(db, [Type::object(db)])) => t.try_iterate(db).is_ok()
forall types t. t.is_assignable_to(db, KnownClass::Iterable.to_specialized_instance(db, [Type::object()])) => t.try_iterate(db).is_ok()
);
}

View file

@ -153,7 +153,7 @@ impl Ty {
.place
.expect_type();
debug_assert!(
matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class(db).class_literal(db).0))
matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class_literal(db)))
);
ty
}

View file

@ -227,7 +227,7 @@ impl<'db> ProtocolInterface<'db> {
place: Place::bound(member.ty()),
qualifiers: member.qualifiers(),
})
.unwrap_or_else(|| Type::object(db).member(db, name))
.unwrap_or_else(|| Type::object().member(db, name))
}
/// Return `true` if `self` extends the interface of `other`, i.e.,

View file

@ -408,8 +408,8 @@ impl<'db> Signature<'db> {
}
/// Return the "bottom" signature, subtype of all other fully-static signatures.
pub(crate) fn bottom(db: &'db dyn Db) -> Self {
Self::new(Parameters::object(db), Some(Type::Never))
pub(crate) fn bottom() -> Self {
Self::new(Parameters::object(), Some(Type::Never))
}
pub(crate) fn with_inherited_generic_context(
@ -704,11 +704,11 @@ impl<'db> Signature<'db> {
&& self
.parameters
.variadic()
.is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db)))
.is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object()))
&& self
.parameters
.keyword_variadic()
.is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db)))
.is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object()))
{
return C::always_satisfiable(db);
}
@ -1142,12 +1142,12 @@ impl<'db> Parameters<'db> {
}
/// Return parameters that represents `(*args: object, **kwargs: object)`.
pub(crate) fn object(db: &'db dyn Db) -> Self {
pub(crate) fn object() -> Self {
Self {
value: vec![
Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object(db)),
Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object()),
Parameter::keyword_variadic(Name::new_static("kwargs"))
.with_annotated_type(Type::object(db)),
.with_annotated_type(Type::object()),
],
is_gradual: false,
}
@ -1274,7 +1274,7 @@ impl<'db> Parameters<'db> {
// so the "top" materialization here is the bottom materialization of the whole Signature.
// It might make sense to flip the materialization here instead.
TypeMapping::Materialize(MaterializationKind::Top) if self.is_gradual => {
Parameters::object(db)
Parameters::object()
}
// TODO: This is wrong, the empty Parameters is not a subtype of all materializations.
// The bottom materialization is not currently representable and implementing it
@ -1779,8 +1779,7 @@ mod tests {
Parameter::positional_or_keyword(Name::new_static("f"))
.with_annotated_type(Type::IntLiteral(4))
.with_default_type(Type::IntLiteral(4)),
Parameter::variadic(Name::new_static("args"))
.with_annotated_type(Type::object(&db)),
Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object()),
Parameter::keyword_only(Name::new_static("g"))
.with_default_type(Type::IntLiteral(5)),
Parameter::keyword_only(Name::new_static("h"))