[ty] refactor Place (#20871)

## Summary

Part of astral-sh/ty#1341

The following changes will be made to `Place`.

* Introduce `TypeOrigin`
* `Place::Type` -> `Place::Defined`
* `Place::Unbound` -> `Place::Undefined`
* `Boundness` -> `Definedness`

`TypeOrigin::Declared`+`Definedness::PossiblyUndefined` are patterns
that weren't considered before, but this PR doesn't address them yet,
only refactors.

## Test Plan

Refactoring
This commit is contained in:
Shunsuke Shibayama 2025-10-16 03:19:19 +09:00 committed by GitHub
parent 4b7f184ab7
commit 9de34e7ac1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 681 additions and 548 deletions

View file

@ -21,83 +21,127 @@ pub(crate) use implicit_globals::{
};
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize)]
pub(crate) enum Boundness {
Bound,
PossiblyUnbound,
pub(crate) enum Definedness {
AlwaysDefined,
PossiblyUndefined,
}
impl Boundness {
impl Definedness {
pub(crate) const fn max(self, other: Self) -> Self {
match (self, other) {
(Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound,
(Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound,
(Definedness::AlwaysDefined, _) | (_, Definedness::AlwaysDefined) => {
Definedness::AlwaysDefined
}
(Definedness::PossiblyUndefined, Definedness::PossiblyUndefined) => {
Definedness::PossiblyUndefined
}
}
}
}
/// The result of a place lookup, which can either be a (possibly unbound) type
/// or a completely unbound place.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize)]
pub(crate) enum TypeOrigin {
Declared,
Inferred,
}
impl TypeOrigin {
pub(crate) const fn is_declared(self) -> bool {
matches!(self, TypeOrigin::Declared)
}
pub(crate) const fn merge(self, other: Self) -> Self {
match (self, other) {
(TypeOrigin::Declared, TypeOrigin::Declared) => TypeOrigin::Declared,
_ => TypeOrigin::Inferred,
}
}
}
/// The result of a place lookup, which can either be a (possibly undefined) type
/// or a completely undefined place.
///
/// If a place has both a binding and a declaration, the result of the binding is used.
///
/// Consider this example:
/// ```py
/// bound = 1
/// declared: int
///
/// if flag:
/// possibly_unbound = 2
/// possibly_undeclared: int
///
/// if flag:
/// bound_or_declared = 1
/// else:
/// bound_or_declared: int
/// ```
///
/// If we look up places in this scope, we would get the following results:
/// ```rs
/// bound: Place::Type(Type::IntLiteral(1), Boundness::Bound),
/// possibly_unbound: Place::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound),
/// non_existent: Place::Unbound,
/// 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, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) enum Place<'db> {
Type(Type<'db>, Boundness),
Unbound,
Defined(Type<'db>, TypeOrigin, Definedness),
Undefined,
}
impl<'db> Place<'db> {
/// Constructor that creates a `Place` with boundness [`Boundness::Bound`].
/// Constructor that creates a [`Place`] with type origin [`TypeOrigin::Inferred`] and definedness [`Definedness::AlwaysDefined`].
pub(crate) fn bound(ty: impl Into<Type<'db>>) -> Self {
Place::Type(ty.into(), Boundness::Bound)
Place::Defined(ty.into(), TypeOrigin::Inferred, Definedness::AlwaysDefined)
}
/// Constructor that creates a [`Place`] with type origin [`TypeOrigin::Declared`] and definedness [`Definedness::AlwaysDefined`].
pub(crate) fn declared(ty: impl Into<Type<'db>>) -> Self {
Place::Defined(ty.into(), TypeOrigin::Declared, Definedness::AlwaysDefined)
}
/// Constructor that creates a [`Place`] with a [`crate::types::TodoType`] type
/// and boundness [`Boundness::Bound`].
/// and definedness [`Definedness::AlwaysDefined`].
#[allow(unused_variables)] // Only unused in release builds
pub(crate) fn todo(message: &'static str) -> Self {
Place::Type(todo_type!(message), Boundness::Bound)
Place::Defined(
todo_type!(message),
TypeOrigin::Inferred,
Definedness::AlwaysDefined,
)
}
pub(crate) fn is_unbound(&self) -> bool {
matches!(self, Place::Unbound)
pub(crate) fn is_undefined(&self) -> bool {
matches!(self, Place::Undefined)
}
/// Returns the type of the place, ignoring possible unboundness.
/// Returns the type of the place, ignoring possible undefinedness.
///
/// If the place is *definitely* unbound, this function will return `None`. Otherwise,
/// if there is at least one control-flow path where the place is bound, return the type.
pub(crate) fn ignore_possibly_unbound(&self) -> Option<Type<'db>> {
/// If the place is *definitely* undefined, this function will return `None`. Otherwise,
/// 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<Type<'db>> {
match self {
Place::Type(ty, _) => Some(*ty),
Place::Unbound => None,
Place::Defined(ty, _, _) => Some(*ty),
Place::Undefined => None,
}
}
#[cfg(test)]
#[track_caller]
pub(crate) fn expect_type(self) -> Type<'db> {
self.ignore_possibly_unbound()
.expect("Expected a (possibly unbound) type, not an unbound place")
self.ignore_possibly_undefined()
.expect("Expected a (possibly undefined) type, not an undefined place")
}
#[must_use]
pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Place<'db> {
match self {
Place::Type(ty, boundness) => Place::Type(f(ty), boundness),
Place::Unbound => Place::Unbound,
Place::Defined(ty, origin, definedness) => Place::Defined(f(ty), origin, definedness),
Place::Undefined => Place::Undefined,
}
}
@ -114,46 +158,47 @@ 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::Type(Type::Union(union), boundness) => union.map_with_boundness(db, |elem| {
Place::Type(*elem, boundness).try_call_dunder_get(db, owner)
}),
Place::Type(Type::Intersection(intersection), boundness) => intersection
Place::Defined(Type::Union(union), origin, definedness) => union
.map_with_boundness(db, |elem| {
Place::Type(*elem, boundness).try_call_dunder_get(db, owner)
Place::Defined(*elem, origin, definedness).try_call_dunder_get(db, owner)
}),
Place::Type(self_ty, boundness) => {
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(self_ty, origin, definedness) => {
if let Some((dunder_get_return_ty, _)) =
self_ty.try_call_dunder_get(db, Type::none(db), owner)
{
Place::Type(dunder_get_return_ty, boundness)
Place::Defined(dunder_get_return_ty, origin, definedness)
} else {
self
}
}
Place::Unbound => Place::Unbound,
Place::Undefined => Place::Undefined,
}
}
pub(crate) const fn is_definitely_bound(&self) -> bool {
matches!(self, Place::Type(_, Boundness::Bound))
matches!(self, Place::Defined(_, _, Definedness::AlwaysDefined))
}
}
impl<'db> From<LookupResult<'db>> for PlaceAndQualifiers<'db> {
fn from(value: LookupResult<'db>) -> Self {
match value {
Ok(type_and_qualifiers) => {
Place::Type(type_and_qualifiers.inner_type(), Boundness::Bound)
.with_qualifiers(type_and_qualifiers.qualifiers())
}
Err(LookupError::Unbound(qualifiers)) => Place::Unbound.with_qualifiers(qualifiers),
Err(LookupError::PossiblyUnbound(type_and_qualifiers)) => {
Place::Type(type_and_qualifiers.inner_type(), Boundness::PossiblyUnbound)
.with_qualifiers(type_and_qualifiers.qualifiers())
}
Ok(type_and_qualifiers) => Place::bound(type_and_qualifiers.inner_type())
.with_qualifiers(type_and_qualifiers.qualifiers()),
Err(LookupError::Undefined(qualifiers)) => Place::Undefined.with_qualifiers(qualifiers),
Err(LookupError::PossiblyUndefined(type_and_qualifiers)) => Place::Defined(
type_and_qualifiers.inner_type(),
TypeOrigin::Inferred,
Definedness::PossiblyUndefined,
)
.with_qualifiers(type_and_qualifiers.qualifiers()),
}
}
}
@ -161,8 +206,8 @@ impl<'db> From<LookupResult<'db>> for PlaceAndQualifiers<'db> {
/// Possible ways in which a place lookup can (possibly or definitely) fail.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum LookupError<'db> {
Unbound(TypeQualifiers),
PossiblyUnbound(TypeAndQualifiers<'db>),
Undefined(TypeQualifiers),
PossiblyUndefined(TypeAndQualifiers<'db>),
}
impl<'db> LookupError<'db> {
@ -174,15 +219,17 @@ impl<'db> LookupError<'db> {
) -> LookupResult<'db> {
let fallback = fallback.into_lookup_result();
match (&self, &fallback) {
(LookupError::Unbound(_), _) => fallback,
(LookupError::PossiblyUnbound { .. }, Err(LookupError::Unbound(_))) => Err(self),
(LookupError::PossiblyUnbound(ty), Ok(ty2)) => Ok(TypeAndQualifiers::new(
(LookupError::Undefined(_), _) => fallback,
(LookupError::PossiblyUndefined { .. }, Err(LookupError::Undefined(_))) => Err(self),
(LookupError::PossiblyUndefined(ty), Ok(ty2)) => Ok(TypeAndQualifiers::new(
UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]),
ty.origin().merge(ty2.origin()),
ty.qualifiers().union(ty2.qualifiers()),
)),
(LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => {
Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new(
(LookupError::PossiblyUndefined(ty), Err(LookupError::PossiblyUndefined(ty2))) => {
Err(LookupError::PossiblyUndefined(TypeAndQualifiers::new(
UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]),
ty.origin().merge(ty2.origin()),
ty.qualifiers().union(ty2.qualifiers()),
)))
}
@ -236,7 +283,7 @@ pub(crate) fn place<'db>(
///
/// Note that all global scopes also include various "implicit globals" such as `__name__`,
/// `__doc__` and `__file__`. This function **does not** consider those symbols; it will return
/// `Place::Unbound` for them. Use the (currently test-only) `global_symbol` query to also include
/// `Place::Undefined` for them. Use the (currently test-only) `global_symbol` query to also include
/// those additional symbols.
///
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
@ -313,7 +360,7 @@ pub(crate) fn imported_symbol<'db>(
)
.or_fall_back_to(db, || {
if name == "__getattr__" {
Place::Unbound.into()
Place::Undefined.into()
} else if name == "__builtins__" {
Place::bound(Type::any()).into()
} else {
@ -324,7 +371,7 @@ pub(crate) fn imported_symbol<'db>(
/// Lookup the type of `symbol` in the builtins namespace.
///
/// Returns `Place::Unbound` if the `builtins` module isn't available for some reason.
/// Returns `Place::Undefined` if the `builtins` module isn't available for some reason.
///
/// Note that this function is only intended for use in the context of the builtins *namespace*
/// and should not be used when a symbol is being explicitly imported from the `builtins` module
@ -354,7 +401,7 @@ pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQua
/// Lookup the type of `symbol` in a given known module.
///
/// Returns `Place::Unbound` if the given known module cannot be resolved for some reason.
/// Returns `Place::Undefined` if the given known module cannot be resolved for some reason.
pub(crate) fn known_module_symbol<'db>(
db: &'db dyn Db,
known_module: KnownModule,
@ -370,7 +417,7 @@ pub(crate) fn known_module_symbol<'db>(
/// Lookup the type of `symbol` in the `typing` module namespace.
///
/// Returns `Place::Unbound` if the `typing` module isn't available for some reason.
/// Returns `Place::Undefined` if the `typing` module isn't available for some reason.
#[inline]
#[cfg(test)]
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> {
@ -379,7 +426,7 @@ pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQuali
/// Lookup the type of `symbol` in the `typing_extensions` module namespace.
///
/// Returns `Place::Unbound` if the `typing_extensions` module isn't available for some reason.
/// Returns `Place::Undefined` if the `typing_extensions` module isn't available for some reason.
#[inline]
pub(crate) fn typing_extensions_symbol<'db>(
db: &'db dyn Db,
@ -479,7 +526,7 @@ impl<'db> PlaceFromDeclarationsResult<'db> {
/// variable: ClassVar[int]
/// ```
/// If we look up the declared type of `variable` in the scope of class `C`, we will get
/// the type `int`, a "declaredness" of [`Boundness::PossiblyUnbound`], and the information
/// the type `int`, a "declaredness" of [`Definedness::PossiblyUndefined`], and the information
/// that this comes with a [`CLASS_VAR`] type qualifier.
///
/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR
@ -492,7 +539,7 @@ pub(crate) struct PlaceAndQualifiers<'db> {
impl Default for PlaceAndQualifiers<'_> {
fn default() -> Self {
PlaceAndQualifiers {
place: Place::Unbound,
place: Place::Undefined,
qualifiers: TypeQualifiers::empty(),
}
}
@ -510,6 +557,21 @@ impl<'db> PlaceAndQualifiers<'db> {
}
}
pub(crate) fn unbound() -> Self {
PlaceAndQualifiers {
place: Place::Undefined,
qualifiers: TypeQualifiers::empty(),
}
}
pub(crate) fn is_undefined(&self) -> bool {
self.place.is_undefined()
}
pub(crate) fn ignore_possibly_undefined(&self) -> Option<Type<'db>> {
self.place.ignore_possibly_undefined()
}
/// Returns `true` if the place has a `ClassVar` type qualifier.
pub(crate) fn is_class_var(&self) -> bool {
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
@ -541,7 +603,7 @@ impl<'db> PlaceAndQualifiers<'db> {
PlaceAndQualifiers { place, qualifiers }
if (qualifiers.contains(TypeQualifiers::FINAL)
&& place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.is_some_and(|ty| ty.is_unknown())) =>
{
Some(*qualifiers)
@ -571,24 +633,24 @@ impl<'db> PlaceAndQualifiers<'db> {
}
/// Transform place and qualifiers into a [`LookupResult`],
/// a [`Result`] type in which the `Ok` variant represents a definitely bound place
/// and the `Err` variant represents a place that is either definitely or possibly unbound.
/// 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> {
match self {
PlaceAndQualifiers {
place: Place::Type(ty, Boundness::Bound),
place: Place::Defined(ty, origin, Definedness::AlwaysDefined),
qualifiers,
} => Ok(TypeAndQualifiers::new(ty, qualifiers)),
} => Ok(TypeAndQualifiers::new(ty, origin, qualifiers)),
PlaceAndQualifiers {
place: Place::Type(ty, Boundness::PossiblyUnbound),
place: Place::Defined(ty, origin, Definedness::PossiblyUndefined),
qualifiers,
} => Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new(
ty, qualifiers,
} => Err(LookupError::PossiblyUndefined(TypeAndQualifiers::new(
ty, origin, qualifiers,
))),
PlaceAndQualifiers {
place: Place::Unbound,
place: Place::Undefined,
qualifiers,
} => Err(LookupError::Unbound(qualifiers)),
} => Err(LookupError::Undefined(qualifiers)),
}
}
@ -612,9 +674,9 @@ impl<'db> PlaceAndQualifiers<'db> {
/// 1. If `self` is definitely unbound, return the result of `fallback_fn()`.
/// 2. Else, if `fallback` is definitely unbound, return `self`.
/// 3. Else, if `self` is possibly unbound and `fallback` is definitely bound,
/// return `Place(<union of self-type and fallback-type>, Boundness::Bound)`
/// return `Place(<union of self-type and fallback-type>, Definedness::AlwaysDefined)`
/// 4. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
/// return `Place(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
/// return `Place(<union of self-type and fallback-type>, Definedness::PossiblyUndefined)`
#[must_use]
pub(crate) fn or_fall_back_to(
self,
@ -693,29 +755,30 @@ 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::Type(Type::Dynamic(DynamicType::Unknown), declaredness),
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::Type(inferred, boundness) => Place::Type(
Place::Defined(inferred, origin, boundness) => Place::Defined(
UnionType::from_elements(db, [Type::unknown(), inferred]),
origin,
boundness,
)
.with_qualifiers(qualifiers),
Place::Unbound => {
Place::Type(Type::unknown(), declaredness).with_qualifiers(qualifiers)
Place::Undefined => {
Place::Defined(Type::unknown(), origin, definedness).with_qualifiers(qualifiers)
}
}
}
// Place is declared, trust the declared type
place_and_quals @ PlaceAndQualifiers {
place: Place::Type(_, Boundness::Bound),
place: Place::Defined(_, _, Definedness::AlwaysDefined),
qualifiers: _,
} => place_and_quals,
// Place is possibly declared
PlaceAndQualifiers {
place: Place::Type(declared_ty, Boundness::PossiblyUnbound),
place: Place::Defined(declared_ty, origin, Definedness::PossiblyUndefined),
qualifiers,
} => {
let bindings = all_considered_bindings();
@ -724,17 +787,18 @@ pub(crate) fn place_by_id<'db>(
let place = match inferred {
// Place is possibly undeclared and definitely unbound
Place::Unbound => {
// TODO: We probably don't want to report `Bound` here. This requires a bit of
Place::Undefined => {
// 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::Type(declared_ty, Boundness::Bound)
Place::Defined(declared_ty, origin, Definedness::AlwaysDefined)
}
// Place is possibly undeclared and (possibly) bound
Place::Type(inferred_ty, boundness) => Place::Type(
Place::Defined(inferred_ty, origin, boundness) => Place::Defined(
UnionType::from_elements(db, [inferred_ty, declared_ty]),
origin,
if boundness_analysis == BoundnessAnalysis::AssumeBound {
Boundness::Bound
Definedness::AlwaysDefined
} else {
boundness
},
@ -745,7 +809,7 @@ pub(crate) fn place_by_id<'db>(
}
// Place is undeclared, return the union of `Unknown` with the inferred type
PlaceAndQualifiers {
place: Place::Unbound,
place: Place::Undefined,
qualifiers: _,
} => {
let bindings = all_considered_bindings();
@ -753,8 +817,8 @@ pub(crate) fn place_by_id<'db>(
let mut inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport);
if boundness_analysis == BoundnessAnalysis::AssumeBound {
if let Place::Type(ty, Boundness::PossiblyUnbound) = inferred {
inferred = Place::Type(ty, Boundness::Bound);
if let Place::Defined(ty, origin, Definedness::PossiblyUndefined) = inferred {
inferred = Place::Defined(ty, origin, Definedness::AlwaysDefined);
}
}
@ -1026,25 +1090,27 @@ fn place_from_bindings_impl<'db>(
};
let boundness = match boundness_analysis {
BoundnessAnalysis::AssumeBound => Boundness::Bound,
BoundnessAnalysis::AssumeBound => Definedness::AlwaysDefined,
BoundnessAnalysis::BasedOnUnboundVisibility => match unbound_visibility() {
Some(Truthiness::AlwaysTrue) => {
unreachable!(
"If we have at least one binding, the implicit `unbound` binding should not be definitely visible"
)
}
Some(Truthiness::AlwaysFalse) | None => Boundness::Bound,
Some(Truthiness::Ambiguous) => Boundness::PossiblyUnbound,
Some(Truthiness::AlwaysFalse) | None => Definedness::AlwaysDefined,
Some(Truthiness::Ambiguous) => Definedness::PossiblyUndefined,
},
};
match deleted_reachability {
Truthiness::AlwaysFalse => Place::Type(ty, boundness),
Truthiness::AlwaysTrue => Place::Unbound,
Truthiness::Ambiguous => Place::Type(ty, Boundness::PossiblyUnbound),
Truthiness::AlwaysFalse => Place::Defined(ty, TypeOrigin::Inferred, boundness),
Truthiness::AlwaysTrue => Place::Undefined,
Truthiness::Ambiguous => {
Place::Defined(ty, TypeOrigin::Inferred, Definedness::PossiblyUndefined)
}
}
} else {
Place::Unbound
Place::Undefined
}
}
@ -1145,7 +1211,8 @@ impl<'db> DeclaredTypeBuilder<'db> {
}
fn build(mut self) -> DeclaredTypeAndConflictingTypes<'db> {
let type_and_quals = TypeAndQualifiers::new(self.inner.build(), self.qualifiers);
let type_and_quals =
TypeAndQualifiers::new(self.inner.build(), TypeOrigin::Declared, self.qualifiers);
if self.conflicting_types.is_empty() {
(type_and_quals, None)
} else {
@ -1245,13 +1312,13 @@ fn place_from_declarations_impl<'db>(
let boundness = match boundness_analysis {
BoundnessAnalysis::AssumeBound => {
if all_declarations_definitely_reachable {
Boundness::Bound
Definedness::AlwaysDefined
} else {
// For declarations, it is important to consider the possibility that they might only
// be bound in one control flow path, while the other path contains a binding. In order
// to even consider the bindings as well in `place_by_id`, we return `PossiblyUnbound`
// here.
Boundness::PossiblyUnbound
Definedness::PossiblyUndefined
}
}
BoundnessAnalysis::BasedOnUnboundVisibility => match undeclared_reachability {
@ -1260,13 +1327,14 @@ fn place_from_declarations_impl<'db>(
"If we have at least one declaration, the implicit `unbound` binding should not be definitely visible"
)
}
Truthiness::AlwaysFalse => Boundness::Bound,
Truthiness::Ambiguous => Boundness::PossiblyUnbound,
Truthiness::AlwaysFalse => Definedness::AlwaysDefined,
Truthiness::Ambiguous => Definedness::PossiblyUndefined,
},
};
let place_and_quals =
Place::Type(declared.inner_type(), boundness).with_qualifiers(declared.qualifiers());
Place::Defined(declared.inner_type(), TypeOrigin::Declared, boundness)
.with_qualifiers(declared.qualifiers());
if let Some(conflicting) = conflicting {
PlaceFromDeclarationsResult::conflict(place_and_quals, conflicting)
@ -1279,7 +1347,7 @@ fn place_from_declarations_impl<'db>(
}
} else {
PlaceFromDeclarationsResult {
place_and_quals: Place::Unbound.into(),
place_and_quals: Place::Undefined.into(),
conflicting_types: None,
single_declaration: None,
}
@ -1314,7 +1382,7 @@ mod implicit_globals {
use crate::Program;
use crate::db::Db;
use crate::place::{Boundness, PlaceAndQualifiers};
use crate::place::{Definedness, PlaceAndQualifiers, TypeOrigin};
use crate::semantic_index::symbol::Symbol;
use crate::semantic_index::{place_table, use_def_map};
use crate::types::{CallableType, KnownClass, Parameter, Parameters, Signature, Type};
@ -1330,16 +1398,16 @@ mod implicit_globals {
.iter()
.any(|module_type_member| module_type_member == name)
{
return Place::Unbound.into();
return Place::Undefined.into();
}
let Type::ClassLiteral(module_type_class) = KnownClass::ModuleType.to_class_literal(db)
else {
return Place::Unbound.into();
return Place::Undefined.into();
};
let module_type_scope = module_type_class.body_scope(db);
let place_table = place_table(db, module_type_scope);
let Some(symbol_id) = place_table.symbol_id(name) else {
return Place::Unbound.into();
return Place::Undefined.into();
};
place_from_declarations(
db,
@ -1348,7 +1416,7 @@ mod implicit_globals {
.ignore_conflicting_declarations()
}
/// Looks up the type of an "implicit global symbol". Returns [`Place::Unbound`] if
/// Looks up the type of an "implicit global symbol". Returns [`Place::Undefined`] if
/// `name` is not present as an implicit symbol in module-global namespaces.
///
/// Implicit global symbols are symbols such as `__doc__`, `__name__`, and `__file__`
@ -1359,7 +1427,7 @@ mod implicit_globals {
/// up in the global scope **from within the same file**. If the symbol is being looked up
/// from outside the file (e.g. via imports), use [`super::imported_symbol`] (or fallback logic
/// like the logic used in that function) instead. The reason is that this function returns
/// [`Place::Unbound`] for `__init__` and `__dict__` (which cannot be found in globals if
/// [`Place::Undefined`] for `__init__` and `__dict__` (which cannot be found in globals if
/// the lookup is being done from the same file) -- but these symbols *are* available in the
/// global scope if they're being imported **from a different file**.
pub(crate) fn module_type_implicit_global_symbol<'db>(
@ -1378,10 +1446,11 @@ mod implicit_globals {
// Created lazily by the warnings machinery; may be absent.
// Model as possibly-unbound to avoid false negatives.
"__warningregistry__" => Place::Type(
"__warningregistry__" => Place::Defined(
KnownClass::Dict
.to_specialized_instance(db, [Type::any(), KnownClass::Int.to_instance(db)]),
Boundness::PossiblyUnbound,
TypeOrigin::Inferred,
Definedness::PossiblyUndefined,
)
.into(),
@ -1398,9 +1467,10 @@ mod implicit_globals {
[KnownClass::Str.to_instance(db), Type::any()],
)),
);
Place::Type(
Place::Defined(
CallableType::function_like(db, signature),
Boundness::PossiblyUnbound,
TypeOrigin::Inferred,
Definedness::PossiblyUndefined,
)
.into()
}
@ -1417,7 +1487,7 @@ mod implicit_globals {
KnownClass::ModuleType.to_instance(db).member(db, name)
}
_ => Place::Unbound.into(),
_ => Place::Undefined.into(),
}
}
@ -1545,7 +1615,7 @@ pub(crate) enum BoundnessAnalysis {
/// `unbound` binding. In the example below, when analyzing the visibility of the
/// `x = <unbound>` binding from the position of the end of the scope, it would be
/// `Truthiness::Ambiguous`, because it could either be visible or not, depending on the
/// `flag()` return value. This would result in a `Boundness::PossiblyUnbound` for `x`.
/// `flag()` return value. This would result in a `Definedness::PossiblyUndefined` for `x`.
///
/// ```py
/// x = <unbound>
@ -1563,21 +1633,30 @@ mod tests {
#[test]
fn test_symbol_or_fall_back_to() {
use Boundness::{Bound, PossiblyUnbound};
use Definedness::{AlwaysDefined, PossiblyUndefined};
use TypeOrigin::Inferred;
let db = setup_db();
let ty1 = Type::IntLiteral(1);
let ty2 = Type::IntLiteral(2);
let unbound = || Place::Unbound.with_qualifiers(TypeQualifiers::empty());
let unbound = || Place::Undefined.with_qualifiers(TypeQualifiers::empty());
let possibly_unbound_ty1 =
|| Place::Type(ty1, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty());
let possibly_unbound_ty2 =
|| Place::Type(ty2, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty());
let possibly_unbound_ty1 = || {
Place::Defined(ty1, Inferred, PossiblyUndefined)
.with_qualifiers(TypeQualifiers::empty())
};
let possibly_unbound_ty2 = || {
Place::Defined(ty2, Inferred, PossiblyUndefined)
.with_qualifiers(TypeQualifiers::empty())
};
let bound_ty1 = || Place::Type(ty1, Bound).with_qualifiers(TypeQualifiers::empty());
let bound_ty2 = || Place::Type(ty2, Bound).with_qualifiers(TypeQualifiers::empty());
let bound_ty1 = || {
Place::Defined(ty1, Inferred, AlwaysDefined).with_qualifiers(TypeQualifiers::empty())
};
let bound_ty2 = || {
Place::Defined(ty2, Inferred, AlwaysDefined).with_qualifiers(TypeQualifiers::empty())
};
// Start from an unbound symbol
assert_eq!(unbound().or_fall_back_to(&db, unbound), unbound());
@ -1594,11 +1673,21 @@ mod tests {
);
assert_eq!(
possibly_unbound_ty1().or_fall_back_to(&db, possibly_unbound_ty2),
Place::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound).into()
Place::Defined(
UnionType::from_elements(&db, [ty1, ty2]),
Inferred,
PossiblyUndefined
)
.into()
);
assert_eq!(
possibly_unbound_ty1().or_fall_back_to(&db, bound_ty2),
Place::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound).into()
Place::Defined(
UnionType::from_elements(&db, [ty1, ty2]),
Inferred,
AlwaysDefined
)
.into()
);
// Start from a definitely bound symbol
@ -1614,7 +1703,7 @@ mod tests {
fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Place<'db>) {
assert!(matches!(
symbol,
Place::Type(Type::NominalInstance(_), Boundness::Bound)
Place::Defined(Type::NominalInstance(_), _, Definedness::AlwaysDefined)
));
assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db));
}

View file

@ -917,13 +917,17 @@ impl ReachabilityConstraints {
)
.place
{
crate::place::Place::Type(_, crate::place::Boundness::Bound) => {
Truthiness::AlwaysTrue
}
crate::place::Place::Type(_, crate::place::Boundness::PossiblyUnbound) => {
Truthiness::Ambiguous
}
crate::place::Place::Unbound => Truthiness::AlwaysFalse,
crate::place::Place::Defined(
_,
_,
crate::place::Definedness::AlwaysDefined,
) => Truthiness::AlwaysTrue,
crate::place::Place::Defined(
_,
_,
crate::place::Definedness::PossiblyUndefined,
) => Truthiness::Ambiguous,
crate::place::Place::Undefined => Truthiness::AlwaysFalse,
}
}
}

View file

@ -32,7 +32,7 @@ pub(crate) use self::signatures::{CallableSignature, Parameter, Parameters, Sign
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
use crate::module_name::ModuleName;
use crate::module_resolver::{KnownModule, resolve_module};
use crate::place::{Boundness, Place, PlaceAndQualifiers, imported_symbol};
use crate::place::{Definedness, Place, PlaceAndQualifiers, TypeOrigin, imported_symbol};
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::place::ScopedPlaceId;
use crate::semantic_index::scope::ScopeId;
@ -1440,7 +1440,7 @@ impl<'db> Type<'db> {
)
.place;
if let Place::Type(ty, Boundness::Bound) = call_symbol {
if let Place::Defined(ty, _, Definedness::AlwaysDefined) = call_symbol {
ty.try_upcast_to_callable(db)
} else {
None
@ -2533,7 +2533,7 @@ impl<'db> Type<'db> {
other
.member(db, member.name())
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.when_none_or(|attribute_type| {
member.has_disjoint_type_from(
db,
@ -2899,14 +2899,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::Type(attribute_type, _) => member.has_disjoint_type_from(
Place::Defined(attribute_type, _, _) => member.has_disjoint_type_from(
db,
attribute_type,
inferable,
disjointness_visitor,
relation_visitor,
),
Place::Unbound => ConstraintSet::from(false),
Place::Undefined => ConstraintSet::from(false),
}
})
})
@ -3136,7 +3136,7 @@ impl<'db> Type<'db> {
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.when_none_or(|dunder_call| {
dunder_call
.has_relation_to_impl(
@ -3458,7 +3458,7 @@ impl<'db> Type<'db> {
),
(Some(KnownClass::FunctionType), "__set__" | "__delete__") => {
// Hard code this knowledge, as we look up `__set__` and `__delete__` on `FunctionType` often.
Some(Place::Unbound.into())
Some(Place::Undefined.into())
}
(Some(KnownClass::Property), "__get__") => Some(
Place::bound(Type::WrapperDescriptor(
@ -3511,7 +3511,7 @@ impl<'db> Type<'db> {
// MRO of the class `object`.
Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Type) => {
if policy.mro_no_object_fallback() {
Some(Place::Unbound.into())
Some(Place::Undefined.into())
} else {
KnownClass::Object
.to_class_literal(db)
@ -3672,7 +3672,7 @@ impl<'db> Type<'db> {
of type variable {} in inferable position",
self.display(db)
);
Place::Unbound.into()
Place::Undefined.into()
}
Type::IntLiteral(_) => KnownClass::Int.to_instance(db).instance_member(db, name),
@ -3692,7 +3692,7 @@ impl<'db> Type<'db> {
.to_instance(db)
.instance_member(db, name),
Type::SpecialForm(_) | Type::KnownInstance(_) => Place::Unbound.into(),
Type::SpecialForm(_) | Type::KnownInstance(_) => Place::Undefined.into(),
Type::PropertyInstance(_) => KnownClass::Property
.to_instance(db)
@ -3710,10 +3710,10 @@ impl<'db> Type<'db> {
// required, as `instance_member` is only called for instance-like types through `member`,
// but we might want to add this in the future.
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => {
Place::Unbound.into()
Place::Undefined.into()
}
Type::TypedDict(_) => Place::Unbound.into(),
Type::TypedDict(_) => Place::Undefined.into(),
Type::TypeAlias(alias) => alias.value_type(db).instance_member(db, name),
}
@ -3726,9 +3726,9 @@ 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::Type(_, _) = 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::Type(_, _)) =
} else if let Some(place @ Place::Defined(_, _, _)) =
self.find_name_in_mro(db, name).map(|inner| inner.place)
{
place
@ -3781,11 +3781,11 @@ impl<'db> Type<'db> {
let descr_get = self.class_member(db, "__get__".into()).place;
if let Place::Type(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| {
if descr_get_boundness == Boundness::Bound {
if descr_get_boundness == Definedness::AlwaysDefined {
bindings.return_type(db)
} else {
UnionType::from_elements(db, [bindings.return_type(db), self])
@ -3824,19 +3824,20 @@ impl<'db> Type<'db> {
//
// The same is true for `Never`.
PlaceAndQualifiers {
place: Place::Type(Type::Dynamic(_) | Type::Never, _),
place: Place::Defined(Type::Dynamic(_) | Type::Never, _, _),
qualifiers: _,
} => (attribute, AttributeKind::DataDescriptor),
PlaceAndQualifiers {
place: Place::Type(Type::Union(union), boundness),
place: Place::Defined(Type::Union(union), origin, boundness),
qualifiers,
} => (
union
.map_with_boundness(db, |elem| {
Place::Type(
Place::Defined(
elem.try_call_dunder_get(db, instance, owner)
.map_or(*elem, |(ty, _)| ty),
origin,
boundness,
)
})
@ -3853,14 +3854,15 @@ impl<'db> Type<'db> {
),
PlaceAndQualifiers {
place: Place::Type(Type::Intersection(intersection), boundness),
place: Place::Defined(Type::Intersection(intersection), origin, boundness),
qualifiers,
} => (
intersection
.map_with_boundness(db, |elem| {
Place::Type(
Place::Defined(
elem.try_call_dunder_get(db, instance, owner)
.map_or(*elem, |(ty, _)| ty),
origin,
boundness,
)
})
@ -3870,13 +3872,16 @@ impl<'db> Type<'db> {
),
PlaceAndQualifiers {
place: Place::Type(attribute_ty, boundness),
place: Place::Defined(attribute_ty, origin, boundness),
qualifiers: _,
} => {
if let Some((return_ty, attribute_kind)) =
attribute_ty.try_call_dunder_get(db, instance, owner)
{
(Place::Type(return_ty, boundness).into(), attribute_kind)
(
Place::Defined(return_ty, origin, boundness).into(),
attribute_kind,
)
} else {
(attribute, AttributeKind::NormalOrNonDataDescriptor)
}
@ -3915,11 +3920,11 @@ impl<'db> Type<'db> {
.iter_positive(db)
.any(|ty| ty.is_data_descriptor_impl(db, any_of_union)),
_ => {
!self.class_member(db, "__set__".into()).place.is_unbound()
!self.class_member(db, "__set__".into()).place.is_undefined()
|| !self
.class_member(db, "__delete__".into())
.place
.is_unbound()
.is_undefined()
}
}
}
@ -3967,25 +3972,28 @@ 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::Type(_, _), _, Place::Unbound) => {
(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::Type(_, Boundness::Bound), AttributeKind::DataDescriptor, _) => {
meta_attr.with_qualifiers(meta_attr_qualifiers)
}
(
meta_attr @ Place::Defined(_, _, Definedness::AlwaysDefined),
AttributeKind::DataDescriptor,
_,
) => meta_attr.with_qualifiers(meta_attr_qualifiers),
// `meta_attr` is the return type of a data descriptor, but the attribute on the
// 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::Type(meta_attr_ty, Boundness::PossiblyUnbound),
Place::Defined(meta_attr_ty, meta_origin, Definedness::PossiblyUndefined),
AttributeKind::DataDescriptor,
Place::Type(fallback_ty, fallback_boundness),
) => Place::Type(
Place::Defined(fallback_ty, fallback_origin, fallback_boundness),
) => Place::Defined(
UnionType::from_elements(db, [meta_attr_ty, fallback_ty]),
meta_origin.merge(fallback_origin),
fallback_boundness,
)
.with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)),
@ -3999,9 +4007,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::Type(_, _),
Place::Defined(_, _, _),
AttributeKind::NormalOrNonDataDescriptor,
fallback @ Place::Type(_, Boundness::Bound),
fallback @ Place::Defined(_, _, Definedness::AlwaysDefined),
) if policy == InstanceFallbackShadowsNonDataDescriptor::Yes => {
fallback.with_qualifiers(fallback_qualifiers)
}
@ -4010,17 +4018,18 @@ 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::Type(meta_attr_ty, meta_attr_boundness),
Place::Defined(meta_attr_ty, meta_origin, meta_attr_boundness),
AttributeKind::NormalOrNonDataDescriptor,
Place::Type(fallback_ty, fallback_boundness),
) => Place::Type(
Place::Defined(fallback_ty, fallback_origin, fallback_boundness),
) => Place::Defined(
UnionType::from_elements(db, [meta_attr_ty, fallback_ty]),
meta_origin.merge(fallback_origin),
meta_attr_boundness.max(fallback_boundness),
)
.with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)),
// If the attribute is not found on the meta-type, we simply return the fallback.
(Place::Unbound, _, fallback) => fallback.with_qualifiers(fallback_qualifiers),
(Place::Undefined, _, fallback) => fallback.with_qualifiers(fallback_qualifiers),
}
}
@ -4183,7 +4192,7 @@ impl<'db> Type<'db> {
Type::ModuleLiteral(module) => module.static_member(db, name_str),
// If a protocol does not include a member and the policy disables falling back to
// `object`, we return `Place::Unbound` here. This short-circuits attribute lookup
// `object`, we return `Place::Undefined` here. This short-circuits attribute lookup
// before we find the "fallback to attribute access on `object`" logic later on
// (otherwise we would infer that all synthesized protocols have `__getattribute__`
// methods, and therefore that all synthesized protocols have all possible attributes.)
@ -4196,13 +4205,13 @@ impl<'db> Type<'db> {
}) if policy.mro_no_object_fallback()
&& !protocol.interface().includes_member(db, name_str) =>
{
Place::Unbound.into()
Place::Undefined.into()
}
_ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
db,
name_str,
Place::Unbound.into(),
Place::Undefined.into(),
InstanceFallbackShadowsNonDataDescriptor::No,
policy,
),
@ -4230,7 +4239,7 @@ impl<'db> Type<'db> {
{
enum_metadata(db, enum_literal.enum_class(db))
.and_then(|metadata| metadata.members.get(enum_literal.name(db)))
.map_or_else(|| Place::Unbound, Place::bound)
.map_or_else(|| Place::Undefined, Place::bound)
.into()
}
@ -4275,7 +4284,7 @@ impl<'db> Type<'db> {
.and_then(|instance| instance.known_class(db)),
Some(KnownClass::ModuleType | KnownClass::GenericAlias)
) {
return Place::Unbound.into();
return Place::Undefined.into();
}
self.try_call_dunder(
@ -4286,14 +4295,14 @@ impl<'db> Type<'db> {
)
.map(|outcome| Place::bound(outcome.return_type(db)))
// TODO: Handle call errors here.
.unwrap_or(Place::Unbound)
.unwrap_or(Place::Undefined)
.into()
};
let custom_getattribute_result = || {
// Avoid cycles when looking up `__getattribute__`
if "__getattribute__" == name.as_str() {
return Place::Unbound.into();
return Place::Undefined.into();
}
// Typeshed has a `__getattribute__` method defined on `builtins.object` so we
@ -4307,29 +4316,29 @@ impl<'db> Type<'db> {
)
.map(|outcome| Place::bound(outcome.return_type(db)))
// TODO: Handle call errors here.
.unwrap_or(Place::Unbound)
.unwrap_or(Place::Undefined)
.into()
};
if result.is_class_var() && self.is_typed_dict() {
// `ClassVar`s on `TypedDictFallback` can not be accessed on inhabitants of `SomeTypedDict`.
// They can only be accessed on `SomeTypedDict` directly.
return Place::Unbound.into();
return Place::Undefined.into();
}
match result {
member @ PlaceAndQualifiers {
place: Place::Type(_, Boundness::Bound),
place: Place::Defined(_, _, Definedness::AlwaysDefined),
qualifiers: _,
} => member,
member @ PlaceAndQualifiers {
place: Place::Type(_, Boundness::PossiblyUnbound),
place: Place::Defined(_, _, Definedness::PossiblyUndefined),
qualifiers: _,
} => member
.or_fall_back_to(db, custom_getattribute_result)
.or_fall_back_to(db, custom_getattr_result),
PlaceAndQualifiers {
place: Place::Unbound,
place: Place::Undefined,
qualifiers: _,
} => custom_getattribute_result().or_fall_back_to(db, custom_getattr_result),
}
@ -4354,14 +4363,11 @@ impl<'db> Type<'db> {
} {
if let Some(metadata) = enum_metadata(db, enum_class) {
if let Some(resolved_name) = metadata.resolve_member(&name) {
return Place::Type(
Type::EnumLiteral(EnumLiteralType::new(
db,
enum_class,
resolved_name,
)),
Boundness::Bound,
)
return Place::bound(Type::EnumLiteral(EnumLiteralType::new(
db,
enum_class,
resolved_name,
)))
.into();
}
}
@ -5367,15 +5373,15 @@ impl<'db> Type<'db> {
)
.place
{
Place::Type(dunder_callable, boundness) => {
Place::Defined(dunder_callable, _, boundness) => {
let mut bindings = dunder_callable.bindings(db);
bindings.replace_callable_type(dunder_callable, self);
if boundness == Boundness::PossiblyUnbound {
if boundness == Definedness::PossiblyUndefined {
bindings.set_dunder_call_is_possibly_unbound();
}
bindings
}
Place::Unbound => CallableBinding::not_callable(self).into(),
Place::Undefined => CallableBinding::not_callable(self).into(),
}
}
@ -5488,17 +5494,17 @@ impl<'db> Type<'db> {
)
.place
{
Place::Type(dunder_callable, boundness) => {
Place::Defined(dunder_callable, _, boundness) => {
let bindings = dunder_callable
.bindings(db)
.match_parameters(db, argument_types)
.check_types(db, argument_types, &tcx)?;
if boundness == Boundness::PossiblyUnbound {
if boundness == Definedness::PossiblyUndefined {
return Err(CallDunderError::PossiblyUnbound(Box::new(bindings)));
}
Ok(bindings)
}
Place::Unbound => Err(CallDunderError::MethodNotAvailable),
Place::Undefined => Err(CallDunderError::MethodNotAvailable),
}
}
@ -6005,16 +6011,16 @@ impl<'db> Type<'db> {
let new_method = self_type.lookup_dunder_new(db, ());
let new_call_outcome = new_method.and_then(|new_method| {
match new_method.place.try_call_dunder_get(db, self_type) {
Place::Type(new_method, boundness) => {
Place::Defined(new_method, _, boundness) => {
let result =
new_method.try_call(db, argument_types.with_self(Some(self_type)).as_ref());
if boundness == Boundness::PossiblyUnbound {
if boundness == Definedness::PossiblyUndefined {
Some(Err(DunderNewCallError::PossiblyUnbound(result.err())))
} else {
Some(result.map_err(DunderNewCallError::CallError))
}
}
Place::Unbound => None,
Place::Undefined => None,
}
});
@ -6034,7 +6040,7 @@ impl<'db> Type<'db> {
| MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
)
.place
.is_unbound()
.is_undefined()
{
Some(init_ty.try_call_dunder(db, "__init__", argument_types, tcx))
} else {
@ -7743,18 +7749,23 @@ impl TypeQualifiers {
#[derive(Clone, Debug, Copy, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
pub(crate) struct TypeAndQualifiers<'db> {
inner: Type<'db>,
origin: TypeOrigin,
qualifiers: TypeQualifiers,
}
impl<'db> TypeAndQualifiers<'db> {
pub(crate) fn new(inner: Type<'db>, qualifiers: TypeQualifiers) -> Self {
Self { inner, qualifiers }
pub(crate) fn new(inner: Type<'db>, origin: TypeOrigin, qualifiers: TypeQualifiers) -> Self {
Self {
inner,
origin,
qualifiers,
}
}
/// Constructor that creates a [`TypeAndQualifiers`] instance with type `Unknown` and no qualifiers.
pub(crate) fn unknown() -> Self {
pub(crate) fn declared(inner: Type<'db>) -> Self {
Self {
inner: Type::unknown(),
inner,
origin: TypeOrigin::Declared,
qualifiers: TypeQualifiers::empty(),
}
}
@ -7764,6 +7775,10 @@ impl<'db> TypeAndQualifiers<'db> {
self.inner
}
pub(crate) fn origin(&self) -> TypeOrigin {
self.origin
}
/// Insert/add an additional type qualifier.
pub(crate) fn add_qualifier(&mut self, qualifier: TypeQualifiers) {
self.qualifiers |= qualifier;
@ -7775,15 +7790,6 @@ impl<'db> TypeAndQualifiers<'db> {
}
}
impl<'db> From<Type<'db>> for TypeAndQualifiers<'db> {
fn from(inner: Type<'db>) -> Self {
Self {
inner,
qualifiers: TypeQualifiers::empty(),
}
}
}
/// Error struct providing information on type(s) that were deemed to be invalid
/// in a type expression context, and the type we should therefore fallback to
/// for the problematic type expression.
@ -7929,7 +7935,7 @@ impl<'db> InvalidTypeExpression<'db> {
let Some(module_member_with_same_name) = ty
.member(db, module_name_final_part)
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
else {
return;
};
@ -10676,18 +10682,18 @@ 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::Type(getattr_type, boundness) = getattr_symbol.place {
if let Place::Defined(getattr_type, origin, boundness) = 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 Place::Type(outcome.return_type(db), boundness).into();
return Place::Defined(outcome.return_type(db), origin, boundness).into();
}
}
}
Place::Unbound.into()
Place::Undefined.into()
}
fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
@ -10722,7 +10728,7 @@ impl<'db> ModuleLiteralType<'db> {
.unwrap_or_default();
// If the normal lookup failed, try to call the module's `__getattr__` function
if place_and_qualifiers.place.is_unbound() {
if place_and_qualifiers.place.is_undefined() {
return self.try_module_getattr(db, name);
}
@ -11119,14 +11125,16 @@ impl<'db> UnionType<'db> {
let mut all_unbound = true;
let mut possibly_unbound = false;
let mut origin = TypeOrigin::Declared;
for ty in self.elements(db) {
let ty_member = transform_fn(ty);
match ty_member {
Place::Unbound => {
Place::Undefined => {
possibly_unbound = true;
}
Place::Type(ty_member, member_boundness) => {
if member_boundness == Boundness::PossiblyUnbound {
Place::Defined(ty_member, member_origin, member_boundness) => {
origin = origin.merge(member_origin);
if member_boundness == Definedness::PossiblyUndefined {
possibly_unbound = true;
}
@ -11137,14 +11145,15 @@ impl<'db> UnionType<'db> {
}
if all_unbound {
Place::Unbound
Place::Undefined
} else {
Place::Type(
Place::Defined(
builder.build(),
origin,
if possibly_unbound {
Boundness::PossiblyUnbound
Definedness::PossiblyUndefined
} else {
Boundness::Bound
Definedness::AlwaysDefined
},
)
}
@ -11160,6 +11169,7 @@ impl<'db> UnionType<'db> {
let mut all_unbound = true;
let mut possibly_unbound = false;
let mut origin = TypeOrigin::Declared;
for ty in self.elements(db) {
let PlaceAndQualifiers {
place: ty_member,
@ -11167,11 +11177,12 @@ impl<'db> UnionType<'db> {
} = transform_fn(ty);
qualifiers |= new_qualifiers;
match ty_member {
Place::Unbound => {
Place::Undefined => {
possibly_unbound = true;
}
Place::Type(ty_member, member_boundness) => {
if member_boundness == Boundness::PossiblyUnbound {
Place::Defined(ty_member, member_origin, member_boundness) => {
origin = origin.merge(member_origin);
if member_boundness == Definedness::PossiblyUndefined {
possibly_unbound = true;
}
@ -11182,14 +11193,15 @@ impl<'db> UnionType<'db> {
}
PlaceAndQualifiers {
place: if all_unbound {
Place::Unbound
Place::Undefined
} else {
Place::Type(
Place::Defined(
builder.build(),
origin,
if possibly_unbound {
Boundness::PossiblyUnbound
Definedness::PossiblyUndefined
} else {
Boundness::Bound
Definedness::AlwaysDefined
},
)
},
@ -11394,13 +11406,15 @@ impl<'db> IntersectionType<'db> {
let mut all_unbound = true;
let mut any_definitely_bound = false;
let mut origin = TypeOrigin::Declared;
for ty in self.positive_elements_or_object(db) {
let ty_member = transform_fn(&ty);
match ty_member {
Place::Unbound => {}
Place::Type(ty_member, member_boundness) => {
Place::Undefined => {}
Place::Defined(ty_member, member_origin, member_boundness) => {
origin = origin.merge(member_origin);
all_unbound = false;
if member_boundness == Boundness::Bound {
if member_boundness == Definedness::AlwaysDefined {
any_definitely_bound = true;
}
@ -11410,14 +11424,15 @@ impl<'db> IntersectionType<'db> {
}
if all_unbound {
Place::Unbound
Place::Undefined
} else {
Place::Type(
Place::Defined(
builder.build(),
origin,
if any_definitely_bound {
Boundness::Bound
Definedness::AlwaysDefined
} else {
Boundness::PossiblyUnbound
Definedness::PossiblyUndefined
},
)
}
@ -11433,6 +11448,7 @@ impl<'db> IntersectionType<'db> {
let mut all_unbound = true;
let mut any_definitely_bound = false;
let mut origin = TypeOrigin::Declared;
for ty in self.positive_elements_or_object(db) {
let PlaceAndQualifiers {
place: member,
@ -11440,10 +11456,11 @@ impl<'db> IntersectionType<'db> {
} = transform_fn(&ty);
qualifiers |= new_qualifiers;
match member {
Place::Unbound => {}
Place::Type(ty_member, member_boundness) => {
Place::Undefined => {}
Place::Defined(ty_member, member_origin, member_boundness) => {
origin = origin.merge(member_origin);
all_unbound = false;
if member_boundness == Boundness::Bound {
if member_boundness == Definedness::AlwaysDefined {
any_definitely_bound = true;
}
@ -11454,14 +11471,15 @@ impl<'db> IntersectionType<'db> {
PlaceAndQualifiers {
place: if all_unbound {
Place::Unbound
Place::Undefined
} else {
Place::Type(
Place::Defined(
builder.build(),
origin,
if any_definitely_bound {
Boundness::Bound
Definedness::AlwaysDefined
} else {
Boundness::PossiblyUnbound
Definedness::PossiblyUndefined
},
)
},

View file

@ -1255,7 +1255,7 @@ mod tests {
let safe_uuid_class = known_module_symbol(&db, KnownModule::Uuid, "SafeUUID")
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.unwrap();
let literals = enum_member_literals(&db, safe_uuid_class.expect_class_literal(), None)

View file

@ -15,7 +15,7 @@ use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Sig
use crate::Program;
use crate::db::Db;
use crate::dunder_all::dunder_all_names;
use crate::place::{Boundness, Place};
use crate::place::{Definedness, Place};
use crate::types::call::arguments::{Expansion, is_expandable_type};
use crate::types::diagnostic::{
CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT,
@ -839,7 +839,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::Type(ty, Boundness::Bound) => {
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
@ -849,10 +849,10 @@ impl<'db> Bindings<'db> {
ty
}
}
Place::Type(ty, Boundness::PossiblyUnbound) => {
Place::Defined(ty, _, Definedness::PossiblyUndefined) => {
union_with_default(ty)
}
Place::Unbound => default,
Place::Undefined => default,
},
);
}
@ -2399,7 +2399,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
)
.place
}) {
Some(Place::Type(keys_method, Boundness::Bound)) => keys_method
Some(Place::Defined(keys_method, _, Definedness::AlwaysDefined)) => keys_method
.try_call(db, &CallArguments::positional([Type::unknown()]))
.ok()
.map_or_else(Type::unknown, |bindings| bindings.return_type(db)),
@ -2717,7 +2717,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
)
.place
{
Place::Type(keys_method, Boundness::Bound) => keys_method
Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method
.try_call(self.db, &CallArguments::none())
.ok()
.and_then(|bindings| {
@ -2762,7 +2762,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
)
.place
{
Place::Type(keys_method, Boundness::Bound) => keys_method
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)),

View file

@ -9,6 +9,7 @@ use super::{
};
use crate::FxOrderMap;
use crate::module_resolver::KnownModule;
use crate::place::TypeOrigin;
use crate::semantic_index::definition::{Definition, DefinitionState};
use crate::semantic_index::scope::{NodeWithScopeKind, Scope};
use crate::semantic_index::symbol::Symbol;
@ -42,7 +43,7 @@ use crate::{
Db, FxIndexMap, FxIndexSet, FxOrderSet, Program,
module_resolver::file_to_module,
place::{
Boundness, LookupError, LookupResult, Place, PlaceAndQualifiers, known_module_symbol,
Definedness, LookupError, LookupResult, Place, PlaceAndQualifiers, known_module_symbol,
place_from_bindings, place_from_declarations,
},
semantic_index::{
@ -749,7 +750,7 @@ impl<'db> ClassType<'db> {
/// class that the lookup is being performed on, and not the class containing the (possibly
/// inherited) member.
///
/// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope
/// Returns [`Place::Undefined`] if `name` cannot be found in this class's scope
/// directly. Use [`ClassType::class_member`] if you require a method that will
/// traverse through the MRO until it finds the member.
pub(super) fn own_class_member(
@ -1055,7 +1056,7 @@ impl<'db> ClassType<'db> {
let (class_literal, specialization) = self.class_literal(db);
if class_literal.is_typed_dict(db) {
return Place::Unbound.into();
return Place::Undefined.into();
}
class_literal
@ -1086,7 +1087,7 @@ impl<'db> ClassType<'db> {
)
.place;
if let Place::Type(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
@ -1105,7 +1106,7 @@ impl<'db> ClassType<'db> {
.place;
let dunder_new_signature = dunder_new_function_symbol
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.and_then(|ty| match ty {
Type::FunctionLiteral(function) => Some(function.signature(db)),
Type::Callable(callable) => Some(callable.signatures(db)),
@ -1156,7 +1157,7 @@ impl<'db> ClassType<'db> {
// 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::Type(ty, _) = dunder_init_function_symbol {
if let Place::Defined(ty, _, _) = dunder_init_function_symbol {
let signature = match ty {
Type::FunctionLiteral(dunder_init_function) => {
Some(dunder_init_function.signature(db))
@ -1208,7 +1209,9 @@ impl<'db> ClassType<'db> {
)
.place;
if let Place::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol {
if let Place::Defined(Type::FunctionLiteral(new_function), _, _) =
new_function_symbol
{
Type::Callable(
new_function
.into_bound_method_type(db, correct_return_type)
@ -2077,7 +2080,7 @@ impl<'db> ClassLiteral<'db> {
let mut dynamic_type_to_intersect_with: Option<Type<'db>> = None;
let mut lookup_result: LookupResult<'db> =
Err(LookupError::Unbound(TypeQualifiers::empty()));
Err(LookupError::Undefined(TypeQualifiers::empty()));
for superclass in mro_iter {
match superclass {
@ -2153,7 +2156,7 @@ impl<'db> ClassLiteral<'db> {
(
PlaceAndQualifiers {
place: Place::Type(ty, _),
place: Place::Defined(ty, _, _),
qualifiers,
},
Some(dynamic_type),
@ -2167,7 +2170,7 @@ impl<'db> ClassLiteral<'db> {
(
PlaceAndQualifiers {
place: Place::Unbound,
place: Place::Undefined,
qualifiers,
},
Some(dynamic_type),
@ -2178,7 +2181,7 @@ impl<'db> ClassLiteral<'db> {
/// Returns the inferred type of the class member named `name`. Only bound members
/// or those marked as `ClassVars` are considered.
///
/// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope
/// Returns [`Place::Undefined`] if `name` cannot be found in this class's scope
/// directly. Use [`ClassLiteral::class_member`] if you require a method that will
/// traverse through the MRO until it finds the member.
pub(super) fn own_class_member(
@ -2190,8 +2193,8 @@ impl<'db> ClassLiteral<'db> {
) -> Member<'db> {
if name == "__dataclass_fields__" && self.dataclass_params(db).is_some() {
// Make this class look like a subclass of the `DataClassInstance` protocol
return Member::declared(
Place::bound(KnownClass::Dict.to_specialized_instance(
return Member {
inner: Place::declared(KnownClass::Dict.to_specialized_instance(
db,
[
KnownClass::Str.to_instance(db),
@ -2199,7 +2202,7 @@ impl<'db> ClassLiteral<'db> {
],
))
.with_qualifiers(TypeQualifiers::CLASS_VAR),
);
};
}
if CodeGeneratorKind::NamedTuple.matches(db, self) {
@ -2243,7 +2246,7 @@ impl<'db> ClassLiteral<'db> {
}
});
if member.is_unbound() {
if member.is_undefined() {
if let Some(synthesized_member) = self.own_synthesized_member(db, specialization, name)
{
return Member::definitely_declared(synthesized_member);
@ -2308,7 +2311,8 @@ impl<'db> ClassLiteral<'db> {
}
let dunder_set = field_ty.class_member(db, "__set__".into());
if let Place::Type(dunder_set, Boundness::Bound) = 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
// possible attributes, they also have a (callable) `__set__` method. The
@ -2429,7 +2433,7 @@ impl<'db> ClassLiteral<'db> {
.to_class_literal(db)
.as_class_literal()?
.own_class_member(db, self.inherited_generic_context(db), None, name)
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.map(|ty| {
ty.apply_type_mapping(
db,
@ -2884,9 +2888,9 @@ impl<'db> ClassLiteral<'db> {
continue;
}
if let Some(attr_ty) = attr.place.ignore_possibly_unbound() {
if let Some(attr_ty) = attr.place.ignore_possibly_undefined() {
let bindings = use_def.end_of_scope_symbol_bindings(symbol_id);
let mut default_ty = place_from_bindings(db, bindings).ignore_possibly_unbound();
let mut default_ty = place_from_bindings(db, bindings).ignore_possibly_undefined();
default_ty =
default_ty.map(|ty| ty.apply_optional_specialization(db, specialization));
@ -2977,7 +2981,7 @@ impl<'db> ClassLiteral<'db> {
name: &str,
) -> PlaceAndQualifiers<'db> {
if self.is_typed_dict(db) {
return Place::Unbound.into();
return Place::Undefined.into();
}
let mut union = UnionBuilder::new(db);
@ -2995,17 +2999,13 @@ impl<'db> ClassLiteral<'db> {
);
}
ClassBase::Class(class) => {
if let Member {
inner:
member @ PlaceAndQualifiers {
place: Place::Type(ty, boundness),
qualifiers,
},
is_declared,
} = class.own_instance_member(db, name)
if let member @ PlaceAndQualifiers {
place: Place::Defined(ty, origin, boundness),
qualifiers,
} = class.own_instance_member(db, name).inner
{
if boundness == Boundness::Bound {
if is_declared {
if boundness == Definedness::AlwaysDefined {
if origin.is_declared() {
// We found a definitely-declared attribute. Discard possibly collected
// inferred types from subclasses and return the declared type.
return member;
@ -3044,15 +3044,16 @@ impl<'db> ClassLiteral<'db> {
}
if union.is_empty() {
Place::Unbound.with_qualifiers(TypeQualifiers::empty())
Place::Undefined.with_qualifiers(TypeQualifiers::empty())
} else {
let boundness = if is_definitely_bound {
Boundness::Bound
Definedness::AlwaysDefined
} else {
Boundness::PossiblyUnbound
Definedness::PossiblyUndefined
};
Place::Type(union.build(), boundness).with_qualifiers(union_qualifiers)
Place::Defined(union.build(), TypeOrigin::Inferred, boundness)
.with_qualifiers(union_qualifiers)
}
}
@ -3104,7 +3105,10 @@ impl<'db> ClassLiteral<'db> {
if let Some(method_def) = method_scope.node().as_function() {
let method_name = method_def.node(&module).name.as_str();
if let Some(Type::FunctionLiteral(method_type)) =
class_member(db, class_body_scope, method_name).ignore_possibly_unbound()
class_member(db, class_body_scope, method_name)
.inner
.place
.ignore_possibly_undefined()
{
let method_decorator = MethodDecorator::try_from_fn_type(db, method_type);
if method_decorator != Ok(target_method_decorator) {
@ -3141,7 +3145,7 @@ impl<'db> ClassLiteral<'db> {
// self.name: <annotation> = …
let annotation = declaration_type(db, declaration);
let annotation = Place::bound(annotation.inner).with_qualifiers(
let annotation = Place::declared(annotation.inner).with_qualifiers(
annotation.qualifiers | TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE,
);
@ -3156,9 +3160,9 @@ impl<'db> ClassLiteral<'db> {
index.expression(value),
TypeContext::default(),
);
return Member::inferred(
Place::bound(inferred_ty).with_qualifiers(all_qualifiers),
);
return Member {
inner: Place::bound(inferred_ty).with_qualifiers(all_qualifiers),
};
}
// If there is no right-hand side, just record that we saw a `Final` qualifier
@ -3166,7 +3170,7 @@ impl<'db> ClassLiteral<'db> {
continue;
}
return Member::declared(annotation);
return Member { inner: annotation };
}
}
@ -3358,11 +3362,13 @@ impl<'db> ClassLiteral<'db> {
}
}
Member::inferred(if is_attribute_bound {
Place::bound(union_of_inferred_types.build()).with_qualifiers(qualifiers)
} else {
Place::Unbound.with_qualifiers(qualifiers)
})
Member {
inner: if is_attribute_bound {
Place::bound(union_of_inferred_types.build()).with_qualifiers(qualifiers)
} else {
Place::Undefined.with_qualifiers(qualifiers)
},
}
}
/// A helper function for `instance_member` that looks up the `name` attribute only on
@ -3384,20 +3390,20 @@ impl<'db> ClassLiteral<'db> {
match declared_and_qualifiers {
PlaceAndQualifiers {
place: mut declared @ Place::Type(declared_ty, declaredness),
place: mut declared @ Place::Defined(declared_ty, _, declaredness),
qualifiers,
} => {
// For the purpose of finding instance attributes, ignore `ClassVar`
// declarations:
if qualifiers.contains(TypeQualifiers::CLASS_VAR) {
declared = Place::Unbound;
declared = Place::Undefined;
}
if qualifiers.contains(TypeQualifiers::INIT_VAR) {
// We ignore `InitVar` declarations on the class body, unless that attribute is overwritten
// by an implicit assignment in a method
if Self::implicit_attribute(db, body_scope, name, MethodDecorator::None)
.is_unbound()
.is_undefined()
{
return Member::unbound();
}
@ -3407,28 +3413,31 @@ impl<'db> ClassLiteral<'db> {
let bindings = use_def.end_of_scope_symbol_bindings(symbol_id);
let inferred = place_from_bindings(db, bindings);
let has_binding = !inferred.is_unbound();
let has_binding = !inferred.is_undefined();
if has_binding {
// The attribute is declared and bound in the class body.
if let Some(implicit_ty) =
Self::implicit_attribute(db, body_scope, name, MethodDecorator::None)
.ignore_possibly_unbound()
.ignore_possibly_undefined()
{
if declaredness == Boundness::Bound {
if declaredness == Definedness::AlwaysDefined {
// If a symbol is definitely declared, and we see
// attribute assignments in methods of the class,
// we trust the declared type.
Member::declared(declared.with_qualifiers(qualifiers))
Member {
inner: declared.with_qualifiers(qualifiers),
}
} else {
Member::declared(
Place::Type(
Member {
inner: Place::Defined(
UnionType::from_elements(db, [declared_ty, implicit_ty]),
TypeOrigin::Declared,
declaredness,
)
.with_qualifiers(qualifiers),
)
}
}
} else {
// The symbol is declared and bound in the class body,
@ -3446,8 +3455,10 @@ impl<'db> ClassLiteral<'db> {
// it is possibly-undeclared. In the latter case, we also
// union with the inferred type from attribute assignments.
if declaredness == Boundness::Bound {
Member::declared(declared.with_qualifiers(qualifiers))
if declaredness == Definedness::AlwaysDefined {
Member {
inner: declared.with_qualifiers(qualifiers),
}
} else {
if let Some(implicit_ty) = Self::implicit_attribute(
db,
@ -3457,24 +3468,27 @@ impl<'db> ClassLiteral<'db> {
)
.inner
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
{
Member::declared(
Place::Type(
Member {
inner: Place::Defined(
UnionType::from_elements(db, [declared_ty, implicit_ty]),
TypeOrigin::Declared,
declaredness,
)
.with_qualifiers(qualifiers),
)
}
} else {
Member::declared(declared.with_qualifiers(qualifiers))
Member {
inner: declared.with_qualifiers(qualifiers),
}
}
}
}
}
PlaceAndQualifiers {
place: Place::Unbound,
place: Place::Undefined,
qualifiers: _,
} => {
// The attribute is not *declared* in the class body. It could still be declared/bound
@ -3678,7 +3692,7 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
.chain(attribute_places_and_qualifiers)
.dedup()
.filter_map(|(name, place_and_qual)| {
place_and_qual.place.ignore_possibly_unbound().map(|ty| {
place_and_qual.ignore_possibly_undefined().map(|ty| {
let variance = if place_and_qual
.qualifiers
// `CLASS_VAR || FINAL` is really `all()`, but
@ -4636,14 +4650,18 @@ impl KnownClass {
) -> Result<ClassLiteral<'_>, KnownClassLookupError<'_>> {
let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place;
match symbol {
Place::Type(Type::ClassLiteral(class_literal), Boundness::Bound) => Ok(class_literal),
Place::Type(Type::ClassLiteral(class_literal), Boundness::PossiblyUnbound) => {
Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal })
Place::Defined(Type::ClassLiteral(class_literal), _, Definedness::AlwaysDefined) => {
Ok(class_literal)
}
Place::Type(found_type, _) => {
Place::Defined(
Type::ClassLiteral(class_literal),
_,
Definedness::PossiblyUndefined,
) => Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }),
Place::Defined(found_type, _, _) => {
Err(KnownClassLookupError::SymbolNotAClass { found_type })
}
Place::Unbound => Err(KnownClassLookupError::ClassNotFound),
Place::Undefined => Err(KnownClassLookupError::ClassNotFound),
}
}
@ -5434,7 +5452,7 @@ enum SlotsKind {
impl SlotsKind {
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
let Place::Type(slots_ty, bound) = base
let Place::Defined(slots_ty, _, bound) = base
.own_class_member(db, base.inherited_generic_context(db), None, "__slots__")
.inner
.place
@ -5442,7 +5460,7 @@ impl SlotsKind {
return Self::NotSpecified;
};
if matches!(bound, Boundness::PossiblyUnbound) {
if matches!(bound, Definedness::PossiblyUndefined) {
return Self::Dynamic;
}

View file

@ -1713,7 +1713,7 @@ mod tests {
let iterator_synthesized = typing_extensions_symbol(&db, "Iterator")
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.unwrap()
.to_instance(&db)
.unwrap()

View file

@ -92,7 +92,7 @@ pub(crate) fn enum_metadata<'db>(
let ignore_place = place_from_bindings(db, ignore_bindings);
match ignore_place {
Place::Type(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_`.
@ -126,10 +126,10 @@ pub(crate) fn enum_metadata<'db>(
let inferred = place_from_bindings(db, bindings);
let value_ty = match inferred {
Place::Unbound => {
Place::Undefined => {
return None;
}
Place::Type(ty, _) => {
Place::Defined(ty, _, _) => {
let special_case = match ty {
Type::Callable(_) | Type::FunctionLiteral(_) => {
// Some types are specifically disallowed for enum members.
@ -143,7 +143,7 @@ pub(crate) fn enum_metadata<'db>(
Some(KnownClass::Member) => Some(
ty.member(db, "value")
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.unwrap_or(Type::unknown()),
),
@ -178,9 +178,9 @@ pub(crate) fn enum_metadata<'db>(
.place;
match dunder_get {
Place::Unbound | Place::Type(Type::Dynamic(_), _) => ty,
Place::Undefined | Place::Defined(Type::Dynamic(_), _, _) => ty,
Place::Type(_, _) => {
Place::Defined(_, _, _) => {
// Descriptors are not considered members.
return None;
}
@ -215,17 +215,17 @@ pub(crate) fn enum_metadata<'db>(
match declared {
PlaceAndQualifiers {
place: Place::Type(Type::Dynamic(DynamicType::Unknown), _),
place: Place::Defined(Type::Dynamic(DynamicType::Unknown), _, _),
qualifiers,
} if qualifiers.contains(TypeQualifiers::FINAL) => {}
PlaceAndQualifiers {
place: Place::Unbound,
place: Place::Undefined,
..
} => {
// Undeclared attributes are considered members
}
PlaceAndQualifiers {
place: Place::Type(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

View file

@ -59,7 +59,7 @@ use ruff_python_ast::{self as ast, ParameterWithDefault};
use ruff_text_size::Ranged;
use crate::module_resolver::{KnownModule, file_to_module};
use crate::place::{Boundness, Place, place_from_bindings};
use crate::place::{Definedness, Place, place_from_bindings};
use crate::semantic_index::ast_ids::HasScopedUseId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::ScopeId;
@ -314,7 +314,7 @@ impl<'db> OverloadLiteral<'db> {
.name
.scoped_use_id(db, scope);
let Place::Type(Type::FunctionLiteral(previous_type), Boundness::Bound) =
let Place::Defined(Type::FunctionLiteral(previous_type), _, Definedness::AlwaysDefined) =
place_from_bindings(db, use_def.bindings_at_use(use_id))
else {
return None;

View file

@ -42,7 +42,7 @@ pub(crate) fn all_declarations_and_bindings<'db>(
place_result
.ignore_conflicting_declarations()
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.map(|ty| {
let symbol = table.symbol(symbol_id);
let member = Member {
@ -71,7 +71,7 @@ pub(crate) fn all_declarations_and_bindings<'db>(
}
}
place_from_bindings(db, bindings)
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.map(|ty| {
let symbol = table.symbol(symbol_id);
let member = Member {
@ -239,7 +239,8 @@ 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::Type(ty, _) = imported_symbol(db, file, symbol_name, None).place
let Place::Defined(ty, _, _) =
imported_symbol(db, file, symbol_name, None).place
else {
continue;
};
@ -327,7 +328,7 @@ impl<'db> AllMembers<'db> {
let parent_scope = parent.body_scope(db);
for memberdef in all_declarations_and_bindings(db, parent_scope) {
let result = ty.member(db, memberdef.member.name.as_str());
let Some(ty) = result.place.ignore_possibly_unbound() else {
let Some(ty) = result.place.ignore_possibly_undefined() else {
continue;
};
self.members.insert(Member {
@ -358,7 +359,7 @@ impl<'db> AllMembers<'db> {
continue;
};
let result = ty.member(db, name);
let Some(ty) = result.place.ignore_possibly_unbound() else {
let Some(ty) = result.place.ignore_possibly_undefined() else {
continue;
};
self.members.insert(Member {
@ -375,7 +376,7 @@ impl<'db> AllMembers<'db> {
// method, but `instance_of_SomeClass.__delattr__` is.
for memberdef in all_declarations_and_bindings(db, class_body_scope) {
let result = ty.member(db, memberdef.member.name.as_str());
let Some(ty) = result.place.ignore_possibly_unbound() else {
let Some(ty) = result.place.ignore_possibly_undefined() else {
continue;
};
self.members.insert(Member {

View file

@ -751,7 +751,7 @@ impl<'db> DefinitionInference<'db> {
None
}
})
.or_else(|| self.fallback_type().map(Into::into))
.or_else(|| self.fallback_type().map(TypeAndQualifiers::declared))
.expect(
"definition should belong to this TypeInference region and \
TypeInferenceBuilder should have inferred a type for it",

View file

@ -22,7 +22,7 @@ use crate::module_resolver::{
};
use crate::node_key::NodeKey;
use crate::place::{
Boundness, ConsideredDefinitions, LookupError, Place, PlaceAndQualifiers,
ConsideredDefinitions, Definedness, LookupError, Place, PlaceAndQualifiers, TypeOrigin,
builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol,
module_type_implicit_global_declaration, module_type_implicit_global_symbol, place,
place_from_bindings, place_from_declarations, typing_extensions_symbol,
@ -136,7 +136,11 @@ enum DeclaredAndInferredType<'db> {
impl<'db> DeclaredAndInferredType<'db> {
fn are_the_same_type(ty: Type<'db>) -> Self {
Self::AreTheSame(ty.into())
Self::AreTheSame(TypeAndQualifiers::new(
ty,
TypeOrigin::Inferred,
TypeQualifiers::empty(),
))
}
}
@ -957,7 +961,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut public_functions = FxHashSet::default();
for place in overloaded_function_places {
if let Place::Type(Type::FunctionLiteral(function), Boundness::Bound) =
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()),
@ -1465,7 +1469,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
// Fall back to implicit module globals for (possibly) unbound names
if !matches!(place_and_quals.place, Place::Type(_, Boundness::Bound)) {
if !place_and_quals.place.is_definitely_bound() {
if let PlaceExprRef::Symbol(symbol) = place {
let symbol_id = place_id.expect_symbol();
@ -1486,38 +1490,40 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let unwrap_declared_ty = || {
resolved_place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.unwrap_or(Type::unknown())
};
// If the place is unbound and its an attribute or subscript place, fall back to normal
// attribute/subscript inference on the root type.
let declared_ty = if resolved_place.is_unbound() && !place_table.place(place_id).is_symbol()
{
if let AnyNodeRef::ExprAttribute(ast::ExprAttribute { value, attr, .. }) = node {
let value_type =
self.infer_maybe_standalone_expression(value, TypeContext::default());
if let Place::Type(ty, Boundness::Bound) = value_type.member(db, attr).place {
// TODO: also consider qualifiers on the attribute
ty
let declared_ty =
if resolved_place.is_undefined() && !place_table.place(place_id).is_symbol() {
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) =
value_type.member(db, attr).place
{
// TODO: also consider qualifiers on the attribute
ty
} else {
unwrap_declared_ty()
}
} else if let AnyNodeRef::ExprSubscript(
subscript @ ast::ExprSubscript {
value, slice, ctx, ..
},
) = node
{
let value_ty = self.infer_expression(value, TypeContext::default());
let slice_ty = self.infer_expression(slice, TypeContext::default());
self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx)
} else {
unwrap_declared_ty()
}
} else if let AnyNodeRef::ExprSubscript(
subscript @ ast::ExprSubscript {
value, slice, ctx, ..
},
) = node
{
let value_ty = self.infer_expression(value, TypeContext::default());
let slice_ty = self.infer_expression(slice, TypeContext::default());
self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx)
} else {
unwrap_declared_ty()
}
} else {
unwrap_declared_ty()
};
};
if qualifiers.contains(TypeQualifiers::FINAL) {
let mut previous_bindings = use_def.bindings_at_definition(binding);
@ -1585,7 +1591,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if value_ty
.class_member(db, attr.id.clone())
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.is_some_and(|ty| ty.may_be_data_descriptor(db))
{
bound_ty = declared_ty;
@ -1644,14 +1650,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if scope.is_global() {
module_type_implicit_global_symbol(self.db(), symbol.name())
} else {
Place::Unbound.into()
Place::Undefined.into()
}
} else {
Place::Unbound.into()
Place::Undefined.into()
}
})
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.unwrap_or(Type::Never);
let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) {
ty
@ -1663,7 +1669,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
inferred_ty.display(self.db())
));
}
TypeAndQualifiers::unknown()
TypeAndQualifiers::declared(Type::unknown())
};
self.declarations.insert(declaration, ty);
}
@ -1702,7 +1708,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(module_type_implicit_declaration) = place
.as_symbol()
.map(|symbol| module_type_implicit_global_symbol(self.db(), symbol.name()))
.and_then(|place| place.place.ignore_possibly_unbound())
.and_then(|place| place.place.ignore_possibly_undefined())
{
let declared_type = declared_ty.inner_type();
if !declared_type
@ -2425,7 +2431,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let declared_and_inferred_ty = if let Some(default_ty) = default_ty {
if default_ty.is_assignable_to(self.db(), declared_ty) {
DeclaredAndInferredType::MightBeDifferent {
declared_ty: declared_ty.into(),
declared_ty: TypeAndQualifiers::declared(declared_ty),
inferred_ty: UnionType::from_elements(self.db(), [declared_ty, default_ty]),
}
} else if (self.in_stub()
@ -3619,14 +3625,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
) {
PlaceAndQualifiers {
place: Place::Type(attr_ty, _),
place: Place::Defined(attr_ty, _, _),
qualifiers: _,
} => attr_ty.is_callable_type(),
_ => false,
};
let member_exists =
!object_ty.member(db, attribute).place.is_unbound();
!object_ty.member(db, attribute).place.is_undefined();
let msg = if !member_exists {
format!(
@ -3693,7 +3699,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
false
}
PlaceAndQualifiers {
place: Place::Type(meta_attr_ty, meta_attr_boundness),
place: Place::Defined(meta_attr_ty, _, meta_attr_boundness),
qualifiers,
} => {
if invalid_assignment_to_final(qualifiers) {
@ -3701,7 +3707,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
let assignable_to_meta_attr =
if let Place::Type(meta_dunder_set, _) =
if let Place::Defined(meta_dunder_set, _, _) =
meta_attr_ty.class_member(db, "__set__".into()).place
{
let dunder_set_result = meta_dunder_set.try_call(
@ -3733,11 +3739,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
};
let assignable_to_instance_attribute = if meta_attr_boundness
== Boundness::PossiblyUnbound
== Definedness::PossiblyUndefined
{
let (assignable, boundness) = if let PlaceAndQualifiers {
place:
Place::Type(instance_attr_ty, instance_attr_boundness),
Place::Defined(instance_attr_ty, _, instance_attr_boundness),
qualifiers,
} =
object_ty.instance_member(db, attribute)
@ -3751,10 +3757,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
instance_attr_boundness,
)
} else {
(true, Boundness::PossiblyUnbound)
(true, Definedness::PossiblyUndefined)
};
if boundness == Boundness::PossiblyUnbound {
if boundness == Definedness::PossiblyUndefined {
report_possibly_missing_attribute(
&self.context,
target,
@ -3772,11 +3778,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
PlaceAndQualifiers {
place: Place::Unbound,
place: Place::Undefined,
..
} => {
if let PlaceAndQualifiers {
place: Place::Type(instance_attr_ty, instance_attr_boundness),
place:
Place::Defined(instance_attr_ty, _, instance_attr_boundness),
qualifiers,
} = object_ty.instance_member(db, attribute)
{
@ -3784,7 +3791,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
return false;
}
if instance_attr_boundness == Boundness::PossiblyUnbound {
if instance_attr_boundness == Definedness::PossiblyUndefined {
report_possibly_missing_attribute(
&self.context,
target,
@ -3818,14 +3825,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => {
match object_ty.class_member(db, attribute.into()) {
PlaceAndQualifiers {
place: Place::Type(meta_attr_ty, meta_attr_boundness),
place: Place::Defined(meta_attr_ty, _, meta_attr_boundness),
qualifiers,
} => {
if invalid_assignment_to_final(qualifiers) {
return false;
}
let assignable_to_meta_attr = if let Place::Type(meta_dunder_set, _) =
let assignable_to_meta_attr = if let Place::Defined(meta_dunder_set, _, _) =
meta_attr_ty.class_member(db, "__set__".into()).place
{
let dunder_set_result = meta_dunder_set.try_call(
@ -3851,20 +3858,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
};
let assignable_to_class_attr = if meta_attr_boundness
== Boundness::PossiblyUnbound
== Definedness::PossiblyUndefined
{
let (assignable, boundness) =
if let Place::Type(class_attr_ty, class_attr_boundness) = object_ty
.find_name_in_mro(db, attribute)
.expect("called on Type::ClassLiteral or Type::SubclassOf")
.place
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")
.place
{
(ensure_assignable_to(class_attr_ty), class_attr_boundness)
} else {
(true, Boundness::PossiblyUnbound)
(true, Definedness::PossiblyUndefined)
};
if boundness == Boundness::PossiblyUnbound {
if boundness == Definedness::PossiblyUndefined {
report_possibly_missing_attribute(
&self.context,
target,
@ -3881,11 +3889,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
assignable_to_meta_attr && assignable_to_class_attr
}
PlaceAndQualifiers {
place: Place::Unbound,
place: Place::Undefined,
..
} => {
if let PlaceAndQualifiers {
place: Place::Type(class_attr_ty, class_attr_boundness),
place: Place::Defined(class_attr_ty, _, class_attr_boundness),
qualifiers,
} = object_ty
.find_name_in_mro(db, attribute)
@ -3895,7 +3903,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
return false;
}
if class_attr_boundness == Boundness::PossiblyUnbound {
if class_attr_boundness == Definedness::PossiblyUndefined {
report_possibly_missing_attribute(
&self.context,
target,
@ -3911,7 +3919,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
!instance
.instance_member(self.db(), attribute)
.place
.is_unbound()
.is_undefined()
});
// Attribute is declared or bound on instance. Forbid access from the class object
@ -3946,7 +3954,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
Type::ModuleLiteral(module) => {
if let Place::Type(attr_ty, _) = module.static_member(db, attribute).place {
if let Place::Defined(attr_ty, _, _) = module.static_member(db, attribute).place {
let assignable = value_ty.is_assignable_to(db, attr_ty);
if assignable {
true
@ -5057,11 +5065,11 @@ 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::Type(ty, boundness),
place: Place::Defined(ty, _, boundness),
qualifiers,
} = module_ty.member(self.db(), name)
{
if &alias.name != "*" && boundness == Boundness::PossiblyUnbound {
if &alias.name != "*" && boundness == Definedness::PossiblyUndefined {
// TODO: Consider loading _both_ the attribute and any submodule and unioning them
// together if the attribute exists but is possibly-unbound.
if let Some(builder) = self
@ -5079,6 +5087,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
&DeclaredAndInferredType::MightBeDifferent {
declared_ty: TypeAndQualifiers {
inner: ty,
origin: TypeOrigin::Declared,
qualifiers,
},
inferred_ty: ty,
@ -5224,7 +5233,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
if !module_type_implicit_global_symbol(self.db(), name)
.place
.is_unbound()
.is_undefined()
{
// This name is an implicit global like `__file__` (but not a built-in like `int`).
continue;
@ -6934,7 +6943,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// (without infinite recursion if we're already in builtins.)
.or_fall_back_to(db, || {
if Some(self.scope()) == builtins_module_scope(db) {
Place::Unbound.into()
Place::Undefined.into()
} else {
builtins_symbol(db, symbol_name)
}
@ -6951,7 +6960,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
typing_extensions_symbol(db, symbol_name)
} else {
Place::Unbound.into()
Place::Undefined.into()
}
});
@ -6961,11 +6970,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let ty =
resolved_after_fallback.unwrap_with_diagnostic(|lookup_error| match lookup_error {
LookupError::Unbound(qualifiers) => {
LookupError::Undefined(qualifiers) => {
self.report_unresolved_reference(name_node);
TypeAndQualifiers::new(Type::unknown(), qualifiers)
TypeAndQualifiers::new(Type::unknown(), TypeOrigin::Inferred, qualifiers)
}
LookupError::PossiblyUnbound(type_when_bound) => {
LookupError::PossiblyUndefined(type_when_bound) => {
if self.is_reachable(name_node) {
report_possibly_unresolved_reference(&self.context, name_node);
}
@ -6996,7 +7005,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.deferred_state.in_string_annotation(),
"Expected the place table to create a place for every valid PlaceExpr node"
);
Place::Unbound
Place::Undefined
};
(place, None)
} else {
@ -7004,7 +7013,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.as_name_expr()
.is_some_and(|name| name.is_invalid())
{
return (Place::Unbound, None);
return (Place::Undefined, None);
}
let use_id = expr_ref.scoped_use_id(db, scope);
@ -7064,7 +7073,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// enclosing scopes in this case. The one exception to this rule is the global fallback
// in class bodies, which we already handled above.
if symbol_resolves_locally {
return Place::Unbound.into();
return Place::Undefined.into();
}
for parent_id in place_table.parents(place_expr) {
@ -7082,8 +7091,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
let (parent_place, _use_id) = self.infer_local_place_load(parent_expr, expr_ref);
if let Place::Type(_, _) = parent_place {
return Place::Unbound.into();
if let Place::Defined(_, _, _) = parent_place {
return Place::Undefined.into();
}
}
@ -7154,13 +7163,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// Don't fall back to non-eager place resolution.
EnclosingSnapshotResult::NotFound => {
if has_root_place_been_reassigned() {
return Place::Unbound.into();
return Place::Undefined.into();
}
continue;
}
EnclosingSnapshotResult::NoLongerInEagerContext => {
if has_root_place_been_reassigned() {
return Place::Unbound.into();
return Place::Undefined.into();
}
}
}
@ -7209,12 +7218,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
&constraint_keys,
)
});
// We could have Place::Unbound here, despite the checks above, for example if
// 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::Type(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, Boundness::Bound);
debug_assert_eq!(boundness, Definedness::AlwaysDefined);
found_some_definition = true;
}
@ -7223,20 +7232,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// declared but doesn't mark it `nonlocal`. The name is therefore resolved,
// and we won't consider any scopes outside of this one.
return if found_some_definition {
Place::Type(nonlocal_union_builder.build(), Boundness::Bound).into()
Place::bound(nonlocal_union_builder.build()).into()
} else {
Place::Unbound.into()
Place::Undefined.into()
};
}
}
}
PlaceAndQualifiers::from(Place::Unbound)
PlaceAndQualifiers::from(Place::Undefined)
// No nonlocal binding? Check the module's explicit globals.
// Avoid infinite recursion if `self.scope` already is the module's global scope.
.or_fall_back_to(db, || {
if file_scope_id.is_global() {
return Place::Unbound.into();
return Place::Undefined.into();
}
if !self.is_deferred() {
@ -7267,14 +7276,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
// There are no visible bindings / constraint here.
EnclosingSnapshotResult::NotFound => {
return Place::Unbound.into();
return Place::Undefined.into();
}
EnclosingSnapshotResult::NoLongerInEagerContext => {}
}
}
let Some(symbol) = place_expr.as_symbol() else {
return Place::Unbound.into();
return Place::Undefined.into();
};
explicit_global_symbol(db, self.file(), symbol.name()).map_type(|ty| {
@ -7287,7 +7296,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
})
});
if let Some(ty) = place.place.ignore_possibly_unbound() {
if let Some(ty) = place.place.ignore_possibly_undefined() {
self.check_deprecated(expr_ref, ty);
}
@ -7360,11 +7369,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Ok(MethodDecorator::ClassMethod) => !Type::instance(self.db(), class)
.class_member(self.db(), id.clone())
.place
.is_unbound(),
.is_undefined(),
Ok(MethodDecorator::None) => !Type::instance(self.db(), class)
.member(self.db(), id)
.place
.is_unbound(),
.is_undefined(),
Ok(MethodDecorator::StaticMethod) | Err(()) => false,
};
@ -7421,7 +7430,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::ExprRef::Attribute(attribute),
);
constraint_keys.extend(keys);
if let Place::Type(ty, Boundness::Bound) = resolved.place {
if let Place::Defined(ty, _, Definedness::AlwaysDefined) = resolved.place {
assigned_type = Some(ty);
}
}
@ -7441,18 +7450,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fallback_place.map_type(|ty| {
self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys)
}).unwrap_with_diagnostic(|lookup_error| match lookup_error {
LookupError::Unbound(_) => {
LookupError::Undefined(_) => {
let report_unresolved_attribute = self.is_reachable(attribute);
if report_unresolved_attribute {
let bound_on_instance = match value_type {
Type::ClassLiteral(class) => {
!class.instance_member(db, None, attr).place.is_unbound()
!class.instance_member(db, None, attr).is_undefined()
}
Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => {
match subclass_of.subclass_of() {
SubclassOfInner::Class(class) => {
!class.instance_member(db, attr).place.is_unbound()
!class.instance_member(db, attr).is_undefined()
}
SubclassOfInner::Dynamic(_) => unreachable!(
"Attribute lookup on a dynamic `SubclassOf` type should always return a bound symbol"
@ -7487,9 +7496,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
Type::unknown().into()
TypeAndQualifiers::new(Type::unknown(), TypeOrigin::Inferred, TypeQualifiers::empty())
}
LookupError::PossiblyUnbound(type_when_bound) => {
LookupError::PossiblyUndefined(type_when_bound) => {
report_possibly_missing_attribute(
&self.context,
attribute,
@ -8064,7 +8073,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let rhs_reflected = right_class.member(self.db(), reflected_dunder).place;
// TODO: if `rhs_reflected` is possibly unbound, we should union the two possible
// Bindings together
if !rhs_reflected.is_unbound()
if !rhs_reflected.is_undefined()
&& rhs_reflected != left_class.member(self.db(), reflected_dunder).place
{
return right_ty
@ -8911,7 +8920,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::Type(contains_dunder, Boundness::Bound) => {
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]))
@ -9092,7 +9101,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::ExprRef::Subscript(subscript),
);
constraint_keys.extend(keys);
if let Place::Type(ty, Boundness::Bound) = 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());
@ -9548,9 +9557,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let dunder_class_getitem_method = value_ty.member(db, "__class_getitem__").place;
match dunder_class_getitem_method {
Place::Unbound => {}
Place::Type(ty, boundness) => {
if boundness == Boundness::PossiblyUnbound {
Place::Undefined => {}
Place::Defined(ty, _, boundness) => {
if boundness == Definedness::PossiblyUndefined {
if let Some(builder) =
context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, value_node)
{

View file

@ -1,6 +1,7 @@
use ruff_python_ast as ast;
use super::{DeferredExpressionState, TypeInferenceBuilder};
use crate::place::TypeOrigin;
use crate::types::diagnostic::{INVALID_TYPE_FORM, report_invalid_arguments_to_annotated};
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
@ -48,21 +49,31 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
builder: &TypeInferenceBuilder<'db, '_>,
) -> TypeAndQualifiers<'db> {
match ty {
Type::SpecialForm(SpecialFormType::ClassVar) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::CLASS_VAR)
}
Type::SpecialForm(SpecialFormType::Final) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL)
}
Type::SpecialForm(SpecialFormType::Required) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::REQUIRED)
}
Type::SpecialForm(SpecialFormType::NotRequired) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::NOT_REQUIRED)
}
Type::SpecialForm(SpecialFormType::ReadOnly) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::READ_ONLY)
}
Type::SpecialForm(SpecialFormType::ClassVar) => TypeAndQualifiers::new(
Type::unknown(),
TypeOrigin::Declared,
TypeQualifiers::CLASS_VAR,
),
Type::SpecialForm(SpecialFormType::Final) => TypeAndQualifiers::new(
Type::unknown(),
TypeOrigin::Declared,
TypeQualifiers::FINAL,
),
Type::SpecialForm(SpecialFormType::Required) => TypeAndQualifiers::new(
Type::unknown(),
TypeOrigin::Declared,
TypeQualifiers::REQUIRED,
),
Type::SpecialForm(SpecialFormType::NotRequired) => TypeAndQualifiers::new(
Type::unknown(),
TypeOrigin::Declared,
TypeQualifiers::NOT_REQUIRED,
),
Type::SpecialForm(SpecialFormType::ReadOnly) => TypeAndQualifiers::new(
Type::unknown(),
TypeOrigin::Declared,
TypeQualifiers::READ_ONLY,
),
Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => {
if let Some(builder) =
builder.context.report_lint(&INVALID_TYPE_FORM, annotation)
@ -70,10 +81,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
builder
.into_diagnostic("`InitVar` may not be used without a type argument");
}
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::INIT_VAR)
TypeAndQualifiers::new(
Type::unknown(),
TypeOrigin::Declared,
TypeQualifiers::INIT_VAR,
)
}
_ => ty
.in_type_expression(
_ => TypeAndQualifiers::declared(
ty.in_type_expression(
builder.db(),
builder.scope(),
builder.typevar_binding_context,
@ -84,8 +99,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
annotation,
builder.is_reachable(annotation),
)
})
.into(),
}),
),
}
}
@ -95,7 +110,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string),
// Annotation expressions also get special handling for `*args` and `**kwargs`.
ast::Expr::Starred(starred) => self.infer_starred_expression(starred).into(),
ast::Expr::Starred(starred) => {
TypeAndQualifiers::declared(self.infer_starred_expression(starred))
}
ast::Expr::BytesLiteral(bytes) => {
if let Some(builder) = self
@ -104,7 +121,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
{
builder.into_diagnostic("Type expressions cannot use bytes literal");
}
TypeAndQualifiers::unknown()
TypeAndQualifiers::declared(Type::unknown())
}
ast::Expr::FString(fstring) => {
@ -112,7 +129,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
builder.into_diagnostic("Type expressions cannot use f-strings");
}
self.infer_fstring_expression(fstring);
TypeAndQualifiers::unknown()
TypeAndQualifiers::declared(Type::unknown())
}
ast::Expr::Attribute(attribute) => match attribute.ctx {
@ -121,20 +138,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
annotation,
self,
),
ast::ExprContext::Invalid => TypeAndQualifiers::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Attribute expression annotation in Store/Del context").into()
}
ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()),
ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared(
todo_type!("Attribute expression annotation in Store/Del context"),
),
},
ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => {
infer_name_or_attribute(self.infer_name_expression(name), annotation, self)
}
ast::ExprContext::Invalid => TypeAndQualifiers::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Name expression annotation in Store/Del context").into()
}
ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()),
ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared(
todo_type!("Name expression annotation in Store/Del context"),
),
},
ast::Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => {
@ -170,7 +187,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_expression(argument, TypeContext::default());
}
self.store_expression_type(slice, Type::unknown());
TypeAndQualifiers::unknown()
TypeAndQualifiers::declared(Type::unknown())
}
} else {
report_invalid_arguments_to_annotated(&self.context, subscript);
@ -225,7 +242,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
got {num_arguments}",
));
}
Type::unknown().into()
TypeAndQualifiers::declared(Type::unknown())
};
if slice.is_tuple_expr() {
self.store_expression_type(slice, type_and_qualifiers.inner_type());
@ -256,22 +273,24 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
got {num_arguments}",
));
}
Type::unknown().into()
TypeAndQualifiers::declared(Type::unknown())
};
if slice.is_tuple_expr() {
self.store_expression_type(slice, type_and_qualifiers.inner_type());
}
type_and_qualifiers
}
_ => self
.infer_subscript_type_expression_no_store(subscript, slice, value_ty)
.into(),
_ => TypeAndQualifiers::declared(
self.infer_subscript_type_expression_no_store(subscript, slice, value_ty),
),
}
}
// All other annotation expressions are (possibly) valid type expressions, so handle
// them there instead.
type_expr => self.infer_type_expression_no_store(type_expr).into(),
type_expr => {
TypeAndQualifiers::declared(self.infer_type_expression_no_store(type_expr))
}
};
self.store_expression_type(annotation, annotation_ty.inner_type());
@ -294,7 +313,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
),
)
}
None => TypeAndQualifiers::unknown(),
None => TypeAndQualifiers::declared(Type::unknown()),
}
}
}

View file

@ -1429,7 +1429,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let ty = value_ty
.member(self.db(), &attr.id)
.place
.ignore_possibly_unbound()
.ignore_possibly_undefined()
.unwrap_or(Type::unknown());
self.store_expression_type(parameters, ty);
ty

View file

@ -1,68 +1,40 @@
use super::Type;
use crate::Db;
use crate::place::{
ConsideredDefinitions, Place, PlaceAndQualifiers, RequiresExplicitReExport, place_by_id,
place_from_bindings,
};
use crate::semantic_index::{place_table, scope::ScopeId, use_def_map};
use crate::types::Type;
/// The return type of certain member-lookup operations. Contains information
/// about the type, type qualifiers, boundness/declaredness, and additional
/// metadata (e.g. whether or not the member was declared)
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
/// about the type, type qualifiers, boundness/declaredness.
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize, Default)]
pub(super) struct Member<'db> {
/// Type, qualifiers, and boundness information of this member
pub(super) inner: PlaceAndQualifiers<'db>,
/// Whether or not this member was explicitly declared (e.g. `attr: int = 1`
/// on the class body or `self.attr: int = 1` in a class method), or if the
/// type was inferred (e.g. `attr = 1` on the class body or `self.attr = 1`
/// in a class method).
pub(super) is_declared: bool,
}
impl Default for Member<'_> {
fn default() -> Self {
Member::inferred(PlaceAndQualifiers::default())
}
}
impl<'db> Member<'db> {
/// Create a new [`Member`] whose type was inferred (rather than explicitly declared).
pub(super) fn inferred(inner: PlaceAndQualifiers<'db>) -> Self {
Self {
inner,
is_declared: false,
}
}
/// Create a new [`Member`] whose type was explicitly declared (rather than inferred).
pub(super) fn declared(inner: PlaceAndQualifiers<'db>) -> Self {
Self {
inner,
is_declared: true,
}
}
/// Create a new [`Member`] whose type was explicitly and definitively declared, i.e.
/// there is no control flow path in which it might be possibly undeclared.
pub(super) fn definitely_declared(ty: Type<'db>) -> Self {
Self::declared(Place::bound(ty).into())
}
/// Represents the absence of a member.
pub(super) fn unbound() -> Self {
Self::inferred(PlaceAndQualifiers::default())
Self {
inner: PlaceAndQualifiers::unbound(),
}
}
/// Returns `true` if the inner place is unbound (i.e. there is no such member).
pub(super) fn is_unbound(&self) -> bool {
self.inner.place.is_unbound()
pub(super) fn definitely_declared(ty: Type<'db>) -> Self {
Self {
inner: Place::declared(ty).into(),
}
}
/// Returns the inner type, unless it is definitely unbound.
pub(super) fn ignore_possibly_unbound(&self) -> Option<Type<'db>> {
self.inner.place.ignore_possibly_unbound()
/// Returns `true` if the inner place is undefined (i.e. there is no such member).
pub(super) fn is_undefined(&self) -> bool {
self.inner.place.is_undefined()
}
/// Returns the inner type, unless it is definitely undefined.
pub(super) fn ignore_possibly_undefined(&self) -> Option<Type<'db>> {
self.inner.place.ignore_possibly_undefined()
}
/// Map a type transformation function over the type of this member.
@ -70,7 +42,6 @@ impl<'db> Member<'db> {
pub(super) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self {
Self {
inner: self.inner.map_type(f),
is_declared: self.is_declared,
}
}
}
@ -89,13 +60,15 @@ pub(super) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str
ConsideredDefinitions::EndOfScope,
);
if !place_and_quals.place.is_unbound() && !place_and_quals.is_init_var() {
if !place_and_quals.is_undefined() && !place_and_quals.is_init_var() {
// Trust the declared type if we see a class-level declaration
return Member::declared(place_and_quals);
return Member {
inner: place_and_quals,
};
}
if let PlaceAndQualifiers {
place: Place::Type(ty, _),
place: Place::Defined(ty, _, _),
qualifiers,
} = place_and_quals
{
@ -106,12 +79,14 @@ pub(super) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str
// TODO: we should not need to calculate inferred type second time. This is a temporary
// solution until the notion of Boundness and Declaredness is split. See #16036, #16264
Member::inferred(match inferred {
Place::Unbound => Place::Unbound.with_qualifiers(qualifiers),
Place::Type(_, boundness) => {
Place::Type(ty, boundness).with_qualifiers(qualifiers)
}
})
Member {
inner: match inferred {
Place::Undefined => Place::Undefined.with_qualifiers(qualifiers),
Place::Defined(_, origin, boundness) => {
Place::Defined(ty, origin, boundness).with_qualifiers(qualifiers)
}
},
}
} else {
Member::unbound()
}

View file

@ -9,7 +9,7 @@ use rustc_hash::FxHashMap;
use crate::types::TypeContext;
use crate::{
Db, FxOrderSet,
place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations},
place::{Definedness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations},
semantic_index::{
SemanticIndex, definition::Definition, place::ScopedPlaceId, place_table, use_def_map,
},
@ -111,7 +111,7 @@ impl<'db> ProtocolClass<'db> {
.into_place_and_conflicting_declarations()
.0
.place
.is_unbound()
.is_undefined()
});
if has_declaration {
@ -645,7 +645,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
ProtocolMemberKind::Method(method) => {
// `__call__` members must be special cased for several reasons:
//
// 1. Looking up `__call__` on the meta-type of a `Callable` type returns `Place::Unbound` currently
// 1. Looking up `__call__` on the meta-type of a `Callable` type returns `Place::Undefined` currently
// 2. Looking up `__call__` on the meta-type of a function-literal type currently returns a type that
// has an extremely vague signature (`(*args, **kwargs) -> Any`), which is not useful for protocol
// checking.
@ -658,11 +658,11 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
};
attribute_type
} else {
let Place::Type(attribute_type, Boundness::Bound) = other
let Place::Defined(attribute_type, _, Definedness::AlwaysDefined) = other
.invoke_descriptor_protocol(
db,
self.name,
Place::Unbound.into(),
Place::Undefined.into(),
InstanceFallbackShadowsNonDataDescriptor::No,
MemberLookupPolicy::default(),
)
@ -685,10 +685,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::Type(_, Boundness::Bound)
Place::Defined(_, _, Definedness::AlwaysDefined)
)),
ProtocolMemberKind::Other(member_type) => {
let Place::Type(attribute_type, Boundness::Bound) =
let Place::Defined(attribute_type, _, Definedness::AlwaysDefined) =
other.member(db, self.name).place
else {
return ConstraintSet::from(false);
@ -798,7 +798,7 @@ fn cached_protocol_interface<'db>(
// type narrowing that uses `isinstance()` or `issubclass()` with
// runtime-checkable protocols.
for (symbol_id, bindings) in use_def_map.all_end_of_scope_symbol_bindings() {
let Some(ty) = place_from_bindings(db, bindings).ignore_possibly_unbound() else {
let Some(ty) = place_from_bindings(db, bindings).ignore_possibly_undefined() else {
continue;
};
direct_members.insert(
@ -809,7 +809,7 @@ fn cached_protocol_interface<'db>(
for (symbol_id, declarations) in use_def_map.all_end_of_scope_symbol_declarations() {
let place = place_from_declarations(db, declarations).ignore_conflicting_declarations();
if let Some(new_type) = place.place.ignore_possibly_unbound() {
if let Some(new_type) = place.place.ignore_possibly_undefined() {
direct_members
.entry(symbol_id)
.and_modify(|(ty, quals, _)| {