[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));
}