use ruff_db::files::File; use crate::dunder_all::dunder_all_names; use crate::module_resolver::{KnownModule, file_to_module}; use crate::semantic_index::definition::{Definition, DefinitionState}; use crate::semantic_index::place::{PlaceExprRef, ScopedPlaceId}; use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{ BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator, place_table, }; use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map}; use crate::types::{ ApplyTypeMappingVisitor, DynamicType, KnownClass, MaterializationKind, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, binding_type, declaration_type, todo_type, }; use crate::{Db, FxOrderSet, Program, resolve_module}; pub(crate) use implicit_globals::{ module_type_implicit_global_declaration, module_type_implicit_global_symbol, }; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize)] pub(crate) enum Boundness { Bound, PossiblyUnbound, } impl Boundness { pub(crate) const fn max(self, other: Self) -> Self { match (self, other) { (Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound, (Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound, } } } /// The result of a place lookup, which can either be a (possibly unbound) type /// or a completely unbound place. /// /// Consider this example: /// ```py /// bound = 1 /// /// if flag: /// possibly_unbound = 2 /// ``` /// /// 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, /// ``` #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(crate) enum Place<'db> { Type(Type<'db>, Boundness), Unbound, } impl<'db> Place<'db> { /// Constructor that creates a `Place` with boundness [`Boundness::Bound`]. pub(crate) fn bound(ty: impl Into>) -> Self { Place::Type(ty.into(), Boundness::Bound) } /// Constructor that creates a [`Place`] with a [`crate::types::TodoType`] type /// and boundness [`Boundness::Bound`]. #[allow(unused_variables)] // Only unused in release builds pub(crate) fn todo(message: &'static str) -> Self { Place::Type(todo_type!(message), Boundness::Bound) } pub(crate) fn is_unbound(&self) -> bool { matches!(self, Place::Unbound) } /// Returns the type of the place, ignoring possible unboundness. /// /// 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> { match self { Place::Type(ty, _) => Some(*ty), Place::Unbound => 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") } #[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, } } #[must_use] pub(crate) fn with_qualifiers(self, qualifiers: TypeQualifiers) -> PlaceAndQualifiers<'db> { PlaceAndQualifiers { place: self, qualifiers, } } /// Try to call `__get__(None, owner)` on the type of this place (not on the meta type). /// If it succeeds, return the `__get__` return type. Otherwise, returns the original place. /// 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 .map_with_boundness(db, |elem| { Place::Type(*elem, boundness).try_call_dunder_get(db, owner) }), Place::Type(self_ty, boundness) => { 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) } else { self } } Place::Unbound => Place::Unbound, } } pub(crate) const fn is_definitely_bound(&self) -> bool { matches!(self, Place::Type(_, Boundness::Bound)) } } impl<'db> From> 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()) } } } } /// 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>), } impl<'db> LookupError<'db> { /// Fallback (wholly or partially) to `fallback` to create a new [`LookupResult`]. pub(crate) fn or_fall_back_to( self, db: &'db dyn Db, fallback: PlaceAndQualifiers<'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( UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]), ty.qualifiers().union(ty2.qualifiers()), )), (LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => { Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new( UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]), ty.qualifiers().union(ty2.qualifiers()), ))) } } } } /// 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. /// /// Note that this type is exactly isomorphic to [`Place`]. /// In the future, we could possibly consider removing `Place` and using this type everywhere instead. pub(crate) type LookupResult<'db> = Result, LookupError<'db>>; /// Infer the public type of a symbol (its type as seen from outside its scope) in the given /// `scope`. #[allow(unused)] pub(crate) fn symbol<'db>( db: &'db dyn Db, scope: ScopeId<'db>, name: &str, considered_definitions: ConsideredDefinitions, ) -> PlaceAndQualifiers<'db> { symbol_impl( db, scope, name, RequiresExplicitReExport::No, considered_definitions, ) } /// Infer the public type of a place (its type as seen from outside its scope) in the given /// `scope`. pub(crate) fn place<'db>( db: &'db dyn Db, scope: ScopeId<'db>, member: PlaceExprRef, considered_definitions: ConsideredDefinitions, ) -> PlaceAndQualifiers<'db> { place_impl( db, scope, member, RequiresExplicitReExport::No, considered_definitions, ) } /// Infer the public type of a class symbol (its type as seen from outside its scope) in the given /// `scope`. pub(crate) fn class_symbol<'db>( db: &'db dyn Db, scope: ScopeId<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { place_table(db, scope) .symbol_id(name) .map(|symbol_id| { let place_and_quals = place_by_id( db, scope, symbol_id.into(), RequiresExplicitReExport::No, ConsideredDefinitions::EndOfScope, ); if !place_and_quals.place.is_unbound() && !place_and_quals.is_init_var() { // Trust the declared type if we see a class-level declaration return place_and_quals; } if let PlaceAndQualifiers { place: Place::Type(ty, _), qualifiers, } = place_and_quals { // Otherwise, we need to check if the symbol has bindings let use_def = use_def_map(db, scope); let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); let inferred = place_from_bindings_impl(db, bindings, RequiresExplicitReExport::No); // 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 match inferred { Place::Unbound => Place::Unbound.with_qualifiers(qualifiers), Place::Type(_, boundness) => { Place::Type(ty, boundness).with_qualifiers(qualifiers) } } } else { Place::Unbound.into() } }) .unwrap_or_default() } /// Infers the public type of an explicit module-global symbol as seen from within the same file. /// /// 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 /// those additional symbols. /// /// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). pub(crate) fn explicit_global_symbol<'db>( db: &'db dyn Db, file: File, name: &str, ) -> PlaceAndQualifiers<'db> { symbol_impl( db, global_scope(db, file), name, RequiresExplicitReExport::No, ConsideredDefinitions::AllReachable, ) } /// Infers the public type of an explicit module-global symbol as seen from within the same file. /// /// Unlike [`explicit_global_symbol`], this function also considers various "implicit globals" /// such as `__name__`, `__doc__` and `__file__`. These are looked up as attributes on `types.ModuleType` /// rather than being looked up as symbols explicitly defined/declared in the global scope. /// /// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). #[allow(unused)] pub(crate) fn global_symbol<'db>( db: &'db dyn Db, file: File, name: &str, ) -> PlaceAndQualifiers<'db> { explicit_global_symbol(db, file, name) .or_fall_back_to(db, || module_type_implicit_global_symbol(db, name)) } /// Infers the public type of an imported symbol. /// /// If `requires_explicit_reexport` is [`None`], it will be inferred from the file's source type. /// For stub files, explicit re-export will be required, while for non-stub files, it will not. pub(crate) fn imported_symbol<'db>( db: &'db dyn Db, file: File, name: &str, requires_explicit_reexport: Option, ) -> PlaceAndQualifiers<'db> { let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| { if file.is_stub(db) { RequiresExplicitReExport::Yes } else { RequiresExplicitReExport::No } }); // If it's not found in the global scope, check if it's present as an instance on // `types.ModuleType` or `builtins.object`. // // We do a more limited version of this in `module_type_implicit_global_symbol`, // but there are two crucial differences here: // - If a member is looked up as an attribute, `__init__` is also available on the module, but // it isn't available as a global from inside the module // - If a member is looked up as an attribute, members on `builtins.object` are also available // (because `types.ModuleType` inherits from `object`); these attributes are also not // available as globals from inside the module. // // The same way as in `module_type_implicit_global_symbol`, however, we need to be careful to // ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with // dynamic imports; we shouldn't use it for `ModuleLiteral` types where we know exactly which // module we're dealing with. symbol_impl( db, global_scope(db, file), name, requires_explicit_reexport, ConsideredDefinitions::EndOfScope, ) .or_fall_back_to(db, || { if name == "__getattr__" { Place::Unbound.into() } else if name == "__builtins__" { Place::bound(Type::any()).into() } else { KnownClass::ModuleType.to_instance(db).member(db, name) } }) } /// Lookup the type of `symbol` in the builtins namespace. /// /// Returns `Place::Unbound` 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 /// (e.g. `from builtins import int`). pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { resolve_module(db, &KnownModule::Builtins.name()) .and_then(|module| { let file = module.file(db)?; Some( symbol_impl( db, global_scope(db, file), symbol, RequiresExplicitReExport::Yes, ConsideredDefinitions::EndOfScope, ) .or_fall_back_to(db, || { // We're looking up in the builtins namespace and not the module, so we should // do the normal lookup in `types.ModuleType` and not the special one as in // `imported_symbol`. module_type_implicit_global_symbol(db, symbol) }), ) }) .unwrap_or_default() } /// Lookup the type of `symbol` in a given known module. /// /// Returns `Place::Unbound` 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, symbol: &str, ) -> PlaceAndQualifiers<'db> { resolve_module(db, &known_module.name()) .and_then(|module| { let file = module.file(db)?; Some(imported_symbol(db, file, symbol, None)) }) .unwrap_or_default() } /// Lookup the type of `symbol` in the `typing` module namespace. /// /// Returns `Place::Unbound` 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> { known_module_symbol(db, KnownModule::Typing, symbol) } /// 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. #[inline] pub(crate) fn typing_extensions_symbol<'db>( db: &'db dyn Db, symbol: &str, ) -> PlaceAndQualifiers<'db> { known_module_symbol(db, KnownModule::TypingExtensions, symbol) } /// Get the `builtins` module scope. /// /// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`. pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option> { core_module_scope(db, KnownModule::Builtins) } /// Get the scope of a core stdlib module. /// /// Can return `None` if a custom typeshed is used that is missing the core module in question. fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option> { let module = resolve_module(db, &core_module.name())?; Some(global_scope(db, module.file(db)?)) } /// Infer the combined type from an iterator of bindings, and return it /// together with boundness information in a [`Place`]. /// /// The type will be a union if there are multiple bindings with different types. pub(super) fn place_from_bindings<'db>( db: &'db dyn Db, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, ) -> Place<'db> { place_from_bindings_impl(db, bindings_with_constraints, RequiresExplicitReExport::No) } /// Build a declared type from a [`DeclarationsIterator`]. /// /// If there is only one declaration, or all declarations declare the same type, returns /// `Ok(..)`. If there are conflicting declarations, returns an `Err(..)` variant with /// a union of the declared types as well as a list of all conflicting types. /// /// This function also returns declaredness information (see [`Place`]) and a set of /// [`TypeQualifiers`] that have been specified on the declaration(s). pub(crate) fn place_from_declarations<'db>( db: &'db dyn Db, declarations: DeclarationsIterator<'_, 'db>, ) -> PlaceFromDeclarationsResult<'db> { place_from_declarations_impl(db, declarations, RequiresExplicitReExport::No) } type DeclaredTypeAndConflictingTypes<'db> = ( TypeAndQualifiers<'db>, Option>>>, ); /// The result of looking up a declared type from declarations; see [`place_from_declarations`]. pub(crate) struct PlaceFromDeclarationsResult<'db> { place_and_quals: PlaceAndQualifiers<'db>, conflicting_types: Option>>>, /// Contains `Some(declaration)` if the declared type originates from exactly one declaration. /// This field is used for backreferences in diagnostics. pub(crate) single_declaration: Option>, } impl<'db> PlaceFromDeclarationsResult<'db> { fn conflict( place_and_quals: PlaceAndQualifiers<'db>, conflicting_types: Box>>, ) -> Self { PlaceFromDeclarationsResult { place_and_quals, conflicting_types: Some(conflicting_types), single_declaration: None, } } pub(crate) fn ignore_conflicting_declarations(self) -> PlaceAndQualifiers<'db> { self.place_and_quals } pub(crate) fn into_place_and_conflicting_declarations( self, ) -> ( PlaceAndQualifiers<'db>, Option>>>, ) { (self.place_and_quals, self.conflicting_types) } } /// A type with declaredness information, and a set of type qualifiers. /// /// This is used to represent the result of looking up the declared type. Consider this /// example: /// ```py /// class C: /// if flag: /// 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 /// that this comes with a [`CLASS_VAR`] type qualifier. /// /// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(crate) struct PlaceAndQualifiers<'db> { pub(crate) place: Place<'db>, pub(crate) qualifiers: TypeQualifiers, } impl Default for PlaceAndQualifiers<'_> { fn default() -> Self { PlaceAndQualifiers { place: Place::Unbound, qualifiers: TypeQualifiers::empty(), } } } impl<'db> PlaceAndQualifiers<'db> { /// Constructor that creates a [`PlaceAndQualifiers`] instance with a [`TodoType`] type /// and no qualifiers. /// /// [`TodoType`]: crate::types::TodoType pub(crate) fn todo(message: &'static str) -> Self { Self { place: Place::todo(message), qualifiers: TypeQualifiers::empty(), } } /// Returns `true` if the place has a `ClassVar` type qualifier. pub(crate) fn is_class_var(&self) -> bool { self.qualifiers.contains(TypeQualifiers::CLASS_VAR) } /// Returns `true` if the place has a `InitVar` type qualifier. pub(crate) fn is_init_var(&self) -> bool { self.qualifiers.contains(TypeQualifiers::INIT_VAR) } /// Returns `true` if the place has a `Required` type qualifier. pub(crate) fn is_required(&self) -> bool { self.qualifiers.contains(TypeQualifiers::REQUIRED) } /// Returns `true` if the place has a `NotRequired` type qualifier. pub(crate) fn is_not_required(&self) -> bool { self.qualifiers.contains(TypeQualifiers::NOT_REQUIRED) } /// Returns `true` if the place has a `ReadOnly` type qualifier. pub(crate) fn is_read_only(&self) -> bool { self.qualifiers.contains(TypeQualifiers::READ_ONLY) } /// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type. pub(crate) fn is_bare_final(&self) -> Option { match self { PlaceAndQualifiers { place, qualifiers } if (qualifiers.contains(TypeQualifiers::FINAL) && place .ignore_possibly_unbound() .is_some_and(|ty| ty.is_unknown())) => { Some(*qualifiers) } _ => None, } } #[must_use] pub(crate) fn map_type( self, f: impl FnOnce(Type<'db>) -> Type<'db>, ) -> PlaceAndQualifiers<'db> { PlaceAndQualifiers { place: self.place.map_type(f), qualifiers: self.qualifiers, } } pub(crate) fn materialize( self, db: &'db dyn Db, materialization_kind: MaterializationKind, visitor: &ApplyTypeMappingVisitor<'db>, ) -> PlaceAndQualifiers<'db> { self.map_type(|ty| ty.materialize(db, materialization_kind, visitor)) } /// 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. pub(crate) fn into_lookup_result(self) -> LookupResult<'db> { match self { PlaceAndQualifiers { place: Place::Type(ty, Boundness::Bound), qualifiers, } => Ok(TypeAndQualifiers::new(ty, qualifiers)), PlaceAndQualifiers { place: Place::Type(ty, Boundness::PossiblyUnbound), qualifiers, } => Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new( ty, qualifiers, ))), PlaceAndQualifiers { place: Place::Unbound, qualifiers, } => Err(LookupError::Unbound(qualifiers)), } } /// Safely unwrap the place and the qualifiers into a [`TypeQualifiers`]. /// /// If the place is definitely unbound or possibly unbound, it will be transformed into a /// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning /// the result of `diagnostic_fn` (which will be a [`TypeQualifiers`]). This allows the caller /// to ensure that a diagnostic is emitted if the place is possibly or definitely unbound. pub(crate) fn unwrap_with_diagnostic( self, diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>, ) -> TypeAndQualifiers<'db> { self.into_lookup_result().unwrap_or_else(diagnostic_fn) } /// Fallback (partially or fully) to another place if `self` is partially or fully unbound. /// /// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`. /// 2. Else, evaluate `fallback_fn()`: /// 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(, Boundness::Bound)` /// 4. Else, if `self` is possibly unbound and `fallback` is possibly unbound, /// return `Place(, Boundness::PossiblyUnbound)` #[must_use] pub(crate) fn or_fall_back_to( self, db: &'db dyn Db, fallback_fn: impl FnOnce() -> PlaceAndQualifiers<'db>, ) -> Self { self.into_lookup_result() .or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn())) .into() } } impl<'db> From> for PlaceAndQualifiers<'db> { fn from(place: Place<'db>) -> Self { place.with_qualifiers(TypeQualifiers::empty()) } } fn place_cycle_recover<'db>( _db: &'db dyn Db, _value: &PlaceAndQualifiers<'db>, _count: u32, _scope: ScopeId<'db>, _place_id: ScopedPlaceId, _requires_explicit_reexport: RequiresExplicitReExport, _considered_definitions: ConsideredDefinitions, ) -> salsa::CycleRecoveryAction> { salsa::CycleRecoveryAction::Iterate } fn place_cycle_initial<'db>( _db: &'db dyn Db, _scope: ScopeId<'db>, _place_id: ScopedPlaceId, _requires_explicit_reexport: RequiresExplicitReExport, _considered_definitions: ConsideredDefinitions, ) -> PlaceAndQualifiers<'db> { Place::bound(Type::Never).into() } #[salsa::tracked(cycle_fn=place_cycle_recover, cycle_initial=place_cycle_initial, heap_size=ruff_memory_usage::heap_size)] fn place_by_id<'db>( db: &'db dyn Db, scope: ScopeId<'db>, place_id: ScopedPlaceId, requires_explicit_reexport: RequiresExplicitReExport, considered_definitions: ConsideredDefinitions, ) -> PlaceAndQualifiers<'db> { let use_def = use_def_map(db, scope); // If the place is declared, the public type is based on declarations; otherwise, it's based // on inference from bindings. let declarations = match considered_definitions { ConsideredDefinitions::EndOfScope => use_def.end_of_scope_declarations(place_id), ConsideredDefinitions::AllReachable => use_def.all_reachable_declarations(place_id), }; let declared = place_from_declarations_impl(db, declarations, requires_explicit_reexport) .ignore_conflicting_declarations(); let all_considered_bindings = || match considered_definitions { ConsideredDefinitions::EndOfScope => use_def.end_of_scope_bindings(place_id), ConsideredDefinitions::AllReachable => use_def.all_reachable_bindings(place_id), }; // If a symbol is undeclared, but qualified with `typing.Final`, we use the right-hand side // inferred type, without unioning with `Unknown`, because it can not be modified. if let Some(qualifiers) = declared.is_bare_final() { let bindings = all_considered_bindings(); return place_from_bindings_impl(db, bindings, requires_explicit_reexport) .with_qualifiers(qualifiers); } match declared { // 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), 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( UnionType::from_elements(db, [Type::unknown(), inferred]), boundness, ) .with_qualifiers(qualifiers), Place::Unbound => { Place::Type(Type::unknown(), declaredness).with_qualifiers(qualifiers) } } } // Place is declared, trust the declared type place_and_quals @ PlaceAndQualifiers { place: Place::Type(_, Boundness::Bound), qualifiers: _, } => place_and_quals, // Place is possibly declared PlaceAndQualifiers { place: Place::Type(declared_ty, Boundness::PossiblyUnbound), qualifiers, } => { let bindings = all_considered_bindings(); let boundness_analysis = bindings.boundness_analysis; let inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport); 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 // design work though as we might want a different behavior for stubs and for // normal modules. Place::Type(declared_ty, Boundness::Bound) } // Place is possibly undeclared and (possibly) bound Place::Type(inferred_ty, boundness) => Place::Type( UnionType::from_elements(db, [inferred_ty, declared_ty]), if boundness_analysis == BoundnessAnalysis::AssumeBound { Boundness::Bound } else { boundness }, ), }; PlaceAndQualifiers { place, qualifiers } } // Place is undeclared, return the union of `Unknown` with the inferred type PlaceAndQualifiers { place: Place::Unbound, qualifiers: _, } => { let bindings = all_considered_bindings(); let boundness_analysis = bindings.boundness_analysis; 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); } } // `__slots__` is a symbol with special behavior in Python's runtime. It can be // modified externally, but those changes do not take effect. We therefore issue // a diagnostic if we see it being modified externally. In type inference, we // can assign a "narrow" type to it even if it is not *declared*. This means, we // do not have to call [`widen_type_for_undeclared_public_symbol`]. // // `TYPE_CHECKING` is a special variable that should only be assigned `False` // at runtime, but is always considered `True` in type checking. // See mdtest/known_constants.md#user-defined-type_checking for details. let is_considered_non_modifiable = place_id.as_symbol().is_some_and(|symbol_id| { matches!( place_table(db, scope).symbol(symbol_id).name().as_str(), "__slots__" | "TYPE_CHECKING" ) }); if scope.file(db).is_stub(db) || scope.scope(db).visibility().is_private() { // We generally trust module-level undeclared places in stubs and do not union // with `Unknown`. If we don't do this, simple aliases like `IOError = OSError` in // stubs would result in `IOError` being a union of `OSError` and `Unknown`, which // leads to all sorts of downstream problems. Similarly, type variables are often // defined as `_T = TypeVar("_T")`, without being declared. // Also, if the scope is private, such as a function scope, // meaning that the place cannot be rewritten from elsewhere, we do not union with `Unknown`. inferred.into() } else { widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable) .into() } } } // TODO (ticket: https://github.com/astral-sh/ruff/issues/14297) Our handling of boundness // currently only depends on bindings, and ignores declarations. This is inconsistent, since // we only look at bindings if the place may be undeclared. Consider the following example: // ```py // x: int // // if flag: // y: int // else // y = 3 // ``` // If we import from this module, we will currently report `x` as a definitely-bound place // (even though it has no bindings at all!) but report `y` as possibly-unbound (even though // every path has either a binding or a declaration for it.) } /// Implementation of [`symbol`]. fn symbol_impl<'db>( db: &'db dyn Db, scope: ScopeId<'db>, name: &str, requires_explicit_reexport: RequiresExplicitReExport, considered_definitions: ConsideredDefinitions, ) -> PlaceAndQualifiers<'db> { let _span = tracing::trace_span!("symbol", ?name).entered(); if name == "platform" && file_to_module(db, scope.file(db)) .is_some_and(|module| module.is_known(db, KnownModule::Sys)) { match Program::get(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { return Place::bound(Type::string_literal(db, platform.as_str())).into(); } crate::PythonPlatform::All => { // Fall through to the looked up type } } } place_table(db, scope) .symbol_id(name) .map(|symbol| { place_by_id( db, scope, symbol.into(), requires_explicit_reexport, considered_definitions, ) }) .unwrap_or_default() } fn place_impl<'db>( db: &'db dyn Db, scope: ScopeId<'db>, place: PlaceExprRef, requires_explicit_reexport: RequiresExplicitReExport, considered_definitions: ConsideredDefinitions, ) -> PlaceAndQualifiers<'db> { let _span = tracing::trace_span!("place_impl", ?place).entered(); place_table(db, scope) .place_id(place) .map(|place| { place_by_id( db, scope, place, requires_explicit_reexport, considered_definitions, ) }) .unwrap_or_default() } /// Implementation of [`place_from_bindings`]. /// /// ## Implementation Note /// This function gets called cross-module. It, therefore, shouldn't /// access any AST nodes from the file containing the declarations. fn place_from_bindings_impl<'db>( db: &'db dyn Db, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, requires_explicit_reexport: RequiresExplicitReExport, ) -> Place<'db> { let predicates = bindings_with_constraints.predicates; let reachability_constraints = bindings_with_constraints.reachability_constraints; let boundness_analysis = bindings_with_constraints.boundness_analysis; let mut bindings_with_constraints = bindings_with_constraints.peekable(); let is_non_exported = |binding: Definition<'db>| { requires_explicit_reexport.is_yes() && !is_reexported(db, binding) }; let unbound_reachability_constraint = match bindings_with_constraints.peek() { Some(BindingWithConstraints { binding, reachability_constraint, narrowing_constraint: _, }) if binding.is_undefined_or(is_non_exported) => Some(*reachability_constraint), _ => None, }; let mut deleted_reachability = Truthiness::AlwaysFalse; // Evaluate this lazily because we don't always need it (for example, if there are no visible // bindings at all, we don't need it), and it can cause us to evaluate reachability constraint // expressions, which is extra work and can lead to cycles. let unbound_visibility = || { unbound_reachability_constraint.map(|reachability_constraint| { reachability_constraints.evaluate(db, predicates, reachability_constraint) }) }; let mut types = bindings_with_constraints.filter_map( |BindingWithConstraints { binding, narrowing_constraint, reachability_constraint, }| { let binding = match binding { DefinitionState::Defined(binding) => binding, DefinitionState::Undefined => { return None; } DefinitionState::Deleted => { deleted_reachability = deleted_reachability.or( reachability_constraints.evaluate(db, predicates, reachability_constraint) ); return None; } }; if is_non_exported(binding) { return None; } let static_reachability = reachability_constraints.evaluate(db, predicates, reachability_constraint); if static_reachability.is_always_false() { // If the static reachability evaluates to false, the binding is either not reachable // from the start of the scope, or there is no control flow path from that binding to // the use of the place that we are investigating. There are three interesting cases // to consider: // // ```py // def f1(): // if False: // x = 1 // use(x) // // def f2(): // y = 1 // return // use(y) // // def f3(flag: bool): // if flag: // z = 1 // else: // z = 2 // return // use(z) // ``` // // In the first case, there is a single binding for `x`, but it is not reachable from // the start of the scope. However, the use of `x` is reachable (`unbound_reachability` // is not always-false). This means that `x` is unbound and we should return `None`. // // In the second case, the binding of `y` is reachable, but there is no control flow // path from the beginning of the scope, through that binding, to the use of `y` that // we are investigating. There is also no control flow path from the start of the // scope, through the implicit `y = ` binding, to the use of `y`. This means // that `unbound_reachability` is always false. Since there are no other bindings, no // control flow path can reach this use of `y`, implying that we are in unreachable // section of code. We return `Never` in order to silence the `unresolve-reference` // diagnostic that would otherwise be emitted at the use of `y`. // // In the third case, we have two bindings for `z`. The first one is visible (there // is a path of control flow from the start of the scope, through that binding, to // the use of `z`). So we consider the case that we now encounter the second binding // `z = 2`, which is not visible due to the early return. The `z = ` binding // is not live (shadowed by the other bindings), so `unbound_reachability` is `None`. // Here, we are *not* in an unreachable section of code. However, it is still okay to // return `Never` in this case, because we will union the types of all bindings, and // `Never` will be eliminated automatically. if unbound_visibility().is_none_or(Truthiness::is_always_false) { return Some(Type::Never); } return None; } let binding_ty = binding_type(db, binding); Some(narrowing_constraint.narrow(db, binding_ty, binding.place(db))) }, ); if let Some(first) = types.next() { let ty = if let Some(second) = types.next() { let mut builder = PublicTypeBuilder::new(db); builder.add(first); builder.add(second); for ty in types { builder.add(ty); } builder.build() } else { first }; let boundness = match boundness_analysis { BoundnessAnalysis::AssumeBound => Boundness::Bound, 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, }, }; match deleted_reachability { Truthiness::AlwaysFalse => Place::Type(ty, boundness), Truthiness::AlwaysTrue => Place::Unbound, Truthiness::Ambiguous => Place::Type(ty, Boundness::PossiblyUnbound), } } else { Place::Unbound } } /// Accumulates types from multiple bindings or declarations, and eventually builds a /// union type from them. /// /// `@overload`ed function literal types are discarded if they are immediately followed /// by their implementation. This is to ensure that we do not merge all of them into the /// union type. The last one will include the other overloads already. struct PublicTypeBuilder<'db> { db: &'db dyn Db, queue: Option>, builder: UnionBuilder<'db>, } impl<'db> PublicTypeBuilder<'db> { fn new(db: &'db dyn Db) -> Self { PublicTypeBuilder { db, queue: None, builder: UnionBuilder::new(db), } } fn add_to_union(&mut self, element: Type<'db>) { self.builder.add_in_place(element); } fn drain_queue(&mut self) { if let Some(queued_element) = self.queue.take() { self.add_to_union(queued_element); } } fn add(&mut self, element: Type<'db>) -> bool { match element { Type::FunctionLiteral(function) => { if function .literal(self.db) .last_definition(self.db) .is_overload(self.db) { self.queue = Some(element); false } else { self.queue = None; self.add_to_union(element); true } } _ => { self.drain_queue(); self.add_to_union(element); true } } } fn build(mut self) -> Type<'db> { self.drain_queue(); self.builder.build() } } /// Accumulates multiple (potentially conflicting) declared types and type qualifiers, /// and eventually builds a union from them. struct DeclaredTypeBuilder<'db> { inner: PublicTypeBuilder<'db>, qualifiers: TypeQualifiers, first_type: Option>, conflicting_types: FxOrderSet>, } impl<'db> DeclaredTypeBuilder<'db> { fn new(db: &'db dyn Db) -> Self { DeclaredTypeBuilder { inner: PublicTypeBuilder::new(db), qualifiers: TypeQualifiers::empty(), first_type: None, conflicting_types: FxOrderSet::default(), } } fn add(&mut self, element: TypeAndQualifiers<'db>) { let element_ty = element.inner_type(); if self.inner.add(element_ty) { if let Some(first_ty) = self.first_type { if !first_ty.is_equivalent_to(self.inner.db, element_ty) { self.conflicting_types.insert(element_ty); } } else { self.first_type = Some(element_ty); } } self.qualifiers = self.qualifiers.union(element.qualifiers()); } fn build(mut self) -> DeclaredTypeAndConflictingTypes<'db> { let type_and_quals = TypeAndQualifiers::new(self.inner.build(), self.qualifiers); if self.conflicting_types.is_empty() { (type_and_quals, None) } else { self.conflicting_types.insert_before( 0, self.first_type .expect("there must be a first type if there are conflicting types"), ); ( type_and_quals, Some(self.conflicting_types.into_boxed_slice()), ) } } } /// Implementation of [`place_from_declarations`]. /// /// ## Implementation Note /// This function gets called cross-module. It, therefore, shouldn't /// access any AST nodes from the file containing the declarations. fn place_from_declarations_impl<'db>( db: &'db dyn Db, declarations: DeclarationsIterator<'_, 'db>, requires_explicit_reexport: RequiresExplicitReExport, ) -> PlaceFromDeclarationsResult<'db> { let predicates = declarations.predicates; let reachability_constraints = declarations.reachability_constraints; let boundness_analysis = declarations.boundness_analysis; let mut declarations = declarations.peekable(); let mut first_declaration = None; let mut exactly_one_declaration = false; let is_non_exported = |declaration: Definition<'db>| { requires_explicit_reexport.is_yes() && !is_reexported(db, declaration) }; let undeclared_reachability = match declarations.peek() { Some(DeclarationWithConstraint { declaration, reachability_constraint, }) if declaration.is_undefined_or(is_non_exported) => { reachability_constraints.evaluate(db, predicates, *reachability_constraint) } _ => Truthiness::AlwaysFalse, }; let mut all_declarations_definitely_reachable = true; let mut types = declarations.filter_map( |DeclarationWithConstraint { declaration, reachability_constraint, }| { let DefinitionState::Defined(declaration) = declaration else { return None; }; if is_non_exported(declaration) { return None; } if first_declaration.is_none() { first_declaration = Some(declaration); exactly_one_declaration = true; } else { exactly_one_declaration = false; } let static_reachability = reachability_constraints.evaluate(db, predicates, reachability_constraint); if static_reachability.is_always_false() { None } else { all_declarations_definitely_reachable = all_declarations_definitely_reachable && static_reachability.is_always_true(); Some(declaration_type(db, declaration)) } }, ); if let Some(first) = types.next() { let (declared, conflicting) = if let Some(second) = types.next() { let mut builder = DeclaredTypeBuilder::new(db); builder.add(first); builder.add(second); for element in types { builder.add(element); } builder.build() } else { (first, None) }; let boundness = match boundness_analysis { BoundnessAnalysis::AssumeBound => { if all_declarations_definitely_reachable { Boundness::Bound } 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 } } BoundnessAnalysis::BasedOnUnboundVisibility => match undeclared_reachability { Truthiness::AlwaysTrue => { unreachable!( "If we have at least one declaration, the implicit `unbound` binding should not be definitely visible" ) } Truthiness::AlwaysFalse => Boundness::Bound, Truthiness::Ambiguous => Boundness::PossiblyUnbound, }, }; let place_and_quals = Place::Type(declared.inner_type(), boundness).with_qualifiers(declared.qualifiers()); if let Some(conflicting) = conflicting { PlaceFromDeclarationsResult::conflict(place_and_quals, conflicting) } else { PlaceFromDeclarationsResult { place_and_quals, conflicting_types: None, single_declaration: first_declaration.filter(|_| exactly_one_declaration), } } } else { PlaceFromDeclarationsResult { place_and_quals: Place::Unbound.into(), conflicting_types: None, single_declaration: None, } } } // Returns `true` if the `definition` is re-exported. // // This will first check if the definition is using the "redundant alias" pattern like `import foo // as foo` or `from foo import bar as bar`. If it's not, it will check whether the symbol is being // exported via `__all__`. fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { // This information is computed by the semantic index builder. if definition.is_reexported(db) { return true; } // At this point, the definition should either be an `import` or `from ... import` statement. // This is because the default value of `is_reexported` is `true` for any other kind of // definition. let Some(all_names) = dunder_all_names(db, definition.file(db)) else { return false; }; let table = place_table(db, definition.scope(db)); let symbol_id = definition.place(db).expect_symbol(); let symbol_name = table.symbol(symbol_id).name(); all_names.contains(symbol_name) } mod implicit_globals { use ruff_python_ast as ast; use ruff_python_ast::name::Name; use crate::Program; use crate::db::Db; use crate::place::{Boundness, PlaceAndQualifiers}; use crate::semantic_index::symbol::Symbol; use crate::semantic_index::{place_table, use_def_map}; use crate::types::{CallableType, KnownClass, Parameter, Parameters, Signature, Type}; use ruff_python_ast::PythonVersion; use super::{Place, place_from_declarations}; pub(crate) fn module_type_implicit_global_declaration<'db>( db: &'db dyn Db, name: &str, ) -> PlaceAndQualifiers<'db> { if !module_type_symbols(db) .iter() .any(|module_type_member| module_type_member == name) { return Place::Unbound.into(); } let Type::ClassLiteral(module_type_class) = KnownClass::ModuleType.to_class_literal(db) else { return Place::Unbound.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(); }; place_from_declarations( db, use_def_map(db, module_type_scope).end_of_scope_symbol_declarations(symbol_id), ) .ignore_conflicting_declarations() } /// Looks up the type of an "implicit global symbol". Returns [`Place::Unbound`] if /// `name` is not present as an implicit symbol in module-global namespaces. /// /// Implicit global symbols are symbols such as `__doc__`, `__name__`, and `__file__` /// that are implicitly defined in every module's global scope. Because their type is /// always the same, we simply look these up as instance attributes on `types.ModuleType`. /// /// Note that this function should only be used as a fallback if a symbol is being looked /// 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 /// 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>( db: &'db dyn Db, name: &str, ) -> PlaceAndQualifiers<'db> { match name { // We special-case `__file__` here because we know that for an internal implicit global // lookup in a Python module, it is always a string, even though typeshed says `str | // None`. "__file__" => Place::bound(KnownClass::Str.to_instance(db)).into(), "__builtins__" => Place::bound(Type::any()).into(), "__debug__" => Place::bound(KnownClass::Bool.to_instance(db)).into(), // Created lazily by the warnings machinery; may be absent. // Model as possibly-unbound to avoid false negatives. "__warningregistry__" => Place::Type( KnownClass::Dict .to_specialized_instance(db, [Type::any(), KnownClass::Int.to_instance(db)]), Boundness::PossiblyUnbound, ) .into(), // Marked as possibly-unbound as it is only present in the module namespace // if at least one global symbol is annotated in the module. "__annotate__" if Program::get(db).python_version(db) >= PythonVersion::PY314 => { let signature = Signature::new( Parameters::new( [Parameter::positional_only(Some(Name::new_static("format"))) .with_annotated_type(KnownClass::Int.to_instance(db))], ), Some(KnownClass::Dict.to_specialized_instance( db, [KnownClass::Str.to_instance(db), Type::any()], )), ); Place::Type( CallableType::function_like(db, signature), Boundness::PossiblyUnbound, ) .into() } // In general we wouldn't check to see whether a symbol exists on a class before doing the // `.member()` call on the instance type -- we'd just do the `.member`() call on the instance // type, since it has the same end result. The reason to only call `.member()` on `ModuleType` // when absolutely necessary is that this function is used in a very hot path (name resolution // in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation. _ if module_type_symbols(db) .iter() .any(|module_type_member| &**module_type_member == name) => { KnownClass::ModuleType.to_instance(db).member(db, name) } _ => Place::Unbound.into(), } } /// An internal micro-optimisation for `module_type_implicit_global_symbol`. /// /// This function returns a list of the symbols that typeshed declares in the /// body scope of the stub for the class `types.ModuleType`. /// /// The returned list excludes the attributes `__dict__` and `__init__`. These are very /// special members that can be accessed as attributes on the module when imported, /// but cannot be accessed as globals *inside* the module. /// /// The list also excludes `__getattr__`. `__getattr__` is even more special: it doesn't /// exist at runtime, but typeshed includes it to reduce false positives associated with /// functions that dynamically import modules and return `Instance(types.ModuleType)`. /// We should ignore it for any known module-literal type. /// /// Conceptually this function could be a `Set` rather than a list, /// but the number of symbols declared in this scope is likely to be very small, /// so the cost of hashing the names is likely to be more expensive than it's worth. #[salsa::tracked( returns(deref), cycle_initial=module_type_symbols_initial, cycle_fn=module_type_symbols_cycle_recover, heap_size=ruff_memory_usage::heap_size )] fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { let Some(module_type) = KnownClass::ModuleType .to_class_literal(db) .into_class_literal() else { // The most likely way we get here is if a user specified a `--custom-typeshed-dir` // without a `types.pyi` stub in the `stdlib/` directory return smallvec::SmallVec::default(); }; let module_type_scope = module_type.body_scope(db); let module_type_symbol_table = place_table(db, module_type_scope); module_type_symbol_table .symbols() .filter(|symbol| symbol.is_declared()) .map(Symbol::name) .filter(|symbol_name| { !matches!( symbol_name.as_str(), "__dict__" | "__getattr__" | "__init__" ) }) .cloned() .collect() } fn module_type_symbols_initial(_db: &dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { smallvec::SmallVec::default() } fn module_type_symbols_cycle_recover( _db: &dyn Db, _value: &smallvec::SmallVec<[ast::name::Name; 8]>, _count: u32, ) -> salsa::CycleRecoveryAction> { salsa::CycleRecoveryAction::Iterate } #[cfg(test)] mod tests { use super::*; use crate::db::tests::setup_db; #[test] fn module_type_symbols_includes_declared_types_but_not_referenced_types() { let db = setup_db(); let symbol_names = module_type_symbols(&db); let dunder_name_symbol_name = ast::name::Name::new_static("__name__"); assert!(symbol_names.contains(&dunder_name_symbol_name)); let property_symbol_name = ast::name::Name::new_static("property"); assert!(!symbol_names.contains(&property_symbol_name)); } } } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub(crate) enum RequiresExplicitReExport { Yes, No, } impl RequiresExplicitReExport { const fn is_yes(self) -> bool { matches!(self, RequiresExplicitReExport::Yes) } } /// Specifies which definitions should be considered when looking up a place. /// /// In the example below, the `EndOfScope` variant would consider the `x = 2` and `x = 3` definitions, /// while the `AllReachable` variant would also consider the `x = 1` definition. /// ```py /// def _(): /// x = 1 /// /// x = 2 /// /// if flag(): /// x = 3 /// ``` #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub(crate) enum ConsideredDefinitions { /// Consider only the definitions that are "live" at the end of the scope, i.e. those /// that have not been shadowed or deleted. EndOfScope, /// Consider all definitions that are reachable from the start of the scope. AllReachable, } /// Specifies how the boundness of a place should be determined. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub(crate) enum BoundnessAnalysis { /// The place is always considered bound. AssumeBound, /// The boundness of the place is determined based on the visibility of the implicit /// `unbound` binding. In the example below, when analyzing the visibility of the /// `x = ` 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`. /// /// ```py /// x = /// /// if flag(): /// x = 1 /// ``` BasedOnUnboundVisibility, } /// Computes a possibly-widened type `Unknown | T_inferred` from the inferred type `T_inferred` /// of a symbol, unless the type is a known-instance type (e.g. `typing.Any`) or the symbol is /// considered non-modifiable (e.g. when the symbol is `@Final`). We need this for public uses /// of symbols that have no declared type. fn widen_type_for_undeclared_public_symbol<'db>( db: &'db dyn Db, inferred: Place<'db>, is_considered_non_modifiable: bool, ) -> Place<'db> { // We special-case known-instance types here since symbols like `typing.Any` are typically // not declared in the stubs (e.g. `Any = object()`), but we still want to treat them as // such. let is_known_instance = inferred .ignore_possibly_unbound() .is_some_and(|ty| matches!(ty, Type::SpecialForm(_) | Type::KnownInstance(_))); if is_considered_non_modifiable || is_known_instance { inferred } else { inferred.map_type(|ty| UnionType::from_elements(db, [Type::unknown(), ty])) } } #[cfg(test)] mod tests { use super::*; use crate::db::tests::setup_db; #[test] fn test_symbol_or_fall_back_to() { use Boundness::{Bound, PossiblyUnbound}; let db = setup_db(); let ty1 = Type::IntLiteral(1); let ty2 = Type::IntLiteral(2); let unbound = || Place::Unbound.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 bound_ty1 = || Place::Type(ty1, Bound).with_qualifiers(TypeQualifiers::empty()); let bound_ty2 = || Place::Type(ty2, Bound).with_qualifiers(TypeQualifiers::empty()); // Start from an unbound symbol assert_eq!(unbound().or_fall_back_to(&db, unbound), unbound()); assert_eq!( unbound().or_fall_back_to(&db, possibly_unbound_ty1), possibly_unbound_ty1() ); assert_eq!(unbound().or_fall_back_to(&db, bound_ty1), bound_ty1()); // Start from a possibly unbound symbol assert_eq!( possibly_unbound_ty1().or_fall_back_to(&db, unbound), possibly_unbound_ty1() ); assert_eq!( possibly_unbound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), Place::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound).into() ); assert_eq!( possibly_unbound_ty1().or_fall_back_to(&db, bound_ty2), Place::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound).into() ); // Start from a definitely bound symbol assert_eq!(bound_ty1().or_fall_back_to(&db, unbound), bound_ty1()); assert_eq!( bound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), bound_ty1() ); assert_eq!(bound_ty1().or_fall_back_to(&db, bound_ty2), bound_ty1()); } #[track_caller] fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Place<'db>) { assert!(matches!( symbol, Place::Type(Type::NominalInstance(_), Boundness::Bound) )); assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db)); } #[test] fn implicit_builtin_globals() { let db = setup_db(); assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__").place); } #[test] fn implicit_typing_globals() { let db = setup_db(); assert_bound_string_symbol(&db, typing_symbol(&db, "__name__").place); } #[test] fn implicit_typing_extensions_globals() { let db = setup_db(); assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__").place); } #[test] fn implicit_sys_globals() { let db = setup_db(); assert_bound_string_symbol( &db, known_module_symbol(&db, KnownModule::Sys, "__name__").place, ); } }