From 81f34fbc8e404cd26fa40ee86a339947dce7617c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 23 Dec 2025 23:19:57 -0500 Subject: [PATCH] [ty] Store un-widened type in `Place` (#22093) ## Summary See: https://github.com/astral-sh/ruff/pull/22025#discussion_r2632724156 --- crates/ty_python_semantic/src/place.rs | 249 +++++++++++++----- .../reachability_constraints.rs | 2 + crates/ty_python_semantic/src/types.rs | 93 ++++--- .../ty_python_semantic/src/types/call/bind.rs | 20 +- crates/ty_python_semantic/src/types/class.rs | 39 +-- .../src/types/diagnostic.rs | 5 +- .../ty_python_semantic/src/types/display.rs | 3 +- crates/ty_python_semantic/src/types/enums.rs | 40 +-- .../ty_python_semantic/src/types/function.rs | 2 +- .../src/types/infer/builder.rs | 111 ++++---- .../src/types/list_members.rs | 4 +- crates/ty_python_semantic/src/types/member.rs | 7 +- .../ty_python_semantic/src/types/overrides.rs | 4 +- .../src/types/protocol_class.rs | 6 +- 14 files changed, 355 insertions(+), 230 deletions(-) diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 0d242fb3a3..f61c24cca9 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -61,6 +61,33 @@ impl TypeOrigin { } } +/// Whether a place's type should be widened with `Unknown` when accessed publicly. +/// +/// For undeclared public symbols (e.g., class attributes without type annotations), +/// the gradual typing guarantee requires that we consider them as potentially +/// modified externally, so their type is widened to a union with `Unknown`. +/// +/// This enum tracks whether such widening should be applied, allowing callers +/// to access either the raw inferred type or the widened public type. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, get_size2::GetSize)] +pub(crate) enum Widening { + /// The type should not be widened with `Unknown`. + #[default] + None, + /// The type should be widened with `Unknown` when accessed publicly. + WithUnknown, +} + +impl Widening { + /// Apply widening to the type if this is `WithUnknown`. + pub(crate) fn apply_if_needed<'db>(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + match self { + Self::None => ty, + Self::WithUnknown => UnionType::from_elements(db, [Type::unknown(), ty]), + } + } +} + /// The result of a place lookup, which can either be a (possibly undefined) type /// or a completely undefined place. /// @@ -83,28 +110,38 @@ impl TypeOrigin { /// /// If we look up places in this scope, we would get the following results: /// ```rs -/// bound: Place::Defined(Literal[1], TypeOrigin::Inferred, Definedness::AlwaysDefined), -/// declared: Place::Defined(int, TypeOrigin::Declared, Definedness::AlwaysDefined), -/// possibly_unbound: Place::Defined(Literal[2], TypeOrigin::Inferred, Definedness::PossiblyUndefined), -/// possibly_undeclared: Place::Defined(int, TypeOrigin::Declared, Definedness::PossiblyUndefined), -/// bound_or_declared: Place::Defined(Literal[1], TypeOrigin::Inferred, Definedness::PossiblyUndefined), +/// bound: Place::Defined(Literal[1], TypeOrigin::Inferred, Definedness::AlwaysDefined, _), +/// declared: Place::Defined(int, TypeOrigin::Declared, Definedness::AlwaysDefined, _), +/// possibly_unbound: Place::Defined(Literal[2], TypeOrigin::Inferred, Definedness::PossiblyUndefined, _), +/// possibly_undeclared: Place::Defined(int, TypeOrigin::Declared, Definedness::PossiblyUndefined, _), +/// bound_or_declared: Place::Defined(Literal[1], TypeOrigin::Inferred, Definedness::PossiblyUndefined, _), /// non_existent: Place::Undefined, /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(crate) enum Place<'db> { - Defined(Type<'db>, TypeOrigin, Definedness), + Defined(Type<'db>, TypeOrigin, Definedness, Widening), Undefined, } impl<'db> Place<'db> { /// Constructor that creates a [`Place`] with type origin [`TypeOrigin::Inferred`] and definedness [`Definedness::AlwaysDefined`]. pub(crate) fn bound(ty: impl Into>) -> Self { - Place::Defined(ty.into(), TypeOrigin::Inferred, Definedness::AlwaysDefined) + Place::Defined( + ty.into(), + TypeOrigin::Inferred, + Definedness::AlwaysDefined, + Widening::None, + ) } /// Constructor that creates a [`Place`] with type origin [`TypeOrigin::Declared`] and definedness [`Definedness::AlwaysDefined`]. pub(crate) fn declared(ty: impl Into>) -> Self { - Place::Defined(ty.into(), TypeOrigin::Declared, Definedness::AlwaysDefined) + Place::Defined( + ty.into(), + TypeOrigin::Declared, + Definedness::AlwaysDefined, + Widening::None, + ) } /// Constructor that creates a [`Place`] with a [`crate::types::TodoType`] type @@ -115,6 +152,7 @@ impl<'db> Place<'db> { todo_type!(message), TypeOrigin::Inferred, Definedness::AlwaysDefined, + Widening::None, ) } @@ -128,7 +166,18 @@ impl<'db> Place<'db> { /// if there is at least one control-flow path where the place is defined, return the type. pub(crate) fn ignore_possibly_undefined(&self) -> Option> { match self { - Place::Defined(ty, _, _) => Some(*ty), + Place::Defined(ty, _, _, _) => Some(*ty), + Place::Undefined => None, + } + } + + /// Returns the type of the place without widening applied. + /// + /// The stored type is always the unwidened type. Widening (union with `Unknown`) + /// is applied lazily when converting to `LookupResult`. + pub(crate) fn unwidened_type(&self) -> Option> { + match self { + Place::Defined(ty, _, _, _) => Some(*ty), Place::Undefined => None, } } @@ -143,7 +192,20 @@ impl<'db> Place<'db> { #[must_use] pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Place<'db> { match self { - Place::Defined(ty, origin, definedness) => Place::Defined(f(ty), origin, definedness), + Place::Defined(ty, origin, definedness, widening) => { + Place::Defined(f(ty), origin, definedness, widening) + } + Place::Undefined => Place::Undefined, + } + } + + /// Set the widening mode for this place. + #[must_use] + pub(crate) fn with_widening(self, widening: Widening) -> Place<'db> { + match self { + Place::Defined(ty, origin, definedness, _) => { + Place::Defined(ty, origin, definedness, widening) + } Place::Undefined => Place::Undefined, } } @@ -161,21 +223,24 @@ impl<'db> Place<'db> { /// This is used to resolve (potential) descriptor attributes. pub(crate) fn try_call_dunder_get(self, db: &'db dyn Db, owner: Type<'db>) -> Place<'db> { match self { - Place::Defined(Type::Union(union), origin, definedness) => union + Place::Defined(Type::Union(union), origin, definedness, widening) => union .map_with_boundness(db, |elem| { - Place::Defined(*elem, origin, definedness).try_call_dunder_get(db, owner) + Place::Defined(*elem, origin, definedness, widening) + .try_call_dunder_get(db, owner) }), - Place::Defined(Type::Intersection(intersection), origin, definedness) => intersection - .map_with_boundness(db, |elem| { - Place::Defined(*elem, origin, definedness).try_call_dunder_get(db, owner) - }), + Place::Defined(Type::Intersection(intersection), origin, definedness, widening) => { + intersection.map_with_boundness(db, |elem| { + Place::Defined(*elem, origin, definedness, widening) + .try_call_dunder_get(db, owner) + }) + } - Place::Defined(self_ty, origin, definedness) => { + Place::Defined(self_ty, origin, definedness, widening) => { if let Some((dunder_get_return_ty, _)) = self_ty.try_call_dunder_get(db, Type::none(db), owner) { - Place::Defined(dunder_get_return_ty, origin, definedness) + Place::Defined(dunder_get_return_ty, origin, definedness, widening) } else { self } @@ -186,7 +251,7 @@ impl<'db> Place<'db> { } pub(crate) const fn is_definitely_bound(&self) -> bool { - matches!(self, Place::Defined(_, _, Definedness::AlwaysDefined)) + matches!(self, Place::Defined(_, _, Definedness::AlwaysDefined, _)) } } @@ -200,6 +265,7 @@ impl<'db> From> for PlaceAndQualifiers<'db> { type_and_qualifiers.inner_type(), TypeOrigin::Inferred, Definedness::PossiblyUndefined, + Widening::None, ) .with_qualifiers(type_and_qualifiers.qualifiers()), } @@ -220,7 +286,7 @@ impl<'db> LookupError<'db> { db: &'db dyn Db, fallback: PlaceAndQualifiers<'db>, ) -> LookupResult<'db> { - let fallback = fallback.into_lookup_result(); + let fallback = fallback.into_lookup_result(db); match (&self, &fallback) { (LookupError::Undefined(_), _) => fallback, (LookupError::PossiblyUndefined { .. }, Err(LookupError::Undefined(_))) => Err(self), @@ -645,18 +711,27 @@ impl<'db> PlaceAndQualifiers<'db> { /// Transform place and qualifiers into a [`LookupResult`], /// a [`Result`] type in which the `Ok` variant represents a definitely defined place /// and the `Err` variant represents a place that is either definitely or possibly undefined. - pub(crate) fn into_lookup_result(self) -> LookupResult<'db> { + /// + /// For places marked with `Widening::WithUnknown`, this applies the gradual typing guarantee + /// by creating a union with `Unknown`. + pub(crate) fn into_lookup_result(self, db: &'db dyn Db) -> LookupResult<'db> { match self { PlaceAndQualifiers { - place: Place::Defined(ty, origin, Definedness::AlwaysDefined), + place: Place::Defined(ty, origin, Definedness::AlwaysDefined, widening), qualifiers, - } => Ok(TypeAndQualifiers::new(ty, origin, qualifiers)), + } => { + let ty = widening.apply_if_needed(db, ty); + Ok(TypeAndQualifiers::new(ty, origin, qualifiers)) + } PlaceAndQualifiers { - place: Place::Defined(ty, origin, Definedness::PossiblyUndefined), + place: Place::Defined(ty, origin, Definedness::PossiblyUndefined, widening), qualifiers, - } => Err(LookupError::PossiblyUndefined(TypeAndQualifiers::new( - ty, origin, qualifiers, - ))), + } => { + let ty = widening.apply_if_needed(db, ty); + Err(LookupError::PossiblyUndefined(TypeAndQualifiers::new( + ty, origin, qualifiers, + ))) + } PlaceAndQualifiers { place: Place::Undefined, qualifiers, @@ -664,17 +739,18 @@ impl<'db> PlaceAndQualifiers<'db> { } } - /// Safely unwrap the place and the qualifiers into a [`TypeQualifiers`]. + /// Safely unwrap the place and the qualifiers into a [`TypeAndQualifiers`]. /// /// If the place is definitely unbound or possibly unbound, it will be transformed into a /// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning - /// the result of `diagnostic_fn` (which will be a [`TypeQualifiers`]). This allows the caller + /// the result of `diagnostic_fn` (which will be a [`TypeAndQualifiers`]). This allows the caller /// to ensure that a diagnostic is emitted if the place is possibly or definitely unbound. pub(crate) fn unwrap_with_diagnostic( self, + db: &'db dyn Db, diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>, ) -> TypeAndQualifiers<'db> { - self.into_lookup_result().unwrap_or_else(diagnostic_fn) + self.into_lookup_result(db).unwrap_or_else(diagnostic_fn) } /// Fallback (partially or fully) to another place if `self` is partially or fully unbound. @@ -693,7 +769,7 @@ impl<'db> PlaceAndQualifiers<'db> { db: &'db dyn Db, fallback_fn: impl FnOnce() -> PlaceAndQualifiers<'db>, ) -> Self { - self.into_lookup_result() + self.into_lookup_result(db) .or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn())) .into() } @@ -707,9 +783,15 @@ impl<'db> PlaceAndQualifiers<'db> { let place = match (previous_place.place, self.place) { // In fixed-point iteration of type inference, the member type must be monotonically widened and not "oscillate". // Here, monotonicity is guaranteed by pre-unioning the type of the previous iteration into the current result. - (Place::Defined(prev_ty, _, _), Place::Defined(ty, origin, definedness)) => { - Place::Defined(ty.cycle_normalized(db, prev_ty, cycle), origin, definedness) - } + ( + Place::Defined(prev_ty, _, _, _), + Place::Defined(ty, origin, definedness, widening), + ) => Place::Defined( + ty.cycle_normalized(db, prev_ty, cycle), + origin, + definedness, + widening, + ), // If a `Place` in the current cycle is `Defined` but `Undefined` in the previous cycle, // that means that its definedness depends on the truthiness of the previous cycle value. // In this case, the definedness of the current cycle `Place` is set to `PossiblyUndefined`. @@ -717,14 +799,17 @@ impl<'db> PlaceAndQualifiers<'db> { // so convergence is guaranteed without resorting to this handling. // However, the handling described above may reduce the exactness of reachability analysis, // so it may be better to remove it. In that case, this branch is necessary. - (Place::Undefined, Place::Defined(ty, origin, _definedness)) => Place::Defined( - ty.recursive_type_normalized(db, cycle), - origin, - Definedness::PossiblyUndefined, - ), + (Place::Undefined, Place::Defined(ty, origin, _definedness, widening)) => { + Place::Defined( + ty.recursive_type_normalized(db, cycle), + origin, + Definedness::PossiblyUndefined, + widening, + ) + } // If a `Place` that was `Defined(Divergent)` in the previous cycle is actually found to be unreachable in the current cycle, // it is set to `Undefined` (because the cycle initial value does not include meaningful reachability information). - (Place::Defined(ty, origin, _definedness), Place::Undefined) => { + (Place::Defined(ty, origin, _definedness, widening), Place::Undefined) => { if cycle.head_ids().any(|id| ty == Type::divergent(id)) { Place::Undefined } else { @@ -732,6 +817,7 @@ impl<'db> PlaceAndQualifiers<'db> { ty.recursive_type_normalized(db, cycle), origin, Definedness::PossiblyUndefined, + widening, ) } } @@ -814,30 +900,32 @@ pub(crate) fn place_by_id<'db>( // Handle bare `ClassVar` annotations by falling back to the union of `Unknown` and the // inferred type. PlaceAndQualifiers { - place: Place::Defined(Type::Dynamic(DynamicType::Unknown), origin, definedness), + place: Place::Defined(Type::Dynamic(DynamicType::Unknown), origin, definedness, _), qualifiers, } if qualifiers.contains(TypeQualifiers::CLASS_VAR) => { let bindings = all_considered_bindings(); match place_from_bindings_impl(db, bindings, requires_explicit_reexport).place { - Place::Defined(inferred, origin, boundness) => Place::Defined( + Place::Defined(inferred, origin, boundness, _) => Place::Defined( UnionType::from_elements(db, [Type::unknown(), inferred]), origin, boundness, + Widening::None, ) .with_qualifiers(qualifiers), Place::Undefined => { - Place::Defined(Type::unknown(), origin, definedness).with_qualifiers(qualifiers) + Place::Defined(Type::unknown(), origin, definedness, Widening::None) + .with_qualifiers(qualifiers) } } } // Place is declared, trust the declared type place_and_quals @ PlaceAndQualifiers { - place: Place::Defined(_, _, Definedness::AlwaysDefined), + place: Place::Defined(_, _, Definedness::AlwaysDefined, _), qualifiers: _, } => place_and_quals, // Place is possibly declared PlaceAndQualifiers { - place: Place::Defined(declared_ty, origin, Definedness::PossiblyUndefined), + place: Place::Defined(declared_ty, origin, Definedness::PossiblyUndefined, _), qualifiers, } => { let bindings = all_considered_bindings(); @@ -850,10 +938,15 @@ pub(crate) fn place_by_id<'db>( // TODO: We probably don't want to report `AlwaysDefined` here. This requires a bit of // design work though as we might want a different behavior for stubs and for // normal modules. - Place::Defined(declared_ty, origin, Definedness::AlwaysDefined) + Place::Defined( + declared_ty, + origin, + Definedness::AlwaysDefined, + Widening::None, + ) } // Place is possibly undeclared and (possibly) bound - Place::Defined(inferred_ty, origin, boundness) => Place::Defined( + Place::Defined(inferred_ty, origin, boundness, _) => Place::Defined( UnionType::from_elements(db, [inferred_ty, declared_ty]), origin, if boundness_analysis == BoundnessAnalysis::AssumeBound { @@ -861,12 +954,13 @@ pub(crate) fn place_by_id<'db>( } else { boundness }, + Widening::None, ), }; PlaceAndQualifiers { place, qualifiers } } - // Place is undeclared, return the union of `Unknown` with the inferred type + // Place is undeclared, infer the type from bindings PlaceAndQualifiers { place: Place::Undefined, qualifiers: _, @@ -877,8 +971,10 @@ pub(crate) fn place_by_id<'db>( place_from_bindings_impl(db, bindings, requires_explicit_reexport).place; if boundness_analysis == BoundnessAnalysis::AssumeBound { - if let Place::Defined(ty, origin, Definedness::PossiblyUndefined) = inferred { - inferred = Place::Defined(ty, origin, Definedness::AlwaysDefined); + if let Place::Defined(ty, origin, Definedness::PossiblyUndefined, widening) = + inferred + { + inferred = Place::Defined(ty, origin, Definedness::AlwaysDefined, widening); } } @@ -926,10 +1022,10 @@ pub(crate) fn place_by_id<'db>( { inferred.into() } else { - // Widen the inferred type of undeclared public symbols by unioning with `Unknown` - inferred - .map_type(|ty| UnionType::from_elements(db, [Type::unknown(), ty])) - .into() + // Gradual typing guarantee: Mark undeclared public symbols for widening. + // The actual union with `Unknown` is applied lazily when converting to + // LookupResult via `into_lookup_result`. + inferred.with_widening(Widening::WithUnknown).into() } } } @@ -1166,11 +1262,16 @@ fn place_from_bindings_impl<'db>( }; match deleted_reachability { - Truthiness::AlwaysFalse => Place::Defined(ty, TypeOrigin::Inferred, boundness), - Truthiness::AlwaysTrue => Place::Undefined, - Truthiness::Ambiguous => { - Place::Defined(ty, TypeOrigin::Inferred, Definedness::PossiblyUndefined) + Truthiness::AlwaysFalse => { + Place::Defined(ty, TypeOrigin::Inferred, boundness, Widening::None) } + Truthiness::AlwaysTrue => Place::Undefined, + Truthiness::Ambiguous => Place::Defined( + ty, + TypeOrigin::Inferred, + Definedness::PossiblyUndefined, + Widening::None, + ), } } else { Place::Undefined @@ -1399,9 +1500,13 @@ fn place_from_declarations_impl<'db>( }, }; - let place_and_quals = - Place::Defined(declared.inner_type(), TypeOrigin::Declared, boundness) - .with_qualifiers(declared.qualifiers()); + let place_and_quals = Place::Defined( + declared.inner_type(), + TypeOrigin::Declared, + boundness, + Widening::None, + ) + .with_qualifiers(declared.qualifiers()); if let Some(conflicting) = conflicting { PlaceFromDeclarationsResult::conflict(place_and_quals, conflicting, first_declaration) @@ -1455,7 +1560,7 @@ mod implicit_globals { use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type}; use ruff_python_ast::PythonVersion; - use super::{Place, place_from_declarations}; + use super::{Place, Widening, place_from_declarations}; pub(crate) fn module_type_implicit_global_declaration<'db>( db: &'db dyn Db, @@ -1518,6 +1623,7 @@ mod implicit_globals { .to_specialized_instance(db, [Type::any(), KnownClass::Int.to_instance(db)]), TypeOrigin::Inferred, Definedness::PossiblyUndefined, + Widening::None, ) .into(), @@ -1539,6 +1645,7 @@ mod implicit_globals { Type::function_like_callable(db, signature), TypeOrigin::Inferred, Definedness::PossiblyUndefined, + Widening::None, ) .into() } @@ -1740,19 +1847,21 @@ mod tests { let unbound = || Place::Undefined.with_qualifiers(TypeQualifiers::empty()); let possibly_unbound_ty1 = || { - Place::Defined(ty1, Inferred, PossiblyUndefined) + Place::Defined(ty1, Inferred, PossiblyUndefined, Widening::None) .with_qualifiers(TypeQualifiers::empty()) }; let possibly_unbound_ty2 = || { - Place::Defined(ty2, Inferred, PossiblyUndefined) + Place::Defined(ty2, Inferred, PossiblyUndefined, Widening::None) .with_qualifiers(TypeQualifiers::empty()) }; let bound_ty1 = || { - Place::Defined(ty1, Inferred, AlwaysDefined).with_qualifiers(TypeQualifiers::empty()) + Place::Defined(ty1, Inferred, AlwaysDefined, Widening::None) + .with_qualifiers(TypeQualifiers::empty()) }; let bound_ty2 = || { - Place::Defined(ty2, Inferred, AlwaysDefined).with_qualifiers(TypeQualifiers::empty()) + Place::Defined(ty2, Inferred, AlwaysDefined, Widening::None) + .with_qualifiers(TypeQualifiers::empty()) }; // Start from an unbound symbol @@ -1773,7 +1882,8 @@ mod tests { Place::Defined( UnionType::from_elements(&db, [ty1, ty2]), Inferred, - PossiblyUndefined + PossiblyUndefined, + Widening::None ) .into() ); @@ -1782,7 +1892,8 @@ mod tests { Place::Defined( UnionType::from_elements(&db, [ty1, ty2]), Inferred, - AlwaysDefined + AlwaysDefined, + Widening::None ) .into() ); @@ -1800,7 +1911,7 @@ mod tests { fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Place<'db>) { assert!(matches!( symbol, - Place::Defined(Type::NominalInstance(_), _, Definedness::AlwaysDefined) + Place::Defined(Type::NominalInstance(_), _, Definedness::AlwaysDefined, _) )); assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db)); } diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index 6f723f4679..e76a83e47a 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -957,11 +957,13 @@ impl ReachabilityConstraints { _, _, crate::place::Definedness::AlwaysDefined, + _, ) => Truthiness::AlwaysTrue, crate::place::Place::Defined( _, _, crate::place::Definedness::PossiblyUndefined, + _, ) => Truthiness::Ambiguous, crate::place::Place::Undefined => Truthiness::AlwaysFalse, } diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index a53d322aa1..8ecf8f550c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -36,7 +36,8 @@ pub(crate) use self::signatures::{CallableSignature, Signature}; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic; use crate::place::{ - Definedness, Place, PlaceAndQualifiers, TypeOrigin, imported_symbol, known_module_symbol, + Definedness, Place, PlaceAndQualifiers, TypeOrigin, Widening, imported_symbol, + known_module_symbol, }; use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::place::ScopedPlaceId; @@ -1855,7 +1856,7 @@ impl<'db> Type<'db> { ) .place; - if let Place::Defined(ty, _, Definedness::AlwaysDefined) = call_symbol { + if let Place::Defined(ty, _, Definedness::AlwaysDefined, _) = call_symbol { ty.try_upcast_to_callable(db) } else { None @@ -3677,13 +3678,14 @@ impl<'db> Type<'db> { disjointness_visitor.visit((self, other), || { protocol.interface(db).members(db).when_any(db, |member| { match other.member(db, member.name()).place { - Place::Defined(attribute_type, _, _) => member.has_disjoint_type_from( - db, - attribute_type, - inferable, - disjointness_visitor, - relation_visitor, - ), + Place::Defined(attribute_type, _, _, _) => member + .has_disjoint_type_from( + db, + attribute_type, + inferable, + disjointness_visitor, + relation_visitor, + ), Place::Undefined => ConstraintSet::from(false), } }) @@ -4654,9 +4656,10 @@ impl<'db> Type<'db> { fn static_member(&self, db: &'db dyn Db, name: &str) -> Place<'db> { if let Type::ModuleLiteral(module) = self { module.static_member(db, name).place - } else if let place @ Place::Defined(_, _, _) = self.class_member(db, name.into()).place { + } else if let place @ Place::Defined(_, _, _, _) = self.class_member(db, name.into()).place + { place - } else if let Some(place @ Place::Defined(_, _, _)) = + } else if let Some(place @ Place::Defined(_, _, _, _)) = self.find_name_in_mro(db, name).map(|inner| inner.place) { place @@ -4717,7 +4720,7 @@ impl<'db> Type<'db> { let descr_get = self.class_member(db, "__get__".into()).place; - if let Place::Defined(descr_get, _, descr_get_boundness) = descr_get { + if let Place::Defined(descr_get, _, descr_get_boundness, _) = descr_get { let return_ty = descr_get .try_call(db, &CallArguments::positional([self, instance, owner])) .map(|bindings| { @@ -4762,12 +4765,12 @@ impl<'db> Type<'db> { // // The same is true for `Never`. PlaceAndQualifiers { - place: Place::Defined(Type::Dynamic(_) | Type::Never, _, _), + place: Place::Defined(Type::Dynamic(_) | Type::Never, _, _, _), qualifiers: _, } => (attribute, AttributeKind::DataDescriptor), PlaceAndQualifiers { - place: Place::Defined(Type::Union(union), origin, boundness), + place: Place::Defined(Type::Union(union), origin, boundness, widening), qualifiers, } => ( union @@ -4777,6 +4780,7 @@ impl<'db> Type<'db> { .map_or(*elem, |(ty, _)| ty), origin, boundness, + widening, ) }) .with_qualifiers(qualifiers), @@ -4792,7 +4796,7 @@ impl<'db> Type<'db> { ), PlaceAndQualifiers { - place: Place::Defined(Type::Intersection(intersection), origin, boundness), + place: Place::Defined(Type::Intersection(intersection), origin, boundness, widening), qualifiers, } => ( intersection @@ -4802,6 +4806,7 @@ impl<'db> Type<'db> { .map_or(*elem, |(ty, _)| ty), origin, boundness, + widening, ) }) .with_qualifiers(qualifiers), @@ -4810,14 +4815,14 @@ impl<'db> Type<'db> { ), PlaceAndQualifiers { - place: Place::Defined(attribute_ty, origin, boundness), + place: Place::Defined(attribute_ty, origin, boundness, widening), qualifiers: _, } => { if let Some((return_ty, attribute_kind)) = attribute_ty.try_call_dunder_get(db, instance, owner) { ( - Place::Defined(return_ty, origin, boundness).into(), + Place::Defined(return_ty, origin, boundness, widening).into(), attribute_kind, ) } else { @@ -4910,14 +4915,14 @@ impl<'db> Type<'db> { match (meta_attr, meta_attr_kind, fallback) { // The fallback type is unbound, so we can just return `meta_attr` unconditionally, // no matter if it's data descriptor, a non-data descriptor, or a normal attribute. - (meta_attr @ Place::Defined(_, _, _), _, Place::Undefined) => { + (meta_attr @ Place::Defined(_, _, _, _), _, Place::Undefined) => { meta_attr.with_qualifiers(meta_attr_qualifiers) } // `meta_attr` is the return type of a data descriptor and definitely bound, so we // return it. ( - meta_attr @ Place::Defined(_, _, Definedness::AlwaysDefined), + meta_attr @ Place::Defined(_, _, Definedness::AlwaysDefined, _), AttributeKind::DataDescriptor, _, ) => meta_attr.with_qualifiers(meta_attr_qualifiers), @@ -4926,13 +4931,14 @@ impl<'db> Type<'db> { // meta-type is possibly-unbound. This means that we "fall through" to the next // stage of the descriptor protocol and union with the fallback type. ( - Place::Defined(meta_attr_ty, meta_origin, Definedness::PossiblyUndefined), + Place::Defined(meta_attr_ty, meta_origin, Definedness::PossiblyUndefined, _), AttributeKind::DataDescriptor, - Place::Defined(fallback_ty, fallback_origin, fallback_boundness), + Place::Defined(fallback_ty, fallback_origin, fallback_boundness, fallback_widening), ) => Place::Defined( UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), meta_origin.merge(fallback_origin), fallback_boundness, + fallback_widening, ) .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), @@ -4945,9 +4951,9 @@ impl<'db> Type<'db> { // would require us to statically infer if an instance attribute is always set, which // is something we currently don't attempt to do. ( - Place::Defined(_, _, _), + Place::Defined(_, _, _, _), AttributeKind::NormalOrNonDataDescriptor, - fallback @ Place::Defined(_, _, Definedness::AlwaysDefined), + fallback @ Place::Defined(_, _, Definedness::AlwaysDefined, _), ) if policy == InstanceFallbackShadowsNonDataDescriptor::Yes => { fallback.with_qualifiers(fallback_qualifiers) } @@ -4956,13 +4962,14 @@ impl<'db> Type<'db> { // unbound or the policy argument is `No`. In both cases, the `fallback` type does // not completely shadow the non-data descriptor, so we build a union of the two. ( - Place::Defined(meta_attr_ty, meta_origin, meta_attr_boundness), + Place::Defined(meta_attr_ty, meta_origin, meta_attr_boundness, _), AttributeKind::NormalOrNonDataDescriptor, - Place::Defined(fallback_ty, fallback_origin, fallback_boundness), + Place::Defined(fallback_ty, fallback_origin, fallback_boundness, fallback_widening), ) => Place::Defined( UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), meta_origin.merge(fallback_origin), meta_attr_boundness.max(fallback_boundness), + fallback_widening, ) .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), @@ -5330,11 +5337,11 @@ impl<'db> Type<'db> { match result { member @ PlaceAndQualifiers { - place: Place::Defined(_, _, Definedness::AlwaysDefined), + place: Place::Defined(_, _, Definedness::AlwaysDefined, _), qualifiers: _, } => member, member @ PlaceAndQualifiers { - place: Place::Defined(_, _, Definedness::PossiblyUndefined), + place: Place::Defined(_, _, Definedness::PossiblyUndefined, _), qualifiers: _, } => member .or_fall_back_to(db, custom_getattribute_result) @@ -6461,7 +6468,7 @@ impl<'db> Type<'db> { ) .place { - Place::Defined(dunder_callable, _, boundness) => { + Place::Defined(dunder_callable, _, boundness, _) => { let mut bindings = dunder_callable.bindings(db); bindings.replace_callable_type(dunder_callable, self); if boundness == Definedness::PossiblyUndefined { @@ -6609,7 +6616,7 @@ impl<'db> Type<'db> { ) .place { - Place::Defined(dunder_callable, _, boundness) => { + Place::Defined(dunder_callable, _, boundness, _) => { let bindings = dunder_callable .bindings(db) .match_parameters(db, argument_types) @@ -7212,7 +7219,7 @@ impl<'db> Type<'db> { let new_call_outcome = new_method.and_then(|new_method| { match new_method.place.try_call_dunder_get(db, self_type) { - Place::Defined(new_method, _, boundness) => { + Place::Defined(new_method, _, boundness, _) => { let argument_types = argument_types.with_self(Some(self_type)); let result = new_method .bindings(db) @@ -7240,7 +7247,7 @@ impl<'db> Type<'db> { .place { Place::Undefined => Err(CallDunderError::MethodNotAvailable), - Place::Defined(dunder_callable, _, boundness) => { + Place::Defined(dunder_callable, _, boundness, _) => { let bindings = dunder_callable .bindings(db) .with_constructor_instance_type(init_ty); @@ -10656,7 +10663,7 @@ impl<'db> TypeVarConstraints<'db> { Place::Undefined => { possibly_unbound = true; } - Place::Defined(ty_member, member_origin, member_boundness) => { + Place::Defined(ty_member, member_origin, member_boundness, _) => { origin = origin.merge(member_origin); if member_boundness == Definedness::PossiblyUndefined { possibly_unbound = true; @@ -10679,6 +10686,7 @@ impl<'db> TypeVarConstraints<'db> { } else { Definedness::AlwaysDefined }, + Widening::None, ) }, qualifiers, @@ -11793,7 +11801,7 @@ impl<'db> BoolError<'db> { ); if let Some((func_span, parameter_span)) = not_boolable_type .member(context.db(), "__bool__") - .into_lookup_result() + .into_lookup_result(context.db()) .ok() .and_then(|quals| quals.inner_type().parameter_span(context.db(), None)) { @@ -11821,7 +11829,7 @@ impl<'db> BoolError<'db> { ); if let Some((func_span, return_type_span)) = not_boolable_type .member(context.db(), "__bool__") - .into_lookup_result() + .into_lookup_result(context.db()) .ok() .and_then(|quals| quals.inner_type().function_spans(context.db())) .and_then(|spans| Some((spans.name, spans.return_type?))) @@ -13445,14 +13453,15 @@ impl<'db> ModuleLiteralType<'db> { // if it exists. First, we need to look up the `__getattr__` function in the module's scope. if let Some(file) = self.module(db).file(db) { let getattr_symbol = imported_symbol(db, file, "__getattr__", None); - if let Place::Defined(getattr_type, origin, boundness) = getattr_symbol.place { + if let Place::Defined(getattr_type, origin, boundness, widening) = getattr_symbol.place + { // If we found a __getattr__ function, try to call it with the name argument if let Ok(outcome) = getattr_type.try_call( db, &CallArguments::positional([Type::string_literal(db, name)]), ) { return PlaceAndQualifiers { - place: Place::Defined(outcome.return_type(db), origin, boundness), + place: Place::Defined(outcome.return_type(db), origin, boundness, widening), qualifiers: TypeQualifiers::FROM_MODULE_GETATTR, }; } @@ -13972,7 +13981,7 @@ impl<'db> UnionType<'db> { Place::Undefined => { possibly_unbound = true; } - Place::Defined(ty_member, member_origin, member_boundness) => { + Place::Defined(ty_member, member_origin, member_boundness, _) => { origin = origin.merge(member_origin); if member_boundness == Definedness::PossiblyUndefined { possibly_unbound = true; @@ -13997,6 +14006,7 @@ impl<'db> UnionType<'db> { } else { Definedness::AlwaysDefined }, + Widening::None, ) } } @@ -14022,7 +14032,7 @@ impl<'db> UnionType<'db> { Place::Undefined => { possibly_unbound = true; } - Place::Defined(ty_member, member_origin, member_boundness) => { + Place::Defined(ty_member, member_origin, member_boundness, _) => { origin = origin.merge(member_origin); if member_boundness == Definedness::PossiblyUndefined { possibly_unbound = true; @@ -14047,6 +14057,7 @@ impl<'db> UnionType<'db> { } else { Definedness::AlwaysDefined }, + Widening::None, ) }, qualifiers, @@ -14399,7 +14410,7 @@ impl<'db> IntersectionType<'db> { let ty_member = transform_fn(&ty); match ty_member { Place::Undefined => {} - Place::Defined(ty_member, member_origin, member_boundness) => { + Place::Defined(ty_member, member_origin, member_boundness, _) => { origin = origin.merge(member_origin); all_unbound = false; if member_boundness == Definedness::AlwaysDefined { @@ -14422,6 +14433,7 @@ impl<'db> IntersectionType<'db> { } else { Definedness::PossiblyUndefined }, + Widening::None, ) } } @@ -14445,7 +14457,7 @@ impl<'db> IntersectionType<'db> { qualifiers |= new_qualifiers; match member { Place::Undefined => {} - Place::Defined(ty_member, member_origin, member_boundness) => { + Place::Defined(ty_member, member_origin, member_boundness, _) => { origin = origin.merge(member_origin); all_unbound = false; if member_boundness == Definedness::AlwaysDefined { @@ -14469,6 +14481,7 @@ impl<'db> IntersectionType<'db> { } else { Definedness::PossiblyUndefined }, + Widening::None, ) }, qualifiers, diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 14bb62ff4e..b456b912e4 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1022,7 +1022,7 @@ impl<'db> Bindings<'db> { // TODO: we could emit a diagnostic here (if default is not set) overload.set_return_type( match instance_ty.static_member(db, attr_name.value(db)) { - Place::Defined(ty, _, Definedness::AlwaysDefined) => { + Place::Defined(ty, _, Definedness::AlwaysDefined, _) => { if ty.is_dynamic() { // Here, we attempt to model the fact that an attribute lookup on // a dynamic type could fail @@ -1032,7 +1032,7 @@ impl<'db> Bindings<'db> { ty } } - Place::Defined(ty, _, Definedness::PossiblyUndefined) => { + Place::Defined(ty, _, Definedness::PossiblyUndefined, _) => { union_with_default(ty) } Place::Undefined => default, @@ -2833,7 +2833,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { ) .place { - Place::Defined(getitem_method, _, Definedness::AlwaysDefined) => getitem_method + Place::Defined(getitem_method, _, Definedness::AlwaysDefined, _) => getitem_method .try_call(db, &CallArguments::positional([Type::unknown()])) .ok() .map_or_else(Type::unknown, |bindings| bindings.return_type(db)), @@ -3439,7 +3439,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { ) .place { - Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method + Place::Defined(keys_method, _, Definedness::AlwaysDefined, _) => keys_method .try_call(self.db, &CallArguments::none()) .ok() .and_then(|bindings| { @@ -3485,10 +3485,14 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { ) .place { - Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method - .try_call(self.db, &CallArguments::positional([Type::unknown()])) - .ok() - .map_or_else(Type::unknown, |bindings| bindings.return_type(self.db)), + Place::Defined(keys_method, _, Definedness::AlwaysDefined, _) => { + keys_method + .try_call(self.db, &CallArguments::positional([Type::unknown()])) + .ok() + .map_or_else(Type::unknown, |bindings| { + bindings.return_type(self.db) + }) + } _ => Type::unknown(), }, ) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 8af71b3eed..70ba8b3b25 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -46,8 +46,8 @@ use crate::types::{ use crate::{ Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, place::{ - Definedness, LookupError, LookupResult, Place, PlaceAndQualifiers, known_module_symbol, - place_from_bindings, place_from_declarations, + Definedness, LookupError, LookupResult, Place, PlaceAndQualifiers, Widening, + known_module_symbol, place_from_bindings, place_from_declarations, }, semantic_index::{ attribute_assignments, @@ -1179,7 +1179,7 @@ impl<'db> ClassType<'db> { ) .place; - if let Place::Defined(Type::BoundMethod(metaclass_dunder_call_function), _, _) = + if let Place::Defined(Type::BoundMethod(metaclass_dunder_call_function), _, _, _) = metaclass_dunder_call_function_symbol { // TODO: this intentionally diverges from step 1 in @@ -1242,7 +1242,7 @@ impl<'db> ClassType<'db> { // If the class defines an `__init__` method, then we synthesize a callable type with the // same parameters as the `__init__` method after it is bound, and with the return type of // the concrete type of `Self`. - let synthesized_dunder_init_callable = if let Place::Defined(ty, _, _) = + let synthesized_dunder_init_callable = if let Place::Defined(ty, _, _, _) = dunder_init_function_symbol { let signature = match ty { @@ -1317,7 +1317,7 @@ impl<'db> ClassType<'db> { ) .place; - if let Place::Defined(Type::FunctionLiteral(mut new_function), _, _) = + if let Place::Defined(Type::FunctionLiteral(mut new_function), _, _, _) = new_function_symbol { if let Some(class_generic_context) = class_generic_context { @@ -2243,7 +2243,7 @@ impl<'db> ClassLiteral<'db> { ( PlaceAndQualifiers { - place: Place::Defined(ty, _, _), + place: Place::Defined(ty, _, _, _), qualifiers, }, Some(dynamic_type), @@ -2360,7 +2360,7 @@ impl<'db> ClassLiteral<'db> { // For enum classes, `nonmember(value)` creates a non-member attribute. // At runtime, the enum metaclass unwraps the value, so accessing the attribute // returns the inner value, not the `nonmember` wrapper. - if let Some(ty) = member.inner.place.ignore_possibly_undefined() { + if let Some(ty) = member.inner.place.unwidened_type() { if let Some(value_ty) = try_unwrap_nonmember_value(db, ty) { if is_enum_class_by_inheritance(db, self) { return Member::definitely_declared(value_ty); @@ -2462,7 +2462,8 @@ impl<'db> ClassLiteral<'db> { } let dunder_set = field_ty.class_member(db, "__set__".into()); - if let Place::Defined(dunder_set, _, Definedness::AlwaysDefined) = dunder_set.place + if let Place::Defined(dunder_set, _, Definedness::AlwaysDefined, _) = + dunder_set.place { // The descriptor handling below is guarded by this not-dynamic check, because // dynamic types like `Any` are valid (data) descriptors: since they have all @@ -3367,7 +3368,7 @@ impl<'db> ClassLiteral<'db> { } ClassBase::Class(class) => { if let member @ PlaceAndQualifiers { - place: Place::Defined(ty, origin, boundness), + place: Place::Defined(ty, origin, boundness, _), qualifiers, } = class.own_instance_member(db, name).inner { @@ -3419,8 +3420,13 @@ impl<'db> ClassLiteral<'db> { Definedness::PossiblyUndefined }; - Place::Defined(union.build(), TypeOrigin::Inferred, boundness) - .with_qualifiers(union_qualifiers) + Place::Defined( + union.build(), + TypeOrigin::Inferred, + boundness, + Widening::None, + ) + .with_qualifiers(union_qualifiers) } } @@ -3773,7 +3779,7 @@ impl<'db> ClassLiteral<'db> { match declared_and_qualifiers { PlaceAndQualifiers { - place: mut declared @ Place::Defined(declared_ty, _, declaredness), + place: mut declared @ Place::Defined(declared_ty, _, declaredness, _), qualifiers, } => { // For the purpose of finding instance attributes, ignore `ClassVar` @@ -3818,6 +3824,7 @@ impl<'db> ClassLiteral<'db> { UnionType::from_elements(db, [declared_ty, implicit_ty]), TypeOrigin::Declared, declaredness, + Widening::None, ) .with_qualifiers(qualifiers), } @@ -3858,6 +3865,7 @@ impl<'db> ClassLiteral<'db> { UnionType::from_elements(db, [declared_ty, implicit_ty]), TypeOrigin::Declared, declaredness, + Widening::None, ) .with_qualifiers(qualifiers), } @@ -5160,15 +5168,16 @@ impl KnownClass { ) -> Result, KnownClassLookupError<'_>> { let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place; match symbol { - Place::Defined(Type::ClassLiteral(class_literal), _, Definedness::AlwaysDefined) => { + Place::Defined(Type::ClassLiteral(class_literal), _, Definedness::AlwaysDefined, _) => { Ok(class_literal) } Place::Defined( Type::ClassLiteral(class_literal), _, Definedness::PossiblyUndefined, + _, ) => Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }), - Place::Defined(found_type, _, _) => { + Place::Defined(found_type, _, _, _) => { Err(KnownClassLookupError::SymbolNotAClass { found_type }) } Place::Undefined => Err(KnownClassLookupError::ClassNotFound), @@ -6047,7 +6056,7 @@ enum SlotsKind { impl SlotsKind { fn from(db: &dyn Db, base: ClassLiteral) -> Self { - let Place::Defined(slots_ty, _, bound) = base + let Place::Defined(slots_ty, _, bound, _) = base .own_class_member(db, base.inherited_generic_context(db), None, "__slots__") .inner .place diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index c092975acb..a7a4d8d3f1 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -3969,8 +3969,9 @@ pub(super) fn report_invalid_method_override<'db>( .place }; - if let Place::Defined(Type::FunctionLiteral(subclass_function), _, _) = class_member(subclass) - && let Place::Defined(Type::FunctionLiteral(superclass_function), _, _) = + if let Place::Defined(Type::FunctionLiteral(subclass_function), _, _, _) = + class_member(subclass) + && let Place::Defined(Type::FunctionLiteral(superclass_function), _, _, _) = class_member(superclass) && let Ok(superclass_function_kind) = MethodDecorator::try_from_fn_type(db, superclass_function) diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index f1c37debeb..3eeb14c86a 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -859,7 +859,8 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.with_type(KnownClass::MethodWrapperType.to_class_literal(self.db)) .write_str("method-wrapper")?; f.write_str(" '")?; - if let Place::Defined(member_ty, _, _) = class_ty.member(self.db, member_name).place + if let Place::Defined(member_ty, _, _, _) = + class_ty.member(self.db, member_name).place { f.with_type(member_ty).write_str(member_name)?; } else { diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index d478389f7a..d6f5f23d28 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -76,7 +76,7 @@ pub(crate) fn enum_metadata<'db>( let ignore_place = place_from_bindings(db, ignore_bindings).place; match ignore_place { - Place::Defined(Type::StringLiteral(ignored_names), _, _) => { + Place::Defined(Type::StringLiteral(ignored_names), _, _, _) => { Some(ignored_names.value(db).split_ascii_whitespace().collect()) } // TODO: support the list-variant of `_ignore_`. @@ -113,7 +113,7 @@ pub(crate) fn enum_metadata<'db>( Place::Undefined => { return None; } - Place::Defined(ty, _, _) => { + Place::Defined(ty, _, _, _) => { let special_case = match ty { Type::Callable(_) | Type::FunctionLiteral(_) => { // Some types are specifically disallowed for enum members. @@ -196,9 +196,9 @@ pub(crate) fn enum_metadata<'db>( .place; match dunder_get { - Place::Undefined | Place::Defined(Type::Dynamic(_), _, _) => ty, + Place::Undefined | Place::Defined(Type::Dynamic(_), _, _, _) => ty, - Place::Defined(_, _, _) => { + Place::Defined(_, _, _, _) => { // Descriptors are not considered members. return None; } @@ -233,7 +233,7 @@ pub(crate) fn enum_metadata<'db>( match declared { PlaceAndQualifiers { - place: Place::Defined(Type::Dynamic(DynamicType::Unknown), _, _), + place: Place::Defined(Type::Dynamic(DynamicType::Unknown), _, _, _), qualifiers, } if qualifiers.contains(TypeQualifiers::FINAL) => {} PlaceAndQualifiers { @@ -243,7 +243,7 @@ pub(crate) fn enum_metadata<'db>( // Undeclared attributes are considered members } PlaceAndQualifiers { - place: Place::Defined(Type::NominalInstance(instance), _, _), + place: Place::Defined(Type::NominalInstance(instance), _, _, _), .. } if instance.has_known_class(db, KnownClass::Member) => { // If the attribute is specifically declared with `enum.member`, it is considered a member @@ -319,34 +319,6 @@ pub(crate) fn try_unwrap_nonmember_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> .unwrap_or(Type::unknown()), ) } - Type::Union(union) => { - // TODO: This is a hack. The proper fix is to avoid unioning Unknown from - // declarations into Place when we have concrete bindings. - // - // For now, we filter out Unknown and expect exactly one nonmember type - // to remain. If there are other non-Unknown types mixed in, we bail out. - let mut non_unknown = union.elements(db).iter().filter(|elem| !elem.is_unknown()); - - let first = non_unknown.next()?; - - // Ensure there's exactly one non-Unknown element. - if non_unknown.next().is_some() { - return None; - } - - if let Type::NominalInstance(instance) = first { - if instance.has_known_class(db, KnownClass::Nonmember) { - return Some( - first - .member(db, "value") - .place - .ignore_possibly_undefined() - .unwrap_or(Type::unknown()), - ); - } - } - None - } _ => None, } } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 82d4473a9b..40e4e953ef 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -374,7 +374,7 @@ impl<'db> OverloadLiteral<'db> { .name .scoped_use_id(db, scope); - let Place::Defined(Type::FunctionLiteral(previous_type), _, Definedness::AlwaysDefined) = + let Place::Defined(Type::FunctionLiteral(previous_type), _, Definedness::AlwaysDefined, _) = place_from_bindings(db, use_def.bindings_at_use(use_id)).place else { return None; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index e90944310b..3b1ceed9d6 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1093,12 +1093,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut public_functions = FxIndexSet::default(); for place in overloaded_function_places { - if let Place::Defined(Type::FunctionLiteral(function), _, Definedness::AlwaysDefined) = - place_from_bindings( - self.db(), - use_def.end_of_scope_symbol_bindings(place.as_symbol().unwrap()), - ) - .place + if let Place::Defined( + Type::FunctionLiteral(function), + _, + Definedness::AlwaysDefined, + _, + ) = place_from_bindings( + self.db(), + use_def.end_of_scope_symbol_bindings(place.as_symbol().unwrap()), + ) + .place { if function.file(self.db()) != self.file() { // If the function is not in this file, we don't need to check it. @@ -1660,7 +1664,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let AnyNodeRef::ExprAttribute(ast::ExprAttribute { value, attr, .. }) = node { let value_type = self.infer_maybe_standalone_expression(value, TypeContext::default()); - if let Place::Defined(ty, _, Definedness::AlwaysDefined) = + if let Place::Defined(ty, _, Definedness::AlwaysDefined, _) = value_type.member(db, attr).place { // TODO: also consider qualifiers on the attribute @@ -4806,7 +4810,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ) { PlaceAndQualifiers { - place: Place::Defined(attr_ty, _, _), + place: Place::Defined(attr_ty, _, _, _), qualifiers: _, } => attr_ty.is_callable_type(), _ => false, @@ -4880,7 +4884,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { false } PlaceAndQualifiers { - place: Place::Defined(meta_attr_ty, _, meta_attr_boundness), + place: Place::Defined(meta_attr_ty, _, meta_attr_boundness, _), qualifiers, } => { if invalid_assignment_to_final(self, qualifiers) { @@ -4888,7 +4892,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let assignable_to_meta_attr = - if let Place::Defined(meta_dunder_set, _, _) = + if let Place::Defined(meta_dunder_set, _, _, _) = meta_attr_ty.class_member(db, "__set__".into()).place { // TODO: We could use the annotated parameter type of `__set__` as @@ -4931,7 +4935,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let (assignable, boundness) = if let PlaceAndQualifiers { place: - Place::Defined(instance_attr_ty, _, instance_attr_boundness), + Place::Defined( + instance_attr_ty, + _, + instance_attr_boundness, + _, + ), qualifiers, } = object_ty.instance_member(db, attribute) @@ -4975,7 +4984,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } => { if let PlaceAndQualifiers { place: - Place::Defined(instance_attr_ty, _, instance_attr_boundness), + Place::Defined(instance_attr_ty, _, instance_attr_boundness, _), qualifiers, } = object_ty.instance_member(db, attribute) { @@ -5021,7 +5030,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { match object_ty.class_member(db, attribute.into()) { PlaceAndQualifiers { - place: Place::Defined(meta_attr_ty, _, meta_attr_boundness), + place: Place::Defined(meta_attr_ty, _, meta_attr_boundness, _), qualifiers, } => { // We may have to perform multi-inference if the meta attribute is possibly unbound. @@ -5032,40 +5041,41 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return false; } - let assignable_to_meta_attr = if let Place::Defined(meta_dunder_set, _, _) = - meta_attr_ty.class_member(db, "__set__".into()).place - { - // TODO: We could use the annotated parameter type of `__set__` as - // type context here. - let dunder_set_result = meta_dunder_set.try_call( - db, - &CallArguments::positional([meta_attr_ty, object_ty, value_ty]), - ); + let assignable_to_meta_attr = + if let Place::Defined(meta_dunder_set, _, _, _) = + meta_attr_ty.class_member(db, "__set__".into()).place + { + // TODO: We could use the annotated parameter type of `__set__` as + // type context here. + let dunder_set_result = meta_dunder_set.try_call( + db, + &CallArguments::positional([meta_attr_ty, object_ty, value_ty]), + ); - if emit_diagnostics { - if let Err(dunder_set_failure) = dunder_set_result.as_ref() { - report_bad_dunder_set_call( - &self.context, - dunder_set_failure, - attribute, - object_ty, - target, - ); + if emit_diagnostics { + if let Err(dunder_set_failure) = dunder_set_result.as_ref() { + report_bad_dunder_set_call( + &self.context, + dunder_set_failure, + attribute, + object_ty, + target, + ); + } } - } - dunder_set_result.is_ok() - } else { - let value_ty = - infer_value_ty(self, TypeContext::new(Some(meta_attr_ty))); - ensure_assignable_to(self, value_ty, meta_attr_ty) - }; + dunder_set_result.is_ok() + } else { + let value_ty = + infer_value_ty(self, TypeContext::new(Some(meta_attr_ty))); + ensure_assignable_to(self, value_ty, meta_attr_ty) + }; let assignable_to_class_attr = if meta_attr_boundness == Definedness::PossiblyUndefined { let (assignable, boundness) = - if let Place::Defined(class_attr_ty, _, class_attr_boundness) = + if let Place::Defined(class_attr_ty, _, class_attr_boundness, _) = object_ty .find_name_in_mro(db, attribute) .expect("called on Type::ClassLiteral or Type::SubclassOf") @@ -5102,7 +5112,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .. } => { if let PlaceAndQualifiers { - place: Place::Defined(class_attr_ty, _, class_attr_boundness), + place: Place::Defined(class_attr_ty, _, class_attr_boundness, _), qualifiers, } = object_ty .find_name_in_mro(db, attribute) @@ -5176,7 +5186,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { module.static_member(db, attribute) }; - if let Place::Defined(attr_ty, _, _) = sym.place { + if let Place::Defined(attr_ty, _, _, _) = sym.place { let value_ty = infer_value_ty(self, TypeContext::new(Some(attr_ty))); let assignable = value_ty.is_assignable_to(db, attr_ty); @@ -6777,7 +6787,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // First try loading the requested attribute from the module. if !import_is_self_referential { if let PlaceAndQualifiers { - place: Place::Defined(ty, _, boundness), + place: Place::Defined(ty, _, boundness, _), qualifiers, } = module_ty.member(self.db(), name) { @@ -9296,7 +9306,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let ty = - resolved_after_fallback.unwrap_with_diagnostic(|lookup_error| match lookup_error { + resolved_after_fallback.unwrap_with_diagnostic(db, |lookup_error| match lookup_error { LookupError::Undefined(qualifiers) => { self.report_unresolved_reference(name_node); TypeAndQualifiers::new(Type::unknown(), TypeOrigin::Inferred, qualifiers) @@ -9418,7 +9428,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } let (parent_place, _use_id) = self.infer_local_place_load(parent_expr, expr_ref); - if let Place::Defined(_, _, _) = parent_place { + if let Place::Defined(_, _, _, _) = parent_place { return Place::Undefined.into(); } } @@ -9561,7 +9571,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }); // We could have `Place::Undefined` here, despite the checks above, for example if // this scope contains a `del` statement but no binding or declaration. - if let Place::Defined(type_, _, boundness) = local_place_and_qualifiers.place { + if let Place::Defined(type_, _, boundness, _) = local_place_and_qualifiers.place + { nonlocal_union_builder.add_in_place(type_); // `ConsideredDefinitions::AllReachable` never returns PossiblyUnbound debug_assert_eq!(boundness, Definedness::AlwaysDefined); @@ -9815,7 +9826,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::ExprRef::Attribute(attribute), ); constraint_keys.extend(keys); - if let Place::Defined(ty, _, Definedness::AlwaysDefined) = resolved.place { + if let Place::Defined(ty, _, Definedness::AlwaysDefined, _) = resolved.place { assigned_type = Some(ty); } } @@ -9836,7 +9847,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }); let attr_name = &attr.id; - let resolved_type = fallback_place.unwrap_with_diagnostic(|lookup_err| match lookup_err { + let resolved_type = fallback_place.unwrap_with_diagnostic(db, |lookup_err| match lookup_err { LookupError::Undefined(_) => { let fallback = || { TypeAndQualifiers::new( @@ -11439,7 +11450,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let contains_dunder = right.class_member(db, "__contains__".into()).place; let compare_result_opt = match contains_dunder { - Place::Defined(contains_dunder, _, Definedness::AlwaysDefined) => { + Place::Defined(contains_dunder, _, Definedness::AlwaysDefined, _) => { // If `__contains__` is available, it is used directly for the membership test. contains_dunder .try_call(db, &CallArguments::positional([right, left])) @@ -11639,7 +11650,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::ExprRef::Subscript(subscript), ); constraint_keys.extend(keys); - if let Place::Defined(ty, _, Definedness::AlwaysDefined) = place.place { + if let Place::Defined(ty, _, Definedness::AlwaysDefined, _) = place.place { // Even if we can obtain the subscript type based on the assignments, we still perform default type inference // (to store the expression type and to report errors). let slice_ty = self.infer_expression(slice, TypeContext::default()); @@ -12671,7 +12682,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match dunder_class_getitem_method { Place::Undefined => {} - Place::Defined(ty, _, boundness) => { + Place::Defined(ty, _, boundness, _) => { if boundness == Definedness::PossiblyUndefined { if let Some(builder) = context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, value_node) diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index 4e4a32c294..9b5c322b7f 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -324,7 +324,7 @@ impl<'db> AllMembers<'db> { for (symbol_id, _) in use_def_map.all_end_of_scope_symbol_declarations() { let symbol_name = place_table.symbol(symbol_id).name(); - let Place::Defined(ty, _, _) = + let Place::Defined(ty, _, _, _) = imported_symbol(db, file, symbol_name, None).place else { continue; @@ -493,7 +493,7 @@ impl<'db> AllMembers<'db> { Some(CodeGeneratorKind::TypedDict) => {} Some(CodeGeneratorKind::DataclassLike(_)) => { for attr in SYNTHETIC_DATACLASS_ATTRIBUTES { - if let Place::Defined(synthetic_member, _, _) = ty.member(db, attr).place { + if let Place::Defined(synthetic_member, _, _, _) = ty.member(db, attr).place { self.members.insert(Member { name: Name::from(*attr), ty: synthetic_member, diff --git a/crates/ty_python_semantic/src/types/member.rs b/crates/ty_python_semantic/src/types/member.rs index dfb6cd47fa..33a6d7c559 100644 --- a/crates/ty_python_semantic/src/types/member.rs +++ b/crates/ty_python_semantic/src/types/member.rs @@ -68,7 +68,7 @@ pub(super) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str } if let PlaceAndQualifiers { - place: Place::Defined(ty, _, _), + place: Place::Defined(ty, _, _, _), qualifiers, } = place_and_quals { @@ -82,8 +82,9 @@ pub(super) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str Member { inner: match inferred { Place::Undefined => Place::Undefined.with_qualifiers(qualifiers), - Place::Defined(_, origin, boundness) => { - Place::Defined(ty, origin, boundness).with_qualifiers(qualifiers) + Place::Defined(_, origin, boundness, widening) => { + Place::Defined(ty, origin, boundness, widening) + .with_qualifiers(qualifiers) } }, } diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index c56581358c..bf6f292e56 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -110,7 +110,7 @@ fn check_class_declaration<'db>( first_reachable_definition, } = member; - let Place::Defined(type_on_subclass_instance, _, _) = + let Place::Defined(type_on_subclass_instance, _, _, _) = Type::instance(db, class).member(db, &member.name).place else { return; @@ -190,7 +190,7 @@ fn check_class_declaration<'db>( .unwrap_or_default(); } - let Place::Defined(superclass_type, _, _) = Type::instance(db, superclass) + let Place::Defined(superclass_type, _, _, _) = Type::instance(db, superclass) .member(db, &member.name) .place else { diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 5d3719b8d9..3c02a3aed8 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -738,7 +738,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { let attribute_type = if self.name == "__call__" { other } else { - let Place::Defined(attribute_type, _, Definedness::AlwaysDefined) = other + let Place::Defined(attribute_type, _, Definedness::AlwaysDefined, _) = other .invoke_descriptor_protocol( db, self.name, @@ -783,10 +783,10 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { // TODO: consider the types of the attribute on `other` for property members ProtocolMemberKind::Property(_) => ConstraintSet::from(matches!( other.member(db, self.name).place, - Place::Defined(_, _, Definedness::AlwaysDefined) + Place::Defined(_, _, Definedness::AlwaysDefined, _) )), ProtocolMemberKind::Other(member_type) => { - let Place::Defined(attribute_type, _, Definedness::AlwaysDefined) = + let Place::Defined(attribute_type, _, Definedness::AlwaysDefined, _) = other.member(db, self.name).place else { return ConstraintSet::from(false);