diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6aa64498291ee..02005655fdba7 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -65,7 +65,7 @@ use crate::types::generics::{ pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder}; use crate::types::known_instance::{InternedConstraintSet, InternedType, UnionTypeInstance}; pub use crate::types::method::{BoundMethodType, KnownBoundMethodType, WrapperDescriptorKind}; -use crate::types::mro::{Mro, MroIterator, StaticMroError}; +use crate::types::mro::{MroIterator, StaticMroError}; pub(crate) use crate::types::narrow::{ NarrowingConstraint, PossiblyNarrowedPlaces, PossiblyNarrowedPlacesBuilder, infer_narrowing_constraint, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 49f0e80484bfd..627c81826cb3a 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1,184 +1,58 @@ -use std::borrow::Cow; -use std::cell::RefCell; use std::fmt::Write; -use std::sync::{LazyLock, Mutex}; +pub(crate) use self::dynamic_literal::{ + DynamicClassAnchor, DynamicClassLiteral, DynamicMetaclassConflict, +}; +pub use self::known::KnownClass; +use self::named_tuple::synthesize_namedtuple_class_member; +pub(super) use self::named_tuple::{ + DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, NamedTupleField, NamedTupleSpec, +}; +pub(crate) use self::static_literal::StaticClassLiteral; use super::{ - BoundTypeVarInstance, MemberLookupPolicy, Mro, MroIterator, SpecialFormType, StaticMroError, - SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase, - function::FunctionType, + BoundTypeVarInstance, MemberLookupPolicy, MroIterator, SpecialFormType, SubclassOfType, Type, + TypeQualifiers, class_base::ClassBase, function::FunctionType, }; use super::{TypeVarVariance, display}; use crate::place::{DefinedPlace, TypeOrigin}; -use crate::semantic_index::definition::{Definition, DefinitionState}; -use crate::semantic_index::scope::{NodeWithScopeKind, Scope}; -use crate::semantic_index::symbol::Symbol; -use crate::semantic_index::{ - DeclarationWithConstraint, SemanticIndex, attribute_declarations, attribute_scopes, -}; -use crate::types::bound_super::BoundSuperError; +use crate::semantic_index::definition::Definition; use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; -use crate::types::context::InferContext; -use crate::types::diagnostic::{INVALID_DATACLASS_OVERRIDE, SUPER_CALL_IN_NAMED_TUPLE_METHOD}; -use crate::types::enums::{ - enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value, -}; -use crate::types::function::{ - AbstractMethodKind, DataclassTransformerFlags, DataclassTransformerParams, KnownFunction, - is_implicit_classmethod, is_implicit_staticmethod, -}; +use crate::types::function::{AbstractMethodKind, DataclassTransformerParams}; use crate::types::generics::{ GenericContext, InferableTypeVars, Specialization, walk_specialization, }; -use crate::types::infer::{infer_expression_type, infer_unpack_types, nearest_enclosing_class}; use crate::types::known_instance::DeprecatedInstance; -use crate::types::member::{Member, class_member}; -use crate::types::mro::DynamicMroError; +use crate::types::member::Member; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; -use crate::types::tuple::{Tuple, TupleSpec, TupleType}; -use crate::types::typed_dict::{TypedDictParams, typed_dict_params_from_class_def}; -use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; +use crate::types::tuple::TupleSpec; use crate::types::{ - ApplyTypeMappingVisitor, Binding, BindingContext, BoundSuperType, CallableType, CallableTypes, - DATACLASS_FLAGS, DataclassFlags, DataclassParams, FindLegacyTypeVarsVisitor, - IntersectionBuilder, KnownInstanceType, MaterializationKind, PropertyInstanceType, TypeContext, - TypeMapping, UnionBuilder, VarianceInferable, binding_type, declaration_type, - determine_upper_bound, + ApplyTypeMappingVisitor, CallableType, CallableTypes, DataclassParams, + FindLegacyTypeVarsVisitor, IntersectionBuilder, TypeContext, TypeMapping, UnionBuilder, + VarianceInferable, }; use crate::{ - Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, + Db, FxIndexMap, FxOrderSet, place::{ Definedness, LookupError, LookupResult, Place, PlaceAndQualifiers, Widening, - known_module_symbol, place_from_bindings, place_from_declarations, - }, - semantic_index::{ - attribute_assignments, - definition::{DefinitionKind, TargetKind}, - place_table, - scope::ScopeId, - semantic_index, use_def_map, - }, - types::{ - CallArguments, CallError, CallErrorKind, MetaclassCandidate, MetaclassTransformInfo, - TypeDefinition, UnionType, definition_expression_type, + place_from_bindings, place_from_declarations, }, + semantic_index::{place_table, use_def_map}, + types::{MetaclassCandidate, TypeDefinition, UnionType}, }; -use indexmap::IndexSet; -use itertools::{Either, Itertools as _}; use ruff_db::diagnostic::Span; use ruff_db::files::File; -use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast, NodeIndex, PythonVersion}; -use ruff_text_size::{Ranged, TextRange}; -use rustc_hash::FxHashSet; -use ty_module_resolver::{KnownModule, file_to_module}; - -fn implicit_attribute_initial<'db>( - _db: &'db dyn Db, - id: salsa::Id, - _class_body_scope: ScopeId<'db>, - _name: String, - _target_method_decorator: MethodDecorator, -) -> Member<'db> { - Member { - inner: Place::bound(Type::divergent(id)).into(), - } -} - -#[allow(clippy::too_many_arguments)] -fn implicit_attribute_cycle_recover<'db>( - db: &'db dyn Db, - cycle: &salsa::Cycle, - previous_member: &Member<'db>, - member: Member<'db>, - _class_body_scope: ScopeId<'db>, - _name: String, - _target_method_decorator: MethodDecorator, -) -> Member<'db> { - let inner = member - .inner - .cycle_normalized(db, previous_member.inner, cycle); - Member { inner } -} - -fn static_class_try_mro_cycle_initial<'db>( - db: &'db dyn Db, - _id: salsa::Id, - self_: StaticClassLiteral<'db>, - specialization: Option>, -) -> Result, StaticMroError<'db>> { - Err(StaticMroError::cycle( - db, - self_.apply_optional_specialization(db, specialization), - )) -} +use ruff_python_ast::{self as ast}; +use ruff_text_size::TextRange; -#[allow(clippy::unnecessary_wraps)] -fn try_metaclass_cycle_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _self_: StaticClassLiteral<'db>, -) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { - Err(MetaclassError { - kind: MetaclassErrorKind::Cycle, - }) -} - -fn explicit_bases_cycle_initial<'db>( - db: &'db dyn Db, - id: salsa::Id, - literal: StaticClassLiteral<'db>, -) -> Box<[Type<'db>]> { - let module = parsed_module(db, literal.file(db)).load(db); - let class_stmt = literal.node(db, &module); - // Try to produce a list of `Divergent` types of the right length. However, if one or more of - // the bases is a starred expression, we don't know how many entries that will eventually - // expand to. - vec![Type::divergent(id); class_stmt.bases().len()].into_boxed_slice() -} - -fn explicit_bases_cycle_fn<'db>( - db: &'db dyn Db, - cycle: &salsa::Cycle, - previous: &[Type<'db>], - current: Box<[Type<'db>]>, - _literal: StaticClassLiteral<'db>, -) -> Box<[Type<'db>]> { - if previous.len() == current.len() { - // As long as the length of bases hasn't changed, use the same "monotonic widening" - // strategy that we use with most types, to avoid oscillations. - current - .iter() - .zip(previous.iter()) - .map(|(curr, prev)| curr.cycle_normalized(db, *prev, cycle)) - .collect() - } else { - // The length of bases has changed, presumably because we expanded a starred expression. We - // don't do "monotonic widening" here, because we don't want to make assumptions about - // which previous entries correspond to which current ones. An oscillation here would be - // unfortunate, but maybe only pathological programs can trigger such a thing. - current - } -} - -#[expect(clippy::unnecessary_wraps)] -fn dynamic_class_try_mro_cycle_initial<'db>( - db: &'db dyn Db, - _id: salsa::Id, - self_: DynamicClassLiteral<'db>, -) -> Result, DynamicMroError<'db>> { - // When there's a cycle, return a minimal MRO with just the class itself and object. - // This breaks the cycle and allows type checking to continue. - Ok(Mro::from([ - ClassBase::Class(ClassType::NonGeneric(self_.into())), - ClassBase::object(db), - ])) -} +mod dynamic_literal; +mod known; +mod named_tuple; +mod static_literal; /// A category of classes with code generation capabilities (with synthesized methods). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] @@ -2147,4178 +2021,174 @@ impl<'db> Field<'db> { } } -/// Representation of a class definition statement in the AST: either a non-generic class, or a -/// generic class that has not been specialized. -/// -/// This does not in itself represent a type, but can be transformed into a [`ClassType`] that -/// does. (For generic classes, this requires specializing its generic context.) -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct StaticClassLiteral<'db> { - /// Name of the class at definition - #[returns(ref)] - pub(crate) name: Name, - - pub(crate) body_scope: ScopeId<'db>, - - pub(crate) known: Option, - - /// If this class is deprecated, this holds the deprecation message. - pub(crate) deprecated: Option>, - - pub(crate) type_check_only: bool, - - pub(crate) dataclass_params: Option>, - pub(crate) dataclass_transformer_params: Option>, - - /// Whether this class is decorated with `@functools.total_ordering` - pub(crate) total_ordering: bool, +impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { + fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { + match self { + Self::Static(class) => class.variance_of(db, typevar), + Self::Dynamic(_) | Self::DynamicNamedTuple(_) => TypeVarVariance::Bivariant, + } + } } -// The Salsa heap is tracked separately. -impl get_size2::GetSize for StaticClassLiteral<'_> {} - -fn generic_context_cycle_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _self: StaticClassLiteral<'db>, -) -> Option> { - None +/// Performs member lookups over an MRO (Method Resolution Order). +/// +/// This struct encapsulates the shared logic for looking up class and instance +/// members by iterating through an MRO. Both `StaticClassLiteral` and `DynamicClassLiteral` +/// use this to avoid duplicating the MRO traversal logic. +pub(super) struct MroLookup<'db, I> { + db: &'db dyn Db, + mro_iter: I, } -#[salsa::tracked] -impl<'db> StaticClassLiteral<'db> { - /// Return `true` if this class represents `known_class` - pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { - self.known(db) == Some(known_class) - } - - pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { - self.is_known(db, KnownClass::Tuple) +impl<'db, I: Iterator>> MroLookup<'db, I> { + /// Create a new MRO lookup from a database and an MRO iterator. + pub(super) fn new(db: &'db dyn Db, mro_iter: I) -> Self { + Self { db, mro_iter } } - /// Returns `true` if this class inherits from a functional namedtuple - /// (`DynamicNamedTupleLiteral`) that has unknown fields. + /// Look up a class member by iterating through the MRO. /// - /// When the base namedtuple's fields were determined dynamically (e.g., from a variable), - /// we can't synthesize precise method signatures and should fall back to `NamedTupleFallback`. - pub(crate) fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { - self.explicit_bases(db).iter().any(|base| match base { - Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) => { - !namedtuple.has_known_fields(db) - } - _ => false, - }) - } - - /// Returns `true` if this class is a dataclass-like class. + /// Parameters: + /// - `name`: The member name to look up + /// - `policy`: Controls which classes in the MRO to skip + /// - `inherited_generic_context`: Generic context for `own_class_member` calls + /// - `is_self_object`: Whether the class itself is `object` (affects policy filtering) /// - /// This covers `@dataclass`-decorated classes, as well as classes created via - /// `dataclass_transform` (function-based, metaclass-based, and base-class-based). - pub(crate) fn is_dataclass_like(self, db: &'db dyn Db) -> bool { - matches!( - CodeGeneratorKind::from_class(db, ClassLiteral::Static(self), None), - Some(CodeGeneratorKind::DataclassLike(_)) - ) - } - - /// Returns a new [`StaticClassLiteral`] with the given dataclass params, preserving all other fields. - pub(crate) fn with_dataclass_params( + /// Returns `ClassMemberResult::TypedDict` if a `TypedDict` base is encountered, + /// allowing the caller to handle this case specially. + /// + /// If we encounter a dynamic type in the MRO, we save it and after traversal: + /// 1. Use it as the type if no other classes define the attribute, or + /// 2. Intersect it with the type from non-dynamic MRO members. + pub(super) fn class_member( self, - db: &'db dyn Db, - dataclass_params: Option>, - ) -> Self { - Self::new( - db, - self.name(db).clone(), - self.body_scope(db), - self.known(db), - self.deprecated(db), - self.type_check_only(db), - dataclass_params, - self.dataclass_transformer_params(db), - self.total_ordering(db), - ) - } - - /// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`, - /// `__ge__`) in its own body (not inherited). Used by `@total_ordering` to determine if - /// synthesis is valid. - #[salsa::tracked] - pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { - let body_scope = self.body_scope(db); - ["__lt__", "__le__", "__gt__", "__ge__"] - .iter() - .any(|method| !class_member(db, body_scope, method).is_undefined()) - } + name: &str, + policy: MemberLookupPolicy, + inherited_generic_context: Option>, + is_self_object: bool, + ) -> ClassMemberResult<'db> { + let db = self.db; + let mut dynamic_type: Option> = None; + let mut lookup_result: LookupResult<'db> = + Err(LookupError::Undefined(TypeQualifiers::empty())); - /// Returns `true` if any class in this class's MRO (excluding `object`) defines an ordering - /// method (`__lt__`, `__le__`, `__gt__`, `__ge__`). Used by `@total_ordering` validation. - pub(super) fn has_ordering_method_in_mro( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> bool { - self.total_ordering_root_method(db, specialization) - .is_some() - } + for superclass in self.mro_iter { + match superclass { + ClassBase::Generic | ClassBase::Protocol => { + // Skip over these very special class bases that aren't really classes. + } + ClassBase::Dynamic(_) => { + // Note: calling `Type::from(superclass).member()` would be incorrect here. + // What we'd really want is a `Type::Any.own_class_member()` method, + // but adding such a method wouldn't make much sense -- it would always return `Any`! + dynamic_type.get_or_insert(Type::from(superclass)); + } + ClassBase::Class(class) => { + let known = class.known(db); - /// Returns the type of the ordering method used by `@total_ordering`, if any. - /// - /// Following `functools.total_ordering` precedence, we prefer `__lt__` > `__le__` > `__gt__` > - /// `__ge__`, regardless of whether the method is defined locally or inherited. - /// - /// Note: We use direct scope lookups here to avoid infinite recursion - /// through `own_class_member` -> `own_synthesized_member`. - pub(super) fn total_ordering_root_method( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> Option> { - const ORDERING_METHODS: [&str; 4] = ["__lt__", "__le__", "__gt__", "__ge__"]; + // Only exclude `object` members if this is not an `object` class itself + if known == Some(KnownClass::Object) + && policy.mro_no_object_fallback() + && !is_self_object + { + continue; + } - for name in ORDERING_METHODS { - for base in self.iter_mro(db, specialization) { - let Some(base_class) = base.into_class() else { - continue; - }; - match base_class.class_literal(db) { - ClassLiteral::Static(base_literal) => { - if base_literal.is_known(db, KnownClass::Object) { - continue; - } - let member = class_member(db, base_literal.body_scope(db), name); - if let Some(ty) = member.ignore_possibly_undefined() { - let base_specialization = base_class - .static_class_literal(db) - .and_then(|(_, spec)| spec); - return Some(ty.apply_optional_specialization(db, base_specialization)); - } + if known == Some(KnownClass::Type) && policy.meta_class_no_type_fallback() { + continue; } - ClassLiteral::Dynamic(dynamic) => { - // Dynamic classes (created with `type()`) can also define ordering methods - // in their namespace dict. - let member = dynamic.own_class_member(db, name); - if let Some(ty) = member.ignore_possibly_undefined() { - return Some(ty); - } + + if matches!(known, Some(KnownClass::Int | KnownClass::Str)) + && policy.mro_no_int_or_str_fallback() + { + continue; } - // Dynamic namedtuples don't define their own ordering methods. - ClassLiteral::DynamicNamedTuple(_) => {} + + lookup_result = lookup_result.or_else(|lookup_error| { + lookup_error.or_fall_back_to( + db, + class + .own_class_member(db, inherited_generic_context, name) + .inner, + ) + }); + } + ClassBase::TypedDict => { + return ClassMemberResult::TypedDict; } } + if lookup_result.is_ok() { + break; + } } - None - } - - pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { - // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code - // the knowledge that this class is not generic. - if self.is_known(db, KnownClass::VersionInfo) { - return None; - } - - // We've already verified that the class literal does not contain both a PEP-695 generic - // scope and a `typing.Generic` base class. - // - // Note that if a class has an explicit legacy generic context (by inheriting from - // `typing.Generic`), and also an implicit one (by inheriting from other generic classes, - // specialized by typevars), the explicit one takes precedence. - self.pep695_generic_context(db) - .or_else(|| self.legacy_generic_context(db)) - .or_else(|| self.inherited_legacy_generic_context(db)) - } - - pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { - self.pep695_generic_context(db).is_some() - } - - #[salsa::tracked(cycle_initial=generic_context_cycle_initial, - heap_size=ruff_memory_usage::heap_size, - )] - pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option> { - let scope = self.body_scope(db); - let file = scope.file(db); - let parsed = parsed_module(db, file).load(db); - let class_def_node = scope.node(db).expect_class().node(&parsed); - class_def_node.type_params.as_ref().map(|type_params| { - let index = semantic_index(db, scope.file(db)); - let definition = index.expect_single_definition(class_def_node); - GenericContext::from_type_params(db, index, definition, type_params) - }) - } - - pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option> { - self.explicit_bases(db).iter().find_map(|base| match base { - Type::KnownInstance( - KnownInstanceType::SubscriptedGeneric(generic_context) - | KnownInstanceType::SubscriptedProtocol(generic_context), - ) => Some(*generic_context), - _ => None, + ClassMemberResult::Done(CompletedMemberLookup { + lookup_result, + dynamic_type, }) } - #[salsa::tracked(cycle_initial=generic_context_cycle_initial, - heap_size=ruff_memory_usage::heap_size, - )] - pub(crate) fn inherited_legacy_generic_context( - self, - db: &'db dyn Db, - ) -> Option> { - GenericContext::from_base_classes( - db, - self.definition(db), - self.explicit_bases(db) - .iter() - .copied() - .filter(|ty| matches!(ty, Type::GenericAlias(_))), - ) - } + /// Look up an instance member by iterating through the MRO. + /// + /// Unlike class member lookup, instance member lookup: + /// - Uses `own_instance_member` to check each class + /// - Builds a union of inferred types from multiple classes + /// - Stops on the first definitely-declared attribute + /// + /// Returns `InstanceMemberResult::TypedDict` if a `TypedDict` base is encountered, + /// allowing the caller to handle this case specially. + pub(super) fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { + let db = self.db; + let mut union = UnionBuilder::new(db); + let mut union_qualifiers = TypeQualifiers::empty(); + let mut is_definitely_bound = false; - /// Returns all of the typevars that are referenced in this class's base class list. - /// (This is used to ensure that classes do not reference typevars from enclosing - /// generic contexts.) - pub(crate) fn typevars_referenced_in_bases( - self, - db: &'db dyn Db, - ) -> FxIndexSet> { - #[derive(Default)] - struct CollectTypeVars<'db> { - typevars: RefCell>>, - recursion_guard: TypeCollector<'db>, - } + for superclass in self.mro_iter { + match superclass { + ClassBase::Generic | ClassBase::Protocol => { + // Skip over these very special class bases that aren't really classes. + } + ClassBase::Dynamic(_) => { + // We already return the dynamic type for class member lookup, so we can + // just return unbound here (to avoid having to build a union of the + // dynamic type with itself). + return InstanceMemberResult::Done(PlaceAndQualifiers::unbound()); + } + ClassBase::Class(class) => { + if let member @ PlaceAndQualifiers { + place: + Place::Defined(DefinedPlace { + ty, + origin, + definedness: boundness, + .. + }), + qualifiers, + } = class.own_instance_member(db, name).inner + { + 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 InstanceMemberResult::Done(member); + } - impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> { - fn should_visit_lazy_type_attributes(&self) -> bool { - false - } + is_definitely_bound = true; + } - fn visit_bound_type_var_type( - &self, - _db: &'db dyn Db, - bound_typevar: BoundTypeVarInstance<'db>, - ) { - self.typevars.borrow_mut().insert(bound_typevar); - } + // If the attribute is not definitely declared on this class, keep looking + // higher up in the MRO, and build a union of all inferred types (and + // possibly-declared types): + union = union.add(ty); - fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) { - walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); - } - } - - let visitor = CollectTypeVars::default(); - for base in self.explicit_bases(db) { - visitor.visit_type(db, *base); - } - visitor.typevars.into_inner() - } - - /// Returns the generic context that should be inherited by any constructor methods of this class. - fn inherited_generic_context(self, db: &'db dyn Db) -> Option> { - self.generic_context(db) - } - - pub(super) fn file(self, db: &dyn Db) -> File { - self.body_scope(db).file(db) - } - - /// Return the original [`ast::StmtClassDef`] node associated with this class - /// - /// ## Note - /// Only call this function from queries in the same file or your - /// query depends on the AST of another file (bad!). - fn node<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast ast::StmtClassDef { - let scope = self.body_scope(db); - scope.node(db).expect_class().node(module) - } - - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - let body_scope = self.body_scope(db); - let index = semantic_index(db, body_scope.file(db)); - index.expect_single_definition(body_scope.node(db).expect_class()) - } - - pub(crate) fn apply_specialization( - self, - db: &'db dyn Db, - f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, - ) -> ClassType<'db> { - match self.generic_context(db) { - None => ClassType::NonGeneric(self.into()), - Some(generic_context) => { - let specialization = f(generic_context); - - ClassType::Generic(GenericAlias::new(db, self, specialization)) - } - } - } - - pub(crate) fn apply_optional_specialization( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - specialization - .unwrap_or_else(|| generic_context.default_specialization(db, self.known(db))) - }) - } - - pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context - .default_specialization(db, self.known(db)) - .materialize_impl( - db, - MaterializationKind::Top, - &ApplyTypeMappingVisitor::default(), - ) - }) - } - - /// Returns the default specialization of this class. For non-generic classes, the class is - /// returned unchanged. For a non-specialized generic class, we return a generic alias that - /// applies the default specialization to the class's typevars. - pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context.default_specialization(db, self.known(db)) - }) - } - - /// Returns the unknown specialization of this class. For non-generic classes, the class is - /// returned unchanged. For a non-specialized generic class, we return a generic alias that - /// maps each of the class's typevars to `Unknown`. - pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context.unknown_specialization(db) - }) - } - - /// Returns a specialization of this class where each typevar is mapped to itself. - pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context.identity_specialization(db) - }) - } - - /// Return an iterator over the inferred types of this class's *explicit* bases. - /// - /// Note that any class (except for `object`) that has no explicit - /// bases will implicitly inherit from `object` at runtime. Nonetheless, - /// this method does *not* include `object` in the bases it iterates over. - /// - /// ## Why is this a salsa query? - /// - /// This is a salsa query to short-circuit the invalidation - /// when the class's AST node changes. - /// - /// Were this not a salsa query, then the calling query - /// would depend on the class's AST and rerun for every change in that file. - #[salsa::tracked(returns(deref), cycle_initial=explicit_bases_cycle_initial, cycle_fn=explicit_bases_cycle_fn, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - tracing::trace!( - "StaticClassLiteral::explicit_bases_query: {}", - self.name(db) - ); - - let module = parsed_module(db, self.file(db)).load(db); - let class_stmt = self.node(db, &module); - - let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); - - match self.known(db) { - Some(KnownClass::VersionInfo) => { - let tuple_type = TupleType::new(db, &TupleSpec::version_info_spec(db)) - .expect("sys.version_info tuple spec should always be a valid tuple"); - - Box::new([ - definition_expression_type(db, class_definition, &class_stmt.bases()[0]), - Type::from(tuple_type.to_class_type(db)), - ]) - } - // Special-case `NotImplementedType`: typeshed says that it inherits from `Any`, - // but this causes more problems than it fixes. - Some(KnownClass::NotImplementedType) => Box::new([]), - _ => class_stmt - .bases() - .iter() - .flat_map(|base_node| { - if let ast::Expr::Starred(starred) = base_node { - let starred_ty = - definition_expression_type(db, class_definition, &starred.value); - // If the starred expression is a fixed-length tuple, unpack it. - if let Some(Tuple::Fixed(tuple)) = starred_ty - .tuple_instance_spec(db) - .map(std::borrow::Cow::into_owned) - { - return Either::Left(tuple.owned_elements().into_vec().into_iter()); - } - // Otherwise, we can't statically determine the bases. - Either::Right(std::iter::once(Type::unknown())) - } else { - Either::Right(std::iter::once(definition_expression_type( - db, - class_definition, - base_node, - ))) - } - }) - .collect(), - } - } - - /// Return `Some()` if this class is known to be a [`DisjointBase`], or `None` if it is not. - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { - if self - .known_function_decorators(db) - .contains(&KnownFunction::DisjointBase) - { - Some(DisjointBase::due_to_decorator(self)) - } else if SlotsKind::from(db, self) == SlotsKind::NotEmpty { - Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Static( - self, - ))) - } else { - None - } - } - - /// Iterate over this class's explicit bases, filtering out any bases that are not class - /// objects, and applying default specialization to any unspecialized generic class literals. - fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { - self.explicit_bases(db) - .iter() - .copied() - .filter_map(|ty| ty.to_class_type(db)) - } - - /// Determine if this class is a protocol. - /// - /// This method relies on the accuracy of the [`KnownClass::is_protocol`] method, - /// which hardcodes knowledge about certain special-cased classes. See the docs on - /// that method for why we do this rather than relying on generalised logic for all - /// classes, including the special-cased ones that are included in the [`KnownClass`] - /// enum. - pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool { - self.known(db) - .map(KnownClass::is_protocol) - .unwrap_or_else(|| { - // Iterate through the last three bases of the class - // searching for `Protocol` or `Protocol[]` in the bases list. - // - // If `Protocol` is present in the bases list of a valid protocol class, it must either: - // - // - be the last base - // - OR be the last-but-one base (with the final base being `Generic[]` or `object`) - // - OR be the last-but-two base (with the penultimate base being `Generic[]` - // and the final base being `object`) - self.explicit_bases(db).iter().rev().take(3).any(|base| { - matches!( - base, - Type::SpecialForm(SpecialFormType::Protocol) - | Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)) - ) - }) - }) - } - - /// Return the types of the decorators on this class - #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] - fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - tracing::trace!("StaticClassLiteral::decorators: {}", self.name(db)); - - let module = parsed_module(db, self.file(db)).load(db); - - let class_stmt = self.node(db, &module); - if class_stmt.decorator_list.is_empty() { - return Box::new([]); - } - - let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); - - class_stmt - .decorator_list - .iter() - .map(|decorator_node| { - definition_expression_type(db, class_definition, &decorator_node.expression) - }) - .collect() - } - - pub(super) fn known_function_decorators( - self, - db: &'db dyn Db, - ) -> impl Iterator + 'db { - self.decorators(db) - .iter() - .filter_map(|deco| deco.as_function_literal()) - .filter_map(|decorator| decorator.known(db)) - } - - /// Iterate through the decorators on this class, returning the position of the first one - /// that matches the given predicate. - pub(super) fn find_decorator_position( - self, - db: &'db dyn Db, - predicate: impl Fn(Type<'db>) -> bool, - ) -> Option { - self.decorators(db) - .iter() - .position(|decorator| predicate(*decorator)) - } - - /// Iterate through the decorators on this class, returning the index of the first one - /// that is either `@dataclass` or `@dataclass(...)`. - pub(super) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { - self.find_decorator_position(db, |ty| match ty { - Type::FunctionLiteral(function) => function.is_known(db, KnownFunction::Dataclass), - Type::DataclassDecorator(_) => true, - _ => false, - }) - } - - /// Is this class final? - pub(super) fn is_final(self, db: &'db dyn Db) -> bool { - self.known_function_decorators(db) - .contains(&KnownFunction::Final) - || enum_metadata(db, ClassLiteral::Static(self)).is_some() - } - - /// Attempt to resolve the [method resolution order] ("MRO") for this class. - /// If the MRO is unresolvable, return an error indicating why the class's MRO - /// cannot be accurately determined. The error returned contains a fallback MRO - /// that will be used instead for the purposes of type inference. - /// - /// The MRO is the tuple of classes that can be retrieved as the `__mro__` - /// attribute on a class at runtime. - /// - /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - #[salsa::tracked(returns(as_ref), cycle_initial=static_class_try_mro_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn try_mro( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> Result, StaticMroError<'db>> { - tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db)); - Mro::of_static_class(db, self, specialization) - } - - /// Iterate over the [method resolution order] ("MRO") of the class. - /// - /// If the MRO could not be accurately resolved, this method falls back to iterating - /// over an MRO that has the class directly inheriting from `Unknown`. Use - /// [`StaticClassLiteral::try_mro`] if you need to distinguish between the success and failure - /// cases rather than simply iterating over the inferred resolution order for the class. - /// - /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(super) fn iter_mro( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> MroIterator<'db> { - MroIterator::new(db, ClassLiteral::Static(self), specialization) - } - - /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of( - self, - db: &'db dyn Db, - specialization: Option>, - other: ClassType<'db>, - ) -> bool { - // `is_subclass_of` is checking the subtype relation, in which gradual types do not - // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. - self.iter_mro(db, specialization) - .contains(&ClassBase::Class(other)) - } - - /// Return `true` if this class constitutes a typed dict specification (inherits from - /// `typing.TypedDict`, either directly or indirectly). - #[salsa::tracked(cycle_initial=|_, _, _| false, heap_size=ruff_memory_usage::heap_size)] - pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { - if let Some(known) = self.known(db) { - return known.is_typed_dict_subclass(); - } - - self.iter_mro(db, None) - .any(|base| matches!(base, ClassBase::TypedDict)) - } - - /// Return `true` if this class is, or inherits from, a `NamedTuple` (inherits from - /// `typing.NamedTuple`, either directly or indirectly, including functional forms like - /// `NamedTuple("X", ...)`). - pub(crate) fn has_named_tuple_class_in_mro(self, db: &'db dyn Db) -> bool { - self.iter_mro(db, None) - .filter_map(ClassBase::into_class) - .any(|base| match base.class_literal(db) { - ClassLiteral::DynamicNamedTuple(_) => true, - ClassLiteral::Dynamic(_) => false, - ClassLiteral::Static(class) => class - .explicit_bases(db) - .contains(&Type::SpecialForm(SpecialFormType::NamedTuple)), - }) - } - - /// Compute `TypedDict` parameters dynamically based on MRO detection and AST parsing. - fn typed_dict_params(self, db: &'db dyn Db) -> Option { - if !self.is_typed_dict(db) { - return None; - } - - let module = parsed_module(db, self.file(db)).load(db); - let class_stmt = self.node(db, &module); - Some(typed_dict_params_from_class_def(class_stmt)) - } - - /// Returns dataclass params for this class, sourced from both dataclass params and dataclass - /// transform params - fn merged_dataclass_params( - self, - db: &'db dyn Db, - field_policy: CodeGeneratorKind<'db>, - ) -> (Option>, Option>) { - let dataclass_params = self.dataclass_params(db); - - let mut transformer_params = - if let CodeGeneratorKind::DataclassLike(Some(transformer_params)) = field_policy { - Some(DataclassParams::from_transformer_params( - db, - transformer_params, - )) - } else { - None - }; - - // Dataclass transformer flags can be overwritten using class arguments. - if let Some(transformer_params) = transformer_params.as_mut() { - if let Some(class_def) = self.definition(db).kind(db).as_class() { - let module = parsed_module(db, self.file(db)).load(db); - - if let Some(arguments) = &class_def.node(&module).arguments { - let mut flags = transformer_params.flags(db); - - for keyword in &arguments.keywords { - if let Some(arg_name) = &keyword.arg { - if let Some(is_set) = - keyword.value.as_boolean_literal_expr().map(|b| b.value) - { - for (flag_name, flag) in DATACLASS_FLAGS { - if arg_name.as_str() == *flag_name { - flags.set(*flag, is_set); - } - } - } - } - } - - *transformer_params = - DataclassParams::new(db, flags, transformer_params.field_specifiers(db)); - } - } - } - - (dataclass_params, transformer_params) - } - - /// Returns the effective frozen status of this class if it's a dataclass-like class. - /// - /// Returns `Some(true)` for a frozen dataclass-like class, `Some(false)` for a non-frozen one, - /// and `None` if the class is not a dataclass-like class, or if the dataclass is neither frozen - /// nor non-frozen. - pub(crate) fn is_frozen_dataclass(self, db: &'db dyn Db) -> Option { - // Check if this is a base-class-based transformer that has dataclass_transformer_params directly - // attached to it (because it is itself decorated with `@dataclass_transform`), or if this class - // has an explicit metaclass that is decorated with `@dataclass_transform`. - // - // In both cases, this signifies that this class is neither frozen nor non-frozen. - // - // See for details. - if self.dataclass_transformer_params(db).is_some() - || self - .try_metaclass(db) - .is_ok_and(|(_, info)| info.is_some_and(|i| i.from_explicit_metaclass)) - { - return None; - } - - if let field_policy @ CodeGeneratorKind::DataclassLike(_) = - CodeGeneratorKind::from_class(db, self.into(), None)? - { - // Otherwise, if this class is a dataclass-like class, determine its frozen status based on - // dataclass params and dataclass transformer params. - Some(self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN)) - } else { - None - } - } - - /// Checks if the given dataclass parameter flag is set for this class. - /// This checks both the `dataclass_params` and `transformer_params`. - fn has_dataclass_param( - self, - db: &'db dyn Db, - field_policy: CodeGeneratorKind<'db>, - param: DataclassFlags, - ) -> bool { - let (dataclass_params, transformer_params) = self.merged_dataclass_params(db, field_policy); - dataclass_params.is_some_and(|params| params.flags(db).contains(param)) - || transformer_params.is_some_and(|params| params.flags(db).contains(param)) - } - - /// Return the explicit `metaclass` of this class, if one is defined. - /// - /// ## Note - /// Only call this function from queries in the same file or your - /// query depends on the AST of another file (bad!). - fn explicit_metaclass(self, db: &'db dyn Db, module: &ParsedModuleRef) -> Option> { - let class_stmt = self.node(db, module); - let metaclass_node = &class_stmt - .arguments - .as_ref()? - .find_keyword("metaclass")? - .value; - - let class_definition = self.definition(db); - - Some(definition_expression_type( - db, - class_definition, - metaclass_node, - )) - } - - /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. - pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - self.try_metaclass(db) - .map(|(ty, _)| ty) - .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) - } - - /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. - #[salsa::tracked(cycle_initial=try_metaclass_cycle_initial, - heap_size=ruff_memory_usage::heap_size, - )] - pub(super) fn try_metaclass( - self, - db: &'db dyn Db, - ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { - tracing::trace!("StaticClassLiteral::try_metaclass: {}", self.name(db)); - - // Identify the class's own metaclass (or take the first base class's metaclass). - let mut base_classes = self.fully_static_explicit_bases(db).peekable(); - - if base_classes.peek().is_some() && self.inheritance_cycle(db).is_some() { - // We emit diagnostics for cyclic class definitions elsewhere. - // Avoid attempting to infer the metaclass if the class is cyclically defined. - return Ok((SubclassOfType::subclass_of_unknown(), None)); - } - - if self.try_mro(db, None).is_err_and(StaticMroError::is_cycle) { - return Ok((SubclassOfType::subclass_of_unknown(), None)); - } - - let module = parsed_module(db, self.file(db)).load(db); - - let explicit_metaclass = self.explicit_metaclass(db, &module); - - // Generic metaclasses parameterized by type variables are not supported. - // `metaclass=Meta[int]` is fine, but `metaclass=Meta[T]` is not. - // See: https://typing.python.org/en/latest/spec/generics.html#generic-metaclasses - if let Some(Type::GenericAlias(alias)) = explicit_metaclass { - let specialization_has_typevars = alias - .specialization(db) - .types(db) - .iter() - .any(|ty| ty.has_typevar_or_typevar_instance(db)); - if specialization_has_typevars { - return Err(MetaclassError { - kind: MetaclassErrorKind::GenericMetaclass, - }); - } - } - - let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { - (metaclass, self) - } else if let Some(base_class) = base_classes.next() { - // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking. - let base_class_literal = base_class - .static_class_literal(db) - .map(|(lit, _)| lit) - .unwrap_or(self); - (base_class.metaclass(db), base_class_literal) - } else { - (KnownClass::Type.to_class_literal(db), self) - }; - - let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) { - MetaclassCandidate { - metaclass: metaclass_ty, - explicit_metaclass_of: class_metaclass_was_from, - } - } else { - let name = Type::string_literal(db, self.name(db)); - let bases = Type::heterogeneous_tuple(db, self.explicit_bases(db)); - let namespace = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); - - // TODO: Other keyword arguments? - let arguments = CallArguments::positional([name, bases, namespace]); - - let return_ty_result = match metaclass.try_call(db, &arguments) { - Ok(bindings) => Ok(bindings.return_type(db)), - - Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError { - kind: MetaclassErrorKind::NotCallable(bindings.callable_type()), - }), - - // TODO we should also check for binding errors that would indicate the metaclass - // does not accept the right arguments - Err(CallError(CallErrorKind::BindingError, bindings)) => { - Ok(bindings.return_type(db)) - } - - Err(CallError(CallErrorKind::PossiblyNotCallable, _)) => Err(MetaclassError { - kind: MetaclassErrorKind::PartlyNotCallable(metaclass), - }), - }; - - return return_ty_result.map(|ty| (ty.to_meta_type(db), None)); - }; - - // Reconcile all base classes' metaclasses with the candidate metaclass. - // - // See: - // - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass - // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 - for base_class in base_classes { - let metaclass = base_class.metaclass(db); - let Some(metaclass) = metaclass.to_class_type(db) else { - continue; - }; - // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking. - let base_class_literal = base_class - .static_class_literal(db) - .map(|(lit, _)| lit) - .unwrap_or(self); - if metaclass.is_subclass_of(db, candidate.metaclass) { - candidate = MetaclassCandidate { - metaclass, - explicit_metaclass_of: base_class_literal, - }; - continue; - } - if candidate.metaclass.is_subclass_of(db, metaclass) { - continue; - } - return Err(MetaclassError { - kind: MetaclassErrorKind::Conflict { - candidate1: candidate, - candidate2: MetaclassCandidate { - metaclass, - explicit_metaclass_of: base_class_literal, - }, - candidate1_is_base_class: explicit_metaclass.is_none(), - }, - }); - } - - let transform_info = candidate - .metaclass - .static_class_literal(db) - .and_then(|(metaclass_literal, _)| metaclass_literal.dataclass_transformer_params(db)) - .map(|params| MetaclassTransformInfo { - params, - from_explicit_metaclass: candidate.explicit_metaclass_of == self, - }); - Ok((candidate.metaclass.into(), transform_info)) - } - - /// Returns the class member of this class named `name`. - /// - /// The member resolves to a member on the class itself or any of its proper superclasses. - /// - /// TODO: Should this be made private...? - pub(super) fn class_member( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - fn into_function_like_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> { - match ty { - Type::Callable(callable_ty) => Type::Callable(CallableType::new( - db, - callable_ty.signatures(db), - CallableTypeKind::FunctionLike, - )), - Type::Union(union) => { - union.map(db, |element| into_function_like_callable(db, *element)) - } - Type::Intersection(intersection) => intersection - .map_positive(db, |element| into_function_like_callable(db, *element)), - _ => ty, - } - } - - let mut member = self.class_member_inner(db, None, name, policy); - - // We generally treat dunder attributes with `Callable` types as function-like callables. - // See `callables_as_descriptors.md` for more details. - if name.starts_with("__") && name.ends_with("__") { - member = member.map_type(|ty| into_function_like_callable(db, ty)); - } - - member - } - - fn class_member_inner( - self, - db: &'db dyn Db, - specialization: Option>, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) - } - - pub(super) fn class_member_from_mro( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - mro_iter: impl Iterator>, - ) -> PlaceAndQualifiers<'db> { - let result = MroLookup::new(db, mro_iter).class_member( - name, - policy, - self.inherited_generic_context(db), - self.is_known(db, KnownClass::Object), - ); - - match result { - ClassMemberResult::Done(result) => result.finalize(db), - - ClassMemberResult::TypedDict => KnownClass::TypedDictFallback - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) - .expect("Will return Some() when called on class literal") - .map_type(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: determine_upper_bound( - db, - self, - None, - ClassBase::is_typed_dict, - ), - }, - TypeContext::default(), - ) - }), - } - } - - /// Returns the inferred type of the class member named `name`. Only bound members - /// or those marked as `ClassVars` are considered. - /// - /// Returns [`Place::Undefined`] if `name` cannot be found in this class's scope - /// directly. Use [`StaticClassLiteral::class_member`] if you require a method that will - /// traverse through the MRO until it finds the member. - pub(super) fn own_class_member( - self, - db: &'db dyn Db, - inherited_generic_context: Option>, - specialization: Option>, - name: &str, - ) -> Member<'db> { - // Check if this class is dataclass-like (either via @dataclass or via dataclass_transform) - if matches!( - CodeGeneratorKind::from_class(db, self.into(), specialization), - Some(CodeGeneratorKind::DataclassLike(_)) - ) { - if name == "__dataclass_fields__" { - // Make this class look like a subclass of the `DataClassInstance` protocol - return Member { - inner: Place::declared(KnownClass::Dict.to_specialized_instance( - db, - &[ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, &[Type::any()]), - ], - )) - .with_qualifiers(TypeQualifiers::CLASS_VAR), - }; - } else if name == "__dataclass_params__" { - // There is no typeshed class for this. For now, we model it as `Any`. - return Member { - inner: Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR), - }; - } - } - - if CodeGeneratorKind::NamedTuple.matches(db, self.into(), specialization) { - if let Some(field) = self - .own_fields(db, specialization, CodeGeneratorKind::NamedTuple) - .get(name) - { - let property_getter_signature = Signature::new( - Parameters::new( - db, - [Parameter::positional_only(Some(Name::new_static("self")))], - ), - field.declared_ty, - ); - let property_getter = Type::single_callable(db, property_getter_signature); - let property = PropertyInstanceType::new(db, Some(property_getter), None); - return Member::definitely_declared(Type::PropertyInstance(property)); - } - } - - let body_scope = self.body_scope(db); - let member = class_member(db, body_scope, name).map_type(|ty| { - // The `__new__` and `__init__` members of a non-specialized generic class are handled - // specially: they inherit the generic context of their class. That lets us treat them - // as generic functions when constructing the class, and infer the specialization of - // the class from the arguments that are passed in. - // - // We might decide to handle other class methods the same way, having them inherit the - // class's generic context, and performing type inference on calls to them to determine - // the specialization of the class. If we do that, we would update this to also apply - // to any method with a `@classmethod` decorator. (`__init__` would remain a special - // case, since it's an _instance_ method where we don't yet know the generic class's - // specialization.) - match (inherited_generic_context, ty, specialization, name) { - ( - Some(generic_context), - Type::FunctionLiteral(function), - Some(_), - "__new__" | "__init__", - ) => Type::FunctionLiteral( - function.with_inherited_generic_context(db, generic_context), - ), - _ => ty, - } - }); - - if member.is_undefined() { - if let Some(synthesized_member) = - self.own_synthesized_member(db, specialization, inherited_generic_context, name) - { - return Member::definitely_declared(synthesized_member); - } - // The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s. - return Self::implicit_attribute(db, body_scope, name, MethodDecorator::ClassMethod); - } - - // For dataclass-like classes, `KW_ONLY` sentinel fields are not real - // class attributes; they are markers used by the dataclass decorator to - // indicate that subsequent fields are keyword-only. Treat them as - // undefined so the MRO falls through to parent classes. - if member - .inner - .place - .unwidened_type() - .is_some_and(|ty| ty.is_instance_of(db, KnownClass::KwOnly)) - && CodeGeneratorKind::from_static_class(db, self, None) - .is_some_and(|policy| matches!(policy, CodeGeneratorKind::DataclassLike(_))) - { - return Member::unbound(); - } - - // For enum classes, `nonmember(value)` creates a non-member attribute. - // At runtime, the enum metaclass unwraps the value, so accessing the attribute - // returns the inner value, not the `nonmember` wrapper. - if let Some(ty) = member.inner.place.unwidened_type() { - if let Some(value_ty) = try_unwrap_nonmember_value(db, ty) { - if is_enum_class_by_inheritance(db, self) { - return Member::definitely_declared(value_ty); - } - } - } - - member - } - - /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or - /// a synthesized `__new__` method for a `NamedTuple`. - pub(super) fn own_synthesized_member( - self, - db: &'db dyn Db, - specialization: Option>, - inherited_generic_context: Option>, - name: &str, - ) -> Option> { - // Handle `@functools.total_ordering`: synthesize comparison methods - // for classes that have `@total_ordering` and define at least one - // ordering method. The decorator requires at least one of __lt__, - // __le__, __gt__, or __ge__ to be defined (either in this class or - // inherited from a superclass, excluding `object`). - // - // Only synthesize methods that are not already defined in the MRO. - // Note: We use direct scope lookups here to avoid infinite recursion - // through `own_class_member` -> `own_synthesized_member`. - if self.total_ordering(db) - && matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") - && !self - .iter_mro(db, specialization) - .filter_map(ClassBase::into_class) - .filter_map(|class| class.static_class_literal(db)) - .filter(|(class, _)| !class.is_known(db, KnownClass::Object)) - .any(|(class, _)| { - class_member(db, class.body_scope(db), name) - .ignore_possibly_undefined() - .is_some() - }) - && self.has_ordering_method_in_mro(db, specialization) - && let Some(root_method_ty) = self.total_ordering_root_method(db, specialization) - && let Some(callables) = root_method_ty.try_upcast_to_callable(db) - { - let bool_ty = KnownClass::Bool.to_instance(db); - let synthesized_callables = callables.map(|callable| { - let signatures = CallableSignature::from_overloads( - callable.signatures(db).iter().map(|signature| { - // The generated methods return a union of the root method's return type - // and `bool`. This is because `@total_ordering` synthesizes methods like: - // def __gt__(self, other): return not (self == other or self < other) - // If `__lt__` returns `int`, then `__gt__` could return `int | bool`. - let return_ty = - UnionType::from_two_elements(db, signature.return_ty, bool_ty); - Signature::new_generic( - signature.generic_context, - signature.parameters().clone(), - return_ty, - ) - }), - ); - CallableType::new(db, signatures, CallableTypeKind::FunctionLike) - }); - - return Some(synthesized_callables.into_type(db)); - } - - let field_policy = CodeGeneratorKind::from_class(db, self.into(), specialization)?; - - let instance_ty = - Type::instance(db, self.apply_optional_specialization(db, specialization)); - - let signature_from_fields = |mut parameters: Vec<_>, return_ty: Type<'db>| { - for (field_name, field) in self.fields(db, specialization, field_policy) { - let (init, mut default_ty, kw_only, alias) = match &field.kind { - FieldKind::NamedTuple { default_ty } => (true, *default_ty, None, None), - FieldKind::Dataclass { - init, - default_ty, - kw_only, - alias, - .. - } => (*init, *default_ty, *kw_only, alias.as_ref()), - FieldKind::TypedDict { .. } => continue, - }; - let mut field_ty = field.declared_ty; - - if name == "__init__" && !init { - // Skip fields with `init=False` - continue; - } - - if field.is_kw_only_sentinel(db) { - // Attributes annotated with `dataclass.KW_ONLY` are not present in the synthesized - // `__init__` method; they are used to indicate that the following parameters are - // keyword-only. - continue; - } - - let dunder_set = field_ty.class_member(db, "__set__".into()); - if let Place::Defined(DefinedPlace { - ty: dunder_set, - definedness: 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 - // problem is that we can't determine the type of the value parameter this way. - // Instead, we want to use the dynamic type itself in this case, so we skip the - // special descriptor handling. - if !dunder_set.is_dynamic() { - // This type of this attribute is a data descriptor. Instead of overwriting the - // descriptor attribute, data-classes will (implicitly) call the `__set__` method - // of the descriptor. This means that the synthesized `__init__` parameter for - // this attribute is determined by possible `value` parameter types with which - // the `__set__` method can be called. - // - // We union parameter types across overloads of a single callable, intersect - // callable bindings inside an intersection element, and union outer elements. - field_ty = dunder_set.bindings(db).map_types(db, |binding| { - let mut value_types = UnionBuilder::new(db); - let mut has_value_type = false; - for overload in binding { - if let Some(value_param) = - overload.signature.parameters().get_positional(2) - { - value_types = value_types.add(value_param.annotated_type()); - has_value_type = true; - } else if overload.signature.parameters().is_gradual() { - value_types = value_types.add(Type::unknown()); - has_value_type = true; - } - } - has_value_type.then(|| value_types.build()) - }); - - // The default value of the attribute is *not* determined by the right hand side - // of the class-body assignment. Instead, the runtime invokes `__get__` on the - // descriptor, as if it had been called on the class itself, i.e. it passes `None` - // for the `instance` argument. - - if let Some(ref mut default_ty) = default_ty { - *default_ty = default_ty - .try_call_dunder_get(db, None, Type::from(self)) - .map(|(return_ty, _)| return_ty) - .unwrap_or_else(Type::unknown); - } - } - } - - let is_kw_only = - matches!(name, "__replace__" | "_replace") || kw_only.unwrap_or(false); - - // Use the alias name if provided, otherwise use the field name - let parameter_name = - Name::new(alias.map(|alias| &**alias).unwrap_or(&**field_name)); - - let mut parameter = if is_kw_only { - Parameter::keyword_only(parameter_name) - } else { - Parameter::positional_or_keyword(parameter_name) - } - .with_annotated_type(field_ty); - - parameter = if matches!(name, "__replace__" | "_replace") { - // When replacing, we know there is a default value for the field - // (the value that is currently assigned to the field) - // assume this to be the declared type of the field - parameter.with_default_type(field_ty) - } else { - parameter.with_optional_default_type(default_ty) - }; - - parameters.push(parameter); - } - - // In the event that we have a mix of keyword-only and positional parameters, we need to sort them - // so that the keyword-only parameters appear after positional parameters. - parameters.sort_by_key(Parameter::is_keyword_only); - - let signature = match name { - "__new__" | "__init__" => Signature::new_generic( - inherited_generic_context.or_else(|| self.inherited_generic_context(db)), - Parameters::new(db, parameters), - return_ty, - ), - _ => Signature::new(Parameters::new(db, parameters), return_ty), - }; - Some(Type::function_like_callable(db, signature)) - }; - - match (field_policy, name) { - (CodeGeneratorKind::DataclassLike(_), "__init__") => { - if !self.has_dataclass_param(db, field_policy, DataclassFlags::INIT) { - return None; - } - - let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) - // TODO: could be `Self`. - .with_annotated_type(instance_ty); - signature_from_fields(vec![self_parameter], Type::none(db)) - } - ( - CodeGeneratorKind::NamedTuple, - "__new__" | "__init__" | "_replace" | "__replace__" | "_fields", - ) if self.namedtuple_base_has_unknown_fields(db) => { - // When the namedtuple base has unknown fields, fall back to NamedTupleFallback - // which has generic signatures that accept any arguments. - KnownClass::NamedTupleFallback - .to_class_literal(db) - .as_class_literal()? - .as_static()? - .own_class_member(db, inherited_generic_context, None, name) - .ignore_possibly_undefined() - .map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: instance_ty, - }, - TypeContext::default(), - ) - }) - } - ( - CodeGeneratorKind::NamedTuple, - "__new__" | "_replace" | "__replace__" | "_fields" | "__slots__", - ) => { - let fields = self.fields(db, specialization, field_policy); - let fields_iter = fields.iter().map(|(name, field)| { - let default_ty = match &field.kind { - FieldKind::NamedTuple { default_ty } => *default_ty, - _ => None, - }; - NamedTupleField { - name: name.clone(), - ty: field.declared_ty, - default: default_ty, - } - }); - synthesize_namedtuple_class_member( - db, - name, - instance_ty, - fields_iter, - specialization.map(|s| s.generic_context(db)), - ) - } - (CodeGeneratorKind::DataclassLike(_), "__lt__" | "__le__" | "__gt__" | "__ge__") => { - if !self.has_dataclass_param(db, field_policy, DataclassFlags::ORDER) { - return None; - } - - let signature = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_or_keyword(Name::new_static("self")) - // TODO: could be `Self`. - .with_annotated_type(instance_ty), - Parameter::positional_or_keyword(Name::new_static("other")) - // TODO: could be `Self`. - .with_annotated_type(instance_ty), - ], - ), - KnownClass::Bool.to_instance(db), - ); - - Some(Type::function_like_callable(db, signature)) - } - (CodeGeneratorKind::DataclassLike(_), "__hash__") => { - let unsafe_hash = - self.has_dataclass_param(db, field_policy, DataclassFlags::UNSAFE_HASH); - let frozen = self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN); - let eq = self.has_dataclass_param(db, field_policy, DataclassFlags::EQ); - - if unsafe_hash || (frozen && eq) { - let signature = Signature::new( - Parameters::new( - db, - [Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(instance_ty)], - ), - KnownClass::Int.to_instance(db), - ); - - Some(Type::function_like_callable(db, signature)) - } else if eq && !frozen { - Some(Type::none(db)) - } else { - // No `__hash__` is generated, fall back to `object.__hash__` - None - } - } - (CodeGeneratorKind::DataclassLike(_), "__match_args__") - if Program::get(db).python_version(db) >= PythonVersion::PY310 => - { - if !self.has_dataclass_param(db, field_policy, DataclassFlags::MATCH_ARGS) { - return None; - } - - let kw_only_default = - self.has_dataclass_param(db, field_policy, DataclassFlags::KW_ONLY); - - let fields = self.fields(db, specialization, field_policy); - let match_args = fields - .iter() - .filter(|(_, field)| { - if let FieldKind::Dataclass { init, kw_only, .. } = &field.kind { - *init && !kw_only.unwrap_or(kw_only_default) - } else { - false - } - }) - .map(|(name, _)| Type::string_literal(db, name)); - Some(Type::heterogeneous_tuple(db, match_args)) - } - (CodeGeneratorKind::DataclassLike(_), "__weakref__") - if Program::get(db).python_version(db) >= PythonVersion::PY311 => - { - if !self.has_dataclass_param(db, field_policy, DataclassFlags::WEAKREF_SLOT) - || !self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) - { - return None; - } - - // This could probably be `weakref | None`, but it does not seem important enough to - // model it precisely. - Some(UnionType::from_two_elements( - db, - Type::any(), - Type::none(db), - )) - } - (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { - KnownClass::NamedTupleFallback - .to_class_literal(db) - .as_class_literal()? - .as_static()? - .own_class_member(db, self.inherited_generic_context(db), None, name) - .ignore_possibly_undefined() - .map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: determine_upper_bound( - db, - self, - specialization, - |base| { - base.into_class() - .is_some_and(|c| c.is_known(db, KnownClass::Tuple)) - }, - ), - }, - TypeContext::default(), - ) - }) - } - (CodeGeneratorKind::DataclassLike(_), "__replace__") - if Program::get(db).python_version(db) >= PythonVersion::PY313 => - { - let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(instance_ty); - - signature_from_fields(vec![self_parameter], instance_ty) - } - (CodeGeneratorKind::DataclassLike(_), "__setattr__") => { - if self.is_frozen_dataclass(db) == Some(true) { - let signature = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(instance_ty), - Parameter::positional_or_keyword(Name::new_static("name")), - Parameter::positional_or_keyword(Name::new_static("value")), - ], - ), - Type::Never, - ); - - return Some(Type::function_like_callable(db, signature)); - } - None - } - (CodeGeneratorKind::DataclassLike(_), "__slots__") - if Program::get(db).python_version(db) >= PythonVersion::PY310 => - { - self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) - .then(|| { - let fields = self.fields(db, specialization, field_policy); - let slots = fields.keys().map(|name| Type::string_literal(db, name)); - Type::heterogeneous_tuple(db, slots) - }) - } - (CodeGeneratorKind::TypedDict, "__setitem__") => { - let fields = self.fields(db, specialization, field_policy); - - // Add (key type, value type) overloads for all TypedDict items ("fields") that are not read-only: - - let mut writeable_fields = fields - .iter() - .filter(|(_, field)| !field.is_read_only()) - .peekable(); - - if writeable_fields.peek().is_none() { - // If there are no writeable fields, synthesize a `__setitem__` that takes - // a `key` of type `Never` to signal that no keys are accepted. This leads - // to slightly more user-friendly error messages compared to returning an - // empty overload set. - return Some(Type::Callable(CallableType::new( - db, - CallableSignature::single(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(Type::Never), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(Type::any()), - ], - ), - Type::none(db), - )), - CallableTypeKind::FunctionLike, - ))); - } - - let overloads = writeable_fields.map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(field.declared_ty), - ], - ), - Type::none(db), - ) - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "__getitem__") => { - let fields = self.fields(db, specialization, field_policy); - - // Add (key -> value type) overloads for all TypedDict items ("fields"): - let overloads = fields.iter().map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ], - ), - field.declared_ty, - ) - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "__delitem__") => { - let fields = self.fields(db, specialization, field_policy); - - // Only non-required fields can be deleted. Required fields cannot be deleted - // because that would violate the TypedDict's structural type. - let mut deletable_fields = fields - .iter() - .filter(|(_, field)| !field.is_required()) - .peekable(); - - if deletable_fields.peek().is_none() { - // If there are no deletable fields (all fields are required), synthesize a - // `__delitem__` that takes a `key` of type `Never` to signal that no keys - // can be deleted. - return Some(Type::Callable(CallableType::new( - db, - CallableSignature::single(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(Type::Never), - ], - ), - Type::none(db), - )), - CallableTypeKind::FunctionLike, - ))); - } - - // Otherwise, add overloads for all deletable fields. - let overloads = deletable_fields.map(|(name, _field)| { - let key_type = Type::string_literal(db, name); - - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ], - ), - Type::none(db), - ) - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "get") => { - let overloads = self - .fields(db, specialization, field_policy) - .iter() - .flat_map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - // For a required key, `.get()` always returns the value type. For a non-required key, - // `.get()` returns the union of the value type and the type of the default argument - // (which defaults to `None`). - - // TODO: For now, we use two overloads here. They can be merged into a single function - // once the generics solver takes default arguments into account. - - let get_sig = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ], - ), - if field.is_required() { - field.declared_ty - } else { - UnionType::from_two_elements(db, field.declared_ty, Type::none(db)) - }, - ); - - let t_default = BoundTypeVarInstance::synthetic( - db, - Name::new_static("T"), - TypeVarVariance::Covariant, - ); - - let get_with_default_sig = Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(Type::TypeVar(t_default)), - ], - ), - if field.is_required() { - field.declared_ty - } else { - UnionType::from_two_elements( - db, - field.declared_ty, - Type::TypeVar(t_default), - ) - }, - ); - - [get_sig, get_with_default_sig] - }) - // Fallback overloads for unknown keys - .chain(std::iter::once({ - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), - ], - ), - UnionType::from_two_elements(db, Type::unknown(), Type::none(db)), - ) - })) - .chain(std::iter::once({ - let t_default = BoundTypeVarInstance::synthetic( - db, - Name::new_static("T"), - TypeVarVariance::Covariant, - ); - - Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(Type::TypeVar(t_default)), - ], - ), - UnionType::from_two_elements( - db, - Type::unknown(), - Type::TypeVar(t_default), - ), - ) - })); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "pop") => { - let fields = self.fields(db, specialization, field_policy); - let overloads = fields - .iter() - .filter(|(_, field)| { - // Only synthesize `pop` for fields that are not required. - !field.is_required() - }) - .flat_map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - // TODO: Similar to above: consider merging these two overloads into one - - // `.pop()` without default - let pop_sig = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ], - ), - field.declared_ty, - ); - - // `.pop()` with a default value - let t_default = BoundTypeVarInstance::synthetic( - db, - Name::new_static("T"), - TypeVarVariance::Covariant, - ); - - let pop_with_default_sig = Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(Type::TypeVar(t_default)), - ], - ), - UnionType::from_two_elements( - db, - field.declared_ty, - Type::TypeVar(t_default), - ), - ); - - [pop_sig, pop_with_default_sig] - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "setdefault") => { - let fields = self.fields(db, specialization, field_policy); - let overloads = fields.iter().map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - // `setdefault` always returns the field type - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(field.declared_ty), - ], - ), - field.declared_ty, - ) - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "update") => { - // TODO: synthesize a set of overloads with precise types - let signature = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::variadic(Name::new_static("args")), - Parameter::keyword_variadic(Name::new_static("kwargs")), - ], - ), - Type::none(db), - ); - - Some(Type::function_like_callable(db, signature)) - } - _ => None, - } - } - - /// Member lookup for classes that inherit from `typing.TypedDict`. - /// - /// This is implemented as a separate method because the item definitions on a `TypedDict`-based - /// class are *not* accessible as class members. Instead, this mostly defers to `TypedDictFallback`, - /// unless `name` corresponds to one of the specialized synthetic members like `__getitem__`. - pub(crate) fn typed_dict_member( - self, - db: &'db dyn Db, - specialization: Option>, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - if let Some(member) = self.own_synthesized_member(db, specialization, None, name) { - Place::bound(member).into() - } else { - KnownClass::TypedDictFallback - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) - .expect("`find_name_in_mro_with_policy` will return `Some()` when called on class literal") - .map_type(|ty| - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: determine_upper_bound( - db, - self, - specialization, - ClassBase::is_typed_dict - ) - }, - TypeContext::default(), - ) - ) - } - } - - /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. - /// - /// See [`StaticClassLiteral::own_fields`] for more details. - #[salsa::tracked( - returns(ref), - cycle_initial=|_, _, _, _, _| FxIndexMap::default(), - heap_size=get_size2::GetSize::get_heap_size)] - pub(crate) fn fields( - self, - db: &'db dyn Db, - specialization: Option>, - field_policy: CodeGeneratorKind<'db>, - ) -> FxIndexMap> { - if field_policy == CodeGeneratorKind::NamedTuple { - // NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the - // fields of this class only. - return self.own_fields(db, specialization, field_policy); - } - - let matching_classes_in_mro: Vec<(StaticClassLiteral<'db>, Option>)> = - self.iter_mro(db, specialization) - .filter_map(|superclass| { - let class = superclass.into_class()?; - // Dynamic classes don't have fields (no class body). - let (class_literal, specialization) = class.static_class_literal(db)?; - if field_policy.matches(db, class_literal.into(), specialization) { - Some((class_literal, specialization)) - } else { - None - } - }) - // We need to collect into a `Vec` here because we iterate the MRO in reverse order - .collect(); - - matching_classes_in_mro - .into_iter() - .rev() - .flat_map(|(class, specialization)| class.own_fields(db, specialization, field_policy)) - // KW_ONLY sentinels are markers, not real fields. Exclude them so - // they cannot shadow an inherited field with the same name. - .filter(|(_, field)| !field.is_kw_only_sentinel(db)) - // We collect into a FxOrderMap here to deduplicate attributes - .collect() - } - - pub(crate) fn validate_members(self, context: &InferContext<'db, '_>) { - let db = context.db(); - let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self, None) else { - return; - }; - let class_body_scope = self.body_scope(db); - let table = place_table(db, class_body_scope); - let use_def = use_def_map(db, class_body_scope); - for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations.clone()); - let attr = result.ignore_conflicting_declarations(); - let symbol = table.symbol(symbol_id); - let name = symbol.name(); - - let Some(Type::FunctionLiteral(literal)) = attr.place.ignore_possibly_undefined() - else { - continue; - }; - - match name.as_str() { - "__setattr__" | "__delattr__" => { - if let CodeGeneratorKind::DataclassLike(_) = field_policy - && self.is_frozen_dataclass(db) == Some(true) - { - if let Some(builder) = context.report_lint( - &INVALID_DATACLASS_OVERRIDE, - literal.node(db, context.file(), context.module()), - ) { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot overwrite attribute `{}` in frozen dataclass `{}`", - name, - self.name(db) - )); - diagnostic.info(name); - } - } - } - "__lt__" | "__le__" | "__gt__" | "__ge__" => { - if let CodeGeneratorKind::DataclassLike(_) = field_policy - && self.has_dataclass_param(db, field_policy, DataclassFlags::ORDER) - { - if let Some(builder) = context.report_lint( - &INVALID_DATACLASS_OVERRIDE, - literal.node(db, context.file(), context.module()), - ) { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot overwrite attribute `{}` in dataclass `{}` with `order=True`", - name, - self.name(db) - )); - diagnostic.info(name); - } - } - } - _ => {} - } - } - } - - /// Returns a map of all annotated attributes defined in the body of this class. - /// This extends the `__annotations__` attribute at runtime by also including default values - /// and computed field properties. - /// - /// For a class body like - /// ```py - /// @dataclass(kw_only=True) - /// class C: - /// x: int - /// y: str = "hello" - /// z: float = field(kw_only=False, default=1.0) - /// ``` - /// we return a map `{"x": Field, "y": Field, "z": Field}` where each `Field` contains - /// the annotated type, default value (if any), and field properties. - /// - /// **Important**: The returned `Field` objects represent our full understanding of the fields, - /// including properties inherited from class-level dataclass parameters (like `kw_only=True`) - /// and dataclass-transform parameters (like `kw_only_default=True`). They do not represent - /// only what is explicitly specified in each field definition. - pub(super) fn own_fields( - self, - db: &'db dyn Db, - specialization: Option>, - field_policy: CodeGeneratorKind, - ) -> FxIndexMap> { - let mut attributes = FxIndexMap::default(); - - let class_body_scope = self.body_scope(db); - let table = place_table(db, class_body_scope); - - let use_def = use_def_map(db, class_body_scope); - - let typed_dict_params = self.typed_dict_params(db); - let mut kw_only_sentinel_field_seen = false; - - for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - // Here, we exclude all declarations that are not annotated assignments. We need this because - // things like function definitions and nested classes would otherwise be considered dataclass - // fields. The check is too broad in the sense that it also excludes (weird) constructs where - // a symbol would have multiple declarations, one of which is an annotated assignment. If we - // want to improve this, we could instead pass a definition-kind filter to the use-def map - // query, or to the `symbol_from_declarations` call below. Doing so would potentially require - // us to generate a union of `__init__` methods. - if !declarations - .clone() - .all(|DeclarationWithConstraint { declaration, .. }| { - declaration.is_undefined_or(|declaration| { - matches!( - declaration.kind(db), - DefinitionKind::AnnotatedAssignment(..) - ) - }) - }) - { - continue; - } - - let symbol = table.symbol(symbol_id); - - let result = place_from_declarations(db, declarations.clone()); - let first_declaration = result.first_declaration; - let attr = result.ignore_conflicting_declarations(); - if attr.is_class_var() { - continue; - } - - 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) - .place - .ignore_possibly_undefined(); - - default_ty = - default_ty.map(|ty| ty.apply_optional_specialization(db, specialization)); - - let mut init = true; - let mut kw_only = None; - let mut alias = None; - if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty { - default_ty = field.default_type(db); - if self - .dataclass_params(db) - .map(|params| params.field_specifiers(db).is_empty()) - .unwrap_or(false) - { - // This happens when constructing a `dataclass` with a `dataclass_transform` - // without defining the `field_specifiers`, meaning it should ignore - // `dataclasses.field` and `dataclasses.Field`. - } else { - init = field.init(db); - kw_only = field.kw_only(db); - alias = field.alias(db); - } - } - - let kind = match field_policy { - CodeGeneratorKind::NamedTuple => FieldKind::NamedTuple { default_ty }, - CodeGeneratorKind::DataclassLike(_) => FieldKind::Dataclass { - default_ty, - init_only: attr.is_init_var(), - init, - kw_only, - alias, - }, - CodeGeneratorKind::TypedDict => { - let is_required = if attr.is_required() { - // Explicit Required[T] annotation - always required - true - } else if attr.is_not_required() { - // Explicit NotRequired[T] annotation - never required - false - } else { - // No explicit qualifier - use class default (`total` parameter) - typed_dict_params - .expect("TypedDictParams should be available for CodeGeneratorKind::TypedDict") - .contains(TypedDictParams::TOTAL) - }; - - FieldKind::TypedDict { - is_required, - is_read_only: attr.is_read_only(), - } - } - }; - - let mut field = Field { - declared_ty: attr_ty.apply_optional_specialization(db, specialization), - kind, - first_declaration, - }; - - // Check if this is a KW_ONLY sentinel and mark subsequent fields as keyword-only - if field.is_kw_only_sentinel(db) { - kw_only_sentinel_field_seen = true; - } - - // If no explicit kw_only setting and we've seen KW_ONLY sentinel, mark as keyword-only - if kw_only_sentinel_field_seen { - if let FieldKind::Dataclass { - kw_only: ref mut kw @ None, - .. - } = field.kind - { - *kw = Some(true); - } - } - - // Resolve the kw_only to the class-level default. This ensures that when fields - // are inherited by child classes, they use their defining class's kw_only default. - if let FieldKind::Dataclass { - kw_only: ref mut kw @ None, - .. - } = field.kind - { - let class_kw_only_default = self - .dataclass_params(db) - .is_some_and(|params| params.flags(db).contains(DataclassFlags::KW_ONLY)) - // TODO this next part should not be necessary, if we were properly - // initializing `dataclass_params` from the dataclass-transform params, for - // metaclass and base-class-based dataclass-transformers. - || matches!( - field_policy, - CodeGeneratorKind::DataclassLike(Some(transformer_params)) - if transformer_params.flags(db).contains(DataclassTransformerFlags::KW_ONLY_DEFAULT) - ); - *kw = Some(class_kw_only_default); - } - - attributes.insert(symbol.name().clone(), field); - } - } - - attributes - } - - /// Look up an instance attribute (available in `__dict__`) of the given name. - /// - /// See [`Type::instance_member`] for more details. - pub(super) fn instance_member( - self, - db: &'db dyn Db, - specialization: Option>, - name: &str, - ) -> PlaceAndQualifiers<'db> { - if self.is_typed_dict(db) { - return Place::Undefined.into(); - } - - match MroLookup::new(db, self.iter_mro(db, specialization)).instance_member(name) { - InstanceMemberResult::Done(result) => result, - InstanceMemberResult::TypedDict => KnownClass::TypedDictFallback - .to_instance(db) - .instance_member(db, name) - .map_type(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: Type::instance(db, self.unknown_specialization(db)), - }, - TypeContext::default(), - ) - }), - } - } - - /// Tries to find declarations/bindings of an attribute named `name` that are only - /// "implicitly" defined (`self.x = …`, `cls.x = …`) in a method of the class that - /// corresponds to `class_body_scope`. The `target_method_decorator` parameter is - /// used to skip methods that do not have the expected decorator. - fn implicit_attribute( - db: &'db dyn Db, - class_body_scope: ScopeId<'db>, - name: &str, - target_method_decorator: MethodDecorator, - ) -> Member<'db> { - Self::implicit_attribute_inner( - db, - class_body_scope, - name.to_string(), - target_method_decorator, - ) - } - - #[salsa::tracked( - cycle_fn=implicit_attribute_cycle_recover, - cycle_initial=implicit_attribute_initial, - heap_size=ruff_memory_usage::heap_size, - )] - pub(super) fn implicit_attribute_inner( - db: &'db dyn Db, - class_body_scope: ScopeId<'db>, - name: String, - target_method_decorator: MethodDecorator, - ) -> Member<'db> { - // If we do not see any declarations of an attribute, neither in the class body nor in - // any method, we build a union of `Unknown` with the inferred types of all bindings of - // that attribute. We include `Unknown` in that union to account for the fact that the - // attribute might be externally modified. - let mut union_of_inferred_types = UnionBuilder::new(db); - let mut qualifiers = TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE; - - let mut is_attribute_bound = false; - - let file = class_body_scope.file(db); - let module = parsed_module(db, file).load(db); - let index = semantic_index(db, file); - let class_map = use_def_map(db, class_body_scope); - let class_table = place_table(db, class_body_scope); - let is_valid_scope = |method_scope: &Scope| { - let Some(method_def) = method_scope.node().as_function() else { - return true; - }; - - // Check the decorators directly on the AST node to determine if this method - // is a classmethod or staticmethod. This is more reliable than checking the - // final evaluated type, which may be wrapped by other decorators like @cache. - let function_node = method_def.node(&module); - let definition = index.expect_single_definition(method_def); - - let mut is_classmethod = false; - let mut is_staticmethod = false; - - for decorator in &function_node.decorator_list { - let decorator_ty = - definition_expression_type(db, definition, &decorator.expression); - if let Type::ClassLiteral(class) = decorator_ty { - match class.known(db) { - Some(KnownClass::Classmethod) => is_classmethod = true, - Some(KnownClass::Staticmethod) => is_staticmethod = true, - _ => {} - } - } - } - - // Also check for implicit classmethods/staticmethods based on method name - let method_name = function_node.name.as_str(); - if is_implicit_classmethod(method_name) { - is_classmethod = true; - } - if is_implicit_staticmethod(method_name) { - is_staticmethod = true; - } - - match target_method_decorator { - MethodDecorator::None => !is_classmethod && !is_staticmethod, - MethodDecorator::ClassMethod => is_classmethod, - MethodDecorator::StaticMethod => is_staticmethod, - } - }; - - // First check declarations - for (attribute_declarations, method_scope_id) in - attribute_declarations(db, class_body_scope, &name) - { - let method_scope = index.scope(method_scope_id); - if !is_valid_scope(method_scope) { - continue; - } - - for attribute_declaration in attribute_declarations { - let DefinitionState::Defined(declaration) = attribute_declaration.declaration - else { - continue; - }; - - let DefinitionKind::AnnotatedAssignment(assignment) = declaration.kind(db) else { - continue; - }; - - // We found an annotated assignment of one of the following forms (using 'self' in these - // examples, but we support arbitrary names for the first parameters of methods): - // - // self.name: - // self.name: = … - - let annotation = declaration_type(db, declaration); - let annotation = Place::declared(annotation.inner).with_qualifiers( - annotation.qualifiers | TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE, - ); - - if let Some(all_qualifiers) = annotation.is_bare_final() { - if let Some(value) = assignment.value(&module) { - // If we see an annotated assignment with a bare `Final` as in - // `self.SOME_CONSTANT: Final = 1`, infer the type from the value - // on the right-hand side. - - let inferred_ty = infer_expression_type( - db, - index.expression(value), - TypeContext::default(), - ); - 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 - qualifiers |= all_qualifiers; - continue; - } - - return Member { inner: annotation }; - } - } - - if !qualifiers.contains(TypeQualifiers::FINAL) { - union_of_inferred_types = union_of_inferred_types.add(Type::unknown()); - } - - for (attribute_assignments, attribute_binding_scope_id) in - attribute_assignments(db, class_body_scope, &name) - { - let binding_scope = index.scope(attribute_binding_scope_id); - if !is_valid_scope(binding_scope) { - continue; - } - - let scope_for_reachability_analysis = { - if binding_scope.node().as_function().is_some() { - binding_scope - } else if binding_scope.is_eager() { - let mut eager_scope_parent = binding_scope; - while eager_scope_parent.is_eager() - && let Some(parent) = eager_scope_parent.parent() - { - eager_scope_parent = index.scope(parent); - } - eager_scope_parent - } else { - binding_scope - } - }; - - // The attribute assignment inherits the reachability of the method which contains it - let is_method_reachable = - if let Some(method_def) = scope_for_reachability_analysis.node().as_function() { - let method = index.expect_single_definition(method_def); - let method_place = class_table - .symbol_id(&method_def.node(&module).name) - .unwrap(); - class_map - .reachable_symbol_bindings(method_place) - .find_map(|bind| { - (bind.binding.is_defined_and(|def| def == method)) - .then(|| class_map.binding_reachability(db, &bind)) - }) - .unwrap_or(Truthiness::AlwaysFalse) - } else { - Truthiness::AlwaysFalse - }; - if is_method_reachable.is_always_false() { - continue; - } - - for attribute_assignment in attribute_assignments { - if let DefinitionState::Undefined = attribute_assignment.binding { - continue; - } - - let DefinitionState::Defined(binding) = attribute_assignment.binding else { - continue; - }; - - if !is_method_reachable.is_always_false() { - is_attribute_bound = true; - } - - match binding.kind(db) { - DefinitionKind::AnnotatedAssignment(_) => { - // Annotated assignments were handled above. This branch is not - // unreachable (because of the `continue` above), but there is - // nothing to do here. - } - DefinitionKind::Assignment(assign) => { - match assign.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // .., self.name, .. = - // (.., self.name, ..) = - // [.., self.name, ..] = - - let unpacked = infer_unpack_types(db, unpack); - - let inferred_ty = unpacked.expression_type(assign.target(&module)); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::Single => { - // We found an un-annotated attribute assignment of the form: - // - // self.name = - - let inferred_ty = infer_expression_type( - db, - index.expression(assign.value(&module)), - TypeContext::default(), - ); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::For(for_stmt) => { - match for_stmt.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // for .., self.name, .. in : - - let unpacked = infer_unpack_types(db, unpack); - let inferred_ty = - unpacked.expression_type(for_stmt.target(&module)); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::Single => { - // We found an attribute assignment like: - // - // for self.name in : - - let iterable_ty = infer_expression_type( - db, - index.expression(for_stmt.iterable(&module)), - TypeContext::default(), - ); - // TODO: Potential diagnostics resulting from the iterable are currently not reported. - let inferred_ty = - iterable_ty.iterate(db).homogeneous_element_type(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::WithItem(with_item) => { - match with_item.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // with as .., self.name, ..: - - let unpacked = infer_unpack_types(db, unpack); - let inferred_ty = - unpacked.expression_type(with_item.target(&module)); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::Single => { - // We found an attribute assignment like: - // - // with as self.name: - - let context_ty = infer_expression_type( - db, - index.expression(with_item.context_expr(&module)), - TypeContext::default(), - ); - let inferred_ty = if with_item.is_async() { - context_ty.aenter(db) - } else { - context_ty.enter(db) - }; - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::Comprehension(comprehension) => { - match comprehension.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // [... for .., self.name, .. in ] - - let unpacked = infer_unpack_types(db, unpack); - - let inferred_ty = - unpacked.expression_type(comprehension.target(&module)); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::Single => { - // We found an attribute assignment like: - // - // [... for self.name in ] - - let iterable_ty = infer_expression_type( - db, - index.expression(comprehension.iterable(&module)), - TypeContext::default(), - ); - // TODO: Potential diagnostics resulting from the iterable are currently not reported. - let inferred_ty = - iterable_ty.iterate(db).homogeneous_element_type(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::AugmentedAssignment(_) => { - // TODO: - } - DefinitionKind::NamedExpression(_) => { - // A named expression whose target is an attribute is syntactically prohibited - } - _ => {} - } - } - } - - 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 - /// this class, not on its superclasses. - fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - // TODO: There are many things that are not yet implemented here: - // - `typing.Final` - // - Proper diagnostics - - let body_scope = self.body_scope(db); - let table = place_table(db, body_scope); - - if let Some(symbol_id) = table.symbol_id(name) { - let use_def = use_def_map(db, body_scope); - - let declarations = use_def.end_of_scope_symbol_declarations(symbol_id); - let declared_and_qualifiers = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); - - match declared_and_qualifiers { - PlaceAndQualifiers { - place: - mut declared @ Place::Defined(DefinedPlace { - ty: declared_ty, - definedness: declaredness, - .. - }), - qualifiers, - } => { - // For the purpose of finding instance attributes, ignore `ClassVar` - // declarations: - if qualifiers.contains(TypeQualifiers::CLASS_VAR) { - 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_undefined() - { - return Member::unbound(); - } - } - - // `KW_ONLY` sentinels are markers, not real instance attributes. - if declared_ty.is_instance_of(db, KnownClass::KwOnly) - && CodeGeneratorKind::from_static_class(db, self, None).is_some_and( - |policy| matches!(policy, CodeGeneratorKind::DataclassLike(_)), - ) - { - return Member::unbound(); - } - - // The attribute is declared in the class body. - - let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let inferred = place_from_bindings(db, bindings).place; - 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_undefined() - { - 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 { - inner: declared.with_qualifiers(qualifiers), - } - } else { - Member { - inner: Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements( - db, - declared_ty, - implicit_ty, - ), - origin: TypeOrigin::Declared, - definedness: declaredness, - widening: Widening::None, - }) - .with_qualifiers(qualifiers), - } - } - } else if self.is_own_dataclass_instance_field(db, name) - && declared_ty - .class_member(db, "__get__".into()) - .place - .is_undefined() - { - // For dataclass-like classes, declared fields are assigned - // by the synthesized `__init__`, so they are instance - // attributes even without an explicit `self.x = ...` - // assignment in a method body. - // - // However, if the declared type is a descriptor (has - // `__get__`), we return unbound so that the descriptor - // protocol in `member_lookup_with_policy` can resolve - // the attribute type through `__get__`. - Member { - inner: declared.with_qualifiers(qualifiers), - } - } else { - // The symbol is declared and bound in the class body, - // but we did not find any attribute assignments in - // methods of the class. This means that the attribute - // has a class-level default value, but it would not be - // found in a `__dict__` lookup. - - Member::unbound() - } - } else { - // The attribute is declared but not bound in the class body. - // We take this as a sign that this is intended to be a pure - // instance attribute, and we trust the declared type, unless - // it is possibly-undeclared. In the latter case, we also - // union with the inferred type from attribute assignments. - - if declaredness == Definedness::AlwaysDefined { - Member { - inner: declared.with_qualifiers(qualifiers), - } - } else { - if let Some(implicit_ty) = Self::implicit_attribute( - db, - body_scope, - name, - MethodDecorator::None, - ) - .inner - .place - .ignore_possibly_undefined() - { - Member { - inner: Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements( - db, - declared_ty, - implicit_ty, - ), - origin: TypeOrigin::Declared, - definedness: declaredness, - widening: Widening::None, - }) - .with_qualifiers(qualifiers), - } - } else { - Member { - inner: declared.with_qualifiers(qualifiers), - } - } - } - } - } - - PlaceAndQualifiers { - place: Place::Undefined, - qualifiers: _, - } => { - // The attribute is not *declared* in the class body. It could still be declared/bound - // in a method. - - Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) - } - } - } else { - // This attribute is neither declared nor bound in the class body. - // It could still be implicitly defined in a method. - - Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) - } - } - - /// Returns `true` if `name` is a non-init-only field directly declared on this - /// dataclass (i.e., a field that corresponds to an instance attribute). - /// - /// This is used to decide whether a bare class-body annotation like `x: int` - /// should be treated as defining an instance attribute: dataclass fields are - /// implicitly assigned in `__init__`, so they behave as instance attributes - /// even though no explicit binding exists in the class body. - fn is_own_dataclass_instance_field(self, db: &'db dyn Db, name: &str) -> bool { - let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self, None) else { - return false; - }; - if !matches!(field_policy, CodeGeneratorKind::DataclassLike(_)) { - return false; - } - - let fields = self.own_fields(db, None, field_policy); - let Some(field) = fields.get(name) else { - return false; - }; - matches!( - field.kind, - FieldKind::Dataclass { - init_only: false, - .. - } - ) - } - - pub(super) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { - Type::instance(db, ClassType::NonGeneric(self.into())) - } - - /// Return this class' involvement in an inheritance cycle, if any. - /// - /// A class definition like this will fail at runtime, - /// but we must be resilient to it or we could panic. - #[salsa::tracked(cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn inheritance_cycle(self, db: &'db dyn Db) -> Option { - /// Return `true` if the class is cyclically defined. - /// - /// Also, populates `visited_classes` with all base classes of `self`. - fn is_cyclically_defined_recursive<'db>( - db: &'db dyn Db, - class: StaticClassLiteral<'db>, - classes_on_stack: &mut IndexSet>, - visited_classes: &mut IndexSet>, - ) -> bool { - let mut result = false; - for explicit_base in class.explicit_bases(db) { - let explicit_base_class_literal = match explicit_base { - Type::ClassLiteral(class_literal) => class_literal.as_static(), - Type::GenericAlias(generic_alias) => Some(generic_alias.origin(db)), - _ => continue, - }; - let Some(explicit_base_class_literal) = explicit_base_class_literal else { - continue; - }; - if !classes_on_stack.insert(explicit_base_class_literal) { - return true; - } - - if visited_classes.insert(explicit_base_class_literal) { - // If we find a cycle, keep searching to check if we can reach the starting class. - result |= is_cyclically_defined_recursive( - db, - explicit_base_class_literal, - classes_on_stack, - visited_classes, - ); - } - classes_on_stack.pop(); - } - result - } - - tracing::trace!("Class::inheritance_cycle: {}", self.name(db)); - - let visited_classes = &mut IndexSet::new(); - if !is_cyclically_defined_recursive(db, self, &mut IndexSet::new(), visited_classes) { - None - } else if visited_classes.contains(&self) { - Some(InheritanceCycle::Participant) - } else { - Some(InheritanceCycle::Inherited) - } - } - - /// Returns a [`Span`] with the range of the class's header. - /// - /// See [`Self::header_range`] for more details. - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { - Span::from(self.file(db)).with_range(self.header_range(db)) - } - - /// Returns the range of the class's "header": the class name - /// and any arguments passed to the `class` statement. E.g. - /// - /// ```ignore - /// class Foo(Bar, metaclass=Baz): ... - /// ^^^^^^^^^^^^^^^^^^^^^^^ - /// ``` - pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { - let class_scope = self.body_scope(db); - let module = parsed_module(db, class_scope.file(db)).load(db); - let class_node = class_scope.node(db).expect_class().node(&module); - let class_name = &class_node.name; - TextRange::new( - class_name.start(), - class_node - .arguments - .as_deref() - .map(Ranged::end) - .unwrap_or_else(|| class_name.end()), - ) - } -} - -#[salsa::tracked] -impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { - #[salsa::tracked(cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size)] - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { - let typevar_in_generic_context = self - .generic_context(db) - .is_some_and(|generic_context| generic_context.variables(db).contains(&typevar)); - - if !typevar_in_generic_context { - return TypeVarVariance::Bivariant; - } - let class_body_scope = self.body_scope(db); - - let file = class_body_scope.file(db); - let index = semantic_index(db, file); - - let explicit_bases_variances = self - .explicit_bases(db) - .iter() - .map(|class| class.variance_of(db, typevar)); - - let default_attribute_variance = { - let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self.into(), None); - // Python 3.13 introduced a synthesized `__replace__` method on dataclasses which uses - // their field types in contravariant position, thus meaning a frozen dataclass must - // still be invariant in its field types. Other synthesized methods on dataclasses are - // not considered here, since they don't use field types in their signatures. TODO: - // ideally we'd have a single source of truth for information about synthesized - // methods, so we just look them up normally and don't hardcode this knowledge here. - let is_frozen_dataclass = Program::get(db).python_version(db) <= PythonVersion::PY312 - && self - .dataclass_params(db) - .is_some_and(|params| params.flags(db).contains(DataclassFlags::FROZEN)); - if is_namedtuple || is_frozen_dataclass { - TypeVarVariance::Covariant - } else { - TypeVarVariance::Invariant - } - }; - - let init_name: &Name = &"__init__".into(); - let new_name: &Name = &"__new__".into(); - - let use_def_map = index.use_def_map(class_body_scope.file_scope_id(db)); - let table = place_table(db, class_body_scope); - let attribute_places_and_qualifiers = - use_def_map - .all_end_of_scope_symbol_declarations() - .map(|(symbol_id, declarations)| { - let place_and_qual = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); - (symbol_id, place_and_qual) - }) - .chain(use_def_map.all_end_of_scope_symbol_bindings().map( - |(symbol_id, bindings)| { - (symbol_id, place_from_bindings(db, bindings).place.into()) - }, - )) - .filter_map(|(symbol_id, place_and_qual)| { - if let Some(name) = table.place(symbol_id).as_symbol().map(Symbol::name) { - (![init_name, new_name].contains(&name)) - .then_some((name.to_string(), place_and_qual)) - } else { - None - } - }); - - // Dataclasses can have some additional synthesized methods (`__eq__`, `__hash__`, - // `__lt__`, etc.) but none of these will have field types type variables in their signatures, so we - // don't need to consider them for variance. - - let attribute_names = attribute_scopes(db, self.body_scope(db)) - .flat_map(|function_scope_id| { - index - .place_table(function_scope_id) - .members() - .filter_map(|member| member.as_instance_attribute()) - .filter(|name| *name != init_name && *name != new_name) - .map(std::string::ToString::to_string) - .collect::>() - }) - .dedup(); - - let attribute_variances = attribute_names - .map(|name| { - let place_and_quals = self.own_instance_member(db, &name).inner; - (name, place_and_quals) - }) - .chain(attribute_places_and_qualifiers) - .dedup() - .filter_map(|(name, place_and_qual)| { - place_and_qual.ignore_possibly_undefined().map(|ty| { - let variance = if place_and_qual - .qualifiers - // `CLASS_VAR || FINAL` is really `all()`, but - // we want to be robust against new qualifiers - .intersects(TypeQualifiers::CLASS_VAR | TypeQualifiers::FINAL) - // We don't allow mutation of methods or properties - || ty.is_function_literal() - || ty.is_property_instance() - // Underscore-prefixed attributes are assumed not to be externally mutated - || name.starts_with('_') - { - // CLASS_VAR: class vars generally shouldn't contain the - // type variable, but they could if it's a - // callable type. They can't be mutated on instances. - // - // FINAL: final attributes are immutable, and thus covariant - TypeVarVariance::Covariant - } else { - default_attribute_variance - }; - ty.with_polarity(variance).variance_of(db, typevar) - }) - }); - - attribute_variances - .chain(explicit_bases_variances) - .collect() - } -} - -impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { - match self { - Self::Static(class) => class.variance_of(db, typevar), - Self::Dynamic(_) | Self::DynamicNamedTuple(_) => TypeVarVariance::Bivariant, - } - } -} - -/// A class created dynamically via a three-argument `type()` call. -/// -/// For example: -/// ```python -/// Foo = type("Foo", (Base,), {"attr": 1}) -/// ``` -/// -/// The type of `Foo` would be `` where `Foo` is a `DynamicClassLiteral` with: -/// - name: "Foo" -/// - members: [("attr", int)] -/// -/// This is called "dynamic" because the class is created dynamically at runtime -/// via a function call rather than a class statement. -/// -/// # Salsa interning -/// -/// This is a Salsa-interned struct. Two different `type()` calls always produce -/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases: -/// -/// ```python -/// Foo1 = type("Foo", (Base,), {}) -/// Foo2 = type("Foo", (Base,), {}) -/// # Foo1 and Foo2 are distinct types -/// ``` -/// -/// The `anchor` field provides stable identity: -/// - For assigned `type()` calls, the `Definition` uniquely identifies the class. -/// - For dangling `type()` calls, a relative node offset anchored to the enclosing scope -/// provides stable identity that only changes when the scope itself changes. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct DynamicClassLiteral<'db> { - /// The name of the class (from the first argument to `type()`). - #[returns(ref)] - pub name: Name, - - /// The anchor for this dynamic class, providing stable identity. - /// - /// - `Definition`: The `type()` call is assigned to a variable. The definition - /// uniquely identifies this class and can be used to find the `type()` call. - /// - `ScopeOffset`: The `type()` call is "dangling" (not assigned). The offset - /// is relative to the enclosing scope's anchor node index. - #[returns(ref)] - pub anchor: DynamicClassAnchor<'db>, - - /// The class members from the namespace dict (third argument to `type()`). - /// Each entry is a (name, type) pair extracted from the dict literal. - #[returns(deref)] - pub members: Box<[(Name, Type<'db>)]>, - - /// Whether the namespace dict (third argument) is dynamic (not a literal dict, - /// or contains non-string-literal keys). When true, attribute lookups on this - /// class and its instances return `Unknown` instead of failing. - pub has_dynamic_namespace: bool, - - /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator - /// or passed to `dataclass()` as a function. - pub dataclass_params: Option>, -} - -/// Anchor for identifying a dynamic class literal. -/// -/// This enum provides stable identity for `DynamicClassLiteral`: -/// - For assigned calls, the `Definition` uniquely identifies the class. -/// - For dangling calls, a relative offset provides stable identity. -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub enum DynamicClassAnchor<'db> { - /// The `type()` call is assigned to a variable. - /// - /// The `Definition` uniquely identifies this class. The `type()` call expression - /// is the `value` of the assignment, so we can get its range from the definition. - Definition(Definition<'db>), - - /// The `type()` call is "dangling" (not assigned to a variable). - /// - /// The offset is relative to the enclosing scope's anchor node index. - /// For module scope, this is equivalent to an absolute index (anchor is 0). - /// - /// The `explicit_bases` are computed eagerly at creation time since dangling - /// calls cannot recursively reference the class being defined. - ScopeOffset { - scope: ScopeId<'db>, - offset: u32, - explicit_bases: Box<[Type<'db>]>, - }, -} - -impl get_size2::GetSize for DynamicClassLiteral<'_> {} - -#[salsa::tracked] -impl<'db> DynamicClassLiteral<'db> { - /// Returns the definition where this class is created, if it was assigned to a variable. - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { - match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => Some(*definition), - DynamicClassAnchor::ScopeOffset { .. } => None, - } - } - - /// Returns the scope in which this dynamic class was created. - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => definition.scope(db), - DynamicClassAnchor::ScopeOffset { scope, .. } => *scope, - } - } - - /// Returns the explicit base classes of this dynamic class. - /// - /// For assigned `type()` calls, bases are computed lazily using deferred inference - /// to handle forward references (e.g., `X = type("X", (tuple["X | None"],), {})`). - /// - /// For dangling `type()` calls, bases are computed eagerly at creation time and - /// stored directly on the anchor, since dangling calls cannot recursively reference - /// the class being defined. - /// - /// Returns an empty slice if the bases cannot be computed (e.g., due to a cycle) - /// or if the bases argument is not a tuple. - /// - /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). - pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { - /// Inner cached function for deferred inference of bases. - /// Only called for assigned `type()` calls where inference was deferred. - #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] - fn deferred_explicit_bases<'db>( - db: &'db dyn Db, - definition: Definition<'db>, - ) -> Box<[Type<'db>]> { - let module = parsed_module(db, definition.file(db)).load(db); - - let value = definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments"); - let call_expr = value - .as_call_expr() - .expect("Definition value should be a call expression"); - - // The `bases` argument is the second positional argument. - let Some(bases_arg) = call_expr.arguments.args.get(1) else { - return Box::default(); - }; - - // Use `definition_expression_type` for deferred inference support. - let bases_type = definition_expression_type(db, definition, bases_arg); - - // For variable-length tuples (like `tuple[type, ...]`), we can't statically - // determine the bases, so return Unknown. - bases_type - .fixed_tuple_elements(db) - .map(Cow::into_owned) - .map(Into::into) - .unwrap_or_else(|| Box::from([Type::unknown()])) - } - - match self.anchor(db) { - // For dangling calls, bases are stored directly on the anchor. - DynamicClassAnchor::ScopeOffset { explicit_bases, .. } => explicit_bases.as_ref(), - // For assigned calls, use deferred inference. - DynamicClassAnchor::Definition(definition) => deferred_explicit_bases(db, *definition), - } - } - - /// Returns a [`Span`] with the range of the `type()` call expression. - /// - /// See [`Self::header_range`] for more details. - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { - Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) - } - - /// Returns the range of the `type()` call expression that created this class. - pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => { - // For definitions, get the range from the definition's value. - // The `type()` call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments") - .range() - } - DynamicClassAnchor::ScopeOffset { offset, .. } => { - // For dangling `type()` calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + *offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() - } - } - } - - /// Get the metaclass of this dynamic class. - /// - /// Derives the metaclass from base classes: finds the most derived metaclass - /// that is a subclass of all other base metaclasses. - /// - /// See - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - self.try_metaclass(db) - .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) - } - - /// Try to get the metaclass of this dynamic class. - /// - /// Returns `Err(DynamicMetaclassConflict)` if there's a metaclass conflict - /// (i.e., two base classes have metaclasses that are not in a subclass relationship). - /// - /// See - pub(crate) fn try_metaclass( - self, - db: &'db dyn Db, - ) -> Result, DynamicMetaclassConflict<'db>> { - let original_bases = self.explicit_bases(db); - - // If no bases, metaclass is `type`. - // To dynamically create a class with no bases that has a custom metaclass, - // you have to invoke that metaclass rather than `type()`. - if original_bases.is_empty() { - return Ok(KnownClass::Type.to_class_literal(db)); - } - - // If there's an MRO error, return unknown to avoid cascading errors. - if self.try_mro(db).is_err() { - return Ok(SubclassOfType::subclass_of_unknown()); - } - - // Convert Types to ClassBases for metaclass computation. - // All bases should convert successfully here: `try_mro()` above would have - // returned `Err(InvalidBases)` if any failed, causing us to return early. - let bases: Vec> = original_bases - .iter() - .filter_map(|base_type| ClassBase::try_from_type(db, *base_type, None)) - .collect(); - - // If all bases failed to convert, return type as the metaclass. - if bases.is_empty() { - return Ok(KnownClass::Type.to_class_literal(db)); - } - - // Start with the first base's metaclass as the candidate. - let mut candidate = bases[0].metaclass(db); - - // Track which base the candidate metaclass came from. - let (mut candidate_base, rest) = bases.split_first().unwrap(); - - // Reconcile with other bases' metaclasses. - for base in rest { - let base_metaclass = base.metaclass(db); - - // Get the ClassType for comparison. - let Some(candidate_class) = candidate.to_class_type(db) else { - // If candidate isn't a class type, keep it as is. - continue; - }; - let Some(base_metaclass_class) = base_metaclass.to_class_type(db) else { - continue; - }; - - // If base's metaclass is more derived, use it. - if base_metaclass_class.is_subclass_of(db, candidate_class) { - candidate = base_metaclass; - candidate_base = base; - continue; - } - - // If candidate is already more derived, keep it. - if candidate_class.is_subclass_of(db, base_metaclass_class) { - continue; - } - - // Conflict: neither metaclass is a subclass of the other. - // Python raises `TypeError: metaclass conflict` at runtime. - return Err(DynamicMetaclassConflict { - metaclass1: candidate_class, - base1: *candidate_base, - metaclass2: base_metaclass_class, - base2: *base, - }); - } - - Ok(candidate) - } - - /// Iterate over the MRO of this class using C3 linearization. - /// - /// The MRO includes the class itself as the first element, followed - /// by the merged base class MROs (consistent with `ClassType::iter_mro`). - /// - /// If the MRO cannot be computed (e.g., due to inconsistent ordering), falls back - /// to iterating over base MROs sequentially with deduplication. - pub(crate) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { - MroIterator::new(db, ClassLiteral::Dynamic(self), None) - } - - /// Look up an instance member by iterating through the MRO. - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - match MroLookup::new(db, self.iter_mro(db)).instance_member(name) { - InstanceMemberResult::Done(result) => result, - InstanceMemberResult::TypedDict => { - // Simplified `TypedDict` handling without type mapping. - KnownClass::TypedDictFallback - .to_instance(db) - .instance_member(db, name) - } - } - } - - /// Look up a class-level member by iterating through the MRO. - /// - /// Uses `MroLookup` with: - /// - No inherited generic context (dynamic classes aren't generic). - /// - `is_self_object = false` (dynamic classes are never `object`). - pub(crate) fn class_member( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - // Check if this dynamic class is dataclass-like (via dataclass_transform inheritance). - if matches!( - CodeGeneratorKind::from_class(db, self.into(), None), - Some(CodeGeneratorKind::DataclassLike(_)) - ) { - if name == "__dataclass_fields__" { - // Make this class look like a subclass of the `DataClassInstance` protocol. - return Place::declared(KnownClass::Dict.to_specialized_instance( - db, - &[ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, &[Type::any()]), - ], - )) - .with_qualifiers(TypeQualifiers::CLASS_VAR); - } else if name == "__dataclass_params__" { - // There is no typeshed class for this. For now, we model it as `Any`. - return Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR); - } - } - - let result = MroLookup::new(db, self.iter_mro(db)).class_member( - name, policy, None, // No inherited generic context. - false, // Dynamic classes are never `object`. - ); - - match result { - ClassMemberResult::Done(result) => result.finalize(db), - ClassMemberResult::TypedDict => { - // Simplified `TypedDict` handling without type mapping. - KnownClass::TypedDictFallback - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) - .expect("Will return Some() when called on class literal") - } - } - } - - /// Look up a class member defined directly on this class (not inherited). - /// - /// Returns [`Member::unbound`] if the member is not found in the namespace dict, - /// unless the namespace is dynamic, in which case returns `Unknown`. - pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - // If the namespace is dynamic (not a literal dict) and the name isn't in `self.members`, - // return Unknown since we can't know what attributes might be defined. - self.members(db) - .iter() - .find_map(|(member_name, ty)| (name == member_name).then_some(*ty)) - .or_else(|| self.has_dynamic_namespace(db).then(Type::unknown)) - .map(Member::definitely_declared) - .unwrap_or_default() - } - - /// Look up an instance member defined directly on this class (not inherited). - /// - /// For dynamic classes, instance members are the same as class members - /// since they come from the namespace dict. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - self.own_class_member(db, name) - } - - /// Try to compute the MRO for this dynamic class. - /// - /// Returns `Ok(Mro)` if successful, or `Err(DynamicMroError)` if there's - /// an error (duplicate bases or C3 linearization failure). - #[salsa::tracked(returns(ref), cycle_initial=dynamic_class_try_mro_cycle_initial, heap_size = ruff_memory_usage::heap_size)] - pub(crate) fn try_mro(self, db: &'db dyn Db) -> Result, DynamicMroError<'db>> { - Mro::of_dynamic_class(db, self) - } - - /// Return `Some()` if this dynamic class is known to be a [`DisjointBase`]. - /// - /// A dynamic class is a disjoint base if `__slots__` is defined in the namespace - /// dictionary and is non-empty. Example: - /// ```python - /// X = type("X", (), {"__slots__": ("a",)}) - /// ``` - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { - // Check if __slots__ is in the members - for (name, ty) in self.members(db) { - if name.as_str() == "__slots__" { - // Check if the slots are non-empty - let is_non_empty = match ty { - // __slots__ = ("a", "b") - Type::NominalInstance(nominal) => nominal.tuple_spec(db).is_some_and(|spec| { - spec.len().into_fixed_length().is_some_and(|len| len > 0) - }), - // __slots__ = "abc" # Same as ("abc",) - Type::LiteralValue(literal) if literal.is_string() => true, - // Other types are considered dynamic/unknown - _ => false, - }; - if is_non_empty { - return Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Dynamic( - self, - ))); - } - } - } - None - } - - /// Returns `true` if this dynamic class defines any ordering method (`__lt__`, `__le__`, - /// `__gt__`, `__ge__`) in its namespace dictionary. Used by `@total_ordering` to determine - /// if synthesis is valid. - /// - /// If the namespace is dynamic, returns `true` since we can't know if ordering methods exist. - pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { - const ORDERING_METHODS: &[&str] = &["__lt__", "__le__", "__gt__", "__ge__"]; - ORDERING_METHODS - .iter() - .any(|name| !self.own_class_member(db, name).is_undefined()) - } - - /// Returns a new [`DynamicClassLiteral`] with the given dataclass params, preserving all other fields. - pub(crate) fn with_dataclass_params( - self, - db: &'db dyn Db, - dataclass_params: Option>, - ) -> Self { - Self::new( - db, - self.name(db).clone(), - self.anchor(db).clone(), - self.members(db), - self.has_dynamic_namespace(db), - dataclass_params, - ) - } -} - -/// Error for metaclass conflicts in dynamic classes. -/// -/// This mirrors `MetaclassErrorKind::Conflict` for regular classes. -#[derive(Debug, Clone)] -pub(crate) struct DynamicMetaclassConflict<'db> { - /// The first conflicting metaclass and its originating base class. - pub(crate) metaclass1: ClassType<'db>, - pub(crate) base1: ClassBase<'db>, - /// The second conflicting metaclass and its originating base class. - pub(crate) metaclass2: ClassType<'db>, - pub(crate) base2: ClassBase<'db>, -} - -/// Create a property type for a namedtuple field. -fn create_field_property<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Type<'db> { - let property_getter_signature = Signature::new( - Parameters::new( - db, - [Parameter::positional_only(Some(Name::new_static("self")))], - ), - field_ty, - ); - let property_getter = Type::single_callable(db, property_getter_signature); - let property = PropertyInstanceType::new(db, Some(property_getter), None); - Type::PropertyInstance(property) -} - -/// Synthesize a namedtuple class member given the field information. -/// -/// This is used by both `DynamicNamedTupleLiteral` and `StaticClassLiteral` (for declarative -/// namedtuples) to avoid duplicating the synthesis logic. -/// -/// The `inherited_generic_context` parameter is used for declarative namedtuples to preserve -/// generic context in the synthesized `__new__` signature. -fn synthesize_namedtuple_class_member<'db>( - db: &'db dyn Db, - name: &str, - instance_ty: Type<'db>, - fields: impl Iterator>, - inherited_generic_context: Option>, -) -> Option> { - match name { - "__new__" => { - // __new__(cls, field1, field2, ...) -> Self - let self_typevar = - BoundTypeVarInstance::synthetic_self(db, instance_ty, BindingContext::Synthetic); - let self_ty = Type::TypeVar(self_typevar); - - let variables = inherited_generic_context - .iter() - .flat_map(|ctx| ctx.variables(db)) - .chain(std::iter::once(self_typevar)); - - let generic_context = GenericContext::from_typevar_instances(db, variables); - - let first_parameter = Parameter::positional_or_keyword(Name::new_static("cls")) - .with_annotated_type(SubclassOfType::from(db, self_typevar)); - - let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { - Parameter::positional_or_keyword(field.name) - .with_annotated_type(field.ty) - .with_optional_default_type(field.default) - })); - - let signature = Signature::new_generic( - Some(generic_context), - Parameters::new(db, parameters), - self_ty, - ); - Some(Type::function_like_callable(db, signature)) - } - "_fields" => { - // _fields: tuple[Literal["field1"], Literal["field2"], ...] - let field_types = fields.map(|field| Type::string_literal(db, &field.name)); - Some(Type::heterogeneous_tuple(db, field_types)) - } - "__slots__" => { - // __slots__: tuple[()] - always empty for namedtuples - Some(Type::empty_tuple(db)) - } - "_replace" | "__replace__" => { - if name == "__replace__" && Program::get(db).python_version(db) < PythonVersion::PY313 { - return None; - } - - // _replace(self, *, field1=..., field2=...) -> Self - let self_ty = Type::TypeVar(BoundTypeVarInstance::synthetic_self( - db, - instance_ty, - BindingContext::Synthetic, - )); - - let first_parameter = Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(self_ty); - - let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { - Parameter::keyword_only(field.name) - .with_annotated_type(field.ty) - .with_default_type(field.ty) - })); - - let signature = Signature::new(Parameters::new(db, parameters), self_ty); - Some(Type::function_like_callable(db, signature)) - } - "__init__" => { - // Namedtuples don't have a custom __init__. All construction happens in __new__. - None - } - _ => { - // Fall back to NamedTupleFallback for other synthesized methods. - KnownClass::NamedTupleFallback - .to_class_literal(db) - .as_class_literal()? - .as_static()? - .own_class_member(db, inherited_generic_context, None, name) - .ignore_possibly_undefined() - } - } -} - -#[derive(Debug, salsa::Update, get_size2::GetSize, Clone, PartialEq, Eq, Hash)] -pub struct NamedTupleField<'db> { - pub(crate) name: Name, - pub(crate) ty: Type<'db>, - pub(crate) default: Option>, -} - -/// A namedtuple created via the functional form `namedtuple(name, fields)` or -/// `NamedTuple(name, fields)`. -/// -/// For example: -/// ```python -/// from collections import namedtuple -/// Point = namedtuple("Point", ["x", "y"]) -/// -/// from typing import NamedTuple -/// Person = NamedTuple("Person", [("name", str), ("age", int)]) -/// ``` -/// -/// The type of `Point` would be `type[Point]` where `Point` is a `DynamicNamedTupleLiteral`. -#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] -pub struct DynamicNamedTupleLiteral<'db> { - /// The name of the namedtuple (from the first argument). - #[returns(ref)] - pub name: Name, - - /// The anchor for this dynamic namedtuple, providing stable identity. - /// - /// - `Definition`: The call is assigned to a variable. The definition - /// uniquely identifies this namedtuple and can be used to find the call. - /// - `ScopeOffset`: The call is "dangling" (not assigned). The offset - /// is relative to the enclosing scope's anchor node index. - #[returns(ref)] - pub anchor: DynamicNamedTupleAnchor<'db>, -} - -impl get_size2::GetSize for DynamicNamedTupleLiteral<'_> {} - -#[salsa::tracked] -impl<'db> DynamicNamedTupleLiteral<'db> { - /// Returns the definition where this namedtuple is created, if it was assigned to a variable. - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { - match self.anchor(db) { - DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } - | DynamicNamedTupleAnchor::TypingDefinition(definition) => Some(*definition), - DynamicNamedTupleAnchor::ScopeOffset { .. } => None, - } - } - - /// Returns the scope in which this dynamic class was created. - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - match self.anchor(db) { - DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } - | DynamicNamedTupleAnchor::TypingDefinition(definition) => definition.scope(db), - DynamicNamedTupleAnchor::ScopeOffset { scope, .. } => *scope, - } - } - - /// Returns an instance type for this dynamic namedtuple. - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { - Type::instance(db, ClassType::NonGeneric(self.into())) - } - - /// Returns the range of the namedtuple call expression. - pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { - DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } - | DynamicNamedTupleAnchor::TypingDefinition(definition) => { - // For definitions, get the range from the definition's value. - // The namedtuple call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments") - .range() - } - DynamicNamedTupleAnchor::ScopeOffset { offset, .. } => { - // For dangling calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() - } - } - } - - /// Returns a [`Span`] pointing to the namedtuple call expression. - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { - Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) - } - - /// Compute the MRO for this namedtuple. - /// - /// The MRO is the MRO of the class's tuple base class, prepended by `self`. - /// For example, `namedtuple("Point", [("x", int), ("y", int)])` has the following MRO: - /// - /// 1. `` - /// 2. `` - /// 3. `` - /// 4. `` - /// 5. `` - /// 6. `` - /// 7. `` - /// 8. `typing.Protocol` - /// 9. `typing.Generic` - /// 10. `` - #[salsa::tracked( - returns(ref), - heap_size=ruff_memory_usage::heap_size, - cycle_initial=dynamic_namedtuple_mro_cycle_initial - )] - pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { - let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); - let tuple_class = self.tuple_base_class(db); - std::iter::once(self_base) - .chain(tuple_class.iter_mro(db)) - .collect() - } - - /// Get the metaclass of this dynamic namedtuple. - /// - /// Namedtuples always have `type` as their metaclass. - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - let _ = self; - KnownClass::Type.to_class_literal(db) - } - - /// Compute the specialized tuple class that this namedtuple inherits from. - /// - /// For example, `namedtuple("Point", [("x", int), ("y", int)])` inherits from `tuple[int, int]`. - pub(crate) fn tuple_base_class(self, db: &'db dyn Db) -> ClassType<'db> { - // If fields are unknown, return `tuple[Unknown, ...]` to avoid false positives - // like index-out-of-bounds errors. - if !self.has_known_fields(db) { - return TupleType::homogeneous(db, Type::unknown()).to_class_type(db); - } - - let field_types = self.fields(db).iter().map(|field| field.ty); - TupleType::heterogeneous(db, field_types) - .map(|t| t.to_class_type(db)) - .unwrap_or_else(|| { - KnownClass::Tuple - .to_class_literal(db) - .as_class_literal() - .expect("tuple should be a class literal") - .default_specialization(db) - }) - } - - /// Look up an instance member defined directly on this class (not inherited). - /// - /// For dynamic namedtuples, instance members are the field names. - /// If fields are unknown (dynamic), returns `Any` for any attribute. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - for field in self.fields(db) { - if field.name == name { - return Member::definitely_declared(field.ty); - } - } - - if !self.has_known_fields(db) { - return Member::definitely_declared(Type::any()); - } - - Member::unbound() - } - - /// Look up an instance member by name (including superclasses). - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - // First check own instance members. - let result = self.own_instance_member(db, name); - if !result.is_undefined() { - return result.inner; - } - - // Fall back to the tuple base type for other attributes. - Type::instance(db, self.tuple_base_class(db)).instance_member(db, name) - } - - /// Look up a class-level member by name. - pub(crate) fn class_member( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - // First check synthesized members and fields. - let member = self.own_class_member(db, name); - if !member.is_undefined() { - return member.inner; - } - - // Fall back to tuple class members. - let result = self - .tuple_base_class(db) - .class_literal(db) - .class_member(db, name, policy); - - // If fields are unknown (dynamic) and the attribute wasn't found, - // return `Any` instead of failing. - if !self.has_known_fields(db) && result.place.is_undefined() { - return Place::bound(Type::any()).into(); - } - - result - } - - /// Look up a class-level member defined directly on this class (not inherited). - /// - /// This only checks synthesized members and field properties, without falling - /// back to tuple or other base classes. - pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - // Handle synthesized namedtuple attributes. - if let Some(ty) = self.synthesized_class_member(db, name) { - return Member::definitely_declared(ty); - } - - // Check if it's a field name (returns a property descriptor). - for field in self.fields(db) { - if field.name == name { - return Member::definitely_declared(create_field_property(db, field.ty)); - } - } - - Member::default() - } - - /// Generate synthesized class members for namedtuples. - fn synthesized_class_member(self, db: &'db dyn Db, name: &str) -> Option> { - let instance_ty = self.to_instance(db); - - // When fields are unknown, handle constructor and field-specific methods specially. - if !self.has_known_fields(db) { - match name { - // For constructors, return a gradual signature that accepts any arguments. - "__new__" | "__init__" => { - let signature = Signature::new(Parameters::gradual_form(), instance_ty); - return Some(Type::function_like_callable(db, signature)); - } - // For other field-specific methods, fall through to NamedTupleFallback. - "_fields" | "_replace" | "__replace__" => { - return KnownClass::NamedTupleFallback - .to_class_literal(db) - .as_class_literal()? - .as_static()? - .own_class_member(db, None, None, name) - .ignore_possibly_undefined() - .map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: instance_ty, - }, - TypeContext::default(), - ) - }); - } - _ => {} - } - } - - let result = synthesize_namedtuple_class_member( - db, - name, - instance_ty, - self.fields(db).iter().cloned(), - None, - ); - // For fallback members from NamedTupleFallback, apply type mapping to handle - // `Self` types. The explicitly synthesized members (__new__, _fields, _replace, - // __replace__) don't need this mapping. - if matches!( - name, - "__new__" | "_fields" | "_replace" | "__replace__" | "__slots__" - ) { - result - } else { - result.map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: instance_ty, - }, - TypeContext::default(), - ) - }) - } - } - - fn spec(self, db: &'db dyn Db) -> NamedTupleSpec<'db> { - #[salsa::tracked(cycle_initial=deferred_spec_initial, heap_size=ruff_memory_usage::heap_size)] - fn deferred_spec<'db>(db: &'db dyn Db, definition: Definition<'db>) -> NamedTupleSpec<'db> { - let module = parsed_module(db, definition.file(db)).load(db); - let node = definition - .kind(db) - .value(&module) - .expect("Expected `NamedTuple` definition to be an assignment") - .as_call_expr() - .expect("Expected `NamedTuple` definition r.h.s. to be a call expression"); - match definition_expression_type(db, definition, &node.arguments.args[1]) { - Type::KnownInstance(KnownInstanceType::NamedTupleSpec(spec)) => spec, - _ => NamedTupleSpec::unknown(db), - } - } - - fn deferred_spec_initial<'db>( - db: &'db dyn Db, - _id: salsa::Id, - _definition: Definition<'db>, - ) -> NamedTupleSpec<'db> { - NamedTupleSpec::unknown(db) - } - - match self.anchor(db) { - DynamicNamedTupleAnchor::CollectionsDefinition { spec, .. } - | DynamicNamedTupleAnchor::ScopeOffset { spec, .. } => *spec, - DynamicNamedTupleAnchor::TypingDefinition(definition) => deferred_spec(db, *definition), - } - } - - fn fields(self, db: &'db dyn Db) -> &'db [NamedTupleField<'db>] { - self.spec(db).fields(db) - } - - fn has_known_fields(self, db: &'db dyn Db) -> bool { - self.spec(db).has_known_fields(db) - } -} - -fn dynamic_namedtuple_mro_cycle_initial<'db>( - db: &'db dyn Db, - _id: salsa::Id, - self_: DynamicNamedTupleLiteral<'db>, -) -> Mro<'db> { - Mro::from_error( - db, - ClassType::NonGeneric(ClassLiteral::DynamicNamedTuple(self_)), - ) -} - -/// Anchor for identifying a dynamic `namedtuple`/`NamedTuple` class literal. -/// -/// This enum provides stable identity for `DynamicNamedTupleLiteral` instances: -/// - For assigned calls, the `Definition` uniquely identifies the class. -/// - For dangling calls, a relative offset provides stable identity. -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub enum DynamicNamedTupleAnchor<'db> { - /// We're dealing with a `collections.namedtuple()` call - /// that's assigned to a variable. - /// - /// The `Definition` uniquely identifies this class. The `namedtuple()` - /// call expression is the `value` of the assignment, so we can get its - /// range from the definition. - CollectionsDefinition { - definition: Definition<'db>, - spec: NamedTupleSpec<'db>, - }, - - /// We're dealing with a `typing.NamedTuple()` call - /// that's assigned to a variable. - /// - /// The `Definition` uniquely identifies this class. The `NamedTuple()` - /// call expression is the `value` of the assignment, so we can get its - /// range from the definition. - /// - /// Unlike the `CollectionsDefinition` variant, this variant does not - /// hold a `NamedTupleSpec`. This is because the spec for a - /// `typing.NamedTuple` call can contain forward references and recursive - /// references that must be evaluated lazily. The spec is computed - /// on-demand from the definition. - TypingDefinition(Definition<'db>), - - /// We're dealing with a `namedtuple()` or `NamedTuple` call that is - /// "dangling" (not assigned to a variable). - /// - /// The offset is relative to the enclosing scope's anchor node index. - /// For module scope, this is equivalent to an absolute index (anchor is 0). - /// - /// Dangling calls can always store the spec. They *can* contain - /// forward references if they appear in class bases: - /// - /// ```python - /// from typing import NamedTuple - /// - /// class F(NamedTuple("F", [("x", "F | None")]): - /// pass - /// ``` - /// - /// But this doesn't matter, because all class bases are deferred in their - /// entirety during type inference. - ScopeOffset { - scope: ScopeId<'db>, - offset: u32, - spec: NamedTupleSpec<'db>, - }, -} - -/// A specification describing the fields of a dynamic `namedtuple` -/// or `NamedTuple` class. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct NamedTupleSpec<'db> { - #[returns(deref)] - pub(crate) fields: Box<[NamedTupleField<'db>]>, - - pub(crate) has_known_fields: bool, -} - -impl<'db> NamedTupleSpec<'db> { - /// Create a [`NamedTupleSpec`] with the given fields. - pub(crate) fn known(db: &'db dyn Db, fields: Box<[NamedTupleField<'db>]>) -> Self { - Self::new(db, fields, true) - } - - /// Create a [`NamedTupleSpec`] that indicates a namedtuple class has unknown fields. - pub(crate) fn unknown(db: &'db dyn Db) -> Self { - Self::new(db, Box::default(), false) - } - - pub(crate) fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - let fields = self - .fields(db) - .iter() - .map(|f| { - Some(NamedTupleField { - name: f.name.clone(), - ty: if nested { - f.ty.recursive_type_normalized_impl(db, div, nested)? - } else { - f.ty.recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div) - }, - default: None, - }) - }) - .collect::>>()?; - - Some(Self::new(db, fields, self.has_known_fields(db))) - } -} - -impl get_size2::GetSize for NamedTupleSpec<'_> {} - -/// Performs member lookups over an MRO (Method Resolution Order). -/// -/// This struct encapsulates the shared logic for looking up class and instance -/// members by iterating through an MRO. Both `StaticClassLiteral` and `DynamicClassLiteral` -/// use this to avoid duplicating the MRO traversal logic. -pub(super) struct MroLookup<'db, I> { - db: &'db dyn Db, - mro_iter: I, -} - -impl<'db, I: Iterator>> MroLookup<'db, I> { - /// Create a new MRO lookup from a database and an MRO iterator. - pub(super) fn new(db: &'db dyn Db, mro_iter: I) -> Self { - Self { db, mro_iter } - } - - /// Look up a class member by iterating through the MRO. - /// - /// Parameters: - /// - `name`: The member name to look up - /// - `policy`: Controls which classes in the MRO to skip - /// - `inherited_generic_context`: Generic context for `own_class_member` calls - /// - `is_self_object`: Whether the class itself is `object` (affects policy filtering) - /// - /// Returns `ClassMemberResult::TypedDict` if a `TypedDict` base is encountered, - /// allowing the caller to handle this case specially. - /// - /// If we encounter a dynamic type in the MRO, we save it and after traversal: - /// 1. Use it as the type if no other classes define the attribute, or - /// 2. Intersect it with the type from non-dynamic MRO members. - pub(super) fn class_member( - self, - name: &str, - policy: MemberLookupPolicy, - inherited_generic_context: Option>, - is_self_object: bool, - ) -> ClassMemberResult<'db> { - let db = self.db; - let mut dynamic_type: Option> = None; - let mut lookup_result: LookupResult<'db> = - Err(LookupError::Undefined(TypeQualifiers::empty())); - - for superclass in self.mro_iter { - match superclass { - ClassBase::Generic | ClassBase::Protocol => { - // Skip over these very special class bases that aren't really classes. - } - ClassBase::Dynamic(_) => { - // Note: calling `Type::from(superclass).member()` would be incorrect here. - // What we'd really want is a `Type::Any.own_class_member()` method, - // but adding such a method wouldn't make much sense -- it would always return `Any`! - dynamic_type.get_or_insert(Type::from(superclass)); - } - ClassBase::Class(class) => { - let known = class.known(db); - - // Only exclude `object` members if this is not an `object` class itself - if known == Some(KnownClass::Object) - && policy.mro_no_object_fallback() - && !is_self_object - { - continue; - } - - if known == Some(KnownClass::Type) && policy.meta_class_no_type_fallback() { - continue; - } - - if matches!(known, Some(KnownClass::Int | KnownClass::Str)) - && policy.mro_no_int_or_str_fallback() - { - continue; - } - - lookup_result = lookup_result.or_else(|lookup_error| { - lookup_error.or_fall_back_to( - db, - class - .own_class_member(db, inherited_generic_context, name) - .inner, - ) - }); - } - ClassBase::TypedDict => { - return ClassMemberResult::TypedDict; - } - } - if lookup_result.is_ok() { - break; - } - } - - ClassMemberResult::Done(CompletedMemberLookup { - lookup_result, - dynamic_type, - }) - } - - /// Look up an instance member by iterating through the MRO. - /// - /// Unlike class member lookup, instance member lookup: - /// - Uses `own_instance_member` to check each class - /// - Builds a union of inferred types from multiple classes - /// - Stops on the first definitely-declared attribute - /// - /// Returns `InstanceMemberResult::TypedDict` if a `TypedDict` base is encountered, - /// allowing the caller to handle this case specially. - pub(super) fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { - let db = self.db; - let mut union = UnionBuilder::new(db); - let mut union_qualifiers = TypeQualifiers::empty(); - let mut is_definitely_bound = false; - - for superclass in self.mro_iter { - match superclass { - ClassBase::Generic | ClassBase::Protocol => { - // Skip over these very special class bases that aren't really classes. - } - ClassBase::Dynamic(_) => { - // We already return the dynamic type for class member lookup, so we can - // just return unbound here (to avoid having to build a union of the - // dynamic type with itself). - return InstanceMemberResult::Done(PlaceAndQualifiers::unbound()); - } - ClassBase::Class(class) => { - if let member @ PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - ty, - origin, - definedness: boundness, - .. - }), - qualifiers, - } = class.own_instance_member(db, name).inner - { - 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 InstanceMemberResult::Done(member); - } - - is_definitely_bound = true; - } - - // If the attribute is not definitely declared on this class, keep looking - // higher up in the MRO, and build a union of all inferred types (and - // possibly-declared types): - union = union.add(ty); - - // TODO: We could raise a diagnostic here if there are conflicting type - // qualifiers - union_qualifiers |= qualifiers; - } - } - ClassBase::TypedDict => { - return InstanceMemberResult::TypedDict; - } + // TODO: We could raise a diagnostic here if there are conflicting type + // qualifiers + union_qualifiers |= qualifiers; + } + } + ClassBase::TypedDict => { + return InstanceMemberResult::TypedDict; + } } } @@ -6458,22 +2328,6 @@ impl std::fmt::Display for QualifiedClassName<'_> { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] -pub(super) enum InheritanceCycle { - /// The class is cyclically defined and is a participant in the cycle. - /// i.e., it inherits either directly or indirectly from itself. - Participant, - /// The class inherits from a class that is a `Participant` in an inheritance cycle, - /// but is not itself a participant. - Inherited, -} - -impl InheritanceCycle { - pub(super) const fn is_participant(self) -> bool { - matches!(self, InheritanceCycle::Participant) - } -} - /// CPython internally considers a class a "solid base" if it has an atypical instance memory layout, /// with additional memory "slots" for each instance, besides the default object metadata and an /// attribute dictionary. Per [PEP 800], however, we use the term "disjoint base" for this concept. @@ -6539,1769 +2393,6 @@ pub(super) enum DisjointBaseKind { DefinesSlots, } -/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow -/// for easier syntax when interacting with very common classes. -/// -/// Feel free to expand this enum if you ever find yourself using the same class in multiple -/// places. -/// Note: good candidates are any classes in `[ty_module_resolver::module::KnownModule]` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] -#[cfg_attr(test, derive(strum_macros::EnumIter))] -pub enum KnownClass { - // To figure out where an stdlib symbol is defined, you can go into `crates/ty_vendored` - // and grep for the symbol name in any `.pyi` file. - - // Builtins - Bool, - Object, - Bytes, - Bytearray, - Type, - Int, - Float, - Complex, - Str, - List, - Tuple, - Set, - FrozenSet, - Dict, - Slice, - Property, - BaseException, - Exception, - BaseExceptionGroup, - ExceptionGroup, - Staticmethod, - Classmethod, - Super, - NotImplementedError, - // enum - Enum, - EnumType, - Auto, - Member, - Nonmember, - StrEnum, - // abc - ABCMeta, - // Types - GenericAlias, - ModuleType, - FunctionType, - MethodType, - MethodWrapperType, - WrapperDescriptorType, - UnionType, - GeneratorType, - AsyncGeneratorType, - CoroutineType, - NotImplementedType, - BuiltinFunctionType, - // Exposed as `types.EllipsisType` on Python >=3.10; - // backported as `builtins.ellipsis` by typeshed on Python <=3.9 - EllipsisType, - // Typeshed - NoneType, // Part of `types` for Python >= 3.10 - // Typing - Awaitable, - Generator, - Deprecated, - StdlibAlias, - SpecialForm, - TypeVar, - ParamSpec, - // typing_extensions.ParamSpec - ExtensionsParamSpec, // must be distinct from typing.ParamSpec, backports new features - ParamSpecArgs, - ParamSpecKwargs, - ProtocolMeta, - TypeVarTuple, - TypeAliasType, - NoDefaultType, - NewType, - SupportsIndex, - Iterable, - Iterator, - Sequence, - Mapping, - // typing_extensions - ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features - // Collections - ChainMap, - Counter, - DefaultDict, - Deque, - OrderedDict, - // sys - VersionInfo, - // dataclasses - Field, - KwOnly, - InitVar, - // _typeshed._type_checker_internals - NamedTupleFallback, - NamedTupleLike, - TypedDictFallback, - // string.templatelib - Template, - // pathlib - Path, - // ty_extensions - ConstraintSet, - GenericContext, - Specialization, -} - -impl KnownClass { - pub(crate) const fn is_bool(self) -> bool { - matches!(self, Self::Bool) - } - - pub(crate) const fn is_special_form(self) -> bool { - matches!(self, Self::SpecialForm) - } - - /// Determine whether instances of this class are always truthy, always falsy, - /// or have an ambiguous truthiness. - /// - /// Returns `None` for `KnownClass::Tuple`, since the truthiness of a tuple - /// depends on its spec. - pub(crate) const fn bool(self) -> Option { - match self { - // N.B. It's only generally safe to infer `Truthiness::AlwaysTrue` for a `KnownClass` - // variant if the class's `__bool__` method always returns the same thing *and* the - // class is `@final`. - // - // E.g. `ModuleType.__bool__` always returns `True`, but `ModuleType` is not `@final`. - // Equally, `range` is `@final`, but its `__bool__` method can return `False`. - Self::EllipsisType - | Self::NoDefaultType - | Self::MethodType - | Self::Slice - | Self::FunctionType - | Self::VersionInfo - | Self::TypeAliasType - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Super - | Self::WrapperDescriptorType - | Self::UnionType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::MethodWrapperType - | Self::CoroutineType - | Self::BuiltinFunctionType - | Self::Template - | Self::Path => Some(Truthiness::AlwaysTrue), - - Self::NoneType => Some(Truthiness::AlwaysFalse), - - Self::BaseException - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Object - | Self::OrderedDict - | Self::BaseExceptionGroup - | Self::Bool - | Self::Str - | Self::List - | Self::GenericAlias - | Self::NewType - | Self::StdlibAlias - | Self::SupportsIndex - | Self::Set - | Self::Int - | Self::Type - | Self::Bytes - | Self::Bytearray - | Self::FrozenSet - | Self::Property - | Self::SpecialForm - | Self::Dict - | Self::ModuleType - | Self::ChainMap - | Self::Complex - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::Float - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 - // and raises a `TypeError` in Python >=3.14 - // (see https://docs.python.org/3/library/constants.html#NotImplemented) - | Self::NotImplementedType - | Self::Staticmethod - | Self::Classmethod - | Self::Awaitable - | Self::Generator - | Self::Deprecated - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::NamedTupleFallback - | Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::ProtocolMeta - | Self::TypedDictFallback => Some(Truthiness::Ambiguous), - - Self::Tuple => None, - } - } - - /// Return `true` if this class is a subclass of `enum.Enum` *and* has enum members, i.e. - /// if it is an "actual" enum, not `enum.Enum` itself or a similar custom enum class. - pub(crate) const fn is_enum_subclass_with_members(self) -> bool { - match self { - KnownClass::Bool - | KnownClass::Object - | KnownClass::Bytes - | KnownClass::Bytearray - | KnownClass::Type - | KnownClass::Int - | KnownClass::Float - | KnownClass::Complex - | KnownClass::Str - | KnownClass::List - | KnownClass::Tuple - | KnownClass::Set - | KnownClass::FrozenSet - | KnownClass::Dict - | KnownClass::Slice - | KnownClass::Property - | KnownClass::BaseException - | KnownClass::NotImplementedError - | KnownClass::Exception - | KnownClass::BaseExceptionGroup - | KnownClass::ExceptionGroup - | KnownClass::Staticmethod - | KnownClass::Classmethod - | KnownClass::Awaitable - | KnownClass::Generator - | KnownClass::Deprecated - | KnownClass::Super - | KnownClass::Enum - | KnownClass::EnumType - | KnownClass::Auto - | KnownClass::Member - | KnownClass::Nonmember - | KnownClass::StrEnum - | KnownClass::ABCMeta - | KnownClass::GenericAlias - | KnownClass::ModuleType - | KnownClass::FunctionType - | KnownClass::MethodType - | KnownClass::MethodWrapperType - | KnownClass::WrapperDescriptorType - | KnownClass::UnionType - | KnownClass::GeneratorType - | KnownClass::AsyncGeneratorType - | KnownClass::CoroutineType - | KnownClass::NoneType - | KnownClass::StdlibAlias - | KnownClass::SpecialForm - | KnownClass::TypeVar - | KnownClass::ExtensionsTypeVar - | KnownClass::ParamSpec - | KnownClass::ExtensionsParamSpec - | KnownClass::ParamSpecArgs - | KnownClass::ParamSpecKwargs - | KnownClass::TypeVarTuple - | KnownClass::TypeAliasType - | KnownClass::NoDefaultType - | KnownClass::NewType - | KnownClass::SupportsIndex - | KnownClass::Iterable - | KnownClass::Iterator - | KnownClass::Sequence - | KnownClass::Mapping - | KnownClass::ChainMap - | KnownClass::Counter - | KnownClass::DefaultDict - | KnownClass::Deque - | KnownClass::OrderedDict - | KnownClass::VersionInfo - | KnownClass::EllipsisType - | KnownClass::NotImplementedType - | KnownClass::Field - | KnownClass::KwOnly - | KnownClass::InitVar - | KnownClass::NamedTupleFallback - | KnownClass::NamedTupleLike - | KnownClass::ConstraintSet - | KnownClass::GenericContext - | KnownClass::Specialization - | KnownClass::TypedDictFallback - | KnownClass::BuiltinFunctionType - | KnownClass::ProtocolMeta - | KnownClass::Template - | KnownClass::Path => false, - } - } - - /// Return `true` if this class is a (true) subclass of `typing.TypedDict`. - pub(crate) const fn is_typed_dict_subclass(self) -> bool { - match self { - KnownClass::Bool - | KnownClass::Object - | KnownClass::Bytes - | KnownClass::Bytearray - | KnownClass::Type - | KnownClass::Int - | KnownClass::Float - | KnownClass::Complex - | KnownClass::Str - | KnownClass::List - | KnownClass::Tuple - | KnownClass::Set - | KnownClass::FrozenSet - | KnownClass::Dict - | KnownClass::Slice - | KnownClass::Property - | KnownClass::BaseException - | KnownClass::Exception - | KnownClass::NotImplementedError - | KnownClass::BaseExceptionGroup - | KnownClass::ExceptionGroup - | KnownClass::Staticmethod - | KnownClass::Classmethod - | KnownClass::Awaitable - | KnownClass::Generator - | KnownClass::Deprecated - | KnownClass::Super - | KnownClass::Enum - | KnownClass::EnumType - | KnownClass::Auto - | KnownClass::Member - | KnownClass::Nonmember - | KnownClass::StrEnum - | KnownClass::ABCMeta - | KnownClass::GenericAlias - | KnownClass::ModuleType - | KnownClass::FunctionType - | KnownClass::MethodType - | KnownClass::MethodWrapperType - | KnownClass::WrapperDescriptorType - | KnownClass::UnionType - | KnownClass::GeneratorType - | KnownClass::AsyncGeneratorType - | KnownClass::CoroutineType - | KnownClass::NoneType - | KnownClass::StdlibAlias - | KnownClass::SpecialForm - | KnownClass::TypeVar - | KnownClass::ExtensionsTypeVar - | KnownClass::ParamSpec - | KnownClass::ExtensionsParamSpec - | KnownClass::ParamSpecArgs - | KnownClass::ParamSpecKwargs - | KnownClass::TypeVarTuple - | KnownClass::TypeAliasType - | KnownClass::NoDefaultType - | KnownClass::NewType - | KnownClass::SupportsIndex - | KnownClass::Iterable - | KnownClass::Iterator - | KnownClass::Sequence - | KnownClass::Mapping - | KnownClass::ChainMap - | KnownClass::Counter - | KnownClass::DefaultDict - | KnownClass::Deque - | KnownClass::OrderedDict - | KnownClass::VersionInfo - | KnownClass::EllipsisType - | KnownClass::NotImplementedType - | KnownClass::Field - | KnownClass::KwOnly - | KnownClass::InitVar - | KnownClass::NamedTupleFallback - | KnownClass::NamedTupleLike - | KnownClass::ConstraintSet - | KnownClass::GenericContext - | KnownClass::Specialization - | KnownClass::TypedDictFallback - | KnownClass::BuiltinFunctionType - | KnownClass::ProtocolMeta - | KnownClass::Template - | KnownClass::Path => false, - } - } - - pub(crate) const fn is_tuple_subclass(self) -> bool { - match self { - KnownClass::Tuple | KnownClass::VersionInfo => true, - - KnownClass::Bool - | KnownClass::Object - | KnownClass::Bytes - | KnownClass::Bytearray - | KnownClass::Type - | KnownClass::Int - | KnownClass::Float - | KnownClass::Complex - | KnownClass::Str - | KnownClass::List - | KnownClass::Set - | KnownClass::FrozenSet - | KnownClass::Dict - | KnownClass::Slice - | KnownClass::Property - | KnownClass::BaseException - | KnownClass::Exception - | KnownClass::NotImplementedError - | KnownClass::BaseExceptionGroup - | KnownClass::ExceptionGroup - | KnownClass::Staticmethod - | KnownClass::Classmethod - | KnownClass::Awaitable - | KnownClass::Generator - | KnownClass::Deprecated - | KnownClass::Super - | KnownClass::Enum - | KnownClass::EnumType - | KnownClass::Auto - | KnownClass::Member - | KnownClass::Nonmember - | KnownClass::StrEnum - | KnownClass::ABCMeta - | KnownClass::GenericAlias - | KnownClass::ModuleType - | KnownClass::FunctionType - | KnownClass::MethodType - | KnownClass::MethodWrapperType - | KnownClass::WrapperDescriptorType - | KnownClass::UnionType - | KnownClass::GeneratorType - | KnownClass::AsyncGeneratorType - | KnownClass::CoroutineType - | KnownClass::NoneType - | KnownClass::StdlibAlias - | KnownClass::SpecialForm - | KnownClass::TypeVar - | KnownClass::ExtensionsTypeVar - | KnownClass::ParamSpec - | KnownClass::ExtensionsParamSpec - | KnownClass::ParamSpecArgs - | KnownClass::ParamSpecKwargs - | KnownClass::TypeVarTuple - | KnownClass::TypeAliasType - | KnownClass::NoDefaultType - | KnownClass::NewType - | KnownClass::SupportsIndex - | KnownClass::Iterable - | KnownClass::Iterator - | KnownClass::Sequence - | KnownClass::Mapping - | KnownClass::ChainMap - | KnownClass::Counter - | KnownClass::DefaultDict - | KnownClass::Deque - | KnownClass::OrderedDict - | KnownClass::EllipsisType - | KnownClass::NotImplementedType - | KnownClass::Field - | KnownClass::KwOnly - | KnownClass::InitVar - | KnownClass::TypedDictFallback - | KnownClass::NamedTupleLike - | KnownClass::NamedTupleFallback - | KnownClass::ConstraintSet - | KnownClass::GenericContext - | KnownClass::Specialization - | KnownClass::BuiltinFunctionType - | KnownClass::ProtocolMeta - | KnownClass::Template - | KnownClass::Path => false, - } - } - - /// Return `true` if this class is a protocol class. - /// - /// In an ideal world, perhaps we wouldn't hardcode this knowledge here; - /// instead, we'd just look at the bases for these classes, as we do for - /// all other classes. However, the special casing here helps us out in - /// two important ways: - /// - /// 1. It helps us avoid Salsa cycles when creating types such as "instance of `str`" - /// and "instance of `sys._version_info`". These types are constructed very early - /// on, but it causes problems if we attempt to infer the types of their bases - /// too soon. - /// 2. It's probably more performant. - const fn is_protocol(self) -> bool { - match self { - Self::SupportsIndex - | Self::Iterable - | Self::Iterator - | Self::Awaitable - | Self::NamedTupleLike - | Self::Generator => true, - - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Tuple - | Self::Int - | Self::Float - | Self::Complex - | Self::FrozenSet - | Self::Str - | Self::Set - | Self::Dict - | Self::List - | Self::Type - | Self::Slice - | Self::Property - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Deprecated - | Self::GenericAlias - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::ModuleType - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::NoneType - | Self::SpecialForm - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::TypeAliasType - | Self::NoDefaultType - | Self::NewType - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Super - | Self::StdlibAlias - | Self::VersionInfo - | Self::EllipsisType - | Self::NotImplementedType - | Self::UnionType - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::NamedTupleFallback - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::TypedDictFallback - | Self::BuiltinFunctionType - | Self::ProtocolMeta - | Self::Template - | Self::Path - | Self::Mapping - | Self::Sequence => false, - } - } - - /// Return `true` if this class is a typeshed fallback class which is used to provide attributes and - /// methods for another type (e.g. `NamedTupleFallback` for actual `NamedTuple`s). These fallback - /// classes need special treatment in some places. For example, implicit usages of `Self` should not - /// be eagerly replaced with the fallback class itself. Instead, `Self` should eventually be treated - /// as referring to the destination type (e.g. the actual `NamedTuple`). - pub(crate) const fn is_fallback_class(self) -> bool { - match self { - KnownClass::Bool - | KnownClass::Object - | KnownClass::Bytes - | KnownClass::Bytearray - | KnownClass::Type - | KnownClass::Int - | KnownClass::Float - | KnownClass::Complex - | KnownClass::Str - | KnownClass::List - | KnownClass::Tuple - | KnownClass::Set - | KnownClass::FrozenSet - | KnownClass::Dict - | KnownClass::Slice - | KnownClass::Property - | KnownClass::BaseException - | KnownClass::Exception - | KnownClass::NotImplementedError - | KnownClass::BaseExceptionGroup - | KnownClass::ExceptionGroup - | KnownClass::Staticmethod - | KnownClass::Classmethod - | KnownClass::Super - | KnownClass::Enum - | KnownClass::EnumType - | KnownClass::Auto - | KnownClass::Member - | KnownClass::Nonmember - | KnownClass::StrEnum - | KnownClass::ABCMeta - | KnownClass::GenericAlias - | KnownClass::ModuleType - | KnownClass::FunctionType - | KnownClass::MethodType - | KnownClass::MethodWrapperType - | KnownClass::WrapperDescriptorType - | KnownClass::UnionType - | KnownClass::GeneratorType - | KnownClass::AsyncGeneratorType - | KnownClass::CoroutineType - | KnownClass::NotImplementedType - | KnownClass::BuiltinFunctionType - | KnownClass::EllipsisType - | KnownClass::NoneType - | KnownClass::Awaitable - | KnownClass::Generator - | KnownClass::Deprecated - | KnownClass::StdlibAlias - | KnownClass::SpecialForm - | KnownClass::TypeVar - | KnownClass::ExtensionsTypeVar - | KnownClass::ParamSpec - | KnownClass::ExtensionsParamSpec - | KnownClass::ParamSpecArgs - | KnownClass::ParamSpecKwargs - | KnownClass::ProtocolMeta - | KnownClass::TypeVarTuple - | KnownClass::TypeAliasType - | KnownClass::NoDefaultType - | KnownClass::NewType - | KnownClass::SupportsIndex - | KnownClass::Iterable - | KnownClass::Iterator - | KnownClass::Sequence - | KnownClass::Mapping - | KnownClass::ChainMap - | KnownClass::Counter - | KnownClass::DefaultDict - | KnownClass::Deque - | KnownClass::OrderedDict - | KnownClass::VersionInfo - | KnownClass::Field - | KnownClass::KwOnly - | KnownClass::NamedTupleLike - | KnownClass::Template - | KnownClass::Path - | KnownClass::ConstraintSet - | KnownClass::GenericContext - | KnownClass::Specialization - | KnownClass::InitVar => false, - KnownClass::NamedTupleFallback | KnownClass::TypedDictFallback => true, - } - } - - pub(crate) fn name(self, db: &dyn Db) -> &'static str { - match self { - Self::Bool => "bool", - Self::Object => "object", - Self::Bytes => "bytes", - Self::Bytearray => "bytearray", - Self::Tuple => "tuple", - Self::Int => "int", - Self::Float => "float", - Self::Complex => "complex", - Self::FrozenSet => "frozenset", - Self::Str => "str", - Self::Set => "set", - Self::Dict => "dict", - Self::List => "list", - Self::Type => "type", - Self::Slice => "slice", - Self::Property => "property", - Self::BaseException => "BaseException", - Self::BaseExceptionGroup => "BaseExceptionGroup", - Self::Exception => "Exception", - Self::NotImplementedError => "NotImplementedError", - Self::ExceptionGroup => "ExceptionGroup", - Self::Staticmethod => "staticmethod", - Self::Classmethod => "classmethod", - Self::Awaitable => "Awaitable", - Self::Generator => "Generator", - Self::Deprecated => "deprecated", - Self::GenericAlias => "GenericAlias", - Self::ModuleType => "ModuleType", - Self::FunctionType => "FunctionType", - Self::MethodType => "MethodType", - Self::UnionType => "UnionType", - Self::MethodWrapperType => "MethodWrapperType", - Self::WrapperDescriptorType => "WrapperDescriptorType", - Self::BuiltinFunctionType => "BuiltinFunctionType", - Self::GeneratorType => "GeneratorType", - Self::AsyncGeneratorType => "AsyncGeneratorType", - Self::CoroutineType => "CoroutineType", - Self::NoneType => "NoneType", - Self::SpecialForm => "_SpecialForm", - Self::TypeVar => "TypeVar", - Self::ExtensionsTypeVar => "TypeVar", - Self::ParamSpec => "ParamSpec", - Self::ExtensionsParamSpec => "ParamSpec", - Self::ParamSpecArgs => "ParamSpecArgs", - Self::ParamSpecKwargs => "ParamSpecKwargs", - Self::TypeVarTuple => "TypeVarTuple", - Self::TypeAliasType => "TypeAliasType", - Self::NoDefaultType => "_NoDefaultType", - Self::NewType => "NewType", - Self::SupportsIndex => "SupportsIndex", - Self::ChainMap => "ChainMap", - Self::Counter => "Counter", - Self::DefaultDict => "defaultdict", - Self::Deque => "deque", - Self::OrderedDict => "OrderedDict", - Self::Enum => "Enum", - Self::EnumType => { - if Program::get(db).python_version(db) >= PythonVersion::PY311 { - "EnumType" - } else { - "EnumMeta" - } - } - Self::Auto => "auto", - Self::Member => "member", - Self::Nonmember => "nonmember", - Self::StrEnum => "StrEnum", - Self::ABCMeta => "ABCMeta", - Self::Super => "super", - Self::Iterable => "Iterable", - Self::Iterator => "Iterator", - Self::Sequence => "Sequence", - Self::Mapping => "Mapping", - // For example, `typing.List` is defined as `List = _Alias()` in typeshed - Self::StdlibAlias => "_Alias", - // This is the name the type of `sys.version_info` has in typeshed, - // which is different to what `type(sys.version_info).__name__` is at runtime. - // (At runtime, `type(sys.version_info).__name__ == "version_info"`, - // which is impossible to replicate in the stubs since the sole instance of the class - // also has that name in the `sys` module.) - Self::VersionInfo => "_version_info", - Self::EllipsisType => { - // Exposed as `types.EllipsisType` on Python >=3.10; - // backported as `builtins.ellipsis` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - "EllipsisType" - } else { - "ellipsis" - } - } - Self::NotImplementedType => { - // Exposed as `types.NotImplementedType` on Python >=3.10; - // backported as `builtins._NotImplementedType` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - "NotImplementedType" - } else { - "_NotImplementedType" - } - } - Self::Field => "Field", - Self::KwOnly => "KW_ONLY", - Self::InitVar => "InitVar", - Self::NamedTupleFallback => "NamedTupleFallback", - Self::NamedTupleLike => "NamedTupleLike", - Self::ConstraintSet => "ConstraintSet", - Self::GenericContext => "GenericContext", - Self::Specialization => "Specialization", - Self::TypedDictFallback => "TypedDictFallback", - Self::Template => "Template", - Self::Path => "Path", - Self::ProtocolMeta => "_ProtocolMeta", - } - } - - pub(super) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ { - struct KnownClassDisplay<'db> { - db: &'db dyn Db, - class: KnownClass, - } - - impl std::fmt::Display for KnownClassDisplay<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let KnownClassDisplay { - class: known_class, - db, - } = *self; - write!( - f, - "{module}.{class}", - module = known_class.canonical_module(db), - class = known_class.name(db) - ) - } - } - - KnownClassDisplay { db, class: self } - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing all possible instances of - /// the class. If this class is generic, this will use the default specialization. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - #[track_caller] - pub fn to_instance(self, db: &dyn Db) -> Type<'_> { - debug_assert_ne!( - self, - KnownClass::Tuple, - "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" - ); - self.to_class_literal(db) - .to_class_type(db) - .map(|class| Type::instance(db, class)) - .unwrap_or_else(Type::unknown) - } - - /// Similar to [`KnownClass::to_instance`], but returns the Unknown-specialization where each type - /// parameter is specialized to `Unknown`. - #[track_caller] - pub(crate) fn to_instance_unknown(self, db: &dyn Db) -> Type<'_> { - debug_assert_ne!( - self, - KnownClass::Tuple, - "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" - ); - self.try_to_class_literal(db) - .map(|literal| Type::instance(db, literal.unknown_specialization(db))) - .unwrap_or_else(Type::unknown) - } - - /// Lookup a generic [`KnownClass`] in typeshed and return a [`Type`] - /// representing a specialization of that class. - /// - /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong - /// number of types, a debug-level log message will be emitted stating this. - pub(crate) fn to_specialized_class_type<'t, 'db, T>( - self, - db: &'db dyn Db, - specialization: T, - ) -> Option> - where - T: Into]>>, - 'db: 't, - { - fn to_specialized_class_type_impl<'db>( - db: &'db dyn Db, - class: KnownClass, - class_literal: StaticClassLiteral<'db>, - specialization: Cow<[Type<'db>]>, - generic_context: GenericContext<'db>, - ) -> ClassType<'db> { - if specialization.len() != generic_context.len(db) { - // a cache of the `KnownClass`es that we have already seen mismatched-arity - // specializations for (and therefore that we've already logged a warning for) - static MESSAGES: LazyLock>> = - LazyLock::new(Mutex::default); - if MESSAGES.lock().unwrap().insert(class) { - tracing::info!( - "Wrong number of types when specializing {}. \ - Falling back to default specialization for the symbol instead.", - class.display(db) - ); - } - return class_literal.default_specialization(db); - } - - class_literal - .apply_specialization(db, |_| generic_context.specialize(db, specialization)) - } - - let class_literal = self.to_class_literal(db).as_class_literal()?.as_static()?; - let generic_context = class_literal.generic_context(db)?; - let specialization = specialization.into(); - - Some(to_specialized_class_type_impl( - db, - self, - class_literal, - specialization, - generic_context, - )) - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] - /// representing all possible instances of the generic class with a specialization. - /// - /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong - /// number of types, a debug-level log message will be emitted stating this. - #[track_caller] - pub(crate) fn to_specialized_instance<'t, 'db, T>( - self, - db: &'db dyn Db, - specialization: T, - ) -> Type<'db> - where - T: Into]>>, - 'db: 't, - { - debug_assert_ne!( - self, - KnownClass::Tuple, - "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" - ); - self.to_specialized_class_type(db, specialization) - .and_then(|class_type| Type::from(class_type).to_instance(db)) - .unwrap_or_else(Type::unknown) - } - - /// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. - /// - /// Return an error if the symbol cannot be found in the expected typeshed module, - /// or if the symbol is not a class definition, or if the symbol is possibly unbound. - fn try_to_class_literal_without_logging( - self, - db: &dyn Db, - ) -> Result, KnownClassLookupError<'_>> { - let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place; - match symbol { - Place::Defined(DefinedPlace { - ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), - definedness: Definedness::AlwaysDefined, - .. - }) => Ok(class_literal), - Place::Defined(DefinedPlace { - ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), - definedness: Definedness::PossiblyUndefined, - .. - }) => Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }), - Place::Defined(DefinedPlace { ty: found_type, .. }) => { - Err(KnownClassLookupError::SymbolNotAClass { found_type }) - } - Place::Undefined => Err(KnownClassLookupError::ClassNotFound), - } - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> { - #[salsa::interned(heap_size=ruff_memory_usage::heap_size)] - struct KnownClassArgument { - class: KnownClass, - } - - fn known_class_to_class_literal_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _class: KnownClassArgument<'db>, - ) -> Option> { - None - } - - #[salsa::tracked(cycle_initial=known_class_to_class_literal_initial, heap_size=ruff_memory_usage::heap_size)] - fn known_class_to_class_literal<'db>( - db: &'db dyn Db, - class: KnownClassArgument<'db>, - ) -> Option> { - let class = class.class(db); - class - .try_to_class_literal_without_logging(db) - .or_else(|lookup_error| { - if matches!( - lookup_error, - KnownClassLookupError::ClassPossiblyUnbound { .. } - ) { - tracing::info!("{}", lookup_error.display(db, class)); - } else { - tracing::info!( - "{}. Falling back to `Unknown` for the symbol instead.", - lookup_error.display(db, class) - ); - } - - match lookup_error { - KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { - Ok(class_literal) - } - KnownClassLookupError::ClassNotFound { .. } - | KnownClassLookupError::SymbolNotAClass { .. } => Err(()), - } - }) - .ok() - } - - known_class_to_class_literal(db, KnownClassArgument::new(db, self)) - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type<'_> { - self.try_to_class_literal(db) - .map(|class| Type::ClassLiteral(ClassLiteral::Static(class))) - .unwrap_or_else(Type::unknown) - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] - /// representing that class and all possible subclasses of the class. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub fn to_subclass_of(self, db: &dyn Db) -> Type<'_> { - self.to_class_literal(db) - .to_class_type(db) - .map(|class| SubclassOfType::from(db, class)) - .unwrap_or_else(SubclassOfType::subclass_of_unknown) - } - - /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, - /// *and* `class` is a subclass of `other`. - pub(super) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - self.try_to_class_literal_without_logging(db) - .is_ok_and(|class| class.is_subclass_of(db, None, other)) - } - - pub(super) fn when_subclass_of<'db, 'c>( - self, - db: &'db dyn Db, - other: ClassType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - ) -> ConstraintSet<'db, 'c> { - ConstraintSet::from_bool(constraints, self.is_subclass_of(db, other)) - } - - /// Return the module in which we should look up the definition for this class - pub(super) fn canonical_module(self, db: &dyn Db) -> KnownModule { - match self { - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Tuple - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Slice - | Self::Super - | Self::Property => KnownModule::Builtins, - Self::VersionInfo => KnownModule::Sys, - Self::ABCMeta => KnownModule::Abc, - Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum => KnownModule::Enum, - Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::MethodType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::MethodWrapperType - | Self::UnionType - | Self::BuiltinFunctionType - | Self::WrapperDescriptorType => KnownModule::Types, - Self::NoneType => KnownModule::Typeshed, - Self::Awaitable - | Self::Generator - | Self::SpecialForm - | Self::TypeVar - | Self::StdlibAlias - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - | Self::ProtocolMeta - | Self::SupportsIndex => KnownModule::Typing, - Self::TypeAliasType - | Self::ExtensionsTypeVar - | Self::TypeVarTuple - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::Deprecated - | Self::NewType => KnownModule::TypingExtensions, - Self::ParamSpec => { - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - KnownModule::Typing - } else { - KnownModule::TypingExtensions - } - } - Self::NoDefaultType => { - let python_version = Program::get(db).python_version(db); - - // typing_extensions has a 3.13+ re-export for the `typing.NoDefault` - // singleton, but not for `typing._NoDefaultType`. So we need to switch - // to `typing._NoDefaultType` for newer versions: - if python_version >= PythonVersion::PY313 { - KnownModule::Typing - } else { - KnownModule::TypingExtensions - } - } - Self::EllipsisType => { - // Exposed as `types.EllipsisType` on Python >=3.10; - // backported as `builtins.ellipsis` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - KnownModule::Types - } else { - KnownModule::Builtins - } - } - Self::NotImplementedType => { - // Exposed as `types.NotImplementedType` on Python >=3.10; - // backported as `builtins._NotImplementedType` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - KnownModule::Types - } else { - KnownModule::Builtins - } - } - Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict => KnownModule::Collections, - Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses, - Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals, - Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization => KnownModule::TyExtensions, - Self::Template => KnownModule::Templatelib, - Self::Path => KnownModule::Pathlib, - } - } - - /// Returns `Some(true)` if all instances of this `KnownClass` compare equal. - /// Returns `None` for `KnownClass::Tuple`, since whether or not a tuple type - /// is single-valued depends on the tuple spec. - pub(super) const fn is_single_valued(self) -> Option { - match self { - Self::NoneType - | Self::NoDefaultType - | Self::VersionInfo - | Self::EllipsisType - | Self::TypeAliasType - | Self::UnionType - | Self::NotImplementedType => Some(true), - - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::Slice - | Self::Property - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Awaitable - | Self::Generator - | Self::Deprecated - | Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::SpecialForm - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::SupportsIndex - | Self::StdlibAlias - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Super - | Self::NewType - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - | Self::NamedTupleFallback - | Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::TypedDictFallback - | Self::BuiltinFunctionType - | Self::ProtocolMeta - | Self::Template - | Self::Path => Some(false), - - Self::Tuple => None, - } - } - - /// Is this class a singleton class? - /// - /// A singleton class is a class where it is known that only one instance can ever exist at runtime. - pub(super) const fn is_singleton(self) -> bool { - match self { - Self::NoneType - | Self::EllipsisType - | Self::NoDefaultType - | Self::VersionInfo - | Self::TypeAliasType - | Self::NotImplementedType => true, - - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Tuple - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::List - | Self::Type - | Self::Slice - | Self::Property - | Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::SpecialForm - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::StdlibAlias - | Self::SupportsIndex - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Awaitable - | Self::Generator - | Self::Deprecated - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Super - | Self::UnionType - | Self::NewType - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - | Self::NamedTupleFallback - | Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::TypedDictFallback - | Self::BuiltinFunctionType - | Self::ProtocolMeta - | Self::Template - | Self::Path => false, - } - } - - pub(super) fn try_from_file_and_name( - db: &dyn Db, - file: File, - class_name: &str, - ) -> Option { - // We assert that this match is exhaustive over the right-hand side in the unit test - // `known_class_roundtrip_from_str()` - let candidates: &[Self] = match class_name { - "bool" => &[Self::Bool], - "object" => &[Self::Object], - "bytes" => &[Self::Bytes], - "bytearray" => &[Self::Bytearray], - "tuple" => &[Self::Tuple], - "type" => &[Self::Type], - "int" => &[Self::Int], - "float" => &[Self::Float], - "complex" => &[Self::Complex], - "str" => &[Self::Str], - "set" => &[Self::Set], - "frozenset" => &[Self::FrozenSet], - "dict" => &[Self::Dict], - "list" => &[Self::List], - "slice" => &[Self::Slice], - "property" => &[Self::Property], - "BaseException" => &[Self::BaseException], - "BaseExceptionGroup" => &[Self::BaseExceptionGroup], - "Exception" => &[Self::Exception], - "NotImplementedError" => &[Self::NotImplementedError], - "ExceptionGroup" => &[Self::ExceptionGroup], - "staticmethod" => &[Self::Staticmethod], - "classmethod" => &[Self::Classmethod], - "Awaitable" => &[Self::Awaitable], - "Generator" => &[Self::Generator], - "deprecated" => &[Self::Deprecated], - "GenericAlias" => &[Self::GenericAlias], - "NoneType" => &[Self::NoneType], - "ModuleType" => &[Self::ModuleType], - "GeneratorType" => &[Self::GeneratorType], - "AsyncGeneratorType" => &[Self::AsyncGeneratorType], - "CoroutineType" => &[Self::CoroutineType], - "FunctionType" => &[Self::FunctionType], - "MethodType" => &[Self::MethodType], - "UnionType" => &[Self::UnionType], - "MethodWrapperType" => &[Self::MethodWrapperType], - "WrapperDescriptorType" => &[Self::WrapperDescriptorType], - "BuiltinFunctionType" => &[Self::BuiltinFunctionType], - "NewType" => &[Self::NewType], - "TypeAliasType" => &[Self::TypeAliasType], - "TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar], - "Iterable" => &[Self::Iterable], - "Iterator" => &[Self::Iterator], - "Sequence" => &[Self::Sequence], - "Mapping" => &[Self::Mapping], - "ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec], - "ParamSpecArgs" => &[Self::ParamSpecArgs], - "ParamSpecKwargs" => &[Self::ParamSpecKwargs], - "TypeVarTuple" => &[Self::TypeVarTuple], - "ChainMap" => &[Self::ChainMap], - "Counter" => &[Self::Counter], - "defaultdict" => &[Self::DefaultDict], - "deque" => &[Self::Deque], - "OrderedDict" => &[Self::OrderedDict], - "_Alias" => &[Self::StdlibAlias], - "_SpecialForm" => &[Self::SpecialForm], - "_NoDefaultType" => &[Self::NoDefaultType], - "SupportsIndex" => &[Self::SupportsIndex], - "Enum" => &[Self::Enum], - "EnumMeta" => &[Self::EnumType], - "EnumType" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - &[Self::EnumType] - } - "StrEnum" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - &[Self::StrEnum] - } - "auto" => &[Self::Auto], - "member" => &[Self::Member], - "nonmember" => &[Self::Nonmember], - "ABCMeta" => &[Self::ABCMeta], - "super" => &[Self::Super], - "_version_info" => &[Self::VersionInfo], - "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { - &[Self::EllipsisType] - } - "EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { - &[Self::EllipsisType] - } - "_NotImplementedType" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { - &[Self::NotImplementedType] - } - "NotImplementedType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { - &[Self::NotImplementedType] - } - "Field" => &[Self::Field], - "KW_ONLY" => &[Self::KwOnly], - "InitVar" => &[Self::InitVar], - "NamedTupleFallback" => &[Self::NamedTupleFallback], - "NamedTupleLike" => &[Self::NamedTupleLike], - "ConstraintSet" => &[Self::ConstraintSet], - "GenericContext" => &[Self::GenericContext], - "Specialization" => &[Self::Specialization], - "TypedDictFallback" => &[Self::TypedDictFallback], - "Template" => &[Self::Template], - "Path" => &[Self::Path], - "_ProtocolMeta" => &[Self::ProtocolMeta], - _ => return None, - }; - - let module = file_to_module(db, file)?.known(db)?; - - candidates - .iter() - .copied() - .find(|&candidate| candidate.check_module(db, module)) - } - - /// Return `true` if the module of `self` matches `module` - fn check_module(self, db: &dyn Db, module: KnownModule) -> bool { - match self { - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Tuple - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::Slice - | Self::Property - | Self::GenericAlias - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::StdlibAlias // no equivalent class exists in typing_extensions, nor ever will - | Self::ModuleType - | Self::VersionInfo - | Self::BaseException - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::EllipsisType - | Self::BaseExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Super - | Self::NotImplementedType - | Self::UnionType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::WrapperDescriptorType - | Self::BuiltinFunctionType - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::NamedTupleFallback - | Self::TypedDictFallback - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::Awaitable - | Self::Generator - | Self::Template - | Self::Path => module == self.canonical_module(db), - Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), - Self::SpecialForm - | Self::TypeAliasType - | Self::NoDefaultType - | Self::SupportsIndex - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - | Self::ProtocolMeta - | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), - Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), - } - } - - /// Evaluate a call to this known class, emit any diagnostics that are necessary - /// as a result of the call, and return the type that results from the call. - pub(super) fn check_call<'db>( - self, - context: &InferContext<'db, '_>, - index: &SemanticIndex<'db>, - overload: &mut Binding<'db>, - call_expression: &ast::ExprCall, - ) { - let db = context.db(); - let scope = context.scope(); - let module = context.module(); - - match self { - KnownClass::Super => { - // Handle the case where `super()` is called with no arguments. - // In this case, we need to infer the two arguments: - // 1. The nearest enclosing class - // 2. The first parameter of the current function (typically `self` or `cls`) - match overload.parameter_types() { - [] => { - let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) - else { - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic(context, call_expression.into()); - overload.set_return_type(Type::unknown()); - return; - }; - - // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. - if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class.into(), None) { - if let Some(builder) = context - .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) - { - builder.into_diagnostic(format_args!( - "Cannot use `super()` in a method of NamedTuple class `{}`", - enclosing_class.name(db) - )); - } - overload.set_return_type(Type::unknown()); - return; - } - - // The type of the first parameter if the given scope is function-like (i.e. function or lambda). - // `None` if the scope is not function-like, or has no parameters. - let first_param = match scope.node(db) { - NodeWithScopeKind::Function(f) => { - f.node(module).parameters.iter().next() - } - NodeWithScopeKind::Lambda(l) => l - .node(module) - .parameters - .as_ref() - .into_iter() - .flatten() - .next(), - _ => None, - }; - - let Some(first_param) = first_param else { - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic(context, call_expression.into()); - overload.set_return_type(Type::unknown()); - return; - }; - - let definition = index.expect_single_definition(first_param); - let first_param = binding_type(db, definition); - - let bound_super = BoundSuperType::build( - db, - Type::ClassLiteral(ClassLiteral::Static(enclosing_class)), - first_param, - ) - .unwrap_or_else(|err| { - err.report_diagnostic(context, call_expression.into()); - Type::unknown() - }); - - overload.set_return_type(bound_super); - } - [Some(pivot_class_type), Some(owner_type)] => { - // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. - if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) { - if CodeGeneratorKind::NamedTuple.matches( - db, - enclosing_class.into(), - None, - ) { - if let Some(builder) = context - .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) - { - builder.into_diagnostic(format_args!( - "Cannot use `super()` in a method of NamedTuple class `{}`", - enclosing_class.name(db) - )); - } - overload.set_return_type(Type::unknown()); - return; - } - } - - let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type) - .unwrap_or_else(|err| { - err.report_diagnostic(context, call_expression.into()); - Type::unknown() - }); - overload.set_return_type(bound_super); - } - _ => {} - } - } - - KnownClass::Deprecated => { - // Parsing something of the form: - // - // @deprecated("message") - // @deprecated("message", category = DeprecationWarning, stacklevel = 1) - // - // "Static type checker behavior is not affected by the category and stacklevel arguments" - // so we only need the message and can ignore everything else. The message is mandatory, - // must be a LiteralString, and always comes first. - // - // We aren't guaranteed to know the static value of a LiteralString, so we need to - // accept that sometimes we will fail to include the message. - // - // We don't do any serious validation/diagnostics here, as the signature for this - // is included in `Type::bindings`. - // - // See: - let [Some(message), ..] = overload.parameter_types() else { - // Checking in Type::bindings will complain about this for us - return; - }; - - overload.set_return_type(Type::KnownInstance(KnownInstanceType::Deprecated( - DeprecatedInstance::new(db, message.as_string_literal()), - ))); - } - - _ => {} - } - } -} - -/// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum KnownClassLookupError<'db> { - /// There is no symbol by that name in the expected typeshed module. - ClassNotFound, - /// There is a symbol by that name in the expected typeshed module, - /// but it's not a class. - SymbolNotAClass { found_type: Type<'db> }, - /// There is a symbol by that name in the expected typeshed module, - /// and it's a class definition, but it's possibly unbound. - ClassPossiblyUnbound { - class_literal: StaticClassLiteral<'db>, - }, -} - -impl<'db> KnownClassLookupError<'db> { - fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db { - struct ErrorDisplay<'db> { - db: &'db dyn Db, - class: KnownClass, - error: KnownClassLookupError<'db>, - } - - impl std::fmt::Display for ErrorDisplay<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let ErrorDisplay { db, class, error } = *self; - - let class = class.display(db); - let python_version = Program::get(db).python_version(db); - - match error { - KnownClassLookupError::ClassNotFound => write!( - f, - "Could not find class `{class}` in typeshed on Python {python_version}", - ), - KnownClassLookupError::SymbolNotAClass { found_type } => write!( - f, - "Error looking up `{class}` in typeshed: expected to find a class definition \ - on Python {python_version}, but found a symbol of type `{found_type}` instead", - found_type = found_type.display(db), - ), - KnownClassLookupError::ClassPossiblyUnbound { .. } => write!( - f, - "Error looking up `{class}` in typeshed on Python {python_version}: \ - expected to find a fully bound symbol, but found one that is possibly unbound", - ), - } - } - } - - ErrorDisplay { - db, - class, - error: *self, - } - } -} - #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(super) struct MetaclassError<'db> { kind: MetaclassErrorKind<'db>, @@ -8392,127 +2483,3 @@ impl SlotsKind { } } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::db::tests::setup_db; - use crate::{PythonVersionSource, PythonVersionWithSource}; - use salsa::Setter; - use strum::IntoEnumIterator; - use ty_module_resolver::resolve_module_confident; - - #[test] - fn known_class_roundtrip_from_str() { - let mut db = setup_db(); - Program::get(&db) - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: PythonVersion::latest_preview(), - source: PythonVersionSource::default(), - }); - for class in KnownClass::iter() { - let class_name = class.name(&db); - let class_module = - resolve_module_confident(&db, &class.canonical_module(&db).name()).unwrap(); - - assert_eq!( - KnownClass::try_from_file_and_name( - &db, - class_module.file(&db).unwrap(), - class_name - ), - Some(class), - "`KnownClass::candidate_from_str` appears to be missing a case for `{class_name}`" - ); - } - } - - #[test] - fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_latest_version() { - let mut db = setup_db(); - - Program::get(&db) - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: PythonVersion::latest_ty(), - source: PythonVersionSource::default(), - }); - - for class in KnownClass::iter() { - // Check the class can be looked up successfully - class.try_to_class_literal_without_logging(&db).unwrap(); - - // We can't call `KnownClass::Tuple.to_instance()`; - // there are assertions to ensure that we always call `Type::homogeneous_tuple()` - // or `Type::heterogeneous_tuple()` instead.` - if class != KnownClass::Tuple { - assert_ne!( - class.to_instance(&db), - Type::unknown(), - "Unexpectedly fell back to `Unknown` for `{class:?}`" - ); - } - } - } - - #[test] - fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() { - let mut db = setup_db(); - - // First, collect the `KnownClass` variants - // and sort them according to the version they were added in. - // This makes the test far faster as it minimizes the number of times - // we need to change the Python version in the loop. - let mut classes: Vec<(KnownClass, PythonVersion)> = KnownClass::iter() - .map(|class| { - let version_added = match class { - KnownClass::Template => PythonVersion::PY314, - KnownClass::UnionType => PythonVersion::PY310, - KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => { - PythonVersion::PY311 - } - KnownClass::GenericAlias => PythonVersion::PY39, - KnownClass::KwOnly => PythonVersion::PY310, - KnownClass::Member | KnownClass::Nonmember | KnownClass::StrEnum => { - PythonVersion::PY311 - } - KnownClass::ParamSpec => PythonVersion::PY310, - _ => PythonVersion::PY37, - }; - (class, version_added) - }) - .collect(); - - classes.sort_unstable_by_key(|(_, version)| *version); - - let program = Program::get(&db); - let mut current_version = program.python_version(&db); - - for (class, version_added) in classes { - if version_added != current_version { - program - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: version_added, - source: PythonVersionSource::default(), - }); - current_version = version_added; - } - - // Check the class can be looked up successfully - class.try_to_class_literal_without_logging(&db).unwrap(); - - // We can't call `KnownClass::Tuple.to_instance()`; - // there are assertions to ensure that we always call `Type::homogeneous_tuple()` - // or `Type::heterogeneous_tuple()` instead.` - if class != KnownClass::Tuple { - assert_ne!( - class.to_instance(&db), - Type::unknown(), - "Unexpectedly fell back to `Unknown` for `{class:?}` on Python {version_added}" - ); - } - } - } -} diff --git a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs new file mode 100644 index 0000000000000..6db6793f7e37d --- /dev/null +++ b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs @@ -0,0 +1,509 @@ +use std::borrow::Cow; + +use ruff_db::{diagnostic::Span, parsed::parsed_module}; +use ruff_python_ast::{self as ast, NodeIndex, name::Name}; +use ruff_text_size::{Ranged, TextRange}; + +use crate::{ + Db, TypeQualifiers, + place::{Place, PlaceAndQualifiers}, + semantic_index::{definition::Definition, scope::ScopeId}, + types::{ + ClassBase, ClassLiteral, ClassType, DataclassParams, KnownClass, MemberLookupPolicy, + SubclassOfType, Type, + class::{ + ClassMemberResult, CodeGeneratorKind, DisjointBase, InstanceMemberResult, MroLookup, + }, + definition_expression_type, + member::Member, + mro::{DynamicMroError, Mro, MroIterator}, + }, +}; + +/// A class created dynamically via a three-argument `type()` call. +/// +/// For example: +/// ```python +/// Foo = type("Foo", (Base,), {"attr": 1}) +/// ``` +/// +/// The type of `Foo` would be `` where `Foo` is a `DynamicClassLiteral` with: +/// - name: "Foo" +/// - members: [("attr", int)] +/// +/// This is called "dynamic" because the class is created dynamically at runtime +/// via a function call rather than a class statement. +/// +/// # Salsa interning +/// +/// This is a Salsa-interned struct. Two different `type()` calls always produce +/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases: +/// +/// ```python +/// Foo1 = type("Foo", (Base,), {}) +/// Foo2 = type("Foo", (Base,), {}) +/// # Foo1 and Foo2 are distinct types +/// ``` +/// +/// The `anchor` field provides stable identity: +/// - For assigned `type()` calls, the `Definition` uniquely identifies the class. +/// - For dangling `type()` calls, a relative node offset anchored to the enclosing scope +/// provides stable identity that only changes when the scope itself changes. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct DynamicClassLiteral<'db> { + /// The name of the class (from the first argument to `type()`). + #[returns(ref)] + pub name: Name, + + /// The anchor for this dynamic class, providing stable identity. + /// + /// - `Definition`: The `type()` call is assigned to a variable. The definition + /// uniquely identifies this class and can be used to find the `type()` call. + /// - `ScopeOffset`: The `type()` call is "dangling" (not assigned). The offset + /// is relative to the enclosing scope's anchor node index. + #[returns(ref)] + pub anchor: DynamicClassAnchor<'db>, + + /// The class members from the namespace dict (third argument to `type()`). + /// Each entry is a (name, type) pair extracted from the dict literal. + #[returns(deref)] + pub members: Box<[(Name, Type<'db>)]>, + + /// Whether the namespace dict (third argument) is dynamic (not a literal dict, + /// or contains non-string-literal keys). When true, attribute lookups on this + /// class and its instances return `Unknown` instead of failing. + pub has_dynamic_namespace: bool, + + /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator + /// or passed to `dataclass()` as a function. + pub dataclass_params: Option>, +} + +/// Anchor for identifying a dynamic class literal. +/// +/// This enum provides stable identity for `DynamicClassLiteral`: +/// - For assigned calls, the `Definition` uniquely identifies the class. +/// - For dangling calls, a relative offset provides stable identity. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub enum DynamicClassAnchor<'db> { + /// The `type()` call is assigned to a variable. + /// + /// The `Definition` uniquely identifies this class. The `type()` call expression + /// is the `value` of the assignment, so we can get its range from the definition. + Definition(Definition<'db>), + + /// The `type()` call is "dangling" (not assigned to a variable). + /// + /// The offset is relative to the enclosing scope's anchor node index. + /// For module scope, this is equivalent to an absolute index (anchor is 0). + /// + /// The `explicit_bases` are computed eagerly at creation time since dangling + /// calls cannot recursively reference the class being defined. + ScopeOffset { + scope: ScopeId<'db>, + offset: u32, + explicit_bases: Box<[Type<'db>]>, + }, +} + +impl get_size2::GetSize for DynamicClassLiteral<'_> {} + +#[salsa::tracked] +impl<'db> DynamicClassLiteral<'db> { + /// Returns the definition where this class is created, if it was assigned to a variable. + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + match self.anchor(db) { + DynamicClassAnchor::Definition(definition) => Some(*definition), + DynamicClassAnchor::ScopeOffset { .. } => None, + } + } + + /// Returns the scope in which this dynamic class was created. + pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + match self.anchor(db) { + DynamicClassAnchor::Definition(definition) => definition.scope(db), + DynamicClassAnchor::ScopeOffset { scope, .. } => *scope, + } + } + + /// Returns the explicit base classes of this dynamic class. + /// + /// For assigned `type()` calls, bases are computed lazily using deferred inference + /// to handle forward references (e.g., `X = type("X", (tuple["X | None"],), {})`). + /// + /// For dangling `type()` calls, bases are computed eagerly at creation time and + /// stored directly on the anchor, since dangling calls cannot recursively reference + /// the class being defined. + /// + /// Returns an empty slice if the bases cannot be computed (e.g., due to a cycle) + /// or if the bases argument is not a tuple. + /// + /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). + pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { + /// Inner cached function for deferred inference of bases. + /// Only called for assigned `type()` calls where inference was deferred. + #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] + fn deferred_explicit_bases<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + ) -> Box<[Type<'db>]> { + let module = parsed_module(db, definition.file(db)).load(db); + + let value = definition + .kind(db) + .value(&module) + .expect("DynamicClassAnchor::Definition should only be used for assignments"); + let call_expr = value + .as_call_expr() + .expect("Definition value should be a call expression"); + + // The `bases` argument is the second positional argument. + let Some(bases_arg) = call_expr.arguments.args.get(1) else { + return Box::default(); + }; + + // Use `definition_expression_type` for deferred inference support. + let bases_type = definition_expression_type(db, definition, bases_arg); + + // For variable-length tuples (like `tuple[type, ...]`), we can't statically + // determine the bases, so return Unknown. + bases_type + .fixed_tuple_elements(db) + .map(Cow::into_owned) + .map(Into::into) + .unwrap_or_else(|| Box::from([Type::unknown()])) + } + + match self.anchor(db) { + // For dangling calls, bases are stored directly on the anchor. + DynamicClassAnchor::ScopeOffset { explicit_bases, .. } => explicit_bases.as_ref(), + // For assigned calls, use deferred inference. + DynamicClassAnchor::Definition(definition) => deferred_explicit_bases(db, *definition), + } + } + + /// Returns a [`Span`] with the range of the `type()` call expression. + /// + /// See [`Self::header_range`] for more details. + pub(super) fn header_span(self, db: &'db dyn Db) -> Span { + Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) + } + + /// Returns the range of the `type()` call expression that created this class. + pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + let scope = self.scope(db); + let file = scope.file(db); + let module = parsed_module(db, file).load(db); + + match self.anchor(db) { + DynamicClassAnchor::Definition(definition) => { + // For definitions, get the range from the definition's value. + // The `type()` call is the value of the assignment. + definition + .kind(db) + .value(&module) + .expect("DynamicClassAnchor::Definition should only be used for assignments") + .range() + } + DynamicClassAnchor::ScopeOffset { offset, .. } => { + // For dangling `type()` calls, compute the absolute index from the offset. + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("anchor should not be NodeIndex::NONE"); + let absolute_index = NodeIndex::from(anchor_u32 + *offset); + + // Get the node and return its range. + let node: &ast::ExprCall = module + .get_by_index(absolute_index) + .try_into() + .expect("scope offset should point to ExprCall"); + node.range() + } + } + } + + /// Get the metaclass of this dynamic class. + /// + /// Derives the metaclass from base classes: finds the most derived metaclass + /// that is a subclass of all other base metaclasses. + /// + /// See + pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + self.try_metaclass(db) + .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) + } + + /// Try to get the metaclass of this dynamic class. + /// + /// Returns `Err(DynamicMetaclassConflict)` if there's a metaclass conflict + /// (i.e., two base classes have metaclasses that are not in a subclass relationship). + /// + /// See + pub(crate) fn try_metaclass( + self, + db: &'db dyn Db, + ) -> Result, DynamicMetaclassConflict<'db>> { + let original_bases = self.explicit_bases(db); + + // If no bases, metaclass is `type`. + // To dynamically create a class with no bases that has a custom metaclass, + // you have to invoke that metaclass rather than `type()`. + if original_bases.is_empty() { + return Ok(KnownClass::Type.to_class_literal(db)); + } + + // If there's an MRO error, return unknown to avoid cascading errors. + if self.try_mro(db).is_err() { + return Ok(SubclassOfType::subclass_of_unknown()); + } + + // Convert Types to ClassBases for metaclass computation. + // All bases should convert successfully here: `try_mro()` above would have + // returned `Err(InvalidBases)` if any failed, causing us to return early. + let bases: Vec> = original_bases + .iter() + .filter_map(|base_type| ClassBase::try_from_type(db, *base_type, None)) + .collect(); + + // If all bases failed to convert, return type as the metaclass. + if bases.is_empty() { + return Ok(KnownClass::Type.to_class_literal(db)); + } + + // Start with the first base's metaclass as the candidate. + let mut candidate = bases[0].metaclass(db); + + // Track which base the candidate metaclass came from. + let (mut candidate_base, rest) = bases.split_first().unwrap(); + + // Reconcile with other bases' metaclasses. + for base in rest { + let base_metaclass = base.metaclass(db); + + // Get the ClassType for comparison. + let Some(candidate_class) = candidate.to_class_type(db) else { + // If candidate isn't a class type, keep it as is. + continue; + }; + let Some(base_metaclass_class) = base_metaclass.to_class_type(db) else { + continue; + }; + + // If base's metaclass is more derived, use it. + if base_metaclass_class.is_subclass_of(db, candidate_class) { + candidate = base_metaclass; + candidate_base = base; + continue; + } + + // If candidate is already more derived, keep it. + if candidate_class.is_subclass_of(db, base_metaclass_class) { + continue; + } + + // Conflict: neither metaclass is a subclass of the other. + // Python raises `TypeError: metaclass conflict` at runtime. + return Err(DynamicMetaclassConflict { + metaclass1: candidate_class, + base1: *candidate_base, + metaclass2: base_metaclass_class, + base2: *base, + }); + } + + Ok(candidate) + } + + /// Iterate over the MRO of this class using C3 linearization. + /// + /// The MRO includes the class itself as the first element, followed + /// by the merged base class MROs (consistent with `ClassType::iter_mro`). + /// + /// If the MRO cannot be computed (e.g., due to inconsistent ordering), falls back + /// to iterating over base MROs sequentially with deduplication. + pub(crate) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { + MroIterator::new(db, ClassLiteral::Dynamic(self), None) + } + + /// Look up an instance member by iterating through the MRO. + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + match MroLookup::new(db, self.iter_mro(db)).instance_member(name) { + InstanceMemberResult::Done(result) => result, + InstanceMemberResult::TypedDict => { + // Simplified `TypedDict` handling without type mapping. + KnownClass::TypedDictFallback + .to_instance(db) + .instance_member(db, name) + } + } + } + + /// Look up a class-level member by iterating through the MRO. + /// + /// Uses `MroLookup` with: + /// - No inherited generic context (dynamic classes aren't generic). + /// - `is_self_object = false` (dynamic classes are never `object`). + pub(crate) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + // Check if this dynamic class is dataclass-like (via dataclass_transform inheritance). + if matches!( + CodeGeneratorKind::from_class(db, self.into(), None), + Some(CodeGeneratorKind::DataclassLike(_)) + ) { + if name == "__dataclass_fields__" { + // Make this class look like a subclass of the `DataClassInstance` protocol. + return Place::declared(KnownClass::Dict.to_specialized_instance( + db, + &[ + KnownClass::Str.to_instance(db), + KnownClass::Field.to_specialized_instance(db, &[Type::any()]), + ], + )) + .with_qualifiers(TypeQualifiers::CLASS_VAR); + } else if name == "__dataclass_params__" { + // There is no typeshed class for this. For now, we model it as `Any`. + return Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR); + } + } + + let result = MroLookup::new(db, self.iter_mro(db)).class_member( + name, policy, None, // No inherited generic context. + false, // Dynamic classes are never `object`. + ); + + match result { + ClassMemberResult::Done(result) => result.finalize(db), + ClassMemberResult::TypedDict => { + // Simplified `TypedDict` handling without type mapping. + KnownClass::TypedDictFallback + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + .expect("Will return Some() when called on class literal") + } + } + } + + /// Look up a class member defined directly on this class (not inherited). + /// + /// Returns [`Member::unbound`] if the member is not found in the namespace dict, + /// unless the namespace is dynamic, in which case returns `Unknown`. + pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + // If the namespace is dynamic (not a literal dict) and the name isn't in `self.members`, + // return Unknown since we can't know what attributes might be defined. + self.members(db) + .iter() + .find_map(|(member_name, ty)| (name == member_name).then_some(*ty)) + .or_else(|| self.has_dynamic_namespace(db).then(Type::unknown)) + .map(Member::definitely_declared) + .unwrap_or_default() + } + + /// Look up an instance member defined directly on this class (not inherited). + /// + /// For dynamic classes, instance members are the same as class members + /// since they come from the namespace dict. + pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + self.own_class_member(db, name) + } + + /// Try to compute the MRO for this dynamic class. + /// + /// Returns `Ok(Mro)` if successful, or `Err(DynamicMroError)` if there's + /// an error (duplicate bases or C3 linearization failure). + #[salsa::tracked(returns(ref), cycle_initial=dynamic_class_try_mro_cycle_initial, heap_size = ruff_memory_usage::heap_size)] + pub(crate) fn try_mro(self, db: &'db dyn Db) -> Result, DynamicMroError<'db>> { + Mro::of_dynamic_class(db, self) + } + + /// Return `Some()` if this dynamic class is known to be a [`DisjointBase`]. + /// + /// A dynamic class is a disjoint base if `__slots__` is defined in the namespace + /// dictionary and is non-empty. Example: + /// ```python + /// X = type("X", (), {"__slots__": ("a",)}) + /// ``` + pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + // Check if __slots__ is in the members + for (name, ty) in self.members(db) { + if name.as_str() == "__slots__" { + // Check if the slots are non-empty + let is_non_empty = match ty { + // __slots__ = ("a", "b") + Type::NominalInstance(nominal) => nominal.tuple_spec(db).is_some_and(|spec| { + spec.len().into_fixed_length().is_some_and(|len| len > 0) + }), + // __slots__ = "abc" # Same as ("abc",) + Type::LiteralValue(literal) if literal.is_string() => true, + // Other types are considered dynamic/unknown + _ => false, + }; + if is_non_empty { + return Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Dynamic( + self, + ))); + } + } + } + None + } + + /// Returns `true` if this dynamic class defines any ordering method (`__lt__`, `__le__`, + /// `__gt__`, `__ge__`) in its namespace dictionary. Used by `@total_ordering` to determine + /// if synthesis is valid. + /// + /// If the namespace is dynamic, returns `true` since we can't know if ordering methods exist. + pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { + const ORDERING_METHODS: &[&str] = &["__lt__", "__le__", "__gt__", "__ge__"]; + ORDERING_METHODS + .iter() + .any(|name| !self.own_class_member(db, name).is_undefined()) + } + + /// Returns a new [`DynamicClassLiteral`] with the given dataclass params, preserving all other fields. + pub(crate) fn with_dataclass_params( + self, + db: &'db dyn Db, + dataclass_params: Option>, + ) -> Self { + Self::new( + db, + self.name(db).clone(), + self.anchor(db).clone(), + self.members(db), + self.has_dynamic_namespace(db), + dataclass_params, + ) + } +} + +/// Error for metaclass conflicts in dynamic classes. +/// +/// This mirrors `MetaclassErrorKind::Conflict` for regular classes. +#[derive(Debug, Clone)] +pub(crate) struct DynamicMetaclassConflict<'db> { + /// The first conflicting metaclass and its originating base class. + pub(crate) metaclass1: ClassType<'db>, + pub(crate) base1: ClassBase<'db>, + /// The second conflicting metaclass and its originating base class. + pub(crate) metaclass2: ClassType<'db>, + pub(crate) base2: ClassBase<'db>, +} + +#[expect(clippy::unnecessary_wraps)] +fn dynamic_class_try_mro_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: DynamicClassLiteral<'db>, +) -> Result, DynamicMroError<'db>> { + // When there's a cycle, return a minimal MRO with just the class itself and object. + // This breaks the cycle and allows type checking to continue. + Ok(Mro::from([ + ClassBase::Class(ClassType::NonGeneric(self_.into())), + ClassBase::object(db), + ])) +} diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs new file mode 100644 index 0000000000000..cc85970a0132a --- /dev/null +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -0,0 +1,1912 @@ +use crate::{ + Db, Program, + place::{DefinedPlace, Definedness, Place, known_module_symbol}, + semantic_index::{SemanticIndex, scope::NodeWithScopeKind}, + types::{ + Binding, ClassLiteral, ClassType, GenericContext, KnownInstanceType, StaticClassLiteral, + SubclassOfType, Truthiness, Type, binding_type, + bound_super::{BoundSuperError, BoundSuperType}, + class::CodeGeneratorKind, + constraints::{ConstraintSet, ConstraintSetBuilder}, + context::InferContext, + diagnostic::SUPER_CALL_IN_NAMED_TUPLE_METHOD, + infer::nearest_enclosing_class, + known_instance::DeprecatedInstance, + }, +}; +use ruff_db::files::File; +use ruff_python_ast as ast; +use ruff_python_ast::PythonVersion; +use rustc_hash::FxHashSet; +use std::{ + borrow::Cow, + sync::{LazyLock, Mutex}, +}; +use ty_module_resolver::{KnownModule, file_to_module}; + +/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow +/// for easier syntax when interacting with very common classes. +/// +/// Feel free to expand this enum if you ever find yourself using the same class in multiple +/// places. +/// Note: good candidates are any classes in `[ty_module_resolver::module::KnownModule]` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +#[cfg_attr(test, derive(strum_macros::EnumIter))] +pub enum KnownClass { + // To figure out where an stdlib symbol is defined, you can go into `crates/ty_vendored` + // and grep for the symbol name in any `.pyi` file. + + // Builtins + Bool, + Object, + Bytes, + Bytearray, + Type, + Int, + Float, + Complex, + Str, + List, + Tuple, + Set, + FrozenSet, + Dict, + Slice, + Property, + BaseException, + Exception, + BaseExceptionGroup, + ExceptionGroup, + Staticmethod, + Classmethod, + Super, + NotImplementedError, + // enum + Enum, + EnumType, + Auto, + Member, + Nonmember, + StrEnum, + // abc + ABCMeta, + // Types + GenericAlias, + ModuleType, + FunctionType, + MethodType, + MethodWrapperType, + WrapperDescriptorType, + UnionType, + GeneratorType, + AsyncGeneratorType, + CoroutineType, + NotImplementedType, + BuiltinFunctionType, + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + EllipsisType, + // Typeshed + NoneType, // Part of `types` for Python >= 3.10 + // Typing + Awaitable, + Generator, + Deprecated, + StdlibAlias, + SpecialForm, + TypeVar, + ParamSpec, + // typing_extensions.ParamSpec + ExtensionsParamSpec, // must be distinct from typing.ParamSpec, backports new features + ParamSpecArgs, + ParamSpecKwargs, + ProtocolMeta, + TypeVarTuple, + TypeAliasType, + NoDefaultType, + NewType, + SupportsIndex, + Iterable, + Iterator, + Sequence, + Mapping, + // typing_extensions + ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features + // Collections + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, + // sys + VersionInfo, + // dataclasses + Field, + KwOnly, + InitVar, + // _typeshed._type_checker_internals + NamedTupleFallback, + NamedTupleLike, + TypedDictFallback, + // string.templatelib + Template, + // pathlib + Path, + // ty_extensions + ConstraintSet, + GenericContext, + Specialization, +} + +impl KnownClass { + pub(crate) const fn is_bool(self) -> bool { + matches!(self, Self::Bool) + } + + pub(crate) const fn is_special_form(self) -> bool { + matches!(self, Self::SpecialForm) + } + + /// Determine whether instances of this class are always truthy, always falsy, + /// or have an ambiguous truthiness. + /// + /// Returns `None` for `KnownClass::Tuple`, since the truthiness of a tuple + /// depends on its spec. + pub(crate) const fn bool(self) -> Option { + match self { + // N.B. It's only generally safe to infer `Truthiness::AlwaysTrue` for a `KnownClass` + // variant if the class's `__bool__` method always returns the same thing *and* the + // class is `@final`. + // + // E.g. `ModuleType.__bool__` always returns `True`, but `ModuleType` is not `@final`. + // Equally, `range` is `@final`, but its `__bool__` method can return `False`. + Self::EllipsisType + | Self::NoDefaultType + | Self::MethodType + | Self::Slice + | Self::FunctionType + | Self::VersionInfo + | Self::TypeAliasType + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Super + | Self::WrapperDescriptorType + | Self::UnionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::MethodWrapperType + | Self::CoroutineType + | Self::BuiltinFunctionType + | Self::Template + | Self::Path => Some(Truthiness::AlwaysTrue), + + Self::NoneType => Some(Truthiness::AlwaysFalse), + + Self::BaseException + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Object + | Self::OrderedDict + | Self::BaseExceptionGroup + | Self::Bool + | Self::Str + | Self::List + | Self::GenericAlias + | Self::NewType + | Self::StdlibAlias + | Self::SupportsIndex + | Self::Set + | Self::Int + | Self::Type + | Self::Bytes + | Self::Bytearray + | Self::FrozenSet + | Self::Property + | Self::SpecialForm + | Self::Dict + | Self::ModuleType + | Self::ChainMap + | Self::Complex + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::Float + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 + // and raises a `TypeError` in Python >=3.14 + // (see https://docs.python.org/3/library/constants.html#NotImplemented) + | Self::NotImplementedType + | Self::Staticmethod + | Self::Classmethod + | Self::Awaitable + | Self::Generator + | Self::Deprecated + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::NamedTupleFallback + | Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::ProtocolMeta + | Self::TypedDictFallback => Some(Truthiness::Ambiguous), + + Self::Tuple => None, + } + } + + /// Return `true` if this class is a subclass of `enum.Enum` *and* has enum members, i.e. + /// if it is an "actual" enum, not `enum.Enum` itself or a similar custom enum class. + pub(crate) const fn is_enum_subclass_with_members(self) -> bool { + match self { + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Tuple + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::NotImplementedError + | KnownClass::Exception + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::StrEnum + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NoneType + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar + | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::Sequence + | KnownClass::Mapping + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::VersionInfo + | KnownClass::EllipsisType + | KnownClass::NotImplementedType + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::InitVar + | KnownClass::NamedTupleFallback + | KnownClass::NamedTupleLike + | KnownClass::ConstraintSet + | KnownClass::GenericContext + | KnownClass::Specialization + | KnownClass::TypedDictFallback + | KnownClass::BuiltinFunctionType + | KnownClass::ProtocolMeta + | KnownClass::Template + | KnownClass::Path => false, + } + } + + /// Return `true` if this class is a (true) subclass of `typing.TypedDict`. + pub(crate) const fn is_typed_dict_subclass(self) -> bool { + match self { + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Tuple + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::Exception + | KnownClass::NotImplementedError + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::StrEnum + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NoneType + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar + | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::Sequence + | KnownClass::Mapping + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::VersionInfo + | KnownClass::EllipsisType + | KnownClass::NotImplementedType + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::InitVar + | KnownClass::NamedTupleFallback + | KnownClass::NamedTupleLike + | KnownClass::ConstraintSet + | KnownClass::GenericContext + | KnownClass::Specialization + | KnownClass::TypedDictFallback + | KnownClass::BuiltinFunctionType + | KnownClass::ProtocolMeta + | KnownClass::Template + | KnownClass::Path => false, + } + } + + pub(crate) const fn is_tuple_subclass(self) -> bool { + match self { + KnownClass::Tuple | KnownClass::VersionInfo => true, + + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::Exception + | KnownClass::NotImplementedError + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::StrEnum + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NoneType + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar + | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::Sequence + | KnownClass::Mapping + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::EllipsisType + | KnownClass::NotImplementedType + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::InitVar + | KnownClass::TypedDictFallback + | KnownClass::NamedTupleLike + | KnownClass::NamedTupleFallback + | KnownClass::ConstraintSet + | KnownClass::GenericContext + | KnownClass::Specialization + | KnownClass::BuiltinFunctionType + | KnownClass::ProtocolMeta + | KnownClass::Template + | KnownClass::Path => false, + } + } + + /// Return `true` if this class is a protocol class. + /// + /// In an ideal world, perhaps we wouldn't hardcode this knowledge here; + /// instead, we'd just look at the bases for these classes, as we do for + /// all other classes. However, the special casing here helps us out in + /// two important ways: + /// + /// 1. It helps us avoid Salsa cycles when creating types such as "instance of `str`" + /// and "instance of `sys._version_info`". These types are constructed very early + /// on, but it causes problems if we attempt to infer the types of their bases + /// too soon. + /// 2. It's probably more performant. + pub(crate) const fn is_protocol(self) -> bool { + match self { + Self::SupportsIndex + | Self::Iterable + | Self::Iterator + | Self::Awaitable + | Self::NamedTupleLike + | Self::Generator => true, + + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Tuple + | Self::Int + | Self::Float + | Self::Complex + | Self::FrozenSet + | Self::Str + | Self::Set + | Self::Dict + | Self::List + | Self::Type + | Self::Slice + | Self::Property + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::Deprecated + | Self::GenericAlias + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::NoneType + | Self::SpecialForm + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::TypeAliasType + | Self::NoDefaultType + | Self::NewType + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Super + | Self::StdlibAlias + | Self::VersionInfo + | Self::EllipsisType + | Self::NotImplementedType + | Self::UnionType + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::NamedTupleFallback + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::TypedDictFallback + | Self::BuiltinFunctionType + | Self::ProtocolMeta + | Self::Template + | Self::Path + | Self::Mapping + | Self::Sequence => false, + } + } + + /// Return `true` if this class is a typeshed fallback class which is used to provide attributes and + /// methods for another type (e.g. `NamedTupleFallback` for actual `NamedTuple`s). These fallback + /// classes need special treatment in some places. For example, implicit usages of `Self` should not + /// be eagerly replaced with the fallback class itself. Instead, `Self` should eventually be treated + /// as referring to the destination type (e.g. the actual `NamedTuple`). + pub(crate) const fn is_fallback_class(self) -> bool { + match self { + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Tuple + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::Exception + | KnownClass::NotImplementedError + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::StrEnum + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NotImplementedType + | KnownClass::BuiltinFunctionType + | KnownClass::EllipsisType + | KnownClass::NoneType + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar + | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::ProtocolMeta + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::Sequence + | KnownClass::Mapping + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::VersionInfo + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::NamedTupleLike + | KnownClass::Template + | KnownClass::Path + | KnownClass::ConstraintSet + | KnownClass::GenericContext + | KnownClass::Specialization + | KnownClass::InitVar => false, + KnownClass::NamedTupleFallback | KnownClass::TypedDictFallback => true, + } + } + + pub(crate) fn name(self, db: &dyn Db) -> &'static str { + match self { + Self::Bool => "bool", + Self::Object => "object", + Self::Bytes => "bytes", + Self::Bytearray => "bytearray", + Self::Tuple => "tuple", + Self::Int => "int", + Self::Float => "float", + Self::Complex => "complex", + Self::FrozenSet => "frozenset", + Self::Str => "str", + Self::Set => "set", + Self::Dict => "dict", + Self::List => "list", + Self::Type => "type", + Self::Slice => "slice", + Self::Property => "property", + Self::BaseException => "BaseException", + Self::BaseExceptionGroup => "BaseExceptionGroup", + Self::Exception => "Exception", + Self::NotImplementedError => "NotImplementedError", + Self::ExceptionGroup => "ExceptionGroup", + Self::Staticmethod => "staticmethod", + Self::Classmethod => "classmethod", + Self::Awaitable => "Awaitable", + Self::Generator => "Generator", + Self::Deprecated => "deprecated", + Self::GenericAlias => "GenericAlias", + Self::ModuleType => "ModuleType", + Self::FunctionType => "FunctionType", + Self::MethodType => "MethodType", + Self::UnionType => "UnionType", + Self::MethodWrapperType => "MethodWrapperType", + Self::WrapperDescriptorType => "WrapperDescriptorType", + Self::BuiltinFunctionType => "BuiltinFunctionType", + Self::GeneratorType => "GeneratorType", + Self::AsyncGeneratorType => "AsyncGeneratorType", + Self::CoroutineType => "CoroutineType", + Self::NoneType => "NoneType", + Self::SpecialForm => "_SpecialForm", + Self::TypeVar => "TypeVar", + Self::ExtensionsTypeVar => "TypeVar", + Self::ParamSpec => "ParamSpec", + Self::ExtensionsParamSpec => "ParamSpec", + Self::ParamSpecArgs => "ParamSpecArgs", + Self::ParamSpecKwargs => "ParamSpecKwargs", + Self::TypeVarTuple => "TypeVarTuple", + Self::TypeAliasType => "TypeAliasType", + Self::NoDefaultType => "_NoDefaultType", + Self::NewType => "NewType", + Self::SupportsIndex => "SupportsIndex", + Self::ChainMap => "ChainMap", + Self::Counter => "Counter", + Self::DefaultDict => "defaultdict", + Self::Deque => "deque", + Self::OrderedDict => "OrderedDict", + Self::Enum => "Enum", + Self::EnumType => { + if Program::get(db).python_version(db) >= PythonVersion::PY311 { + "EnumType" + } else { + "EnumMeta" + } + } + Self::Auto => "auto", + Self::Member => "member", + Self::Nonmember => "nonmember", + Self::StrEnum => "StrEnum", + Self::ABCMeta => "ABCMeta", + Self::Super => "super", + Self::Iterable => "Iterable", + Self::Iterator => "Iterator", + Self::Sequence => "Sequence", + Self::Mapping => "Mapping", + // For example, `typing.List` is defined as `List = _Alias()` in typeshed + Self::StdlibAlias => "_Alias", + // This is the name the type of `sys.version_info` has in typeshed, + // which is different to what `type(sys.version_info).__name__` is at runtime. + // (At runtime, `type(sys.version_info).__name__ == "version_info"`, + // which is impossible to replicate in the stubs since the sole instance of the class + // also has that name in the `sys` module.) + Self::VersionInfo => "_version_info", + Self::EllipsisType => { + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + "EllipsisType" + } else { + "ellipsis" + } + } + Self::NotImplementedType => { + // Exposed as `types.NotImplementedType` on Python >=3.10; + // backported as `builtins._NotImplementedType` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + "NotImplementedType" + } else { + "_NotImplementedType" + } + } + Self::Field => "Field", + Self::KwOnly => "KW_ONLY", + Self::InitVar => "InitVar", + Self::NamedTupleFallback => "NamedTupleFallback", + Self::NamedTupleLike => "NamedTupleLike", + Self::ConstraintSet => "ConstraintSet", + Self::GenericContext => "GenericContext", + Self::Specialization => "Specialization", + Self::TypedDictFallback => "TypedDictFallback", + Self::Template => "Template", + Self::Path => "Path", + Self::ProtocolMeta => "_ProtocolMeta", + } + } + + pub(crate) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ { + struct KnownClassDisplay<'db> { + db: &'db dyn Db, + class: KnownClass, + } + + impl std::fmt::Display for KnownClassDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let KnownClassDisplay { + class: known_class, + db, + } = *self; + write!( + f, + "{module}.{class}", + module = known_class.canonical_module(db), + class = known_class.name(db) + ) + } + } + + KnownClassDisplay { db, class: self } + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing all possible instances of + /// the class. If this class is generic, this will use the default specialization. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + #[track_caller] + pub fn to_instance(self, db: &dyn Db) -> Type<'_> { + debug_assert_ne!( + self, + KnownClass::Tuple, + "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" + ); + self.to_class_literal(db) + .to_class_type(db) + .map(|class| Type::instance(db, class)) + .unwrap_or_else(Type::unknown) + } + + /// Similar to [`KnownClass::to_instance`], but returns the Unknown-specialization where each type + /// parameter is specialized to `Unknown`. + #[track_caller] + pub(crate) fn to_instance_unknown(self, db: &dyn Db) -> Type<'_> { + debug_assert_ne!( + self, + KnownClass::Tuple, + "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" + ); + self.try_to_class_literal(db) + .map(|literal| Type::instance(db, literal.unknown_specialization(db))) + .unwrap_or_else(Type::unknown) + } + + /// Lookup a generic [`KnownClass`] in typeshed and return a [`Type`] + /// representing a specialization of that class. + /// + /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong + /// number of types, a debug-level log message will be emitted stating this. + pub(crate) fn to_specialized_class_type<'t, 'db, T>( + self, + db: &'db dyn Db, + specialization: T, + ) -> Option> + where + T: Into]>>, + 'db: 't, + { + fn to_specialized_class_type_impl<'db>( + db: &'db dyn Db, + class: KnownClass, + class_literal: StaticClassLiteral<'db>, + specialization: Cow<[Type<'db>]>, + generic_context: GenericContext<'db>, + ) -> ClassType<'db> { + if specialization.len() != generic_context.len(db) { + // a cache of the `KnownClass`es that we have already seen mismatched-arity + // specializations for (and therefore that we've already logged a warning for) + static MESSAGES: LazyLock>> = + LazyLock::new(Mutex::default); + if MESSAGES.lock().unwrap().insert(class) { + tracing::info!( + "Wrong number of types when specializing {}. \ + Falling back to default specialization for the symbol instead.", + class.display(db) + ); + } + return class_literal.default_specialization(db); + } + + class_literal + .apply_specialization(db, |_| generic_context.specialize(db, specialization)) + } + + let class_literal = self.to_class_literal(db).as_class_literal()?.as_static()?; + let generic_context = class_literal.generic_context(db)?; + let specialization = specialization.into(); + + Some(to_specialized_class_type_impl( + db, + self, + class_literal, + specialization, + generic_context, + )) + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] + /// representing all possible instances of the generic class with a specialization. + /// + /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong + /// number of types, a debug-level log message will be emitted stating this. + #[track_caller] + pub(crate) fn to_specialized_instance<'t, 'db, T>( + self, + db: &'db dyn Db, + specialization: T, + ) -> Type<'db> + where + T: Into]>>, + 'db: 't, + { + debug_assert_ne!( + self, + KnownClass::Tuple, + "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" + ); + self.to_specialized_class_type(db, specialization) + .and_then(|class_type| Type::from(class_type).to_instance(db)) + .unwrap_or_else(Type::unknown) + } + + /// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// Return an error if the symbol cannot be found in the expected typeshed module, + /// or if the symbol is not a class definition, or if the symbol is possibly unbound. + fn try_to_class_literal_without_logging( + self, + db: &dyn Db, + ) -> Result, KnownClassLookupError<'_>> { + let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place; + match symbol { + Place::Defined(DefinedPlace { + ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), + definedness: Definedness::AlwaysDefined, + .. + }) => Ok(class_literal), + Place::Defined(DefinedPlace { + ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), + definedness: Definedness::PossiblyUndefined, + .. + }) => Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }), + Place::Defined(DefinedPlace { ty: found_type, .. }) => { + Err(KnownClassLookupError::SymbolNotAClass { found_type }) + } + Place::Undefined => Err(KnownClassLookupError::ClassNotFound), + } + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> { + #[salsa::interned(heap_size=ruff_memory_usage::heap_size)] + struct KnownClassArgument { + class: KnownClass, + } + + fn known_class_to_class_literal_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _class: KnownClassArgument<'db>, + ) -> Option> { + None + } + + #[salsa::tracked(cycle_initial=known_class_to_class_literal_initial, heap_size=ruff_memory_usage::heap_size)] + fn known_class_to_class_literal<'db>( + db: &'db dyn Db, + class: KnownClassArgument<'db>, + ) -> Option> { + let class = class.class(db); + class + .try_to_class_literal_without_logging(db) + .or_else(|lookup_error| { + if matches!( + lookup_error, + KnownClassLookupError::ClassPossiblyUnbound { .. } + ) { + tracing::info!("{}", lookup_error.display(db, class)); + } else { + tracing::info!( + "{}. Falling back to `Unknown` for the symbol instead.", + lookup_error.display(db, class) + ); + } + + match lookup_error { + KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { + Ok(class_literal) + } + KnownClassLookupError::ClassNotFound { .. } + | KnownClassLookupError::SymbolNotAClass { .. } => Err(()), + } + }) + .ok() + } + + known_class_to_class_literal(db, KnownClassArgument::new(db, self)) + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type<'_> { + self.try_to_class_literal(db) + .map(|class| Type::ClassLiteral(ClassLiteral::Static(class))) + .unwrap_or_else(Type::unknown) + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] + /// representing that class and all possible subclasses of the class. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub fn to_subclass_of(self, db: &dyn Db) -> Type<'_> { + self.to_class_literal(db) + .to_class_type(db) + .map(|class| SubclassOfType::from(db, class)) + .unwrap_or_else(SubclassOfType::subclass_of_unknown) + } + + /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, + /// *and* `class` is a subclass of `other`. + pub(crate) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + self.try_to_class_literal_without_logging(db) + .is_ok_and(|class| class.is_subclass_of(db, None, other)) + } + + pub(crate) fn when_subclass_of<'db, 'c>( + self, + db: &'db dyn Db, + other: ClassType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, + ) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(constraints, self.is_subclass_of(db, other)) + } + + /// Return the module in which we should look up the definition for this class + pub(super) fn canonical_module(self, db: &dyn Db) -> KnownModule { + match self { + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Type + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Tuple + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::Slice + | Self::Super + | Self::Property => KnownModule::Builtins, + Self::VersionInfo => KnownModule::Sys, + Self::ABCMeta => KnownModule::Abc, + Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum => KnownModule::Enum, + Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::MethodWrapperType + | Self::UnionType + | Self::BuiltinFunctionType + | Self::WrapperDescriptorType => KnownModule::Types, + Self::NoneType => KnownModule::Typeshed, + Self::Awaitable + | Self::Generator + | Self::SpecialForm + | Self::TypeVar + | Self::StdlibAlias + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + | Self::ProtocolMeta + | Self::SupportsIndex => KnownModule::Typing, + Self::TypeAliasType + | Self::ExtensionsTypeVar + | Self::TypeVarTuple + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::Deprecated + | Self::NewType => KnownModule::TypingExtensions, + Self::ParamSpec => { + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + KnownModule::Typing + } else { + KnownModule::TypingExtensions + } + } + Self::NoDefaultType => { + let python_version = Program::get(db).python_version(db); + + // typing_extensions has a 3.13+ re-export for the `typing.NoDefault` + // singleton, but not for `typing._NoDefaultType`. So we need to switch + // to `typing._NoDefaultType` for newer versions: + if python_version >= PythonVersion::PY313 { + KnownModule::Typing + } else { + KnownModule::TypingExtensions + } + } + Self::EllipsisType => { + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + KnownModule::Types + } else { + KnownModule::Builtins + } + } + Self::NotImplementedType => { + // Exposed as `types.NotImplementedType` on Python >=3.10; + // backported as `builtins._NotImplementedType` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + KnownModule::Types + } else { + KnownModule::Builtins + } + } + Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict => KnownModule::Collections, + Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses, + Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals, + Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization => KnownModule::TyExtensions, + Self::Template => KnownModule::Templatelib, + Self::Path => KnownModule::Pathlib, + } + } + + /// Returns `Some(true)` if all instances of this `KnownClass` compare equal. + /// Returns `None` for `KnownClass::Tuple`, since whether or not a tuple type + /// is single-valued depends on the tuple spec. + pub(crate) const fn is_single_valued(self) -> Option { + match self { + Self::NoneType + | Self::NoDefaultType + | Self::VersionInfo + | Self::EllipsisType + | Self::TypeAliasType + | Self::UnionType + | Self::NotImplementedType => Some(true), + + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Type + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::Slice + | Self::Property + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::Awaitable + | Self::Generator + | Self::Deprecated + | Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::SpecialForm + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::SupportsIndex + | Self::StdlibAlias + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Super + | Self::NewType + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + | Self::NamedTupleFallback + | Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::TypedDictFallback + | Self::BuiltinFunctionType + | Self::ProtocolMeta + | Self::Template + | Self::Path => Some(false), + + Self::Tuple => None, + } + } + + /// Is this class a singleton class? + /// + /// A singleton class is a class where it is known that only one instance can ever exist at runtime. + pub(crate) const fn is_singleton(self) -> bool { + match self { + Self::NoneType + | Self::EllipsisType + | Self::NoDefaultType + | Self::VersionInfo + | Self::TypeAliasType + | Self::NotImplementedType => true, + + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Tuple + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::List + | Self::Type + | Self::Slice + | Self::Property + | Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::SpecialForm + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::StdlibAlias + | Self::SupportsIndex + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::Awaitable + | Self::Generator + | Self::Deprecated + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Super + | Self::UnionType + | Self::NewType + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + | Self::NamedTupleFallback + | Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::TypedDictFallback + | Self::BuiltinFunctionType + | Self::ProtocolMeta + | Self::Template + | Self::Path => false, + } + } + + pub(crate) fn try_from_file_and_name( + db: &dyn Db, + file: File, + class_name: &str, + ) -> Option { + // We assert that this match is exhaustive over the right-hand side in the unit test + // `known_class_roundtrip_from_str()` + let candidates: &[Self] = match class_name { + "bool" => &[Self::Bool], + "object" => &[Self::Object], + "bytes" => &[Self::Bytes], + "bytearray" => &[Self::Bytearray], + "tuple" => &[Self::Tuple], + "type" => &[Self::Type], + "int" => &[Self::Int], + "float" => &[Self::Float], + "complex" => &[Self::Complex], + "str" => &[Self::Str], + "set" => &[Self::Set], + "frozenset" => &[Self::FrozenSet], + "dict" => &[Self::Dict], + "list" => &[Self::List], + "slice" => &[Self::Slice], + "property" => &[Self::Property], + "BaseException" => &[Self::BaseException], + "BaseExceptionGroup" => &[Self::BaseExceptionGroup], + "Exception" => &[Self::Exception], + "NotImplementedError" => &[Self::NotImplementedError], + "ExceptionGroup" => &[Self::ExceptionGroup], + "staticmethod" => &[Self::Staticmethod], + "classmethod" => &[Self::Classmethod], + "Awaitable" => &[Self::Awaitable], + "Generator" => &[Self::Generator], + "deprecated" => &[Self::Deprecated], + "GenericAlias" => &[Self::GenericAlias], + "NoneType" => &[Self::NoneType], + "ModuleType" => &[Self::ModuleType], + "GeneratorType" => &[Self::GeneratorType], + "AsyncGeneratorType" => &[Self::AsyncGeneratorType], + "CoroutineType" => &[Self::CoroutineType], + "FunctionType" => &[Self::FunctionType], + "MethodType" => &[Self::MethodType], + "UnionType" => &[Self::UnionType], + "MethodWrapperType" => &[Self::MethodWrapperType], + "WrapperDescriptorType" => &[Self::WrapperDescriptorType], + "BuiltinFunctionType" => &[Self::BuiltinFunctionType], + "NewType" => &[Self::NewType], + "TypeAliasType" => &[Self::TypeAliasType], + "TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar], + "Iterable" => &[Self::Iterable], + "Iterator" => &[Self::Iterator], + "Sequence" => &[Self::Sequence], + "Mapping" => &[Self::Mapping], + "ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec], + "ParamSpecArgs" => &[Self::ParamSpecArgs], + "ParamSpecKwargs" => &[Self::ParamSpecKwargs], + "TypeVarTuple" => &[Self::TypeVarTuple], + "ChainMap" => &[Self::ChainMap], + "Counter" => &[Self::Counter], + "defaultdict" => &[Self::DefaultDict], + "deque" => &[Self::Deque], + "OrderedDict" => &[Self::OrderedDict], + "_Alias" => &[Self::StdlibAlias], + "_SpecialForm" => &[Self::SpecialForm], + "_NoDefaultType" => &[Self::NoDefaultType], + "SupportsIndex" => &[Self::SupportsIndex], + "Enum" => &[Self::Enum], + "EnumMeta" => &[Self::EnumType], + "EnumType" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { + &[Self::EnumType] + } + "StrEnum" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { + &[Self::StrEnum] + } + "auto" => &[Self::Auto], + "member" => &[Self::Member], + "nonmember" => &[Self::Nonmember], + "ABCMeta" => &[Self::ABCMeta], + "super" => &[Self::Super], + "_version_info" => &[Self::VersionInfo], + "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { + &[Self::EllipsisType] + } + "EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { + &[Self::EllipsisType] + } + "_NotImplementedType" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { + &[Self::NotImplementedType] + } + "NotImplementedType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { + &[Self::NotImplementedType] + } + "Field" => &[Self::Field], + "KW_ONLY" => &[Self::KwOnly], + "InitVar" => &[Self::InitVar], + "NamedTupleFallback" => &[Self::NamedTupleFallback], + "NamedTupleLike" => &[Self::NamedTupleLike], + "ConstraintSet" => &[Self::ConstraintSet], + "GenericContext" => &[Self::GenericContext], + "Specialization" => &[Self::Specialization], + "TypedDictFallback" => &[Self::TypedDictFallback], + "Template" => &[Self::Template], + "Path" => &[Self::Path], + "_ProtocolMeta" => &[Self::ProtocolMeta], + _ => return None, + }; + + let module = file_to_module(db, file)?.known(db)?; + + candidates + .iter() + .copied() + .find(|&candidate| candidate.check_module(db, module)) + } + + /// Return `true` if the module of `self` matches `module` + fn check_module(self, db: &dyn Db, module: KnownModule) -> bool { + match self { + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Type + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Tuple + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::Slice + | Self::Property + | Self::GenericAlias + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::StdlibAlias // no equivalent class exists in typing_extensions, nor ever will + | Self::ModuleType + | Self::VersionInfo + | Self::BaseException + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::EllipsisType + | Self::BaseExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Super + | Self::NotImplementedType + | Self::UnionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::WrapperDescriptorType + | Self::BuiltinFunctionType + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::NamedTupleFallback + | Self::TypedDictFallback + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::Awaitable + | Self::Generator + | Self::Template + | Self::Path => module == self.canonical_module(db), + Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), + Self::SpecialForm + | Self::TypeAliasType + | Self::NoDefaultType + | Self::SupportsIndex + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + | Self::ProtocolMeta + | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), + Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), + } + } + + /// Evaluate a call to this known class, emit any diagnostics that are necessary + /// as a result of the call, and return the type that results from the call. + pub(crate) fn check_call<'db>( + self, + context: &InferContext<'db, '_>, + index: &SemanticIndex<'db>, + overload: &mut Binding<'db>, + call_expression: &ast::ExprCall, + ) { + let db = context.db(); + let scope = context.scope(); + let module = context.module(); + + match self { + KnownClass::Super => { + // Handle the case where `super()` is called with no arguments. + // In this case, we need to infer the two arguments: + // 1. The nearest enclosing class + // 2. The first parameter of the current function (typically `self` or `cls`) + match overload.parameter_types() { + [] => { + let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) + else { + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic(context, call_expression.into()); + overload.set_return_type(Type::unknown()); + return; + }; + + // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. + if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class.into(), None) { + if let Some(builder) = context + .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) + { + builder.into_diagnostic(format_args!( + "Cannot use `super()` in a method of NamedTuple class `{}`", + enclosing_class.name(db) + )); + } + overload.set_return_type(Type::unknown()); + return; + } + + // The type of the first parameter if the given scope is function-like (i.e. function or lambda). + // `None` if the scope is not function-like, or has no parameters. + let first_param = match scope.node(db) { + NodeWithScopeKind::Function(f) => { + f.node(module).parameters.iter().next() + } + NodeWithScopeKind::Lambda(l) => l + .node(module) + .parameters + .as_ref() + .into_iter() + .flatten() + .next(), + _ => None, + }; + + let Some(first_param) = first_param else { + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic(context, call_expression.into()); + overload.set_return_type(Type::unknown()); + return; + }; + + let definition = index.expect_single_definition(first_param); + let first_param = binding_type(db, definition); + + let bound_super = BoundSuperType::build( + db, + Type::ClassLiteral(ClassLiteral::Static(enclosing_class)), + first_param, + ) + .unwrap_or_else(|err| { + err.report_diagnostic(context, call_expression.into()); + Type::unknown() + }); + + overload.set_return_type(bound_super); + } + [Some(pivot_class_type), Some(owner_type)] => { + // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. + if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) { + if CodeGeneratorKind::NamedTuple.matches( + db, + enclosing_class.into(), + None, + ) { + if let Some(builder) = context + .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) + { + builder.into_diagnostic(format_args!( + "Cannot use `super()` in a method of NamedTuple class `{}`", + enclosing_class.name(db) + )); + } + overload.set_return_type(Type::unknown()); + return; + } + } + + let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type) + .unwrap_or_else(|err| { + err.report_diagnostic(context, call_expression.into()); + Type::unknown() + }); + overload.set_return_type(bound_super); + } + _ => {} + } + } + + KnownClass::Deprecated => { + // Parsing something of the form: + // + // @deprecated("message") + // @deprecated("message", category = DeprecationWarning, stacklevel = 1) + // + // "Static type checker behavior is not affected by the category and stacklevel arguments" + // so we only need the message and can ignore everything else. The message is mandatory, + // must be a LiteralString, and always comes first. + // + // We aren't guaranteed to know the static value of a LiteralString, so we need to + // accept that sometimes we will fail to include the message. + // + // We don't do any serious validation/diagnostics here, as the signature for this + // is included in `Type::bindings`. + // + // See: + let [Some(message), ..] = overload.parameter_types() else { + // Checking in Type::bindings will complain about this for us + return; + }; + + overload.set_return_type(Type::KnownInstance(KnownInstanceType::Deprecated( + DeprecatedInstance::new(db, message.as_string_literal()), + ))); + } + + _ => {} + } + } +} + +/// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum KnownClassLookupError<'db> { + /// There is no symbol by that name in the expected typeshed module. + ClassNotFound, + /// There is a symbol by that name in the expected typeshed module, + /// but it's not a class. + SymbolNotAClass { found_type: Type<'db> }, + /// There is a symbol by that name in the expected typeshed module, + /// and it's a class definition, but it's possibly unbound. + ClassPossiblyUnbound { + class_literal: StaticClassLiteral<'db>, + }, +} + +impl<'db> KnownClassLookupError<'db> { + fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db { + struct ErrorDisplay<'db> { + db: &'db dyn Db, + class: KnownClass, + error: KnownClassLookupError<'db>, + } + + impl std::fmt::Display for ErrorDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ErrorDisplay { db, class, error } = *self; + + let class = class.display(db); + let python_version = Program::get(db).python_version(db); + + match error { + KnownClassLookupError::ClassNotFound => write!( + f, + "Could not find class `{class}` in typeshed on Python {python_version}", + ), + KnownClassLookupError::SymbolNotAClass { found_type } => write!( + f, + "Error looking up `{class}` in typeshed: expected to find a class definition \ + on Python {python_version}, but found a symbol of type `{found_type}` instead", + found_type = found_type.display(db), + ), + KnownClassLookupError::ClassPossiblyUnbound { .. } => write!( + f, + "Error looking up `{class}` in typeshed on Python {python_version}: \ + expected to find a fully bound symbol, but found one that is possibly unbound", + ), + } + } + } + + ErrorDisplay { + db, + class, + error: *self, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::tests::setup_db; + use crate::{PythonVersionSource, PythonVersionWithSource}; + use salsa::Setter; + use strum::IntoEnumIterator; + use ty_module_resolver::resolve_module_confident; + + #[test] + fn known_class_roundtrip_from_str() { + let mut db = setup_db(); + Program::get(&db) + .set_python_version_with_source(&mut db) + .to(PythonVersionWithSource { + version: PythonVersion::latest_preview(), + source: PythonVersionSource::default(), + }); + for class in KnownClass::iter() { + let class_name = class.name(&db); + let class_module = + resolve_module_confident(&db, &class.canonical_module(&db).name()).unwrap(); + + assert_eq!( + KnownClass::try_from_file_and_name( + &db, + class_module.file(&db).unwrap(), + class_name + ), + Some(class), + "`KnownClass::candidate_from_str` appears to be missing a case for `{class_name}`" + ); + } + } + + #[test] + fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_latest_version() { + let mut db = setup_db(); + + Program::get(&db) + .set_python_version_with_source(&mut db) + .to(PythonVersionWithSource { + version: PythonVersion::latest_ty(), + source: PythonVersionSource::default(), + }); + + for class in KnownClass::iter() { + // Check the class can be looked up successfully + class.try_to_class_literal_without_logging(&db).unwrap(); + + // We can't call `KnownClass::Tuple.to_instance()`; + // there are assertions to ensure that we always call `Type::homogeneous_tuple()` + // or `Type::heterogeneous_tuple()` instead.` + if class != KnownClass::Tuple { + assert_ne!( + class.to_instance(&db), + Type::unknown(), + "Unexpectedly fell back to `Unknown` for `{class:?}`" + ); + } + } + } + + #[test] + fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() { + let mut db = setup_db(); + + // First, collect the `KnownClass` variants + // and sort them according to the version they were added in. + // This makes the test far faster as it minimizes the number of times + // we need to change the Python version in the loop. + let mut classes: Vec<(KnownClass, PythonVersion)> = KnownClass::iter() + .map(|class| { + let version_added = match class { + KnownClass::Template => PythonVersion::PY314, + KnownClass::UnionType => PythonVersion::PY310, + KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => { + PythonVersion::PY311 + } + KnownClass::GenericAlias => PythonVersion::PY39, + KnownClass::KwOnly => PythonVersion::PY310, + KnownClass::Member | KnownClass::Nonmember | KnownClass::StrEnum => { + PythonVersion::PY311 + } + KnownClass::ParamSpec => PythonVersion::PY310, + _ => PythonVersion::PY37, + }; + (class, version_added) + }) + .collect(); + + classes.sort_unstable_by_key(|(_, version)| *version); + + let program = Program::get(&db); + let mut current_version = program.python_version(&db); + + for (class, version_added) in classes { + if version_added != current_version { + program + .set_python_version_with_source(&mut db) + .to(PythonVersionWithSource { + version: version_added, + source: PythonVersionSource::default(), + }); + current_version = version_added; + } + + // Check the class can be looked up successfully + class.try_to_class_literal_without_logging(&db).unwrap(); + + // We can't call `KnownClass::Tuple.to_instance()`; + // there are assertions to ensure that we always call `Type::homogeneous_tuple()` + // or `Type::heterogeneous_tuple()` instead.` + if class != KnownClass::Tuple { + assert_ne!( + class.to_instance(&db), + Type::unknown(), + "Unexpectedly fell back to `Unknown` for `{class:?}` on Python {version_added}" + ); + } + } + } +} diff --git a/crates/ty_python_semantic/src/types/class/named_tuple.rs b/crates/ty_python_semantic/src/types/class/named_tuple.rs new file mode 100644 index 0000000000000..3c4b570f8e928 --- /dev/null +++ b/crates/ty_python_semantic/src/types/class/named_tuple.rs @@ -0,0 +1,582 @@ +use ruff_db::{diagnostic::Span, parsed::parsed_module}; +use ruff_python_ast as ast; +use ruff_python_ast::{NodeIndex, PythonVersion, name::Name}; +use ruff_text_size::{Ranged, TextRange}; + +use crate::{ + Db, Program, + place::{Place, PlaceAndQualifiers}, + semantic_index::{definition::Definition, scope::ScopeId}, + types::{ + BindingContext, BoundTypeVarInstance, ClassBase, ClassLiteral, ClassType, GenericContext, + KnownClass, KnownInstanceType, MemberLookupPolicy, Parameter, Parameters, + PropertyInstanceType, Signature, SubclassOfType, Type, TypeContext, TypeMapping, + definition_expression_type, member::Member, mro::Mro, tuple::TupleType, + }, +}; + +/// Synthesize a namedtuple class member given the field information. +/// +/// This is used by both `DynamicNamedTupleLiteral` and `StaticClassLiteral` (for declarative +/// namedtuples) to avoid duplicating the synthesis logic. +/// +/// The `inherited_generic_context` parameter is used for declarative namedtuples to preserve +/// generic context in the synthesized `__new__` signature. +pub(super) fn synthesize_namedtuple_class_member<'db>( + db: &'db dyn Db, + name: &str, + instance_ty: Type<'db>, + fields: impl Iterator>, + inherited_generic_context: Option>, +) -> Option> { + match name { + "__new__" => { + // __new__(cls, field1, field2, ...) -> Self + let self_typevar = + BoundTypeVarInstance::synthetic_self(db, instance_ty, BindingContext::Synthetic); + let self_ty = Type::TypeVar(self_typevar); + + let variables = inherited_generic_context + .iter() + .flat_map(|ctx| ctx.variables(db)) + .chain(std::iter::once(self_typevar)); + + let generic_context = GenericContext::from_typevar_instances(db, variables); + + let first_parameter = Parameter::positional_or_keyword(Name::new_static("cls")) + .with_annotated_type(SubclassOfType::from(db, self_typevar)); + + let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { + Parameter::positional_or_keyword(field.name) + .with_annotated_type(field.ty) + .with_optional_default_type(field.default) + })); + + let signature = Signature::new_generic( + Some(generic_context), + Parameters::new(db, parameters), + self_ty, + ); + Some(Type::function_like_callable(db, signature)) + } + "_fields" => { + // _fields: tuple[Literal["field1"], Literal["field2"], ...] + let field_types = fields.map(|field| Type::string_literal(db, &field.name)); + Some(Type::heterogeneous_tuple(db, field_types)) + } + "__slots__" => { + // __slots__: tuple[()] - always empty for namedtuples + Some(Type::empty_tuple(db)) + } + "_replace" | "__replace__" => { + if name == "__replace__" && Program::get(db).python_version(db) < PythonVersion::PY313 { + return None; + } + + // _replace(self, *, field1=..., field2=...) -> Self + let self_ty = Type::TypeVar(BoundTypeVarInstance::synthetic_self( + db, + instance_ty, + BindingContext::Synthetic, + )); + + let first_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(self_ty); + + let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { + Parameter::keyword_only(field.name) + .with_annotated_type(field.ty) + .with_default_type(field.ty) + })); + + let signature = Signature::new(Parameters::new(db, parameters), self_ty); + Some(Type::function_like_callable(db, signature)) + } + "__init__" => { + // Namedtuples don't have a custom __init__. All construction happens in __new__. + None + } + _ => { + // Fall back to NamedTupleFallback for other synthesized methods. + KnownClass::NamedTupleFallback + .to_class_literal(db) + .as_class_literal()? + .as_static()? + .own_class_member(db, inherited_generic_context, None, name) + .ignore_possibly_undefined() + } + } +} + +#[derive(Debug, salsa::Update, get_size2::GetSize, Clone, PartialEq, Eq, Hash)] +pub struct NamedTupleField<'db> { + pub(crate) name: Name, + pub(crate) ty: Type<'db>, + pub(crate) default: Option>, +} + +/// A namedtuple created via the functional form `namedtuple(name, fields)` or +/// `NamedTuple(name, fields)`. +/// +/// For example: +/// ```python +/// from collections import namedtuple +/// Point = namedtuple("Point", ["x", "y"]) +/// +/// from typing import NamedTuple +/// Person = NamedTuple("Person", [("name", str), ("age", int)]) +/// ``` +/// +/// The type of `Point` would be `type[Point]` where `Point` is a `DynamicNamedTupleLiteral`. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +pub struct DynamicNamedTupleLiteral<'db> { + /// The name of the namedtuple (from the first argument). + #[returns(ref)] + pub name: Name, + + /// The anchor for this dynamic namedtuple, providing stable identity. + /// + /// - `Definition`: The call is assigned to a variable. The definition + /// uniquely identifies this namedtuple and can be used to find the call. + /// - `ScopeOffset`: The call is "dangling" (not assigned). The offset + /// is relative to the enclosing scope's anchor node index. + #[returns(ref)] + pub anchor: DynamicNamedTupleAnchor<'db>, +} + +impl get_size2::GetSize for DynamicNamedTupleLiteral<'_> {} + +#[salsa::tracked] +impl<'db> DynamicNamedTupleLiteral<'db> { + /// Returns the definition where this namedtuple is created, if it was assigned to a variable. + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + match self.anchor(db) { + DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } + | DynamicNamedTupleAnchor::TypingDefinition(definition) => Some(*definition), + DynamicNamedTupleAnchor::ScopeOffset { .. } => None, + } + } + + /// Returns the scope in which this dynamic class was created. + pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + match self.anchor(db) { + DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } + | DynamicNamedTupleAnchor::TypingDefinition(definition) => definition.scope(db), + DynamicNamedTupleAnchor::ScopeOffset { scope, .. } => *scope, + } + } + + /// Returns an instance type for this dynamic namedtuple. + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + Type::instance(db, ClassType::NonGeneric(self.into())) + } + + /// Returns the range of the namedtuple call expression. + pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + let scope = self.scope(db); + let file = scope.file(db); + let module = parsed_module(db, file).load(db); + + match self.anchor(db) { + DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } + | DynamicNamedTupleAnchor::TypingDefinition(definition) => { + // For definitions, get the range from the definition's value. + // The namedtuple call is the value of the assignment. + definition + .kind(db) + .value(&module) + .expect("DynamicClassAnchor::Definition should only be used for assignments") + .range() + } + DynamicNamedTupleAnchor::ScopeOffset { offset, .. } => { + // For dangling calls, compute the absolute index from the offset. + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("anchor should not be NodeIndex::NONE"); + let absolute_index = NodeIndex::from(anchor_u32 + offset); + + // Get the node and return its range. + let node: &ast::ExprCall = module + .get_by_index(absolute_index) + .try_into() + .expect("scope offset should point to ExprCall"); + node.range() + } + } + } + + /// Returns a [`Span`] pointing to the namedtuple call expression. + pub(super) fn header_span(self, db: &'db dyn Db) -> Span { + Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) + } + + /// Compute the MRO for this namedtuple. + /// + /// The MRO is the MRO of the class's tuple base class, prepended by `self`. + /// For example, `namedtuple("Point", [("x", int), ("y", int)])` has the following MRO: + /// + /// 1. `` + /// 2. `` + /// 3. `` + /// 4. `` + /// 5. `` + /// 6. `` + /// 7. `` + /// 8. `typing.Protocol` + /// 9. `typing.Generic` + /// 10. `` + #[salsa::tracked( + returns(ref), + heap_size=ruff_memory_usage::heap_size, + cycle_initial=dynamic_namedtuple_mro_cycle_initial + )] + pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { + let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); + let tuple_class = self.tuple_base_class(db); + std::iter::once(self_base) + .chain(tuple_class.iter_mro(db)) + .collect() + } + + /// Get the metaclass of this dynamic namedtuple. + /// + /// Namedtuples always have `type` as their metaclass. + pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + let _ = self; + KnownClass::Type.to_class_literal(db) + } + + /// Compute the specialized tuple class that this namedtuple inherits from. + /// + /// For example, `namedtuple("Point", [("x", int), ("y", int)])` inherits from `tuple[int, int]`. + pub(crate) fn tuple_base_class(self, db: &'db dyn Db) -> ClassType<'db> { + // If fields are unknown, return `tuple[Unknown, ...]` to avoid false positives + // like index-out-of-bounds errors. + if !self.has_known_fields(db) { + return TupleType::homogeneous(db, Type::unknown()).to_class_type(db); + } + + let field_types = self.fields(db).iter().map(|field| field.ty); + TupleType::heterogeneous(db, field_types) + .map(|t| t.to_class_type(db)) + .unwrap_or_else(|| { + KnownClass::Tuple + .to_class_literal(db) + .as_class_literal() + .expect("tuple should be a class literal") + .default_specialization(db) + }) + } + + /// Look up an instance member defined directly on this class (not inherited). + /// + /// For dynamic namedtuples, instance members are the field names. + /// If fields are unknown (dynamic), returns `Any` for any attribute. + pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + for field in self.fields(db) { + if field.name == name { + return Member::definitely_declared(field.ty); + } + } + + if !self.has_known_fields(db) { + return Member::definitely_declared(Type::any()); + } + + Member::unbound() + } + + /// Look up an instance member by name (including superclasses). + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + // First check own instance members. + let result = self.own_instance_member(db, name); + if !result.is_undefined() { + return result.inner; + } + + // Fall back to the tuple base type for other attributes. + Type::instance(db, self.tuple_base_class(db)).instance_member(db, name) + } + + /// Look up a class-level member by name. + pub(crate) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + // First check synthesized members and fields. + let member = self.own_class_member(db, name); + if !member.is_undefined() { + return member.inner; + } + + // Fall back to tuple class members. + let result = self + .tuple_base_class(db) + .class_literal(db) + .class_member(db, name, policy); + + // If fields are unknown (dynamic) and the attribute wasn't found, + // return `Any` instead of failing. + if !self.has_known_fields(db) && result.place.is_undefined() { + return Place::bound(Type::any()).into(); + } + + result + } + + /// Look up a class-level member defined directly on this class (not inherited). + /// + /// This only checks synthesized members and field properties, without falling + /// back to tuple or other base classes. + pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + // Handle synthesized namedtuple attributes. + if let Some(ty) = self.synthesized_class_member(db, name) { + return Member::definitely_declared(ty); + } + + // Check if it's a field name (returns a property descriptor). + for field in self.fields(db) { + if field.name == name { + return Member::definitely_declared(create_field_property(db, field.ty)); + } + } + + Member::default() + } + + /// Generate synthesized class members for namedtuples. + fn synthesized_class_member(self, db: &'db dyn Db, name: &str) -> Option> { + let instance_ty = self.to_instance(db); + + // When fields are unknown, handle constructor and field-specific methods specially. + if !self.has_known_fields(db) { + match name { + // For constructors, return a gradual signature that accepts any arguments. + "__new__" | "__init__" => { + let signature = Signature::new(Parameters::gradual_form(), instance_ty); + return Some(Type::function_like_callable(db, signature)); + } + // For other field-specific methods, fall through to NamedTupleFallback. + "_fields" | "_replace" | "__replace__" => { + return KnownClass::NamedTupleFallback + .to_class_literal(db) + .as_class_literal()? + .as_static()? + .own_class_member(db, None, None, name) + .ignore_possibly_undefined() + .map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: instance_ty, + }, + TypeContext::default(), + ) + }); + } + _ => {} + } + } + + let result = synthesize_namedtuple_class_member( + db, + name, + instance_ty, + self.fields(db).iter().cloned(), + None, + ); + // For fallback members from NamedTupleFallback, apply type mapping to handle + // `Self` types. The explicitly synthesized members (__new__, _fields, _replace, + // __replace__) don't need this mapping. + if matches!( + name, + "__new__" | "_fields" | "_replace" | "__replace__" | "__slots__" + ) { + result + } else { + result.map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: instance_ty, + }, + TypeContext::default(), + ) + }) + } + } + + fn spec(self, db: &'db dyn Db) -> NamedTupleSpec<'db> { + #[salsa::tracked(cycle_initial=deferred_spec_initial, heap_size=ruff_memory_usage::heap_size)] + fn deferred_spec<'db>(db: &'db dyn Db, definition: Definition<'db>) -> NamedTupleSpec<'db> { + let module = parsed_module(db, definition.file(db)).load(db); + let node = definition + .kind(db) + .value(&module) + .expect("Expected `NamedTuple` definition to be an assignment") + .as_call_expr() + .expect("Expected `NamedTuple` definition r.h.s. to be a call expression"); + match definition_expression_type(db, definition, &node.arguments.args[1]) { + Type::KnownInstance(KnownInstanceType::NamedTupleSpec(spec)) => spec, + _ => NamedTupleSpec::unknown(db), + } + } + + fn deferred_spec_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + _definition: Definition<'db>, + ) -> NamedTupleSpec<'db> { + NamedTupleSpec::unknown(db) + } + + match self.anchor(db) { + DynamicNamedTupleAnchor::CollectionsDefinition { spec, .. } + | DynamicNamedTupleAnchor::ScopeOffset { spec, .. } => *spec, + DynamicNamedTupleAnchor::TypingDefinition(definition) => deferred_spec(db, *definition), + } + } + + fn fields(self, db: &'db dyn Db) -> &'db [NamedTupleField<'db>] { + self.spec(db).fields(db) + } + + pub(super) fn has_known_fields(self, db: &'db dyn Db) -> bool { + self.spec(db).has_known_fields(db) + } +} + +fn dynamic_namedtuple_mro_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: DynamicNamedTupleLiteral<'db>, +) -> Mro<'db> { + Mro::from_error( + db, + ClassType::NonGeneric(ClassLiteral::DynamicNamedTuple(self_)), + ) +} + +/// Anchor for identifying a dynamic `namedtuple`/`NamedTuple` class literal. +/// +/// This enum provides stable identity for `DynamicNamedTupleLiteral` instances: +/// - For assigned calls, the `Definition` uniquely identifies the class. +/// - For dangling calls, a relative offset provides stable identity. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub enum DynamicNamedTupleAnchor<'db> { + /// We're dealing with a `collections.namedtuple()` call + /// that's assigned to a variable. + /// + /// The `Definition` uniquely identifies this class. The `namedtuple()` + /// call expression is the `value` of the assignment, so we can get its + /// range from the definition. + CollectionsDefinition { + definition: Definition<'db>, + spec: NamedTupleSpec<'db>, + }, + + /// We're dealing with a `typing.NamedTuple()` call + /// that's assigned to a variable. + /// + /// The `Definition` uniquely identifies this class. The `NamedTuple()` + /// call expression is the `value` of the assignment, so we can get its + /// range from the definition. + /// + /// Unlike the `CollectionsDefinition` variant, this variant does not + /// hold a `NamedTupleSpec`. This is because the spec for a + /// `typing.NamedTuple` call can contain forward references and recursive + /// references that must be evaluated lazily. The spec is computed + /// on-demand from the definition. + TypingDefinition(Definition<'db>), + + /// We're dealing with a `namedtuple()` or `NamedTuple` call that is + /// "dangling" (not assigned to a variable). + /// + /// The offset is relative to the enclosing scope's anchor node index. + /// For module scope, this is equivalent to an absolute index (anchor is 0). + /// + /// Dangling calls can always store the spec. They *can* contain + /// forward references if they appear in class bases: + /// + /// ```python + /// from typing import NamedTuple + /// + /// class F(NamedTuple("F", [("x", "F | None")]): + /// pass + /// ``` + /// + /// But this doesn't matter, because all class bases are deferred in their + /// entirety during type inference. + ScopeOffset { + scope: ScopeId<'db>, + offset: u32, + spec: NamedTupleSpec<'db>, + }, +} + +/// A specification describing the fields of a dynamic `namedtuple` +/// or `NamedTuple` class. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct NamedTupleSpec<'db> { + #[returns(deref)] + pub(crate) fields: Box<[NamedTupleField<'db>]>, + + pub(crate) has_known_fields: bool, +} + +impl<'db> NamedTupleSpec<'db> { + /// Create a [`NamedTupleSpec`] with the given fields. + pub(crate) fn known(db: &'db dyn Db, fields: Box<[NamedTupleField<'db>]>) -> Self { + Self::new(db, fields, true) + } + + /// Create a [`NamedTupleSpec`] that indicates a namedtuple class has unknown fields. + pub(crate) fn unknown(db: &'db dyn Db) -> Self { + Self::new(db, Box::default(), false) + } + + pub(crate) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + let fields = self + .fields(db) + .iter() + .map(|f| { + Some(NamedTupleField { + name: f.name.clone(), + ty: if nested { + f.ty.recursive_type_normalized_impl(db, div, nested)? + } else { + f.ty.recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div) + }, + default: None, + }) + }) + .collect::>>()?; + + Some(Self::new(db, fields, self.has_known_fields(db))) + } +} + +impl get_size2::GetSize for NamedTupleSpec<'_> {} + +/// Create a property type for a namedtuple field. +fn create_field_property<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Type<'db> { + let property_getter_signature = Signature::new( + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("self")))], + ), + field_ty, + ); + let property_getter = Type::single_callable(db, property_getter_signature); + let property = PropertyInstanceType::new(db, Some(property_getter), None); + Type::PropertyInstance(property) +} diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs new file mode 100644 index 0000000000000..972d3908a5aa5 --- /dev/null +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -0,0 +1,3129 @@ +use itertools::{Either, Itertools}; +use ruff_db::{ + diagnostic::Span, + files::File, + parsed::{ParsedModuleRef, parsed_module}, +}; +use ruff_python_ast as ast; +use ruff_python_ast::{PythonVersion, name::Name}; +use ruff_text_size::{Ranged, TextRange}; +use std::cell::RefCell; + +use crate::{ + Db, FxIndexMap, FxIndexSet, Program, TypeQualifiers, + place::{ + DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, Widening, + place_from_bindings, place_from_declarations, + }, + semantic_index::{ + DeclarationWithConstraint, attribute_assignments, attribute_declarations, attribute_scopes, + definition::{Definition, DefinitionKind, DefinitionState, TargetKind}, + place_table, + scope::{Scope, ScopeId}, + semantic_index, + symbol::Symbol, + use_def_map, + }, + types::{ + ApplyTypeMappingVisitor, BoundTypeVarInstance, CallArguments, CallableType, ClassBase, + ClassLiteral, ClassType, DATACLASS_FLAGS, DataclassFlags, DataclassParams, GenericAlias, + GenericContext, KnownClass, KnownInstanceType, MaterializationKind, MemberLookupPolicy, + MetaclassCandidate, MetaclassTransformInfo, Parameter, Parameters, PropertyInstanceType, + Signature, SpecialFormType, StaticMroError, SubclassOfType, Truthiness, Type, TypeContext, + TypeMapping, TypeVarVariance, UnionBuilder, UnionType, + call::{CallError, CallErrorKind}, + callable::CallableTypeKind, + class::{ + ClassMemberResult, CodeGeneratorKind, DisjointBase, Field, FieldKind, + InstanceMemberResult, MetaclassError, MetaclassErrorKind, MethodDecorator, MroLookup, + NamedTupleField, SlotsKind, synthesize_namedtuple_class_member, + }, + context::InferContext, + declaration_type, definition_expression_type, determine_upper_bound, + diagnostic::INVALID_DATACLASS_OVERRIDE, + enums::{enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value}, + function::{ + DataclassTransformerFlags, DataclassTransformerParams, KnownFunction, + is_implicit_classmethod, is_implicit_staticmethod, + }, + generics::Specialization, + infer::infer_unpack_types, + infer_expression_type, + known_instance::DeprecatedInstance, + member::{Member, class_member}, + mro::{Mro, MroIterator}, + signatures::CallableSignature, + tuple::{Tuple, TupleSpec, TupleType}, + typed_dict::{TypedDictParams, typed_dict_params_from_class_def}, + variance::VarianceInferable, + visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}, + }, +}; + +/// Representation of a class definition statement in the AST: either a non-generic class, or a +/// generic class that has not been specialized. +/// +/// This does not in itself represent a type, but can be transformed into a [`ClassType`] that +/// does. (For generic classes, this requires specializing its generic context.) +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct StaticClassLiteral<'db> { + /// Name of the class at definition + #[returns(ref)] + pub(crate) name: Name, + + pub(crate) body_scope: ScopeId<'db>, + + pub(crate) known: Option, + + /// If this class is deprecated, this holds the deprecation message. + pub(crate) deprecated: Option>, + + pub(crate) type_check_only: bool, + + pub(crate) dataclass_params: Option>, + pub(crate) dataclass_transformer_params: Option>, + + /// Whether this class is decorated with `@functools.total_ordering` + pub(crate) total_ordering: bool, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for StaticClassLiteral<'_> {} + +fn generic_context_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: StaticClassLiteral<'db>, +) -> Option> { + None +} + +#[salsa::tracked] +impl<'db> StaticClassLiteral<'db> { + /// Return `true` if this class represents `known_class` + pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { + self.known(db) == Some(known_class) + } + + pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { + self.is_known(db, KnownClass::Tuple) + } + + /// Returns `true` if this class inherits from a functional namedtuple + /// (`DynamicNamedTupleLiteral`) that has unknown fields. + /// + /// When the base namedtuple's fields were determined dynamically (e.g., from a variable), + /// we can't synthesize precise method signatures and should fall back to `NamedTupleFallback`. + pub(crate) fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { + self.explicit_bases(db).iter().any(|base| match base { + Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) => { + !namedtuple.has_known_fields(db) + } + _ => false, + }) + } + + /// Returns `true` if this class is a dataclass-like class. + /// + /// This covers `@dataclass`-decorated classes, as well as classes created via + /// `dataclass_transform` (function-based, metaclass-based, and base-class-based). + pub(crate) fn is_dataclass_like(self, db: &'db dyn Db) -> bool { + matches!( + CodeGeneratorKind::from_class(db, ClassLiteral::Static(self), None), + Some(CodeGeneratorKind::DataclassLike(_)) + ) + } + + /// Returns a new [`StaticClassLiteral`] with the given dataclass params, preserving all other fields. + pub(crate) fn with_dataclass_params( + self, + db: &'db dyn Db, + dataclass_params: Option>, + ) -> Self { + Self::new( + db, + self.name(db).clone(), + self.body_scope(db), + self.known(db), + self.deprecated(db), + self.type_check_only(db), + dataclass_params, + self.dataclass_transformer_params(db), + self.total_ordering(db), + ) + } + + /// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`, + /// `__ge__`) in its own body (not inherited). Used by `@total_ordering` to determine if + /// synthesis is valid. + #[salsa::tracked] + pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { + let body_scope = self.body_scope(db); + ["__lt__", "__le__", "__gt__", "__ge__"] + .iter() + .any(|method| !class_member(db, body_scope, method).is_undefined()) + } + + /// Returns `true` if any class in this class's MRO (excluding `object`) defines an ordering + /// method (`__lt__`, `__le__`, `__gt__`, `__ge__`). Used by `@total_ordering` validation. + pub(crate) fn has_ordering_method_in_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> bool { + self.total_ordering_root_method(db, specialization) + .is_some() + } + + /// Returns the type of the ordering method used by `@total_ordering`, if any. + /// + /// Following `functools.total_ordering` precedence, we prefer `__lt__` > `__le__` > `__gt__` > + /// `__ge__`, regardless of whether the method is defined locally or inherited. + /// + /// Note: We use direct scope lookups here to avoid infinite recursion + /// through `own_class_member` -> `own_synthesized_member`. + pub(super) fn total_ordering_root_method( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Option> { + const ORDERING_METHODS: [&str; 4] = ["__lt__", "__le__", "__gt__", "__ge__"]; + + for name in ORDERING_METHODS { + for base in self.iter_mro(db, specialization) { + let Some(base_class) = base.into_class() else { + continue; + }; + match base_class.class_literal(db) { + ClassLiteral::Static(base_literal) => { + if base_literal.is_known(db, KnownClass::Object) { + continue; + } + let member = class_member(db, base_literal.body_scope(db), name); + if let Some(ty) = member.ignore_possibly_undefined() { + let base_specialization = base_class + .static_class_literal(db) + .and_then(|(_, spec)| spec); + return Some(ty.apply_optional_specialization(db, base_specialization)); + } + } + ClassLiteral::Dynamic(dynamic) => { + // Dynamic classes (created with `type()`) can also define ordering methods + // in their namespace dict. + let member = dynamic.own_class_member(db, name); + if let Some(ty) = member.ignore_possibly_undefined() { + return Some(ty); + } + } + // Dynamic namedtuples don't define their own ordering methods. + ClassLiteral::DynamicNamedTuple(_) => {} + } + } + } + + None + } + + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code + // the knowledge that this class is not generic. + if self.is_known(db, KnownClass::VersionInfo) { + return None; + } + + // We've already verified that the class literal does not contain both a PEP-695 generic + // scope and a `typing.Generic` base class. + // + // Note that if a class has an explicit legacy generic context (by inheriting from + // `typing.Generic`), and also an implicit one (by inheriting from other generic classes, + // specialized by typevars), the explicit one takes precedence. + self.pep695_generic_context(db) + .or_else(|| self.legacy_generic_context(db)) + .or_else(|| self.inherited_legacy_generic_context(db)) + } + + pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + self.pep695_generic_context(db).is_some() + } + + #[salsa::tracked(cycle_initial=generic_context_cycle_initial, + heap_size=ruff_memory_usage::heap_size, + )] + pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option> { + let scope = self.body_scope(db); + let file = scope.file(db); + let parsed = parsed_module(db, file).load(db); + let class_def_node = scope.node(db).expect_class().node(&parsed); + class_def_node.type_params.as_ref().map(|type_params| { + let index = semantic_index(db, scope.file(db)); + let definition = index.expect_single_definition(class_def_node); + GenericContext::from_type_params(db, index, definition, type_params) + }) + } + + pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option> { + self.explicit_bases(db).iter().find_map(|base| match base { + Type::KnownInstance( + KnownInstanceType::SubscriptedGeneric(generic_context) + | KnownInstanceType::SubscriptedProtocol(generic_context), + ) => Some(*generic_context), + _ => None, + }) + } + + #[salsa::tracked(cycle_initial=generic_context_cycle_initial, + heap_size=ruff_memory_usage::heap_size, + )] + pub(crate) fn inherited_legacy_generic_context( + self, + db: &'db dyn Db, + ) -> Option> { + GenericContext::from_base_classes( + db, + self.definition(db), + self.explicit_bases(db) + .iter() + .copied() + .filter(|ty| matches!(ty, Type::GenericAlias(_))), + ) + } + + /// Returns all of the typevars that are referenced in this class's base class list. + /// (This is used to ensure that classes do not reference typevars from enclosing + /// generic contexts.) + pub(crate) fn typevars_referenced_in_bases( + self, + db: &'db dyn Db, + ) -> FxIndexSet> { + #[derive(Default)] + struct CollectTypeVars<'db> { + typevars: RefCell>>, + recursion_guard: TypeCollector<'db>, + } + + impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> { + fn should_visit_lazy_type_attributes(&self) -> bool { + false + } + + fn visit_bound_type_var_type( + &self, + _db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, + ) { + self.typevars.borrow_mut().insert(bound_typevar); + } + + fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) { + walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); + } + } + + let visitor = CollectTypeVars::default(); + for base in self.explicit_bases(db) { + visitor.visit_type(db, *base); + } + visitor.typevars.into_inner() + } + + /// Returns the generic context that should be inherited by any constructor methods of this class. + pub(super) fn inherited_generic_context(self, db: &'db dyn Db) -> Option> { + self.generic_context(db) + } + + pub(crate) fn file(self, db: &dyn Db) -> File { + self.body_scope(db).file(db) + } + + /// Return the original [`ast::StmtClassDef`] node associated with this class + /// + /// ## Note + /// Only call this function from queries in the same file or your + /// query depends on the AST of another file (bad!). + fn node<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast ast::StmtClassDef { + let scope = self.body_scope(db); + scope.node(db).expect_class().node(module) + } + + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + let body_scope = self.body_scope(db); + let index = semantic_index(db, body_scope.file(db)); + index.expect_single_definition(body_scope.node(db).expect_class()) + } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> ClassType<'db> { + match self.generic_context(db) { + None => ClassType::NonGeneric(self.into()), + Some(generic_context) => { + let specialization = f(generic_context); + + ClassType::Generic(GenericAlias::new(db, self, specialization)) + } + } + } + + pub(crate) fn apply_optional_specialization( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + specialization + .unwrap_or_else(|| generic_context.default_specialization(db, self.known(db))) + }) + } + + pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context + .default_specialization(db, self.known(db)) + .materialize_impl( + db, + MaterializationKind::Top, + &ApplyTypeMappingVisitor::default(), + ) + }) + } + + /// Returns the default specialization of this class. For non-generic classes, the class is + /// returned unchanged. For a non-specialized generic class, we return a generic alias that + /// applies the default specialization to the class's typevars. + pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context.default_specialization(db, self.known(db)) + }) + } + + /// Returns the unknown specialization of this class. For non-generic classes, the class is + /// returned unchanged. For a non-specialized generic class, we return a generic alias that + /// maps each of the class's typevars to `Unknown`. + pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context.unknown_specialization(db) + }) + } + + /// Returns a specialization of this class where each typevar is mapped to itself. + pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context.identity_specialization(db) + }) + } + + /// Return an iterator over the inferred types of this class's *explicit* bases. + /// + /// Note that any class (except for `object`) that has no explicit + /// bases will implicitly inherit from `object` at runtime. Nonetheless, + /// this method does *not* include `object` in the bases it iterates over. + /// + /// ## Why is this a salsa query? + /// + /// This is a salsa query to short-circuit the invalidation + /// when the class's AST node changes. + /// + /// Were this not a salsa query, then the calling query + /// would depend on the class's AST and rerun for every change in that file. + #[salsa::tracked(returns(deref), cycle_initial=explicit_bases_cycle_initial, cycle_fn=explicit_bases_cycle_fn, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + tracing::trace!( + "StaticClassLiteral::explicit_bases_query: {}", + self.name(db) + ); + + let module = parsed_module(db, self.file(db)).load(db); + let class_stmt = self.node(db, &module); + + let class_definition = + semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + + match self.known(db) { + Some(KnownClass::VersionInfo) => { + let tuple_type = TupleType::new(db, &TupleSpec::version_info_spec(db)) + .expect("sys.version_info tuple spec should always be a valid tuple"); + + Box::new([ + definition_expression_type(db, class_definition, &class_stmt.bases()[0]), + Type::from(tuple_type.to_class_type(db)), + ]) + } + // Special-case `NotImplementedType`: typeshed says that it inherits from `Any`, + // but this causes more problems than it fixes. + Some(KnownClass::NotImplementedType) => Box::new([]), + _ => class_stmt + .bases() + .iter() + .flat_map(|base_node| { + if let ast::Expr::Starred(starred) = base_node { + let starred_ty = + definition_expression_type(db, class_definition, &starred.value); + // If the starred expression is a fixed-length tuple, unpack it. + if let Some(Tuple::Fixed(tuple)) = starred_ty + .tuple_instance_spec(db) + .map(std::borrow::Cow::into_owned) + { + return Either::Left(tuple.owned_elements().into_vec().into_iter()); + } + // Otherwise, we can't statically determine the bases. + Either::Right(std::iter::once(Type::unknown())) + } else { + Either::Right(std::iter::once(definition_expression_type( + db, + class_definition, + base_node, + ))) + } + }) + .collect(), + } + } + + /// Return `Some()` if this class is known to be a [`DisjointBase`], or `None` if it is not. + pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + if self + .known_function_decorators(db) + .contains(&KnownFunction::DisjointBase) + { + Some(DisjointBase::due_to_decorator(self)) + } else if SlotsKind::from(db, self) == SlotsKind::NotEmpty { + Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Static( + self, + ))) + } else { + None + } + } + + /// Iterate over this class's explicit bases, filtering out any bases that are not class + /// objects, and applying default specialization to any unspecialized generic class literals. + fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { + self.explicit_bases(db) + .iter() + .copied() + .filter_map(|ty| ty.to_class_type(db)) + } + + /// Determine if this class is a protocol. + /// + /// This method relies on the accuracy of the [`KnownClass::is_protocol`] method, + /// which hardcodes knowledge about certain special-cased classes. See the docs on + /// that method for why we do this rather than relying on generalised logic for all + /// classes, including the special-cased ones that are included in the [`KnownClass`] + /// enum. + pub(crate) fn is_protocol(self, db: &'db dyn Db) -> bool { + self.known(db) + .map(KnownClass::is_protocol) + .unwrap_or_else(|| { + // Iterate through the last three bases of the class + // searching for `Protocol` or `Protocol[]` in the bases list. + // + // If `Protocol` is present in the bases list of a valid protocol class, it must either: + // + // - be the last base + // - OR be the last-but-one base (with the final base being `Generic[]` or `object`) + // - OR be the last-but-two base (with the penultimate base being `Generic[]` + // and the final base being `object`) + self.explicit_bases(db).iter().rev().take(3).any(|base| { + matches!( + base, + Type::SpecialForm(SpecialFormType::Protocol) + | Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)) + ) + }) + }) + } + + /// Return the types of the decorators on this class + #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] + fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + tracing::trace!("StaticClassLiteral::decorators: {}", self.name(db)); + + let module = parsed_module(db, self.file(db)).load(db); + + let class_stmt = self.node(db, &module); + if class_stmt.decorator_list.is_empty() { + return Box::new([]); + } + + let class_definition = + semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + + class_stmt + .decorator_list + .iter() + .map(|decorator_node| { + definition_expression_type(db, class_definition, &decorator_node.expression) + }) + .collect() + } + + pub(crate) fn known_function_decorators( + self, + db: &'db dyn Db, + ) -> impl Iterator + 'db { + self.decorators(db) + .iter() + .filter_map(|deco| deco.as_function_literal()) + .filter_map(|decorator| decorator.known(db)) + } + + /// Iterate through the decorators on this class, returning the position of the first one + /// that matches the given predicate. + pub(super) fn find_decorator_position( + self, + db: &'db dyn Db, + predicate: impl Fn(Type<'db>) -> bool, + ) -> Option { + self.decorators(db) + .iter() + .position(|decorator| predicate(*decorator)) + } + + /// Iterate through the decorators on this class, returning the index of the first one + /// that is either `@dataclass` or `@dataclass(...)`. + pub(crate) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { + self.find_decorator_position(db, |ty| match ty { + Type::FunctionLiteral(function) => function.is_known(db, KnownFunction::Dataclass), + Type::DataclassDecorator(_) => true, + _ => false, + }) + } + + /// Is this class final? + pub(crate) fn is_final(self, db: &'db dyn Db) -> bool { + self.known_function_decorators(db) + .contains(&KnownFunction::Final) + || enum_metadata(db, ClassLiteral::Static(self)).is_some() + } + + /// Attempt to resolve the [method resolution order] ("MRO") for this class. + /// If the MRO is unresolvable, return an error indicating why the class's MRO + /// cannot be accurately determined. The error returned contains a fallback MRO + /// that will be used instead for the purposes of type inference. + /// + /// The MRO is the tuple of classes that can be retrieved as the `__mro__` + /// attribute on a class at runtime. + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + #[salsa::tracked(returns(as_ref), cycle_initial=static_class_try_mro_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn try_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Result, StaticMroError<'db>> { + tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db)); + Mro::of_static_class(db, self, specialization) + } + + /// Iterate over the [method resolution order] ("MRO") of the class. + /// + /// If the MRO could not be accurately resolved, this method falls back to iterating + /// over an MRO that has the class directly inheriting from `Unknown`. Use + /// [`StaticClassLiteral::try_mro`] if you need to distinguish between the success and failure + /// cases rather than simply iterating over the inferred resolution order for the class. + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(crate) fn iter_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> MroIterator<'db> { + MroIterator::new(db, ClassLiteral::Static(self), specialization) + } + + /// Return `true` if `other` is present in this class's MRO. + pub(super) fn is_subclass_of( + self, + db: &'db dyn Db, + specialization: Option>, + other: ClassType<'db>, + ) -> bool { + // `is_subclass_of` is checking the subtype relation, in which gradual types do not + // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. + self.iter_mro(db, specialization) + .contains(&ClassBase::Class(other)) + } + + /// Return `true` if this class constitutes a typed dict specification (inherits from + /// `typing.TypedDict`, either directly or indirectly). + #[salsa::tracked(cycle_initial=|_, _, _| false, heap_size=ruff_memory_usage::heap_size)] + pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { + if let Some(known) = self.known(db) { + return known.is_typed_dict_subclass(); + } + + self.iter_mro(db, None) + .any(|base| matches!(base, ClassBase::TypedDict)) + } + + /// Return `true` if this class is, or inherits from, a `NamedTuple` (inherits from + /// `typing.NamedTuple`, either directly or indirectly, including functional forms like + /// `NamedTuple("X", ...)`). + pub(crate) fn has_named_tuple_class_in_mro(self, db: &'db dyn Db) -> bool { + self.iter_mro(db, None) + .filter_map(ClassBase::into_class) + .any(|base| match base.class_literal(db) { + ClassLiteral::DynamicNamedTuple(_) => true, + ClassLiteral::Dynamic(_) => false, + ClassLiteral::Static(class) => class + .explicit_bases(db) + .contains(&Type::SpecialForm(SpecialFormType::NamedTuple)), + }) + } + + /// Compute `TypedDict` parameters dynamically based on MRO detection and AST parsing. + fn typed_dict_params(self, db: &'db dyn Db) -> Option { + if !self.is_typed_dict(db) { + return None; + } + + let module = parsed_module(db, self.file(db)).load(db); + let class_stmt = self.node(db, &module); + Some(typed_dict_params_from_class_def(class_stmt)) + } + + /// Returns dataclass params for this class, sourced from both dataclass params and dataclass + /// transform params + fn merged_dataclass_params( + self, + db: &'db dyn Db, + field_policy: CodeGeneratorKind<'db>, + ) -> (Option>, Option>) { + let dataclass_params = self.dataclass_params(db); + + let mut transformer_params = + if let CodeGeneratorKind::DataclassLike(Some(transformer_params)) = field_policy { + Some(DataclassParams::from_transformer_params( + db, + transformer_params, + )) + } else { + None + }; + + // Dataclass transformer flags can be overwritten using class arguments. + if let Some(transformer_params) = transformer_params.as_mut() { + if let Some(class_def) = self.definition(db).kind(db).as_class() { + let module = parsed_module(db, self.file(db)).load(db); + + if let Some(arguments) = &class_def.node(&module).arguments { + let mut flags = transformer_params.flags(db); + + for keyword in &arguments.keywords { + if let Some(arg_name) = &keyword.arg { + if let Some(is_set) = + keyword.value.as_boolean_literal_expr().map(|b| b.value) + { + for (flag_name, flag) in DATACLASS_FLAGS { + if arg_name.as_str() == *flag_name { + flags.set(*flag, is_set); + } + } + } + } + } + + *transformer_params = + DataclassParams::new(db, flags, transformer_params.field_specifiers(db)); + } + } + } + + (dataclass_params, transformer_params) + } + + /// Returns the effective frozen status of this class if it's a dataclass-like class. + /// + /// Returns `Some(true)` for a frozen dataclass-like class, `Some(false)` for a non-frozen one, + /// and `None` if the class is not a dataclass-like class, or if the dataclass is neither frozen + /// nor non-frozen. + pub(crate) fn is_frozen_dataclass(self, db: &'db dyn Db) -> Option { + // Check if this is a base-class-based transformer that has dataclass_transformer_params directly + // attached to it (because it is itself decorated with `@dataclass_transform`), or if this class + // has an explicit metaclass that is decorated with `@dataclass_transform`. + // + // In both cases, this signifies that this class is neither frozen nor non-frozen. + // + // See for details. + if self.dataclass_transformer_params(db).is_some() + || self + .try_metaclass(db) + .is_ok_and(|(_, info)| info.is_some_and(|i| i.from_explicit_metaclass)) + { + return None; + } + + if let field_policy @ CodeGeneratorKind::DataclassLike(_) = + CodeGeneratorKind::from_class(db, self.into(), None)? + { + // Otherwise, if this class is a dataclass-like class, determine its frozen status based on + // dataclass params and dataclass transformer params. + Some(self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN)) + } else { + None + } + } + + /// Checks if the given dataclass parameter flag is set for this class. + /// This checks both the `dataclass_params` and `transformer_params`. + fn has_dataclass_param( + self, + db: &'db dyn Db, + field_policy: CodeGeneratorKind<'db>, + param: DataclassFlags, + ) -> bool { + let (dataclass_params, transformer_params) = self.merged_dataclass_params(db, field_policy); + dataclass_params.is_some_and(|params| params.flags(db).contains(param)) + || transformer_params.is_some_and(|params| params.flags(db).contains(param)) + } + + /// Return the explicit `metaclass` of this class, if one is defined. + /// + /// ## Note + /// Only call this function from queries in the same file or your + /// query depends on the AST of another file (bad!). + fn explicit_metaclass(self, db: &'db dyn Db, module: &ParsedModuleRef) -> Option> { + let class_stmt = self.node(db, module); + let metaclass_node = &class_stmt + .arguments + .as_ref()? + .find_keyword("metaclass")? + .value; + + let class_definition = self.definition(db); + + Some(definition_expression_type( + db, + class_definition, + metaclass_node, + )) + } + + /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. + pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + self.try_metaclass(db) + .map(|(ty, _)| ty) + .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) + } + + /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. + #[salsa::tracked(cycle_initial=try_metaclass_cycle_initial, + heap_size=ruff_memory_usage::heap_size, + )] + pub(crate) fn try_metaclass( + self, + db: &'db dyn Db, + ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { + tracing::trace!("StaticClassLiteral::try_metaclass: {}", self.name(db)); + + // Identify the class's own metaclass (or take the first base class's metaclass). + let mut base_classes = self.fully_static_explicit_bases(db).peekable(); + + if base_classes.peek().is_some() && self.inheritance_cycle(db).is_some() { + // We emit diagnostics for cyclic class definitions elsewhere. + // Avoid attempting to infer the metaclass if the class is cyclically defined. + return Ok((SubclassOfType::subclass_of_unknown(), None)); + } + + if self.try_mro(db, None).is_err_and(StaticMroError::is_cycle) { + return Ok((SubclassOfType::subclass_of_unknown(), None)); + } + + let module = parsed_module(db, self.file(db)).load(db); + + let explicit_metaclass = self.explicit_metaclass(db, &module); + + // Generic metaclasses parameterized by type variables are not supported. + // `metaclass=Meta[int]` is fine, but `metaclass=Meta[T]` is not. + // See: https://typing.python.org/en/latest/spec/generics.html#generic-metaclasses + if let Some(Type::GenericAlias(alias)) = explicit_metaclass { + let specialization_has_typevars = alias + .specialization(db) + .types(db) + .iter() + .any(|ty| ty.has_typevar_or_typevar_instance(db)); + if specialization_has_typevars { + return Err(MetaclassError { + kind: MetaclassErrorKind::GenericMetaclass, + }); + } + } + + let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { + (metaclass, self) + } else if let Some(base_class) = base_classes.next() { + // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking. + let base_class_literal = base_class + .static_class_literal(db) + .map(|(lit, _)| lit) + .unwrap_or(self); + (base_class.metaclass(db), base_class_literal) + } else { + (KnownClass::Type.to_class_literal(db), self) + }; + + let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) { + MetaclassCandidate { + metaclass: metaclass_ty, + explicit_metaclass_of: class_metaclass_was_from, + } + } else { + let name = Type::string_literal(db, self.name(db)); + let bases = Type::heterogeneous_tuple(db, self.explicit_bases(db)); + let namespace = KnownClass::Dict + .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); + + // TODO: Other keyword arguments? + let arguments = CallArguments::positional([name, bases, namespace]); + + let return_ty_result = match metaclass.try_call(db, &arguments) { + Ok(bindings) => Ok(bindings.return_type(db)), + + Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError { + kind: MetaclassErrorKind::NotCallable(bindings.callable_type()), + }), + + // TODO we should also check for binding errors that would indicate the metaclass + // does not accept the right arguments + Err(CallError(CallErrorKind::BindingError, bindings)) => { + Ok(bindings.return_type(db)) + } + + Err(CallError(CallErrorKind::PossiblyNotCallable, _)) => Err(MetaclassError { + kind: MetaclassErrorKind::PartlyNotCallable(metaclass), + }), + }; + + return return_ty_result.map(|ty| (ty.to_meta_type(db), None)); + }; + + // Reconcile all base classes' metaclasses with the candidate metaclass. + // + // See: + // - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass + // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 + for base_class in base_classes { + let metaclass = base_class.metaclass(db); + let Some(metaclass) = metaclass.to_class_type(db) else { + continue; + }; + // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking. + let base_class_literal = base_class + .static_class_literal(db) + .map(|(lit, _)| lit) + .unwrap_or(self); + if metaclass.is_subclass_of(db, candidate.metaclass) { + candidate = MetaclassCandidate { + metaclass, + explicit_metaclass_of: base_class_literal, + }; + continue; + } + if candidate.metaclass.is_subclass_of(db, metaclass) { + continue; + } + return Err(MetaclassError { + kind: MetaclassErrorKind::Conflict { + candidate1: candidate, + candidate2: MetaclassCandidate { + metaclass, + explicit_metaclass_of: base_class_literal, + }, + candidate1_is_base_class: explicit_metaclass.is_none(), + }, + }); + } + + let transform_info = candidate + .metaclass + .static_class_literal(db) + .and_then(|(metaclass_literal, _)| metaclass_literal.dataclass_transformer_params(db)) + .map(|params| MetaclassTransformInfo { + params, + from_explicit_metaclass: candidate.explicit_metaclass_of == self, + }); + Ok((candidate.metaclass.into(), transform_info)) + } + + /// Returns the class member of this class named `name`. + /// + /// The member resolves to a member on the class itself or any of its proper superclasses. + /// + /// TODO: Should this be made private...? + pub(super) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + fn into_function_like_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> { + match ty { + Type::Callable(callable_ty) => Type::Callable(CallableType::new( + db, + callable_ty.signatures(db), + CallableTypeKind::FunctionLike, + )), + Type::Union(union) => { + union.map(db, |element| into_function_like_callable(db, *element)) + } + Type::Intersection(intersection) => intersection + .map_positive(db, |element| into_function_like_callable(db, *element)), + _ => ty, + } + } + + let mut member = self.class_member_inner(db, None, name, policy); + + // We generally treat dunder attributes with `Callable` types as function-like callables. + // See `callables_as_descriptors.md` for more details. + if name.starts_with("__") && name.ends_with("__") { + member = member.map_type(|ty| into_function_like_callable(db, ty)); + } + + member + } + + pub(super) fn class_member_inner( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) + } + + pub(crate) fn class_member_from_mro( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + mro_iter: impl Iterator>, + ) -> PlaceAndQualifiers<'db> { + let result = MroLookup::new(db, mro_iter).class_member( + name, + policy, + self.inherited_generic_context(db), + self.is_known(db, KnownClass::Object), + ); + + match result { + ClassMemberResult::Done(result) => result.finalize(db), + + ClassMemberResult::TypedDict => KnownClass::TypedDictFallback + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + .expect("Will return Some() when called on class literal") + .map_type(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: determine_upper_bound( + db, + self, + None, + ClassBase::is_typed_dict, + ), + }, + TypeContext::default(), + ) + }), + } + } + + /// Returns the inferred type of the class member named `name`. Only bound members + /// or those marked as `ClassVars` are considered. + /// + /// Returns [`Place::Undefined`] if `name` cannot be found in this class's scope + /// directly. Use [`StaticClassLiteral::class_member`] if you require a method that will + /// traverse through the MRO until it finds the member. + pub(super) fn own_class_member( + self, + db: &'db dyn Db, + inherited_generic_context: Option>, + specialization: Option>, + name: &str, + ) -> Member<'db> { + // Check if this class is dataclass-like (either via @dataclass or via dataclass_transform) + if matches!( + CodeGeneratorKind::from_class(db, self.into(), specialization), + Some(CodeGeneratorKind::DataclassLike(_)) + ) { + if name == "__dataclass_fields__" { + // Make this class look like a subclass of the `DataClassInstance` protocol + return Member { + inner: Place::declared(KnownClass::Dict.to_specialized_instance( + db, + &[ + KnownClass::Str.to_instance(db), + KnownClass::Field.to_specialized_instance(db, &[Type::any()]), + ], + )) + .with_qualifiers(TypeQualifiers::CLASS_VAR), + }; + } else if name == "__dataclass_params__" { + // There is no typeshed class for this. For now, we model it as `Any`. + return Member { + inner: Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR), + }; + } + } + + if CodeGeneratorKind::NamedTuple.matches(db, self.into(), specialization) { + if let Some(field) = self + .own_fields(db, specialization, CodeGeneratorKind::NamedTuple) + .get(name) + { + let property_getter_signature = Signature::new( + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("self")))], + ), + field.declared_ty, + ); + let property_getter = Type::single_callable(db, property_getter_signature); + let property = PropertyInstanceType::new(db, Some(property_getter), None); + return Member::definitely_declared(Type::PropertyInstance(property)); + } + } + + let body_scope = self.body_scope(db); + let member = class_member(db, body_scope, name).map_type(|ty| { + // The `__new__` and `__init__` members of a non-specialized generic class are handled + // specially: they inherit the generic context of their class. That lets us treat them + // as generic functions when constructing the class, and infer the specialization of + // the class from the arguments that are passed in. + // + // We might decide to handle other class methods the same way, having them inherit the + // class's generic context, and performing type inference on calls to them to determine + // the specialization of the class. If we do that, we would update this to also apply + // to any method with a `@classmethod` decorator. (`__init__` would remain a special + // case, since it's an _instance_ method where we don't yet know the generic class's + // specialization.) + match (inherited_generic_context, ty, specialization, name) { + ( + Some(generic_context), + Type::FunctionLiteral(function), + Some(_), + "__new__" | "__init__", + ) => Type::FunctionLiteral( + function.with_inherited_generic_context(db, generic_context), + ), + _ => ty, + } + }); + + if member.is_undefined() { + if let Some(synthesized_member) = + self.own_synthesized_member(db, specialization, inherited_generic_context, name) + { + return Member::definitely_declared(synthesized_member); + } + // The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s. + return Self::implicit_attribute(db, body_scope, name, MethodDecorator::ClassMethod); + } + + // For dataclass-like classes, `KW_ONLY` sentinel fields are not real + // class attributes; they are markers used by the dataclass decorator to + // indicate that subsequent fields are keyword-only. Treat them as + // undefined so the MRO falls through to parent classes. + if member + .inner + .place + .unwidened_type() + .is_some_and(|ty| ty.is_instance_of(db, KnownClass::KwOnly)) + && CodeGeneratorKind::from_static_class(db, self, None) + .is_some_and(|policy| matches!(policy, CodeGeneratorKind::DataclassLike(_))) + { + return Member::unbound(); + } + + // For enum classes, `nonmember(value)` creates a non-member attribute. + // At runtime, the enum metaclass unwraps the value, so accessing the attribute + // returns the inner value, not the `nonmember` wrapper. + if let Some(ty) = member.inner.place.unwidened_type() { + if let Some(value_ty) = try_unwrap_nonmember_value(db, ty) { + if is_enum_class_by_inheritance(db, self) { + return Member::definitely_declared(value_ty); + } + } + } + + member + } + + /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or + /// a synthesized `__new__` method for a `NamedTuple`. + pub(crate) fn own_synthesized_member( + self, + db: &'db dyn Db, + specialization: Option>, + inherited_generic_context: Option>, + name: &str, + ) -> Option> { + // Handle `@functools.total_ordering`: synthesize comparison methods + // for classes that have `@total_ordering` and define at least one + // ordering method. The decorator requires at least one of __lt__, + // __le__, __gt__, or __ge__ to be defined (either in this class or + // inherited from a superclass, excluding `object`). + // + // Only synthesize methods that are not already defined in the MRO. + // Note: We use direct scope lookups here to avoid infinite recursion + // through `own_class_member` -> `own_synthesized_member`. + if self.total_ordering(db) + && matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") + && !self + .iter_mro(db, specialization) + .filter_map(ClassBase::into_class) + .filter_map(|class| class.static_class_literal(db)) + .filter(|(class, _)| !class.is_known(db, KnownClass::Object)) + .any(|(class, _)| { + class_member(db, class.body_scope(db), name) + .ignore_possibly_undefined() + .is_some() + }) + && self.has_ordering_method_in_mro(db, specialization) + && let Some(root_method_ty) = self.total_ordering_root_method(db, specialization) + && let Some(callables) = root_method_ty.try_upcast_to_callable(db) + { + let bool_ty = KnownClass::Bool.to_instance(db); + let synthesized_callables = callables.map(|callable| { + let signatures = CallableSignature::from_overloads( + callable.signatures(db).iter().map(|signature| { + // The generated methods return a union of the root method's return type + // and `bool`. This is because `@total_ordering` synthesizes methods like: + // def __gt__(self, other): return not (self == other or self < other) + // If `__lt__` returns `int`, then `__gt__` could return `int | bool`. + let return_ty = + UnionType::from_two_elements(db, signature.return_ty, bool_ty); + Signature::new_generic( + signature.generic_context, + signature.parameters().clone(), + return_ty, + ) + }), + ); + CallableType::new(db, signatures, CallableTypeKind::FunctionLike) + }); + + return Some(synthesized_callables.into_type(db)); + } + + let field_policy = CodeGeneratorKind::from_class(db, self.into(), specialization)?; + + let instance_ty = + Type::instance(db, self.apply_optional_specialization(db, specialization)); + + let signature_from_fields = |mut parameters: Vec<_>, return_ty: Type<'db>| { + for (field_name, field) in self.fields(db, specialization, field_policy) { + let (init, mut default_ty, kw_only, alias) = match &field.kind { + FieldKind::NamedTuple { default_ty } => (true, *default_ty, None, None), + FieldKind::Dataclass { + init, + default_ty, + kw_only, + alias, + .. + } => (*init, *default_ty, *kw_only, alias.as_ref()), + FieldKind::TypedDict { .. } => continue, + }; + let mut field_ty = field.declared_ty; + + if name == "__init__" && !init { + // Skip fields with `init=False` + continue; + } + + if field.is_kw_only_sentinel(db) { + // Attributes annotated with `dataclass.KW_ONLY` are not present in the synthesized + // `__init__` method; they are used to indicate that the following parameters are + // keyword-only. + continue; + } + + let dunder_set = field_ty.class_member(db, "__set__".into()); + if let Place::Defined(DefinedPlace { + ty: dunder_set, + definedness: 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 + // problem is that we can't determine the type of the value parameter this way. + // Instead, we want to use the dynamic type itself in this case, so we skip the + // special descriptor handling. + if !dunder_set.is_dynamic() { + // This type of this attribute is a data descriptor. Instead of overwriting the + // descriptor attribute, data-classes will (implicitly) call the `__set__` method + // of the descriptor. This means that the synthesized `__init__` parameter for + // this attribute is determined by possible `value` parameter types with which + // the `__set__` method can be called. + // + // We union parameter types across overloads of a single callable, intersect + // callable bindings inside an intersection element, and union outer elements. + field_ty = dunder_set.bindings(db).map_types(db, |binding| { + let mut value_types = UnionBuilder::new(db); + let mut has_value_type = false; + for overload in binding { + if let Some(value_param) = + overload.signature.parameters().get_positional(2) + { + value_types = value_types.add(value_param.annotated_type()); + has_value_type = true; + } else if overload.signature.parameters().is_gradual() { + value_types = value_types.add(Type::unknown()); + has_value_type = true; + } + } + has_value_type.then(|| value_types.build()) + }); + + // The default value of the attribute is *not* determined by the right hand side + // of the class-body assignment. Instead, the runtime invokes `__get__` on the + // descriptor, as if it had been called on the class itself, i.e. it passes `None` + // for the `instance` argument. + + if let Some(ref mut default_ty) = default_ty { + *default_ty = default_ty + .try_call_dunder_get(db, None, Type::from(self)) + .map(|(return_ty, _)| return_ty) + .unwrap_or_else(Type::unknown); + } + } + } + + let is_kw_only = + matches!(name, "__replace__" | "_replace") || kw_only.unwrap_or(false); + + // Use the alias name if provided, otherwise use the field name + let parameter_name = + Name::new(alias.map(|alias| &**alias).unwrap_or(&**field_name)); + + let mut parameter = if is_kw_only { + Parameter::keyword_only(parameter_name) + } else { + Parameter::positional_or_keyword(parameter_name) + } + .with_annotated_type(field_ty); + + parameter = if matches!(name, "__replace__" | "_replace") { + // When replacing, we know there is a default value for the field + // (the value that is currently assigned to the field) + // assume this to be the declared type of the field + parameter.with_default_type(field_ty) + } else { + parameter.with_optional_default_type(default_ty) + }; + + parameters.push(parameter); + } + + // In the event that we have a mix of keyword-only and positional parameters, we need to sort them + // so that the keyword-only parameters appear after positional parameters. + parameters.sort_by_key(Parameter::is_keyword_only); + + let signature = match name { + "__new__" | "__init__" => Signature::new_generic( + inherited_generic_context.or_else(|| self.inherited_generic_context(db)), + Parameters::new(db, parameters), + return_ty, + ), + _ => Signature::new(Parameters::new(db, parameters), return_ty), + }; + Some(Type::function_like_callable(db, signature)) + }; + + match (field_policy, name) { + (CodeGeneratorKind::DataclassLike(_), "__init__") => { + if !self.has_dataclass_param(db, field_policy, DataclassFlags::INIT) { + return None; + } + + let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + // TODO: could be `Self`. + .with_annotated_type(instance_ty); + signature_from_fields(vec![self_parameter], Type::none(db)) + } + ( + CodeGeneratorKind::NamedTuple, + "__new__" | "__init__" | "_replace" | "__replace__" | "_fields", + ) if self.namedtuple_base_has_unknown_fields(db) => { + // When the namedtuple base has unknown fields, fall back to NamedTupleFallback + // which has generic signatures that accept any arguments. + KnownClass::NamedTupleFallback + .to_class_literal(db) + .as_class_literal()? + .as_static()? + .own_class_member(db, inherited_generic_context, None, name) + .ignore_possibly_undefined() + .map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: instance_ty, + }, + TypeContext::default(), + ) + }) + } + ( + CodeGeneratorKind::NamedTuple, + "__new__" | "_replace" | "__replace__" | "_fields" | "__slots__", + ) => { + let fields = self.fields(db, specialization, field_policy); + let fields_iter = fields.iter().map(|(name, field)| { + let default_ty = match &field.kind { + FieldKind::NamedTuple { default_ty } => *default_ty, + _ => None, + }; + NamedTupleField { + name: name.clone(), + ty: field.declared_ty, + default: default_ty, + } + }); + synthesize_namedtuple_class_member( + db, + name, + instance_ty, + fields_iter, + specialization.map(|s| s.generic_context(db)), + ) + } + (CodeGeneratorKind::DataclassLike(_), "__lt__" | "__le__" | "__gt__" | "__ge__") => { + if !self.has_dataclass_param(db, field_policy, DataclassFlags::ORDER) { + return None; + } + + let signature = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("self")) + // TODO: could be `Self`. + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("other")) + // TODO: could be `Self`. + .with_annotated_type(instance_ty), + ], + ), + KnownClass::Bool.to_instance(db), + ); + + Some(Type::function_like_callable(db, signature)) + } + (CodeGeneratorKind::DataclassLike(_), "__hash__") => { + let unsafe_hash = + self.has_dataclass_param(db, field_policy, DataclassFlags::UNSAFE_HASH); + let frozen = self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN); + let eq = self.has_dataclass_param(db, field_policy, DataclassFlags::EQ); + + if unsafe_hash || (frozen && eq) { + let signature = Signature::new( + Parameters::new( + db, + [Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty)], + ), + KnownClass::Int.to_instance(db), + ); + + Some(Type::function_like_callable(db, signature)) + } else if eq && !frozen { + Some(Type::none(db)) + } else { + // No `__hash__` is generated, fall back to `object.__hash__` + None + } + } + (CodeGeneratorKind::DataclassLike(_), "__match_args__") + if Program::get(db).python_version(db) >= PythonVersion::PY310 => + { + if !self.has_dataclass_param(db, field_policy, DataclassFlags::MATCH_ARGS) { + return None; + } + + let kw_only_default = + self.has_dataclass_param(db, field_policy, DataclassFlags::KW_ONLY); + + let fields = self.fields(db, specialization, field_policy); + let match_args = fields + .iter() + .filter(|(_, field)| { + if let FieldKind::Dataclass { init, kw_only, .. } = &field.kind { + *init && !kw_only.unwrap_or(kw_only_default) + } else { + false + } + }) + .map(|(name, _)| Type::string_literal(db, name)); + Some(Type::heterogeneous_tuple(db, match_args)) + } + (CodeGeneratorKind::DataclassLike(_), "__weakref__") + if Program::get(db).python_version(db) >= PythonVersion::PY311 => + { + if !self.has_dataclass_param(db, field_policy, DataclassFlags::WEAKREF_SLOT) + || !self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) + { + return None; + } + + // This could probably be `weakref | None`, but it does not seem important enough to + // model it precisely. + Some(UnionType::from_two_elements( + db, + Type::any(), + Type::none(db), + )) + } + (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { + KnownClass::NamedTupleFallback + .to_class_literal(db) + .as_class_literal()? + .as_static()? + .own_class_member(db, self.inherited_generic_context(db), None, name) + .ignore_possibly_undefined() + .map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: determine_upper_bound( + db, + self, + specialization, + |base| { + base.into_class() + .is_some_and(|c| c.is_known(db, KnownClass::Tuple)) + }, + ), + }, + TypeContext::default(), + ) + }) + } + (CodeGeneratorKind::DataclassLike(_), "__replace__") + if Program::get(db).python_version(db) >= PythonVersion::PY313 => + { + let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty); + + signature_from_fields(vec![self_parameter], instance_ty) + } + (CodeGeneratorKind::DataclassLike(_), "__setattr__") => { + if self.is_frozen_dataclass(db) == Some(true) { + let signature = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("name")), + Parameter::positional_or_keyword(Name::new_static("value")), + ], + ), + Type::Never, + ); + + return Some(Type::function_like_callable(db, signature)); + } + None + } + (CodeGeneratorKind::DataclassLike(_), "__slots__") + if Program::get(db).python_version(db) >= PythonVersion::PY310 => + { + self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) + .then(|| { + let fields = self.fields(db, specialization, field_policy); + let slots = fields.keys().map(|name| Type::string_literal(db, name)); + Type::heterogeneous_tuple(db, slots) + }) + } + (CodeGeneratorKind::TypedDict, "__setitem__") => { + let fields = self.fields(db, specialization, field_policy); + + // Add (key type, value type) overloads for all TypedDict items ("fields") that are not read-only: + + let mut writeable_fields = fields + .iter() + .filter(|(_, field)| !field.is_read_only()) + .peekable(); + + if writeable_fields.peek().is_none() { + // If there are no writeable fields, synthesize a `__setitem__` that takes + // a `key` of type `Never` to signal that no keys are accepted. This leads + // to slightly more user-friendly error messages compared to returning an + // empty overload set. + return Some(Type::Callable(CallableType::new( + db, + CallableSignature::single(Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(Type::Never), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::any()), + ], + ), + Type::none(db), + )), + CallableTypeKind::FunctionLike, + ))); + } + + let overloads = writeable_fields.map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(field.declared_ty), + ], + ), + Type::none(db), + ) + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "__getitem__") => { + let fields = self.fields(db, specialization, field_policy); + + // Add (key -> value type) overloads for all TypedDict items ("fields"): + let overloads = fields.iter().map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), + field.declared_ty, + ) + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "__delitem__") => { + let fields = self.fields(db, specialization, field_policy); + + // Only non-required fields can be deleted. Required fields cannot be deleted + // because that would violate the TypedDict's structural type. + let mut deletable_fields = fields + .iter() + .filter(|(_, field)| !field.is_required()) + .peekable(); + + if deletable_fields.peek().is_none() { + // If there are no deletable fields (all fields are required), synthesize a + // `__delitem__` that takes a `key` of type `Never` to signal that no keys + // can be deleted. + return Some(Type::Callable(CallableType::new( + db, + CallableSignature::single(Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(Type::Never), + ], + ), + Type::none(db), + )), + CallableTypeKind::FunctionLike, + ))); + } + + // Otherwise, add overloads for all deletable fields. + let overloads = deletable_fields.map(|(name, _field)| { + let key_type = Type::string_literal(db, name); + + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), + Type::none(db), + ) + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "get") => { + let overloads = self + .fields(db, specialization, field_policy) + .iter() + .flat_map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + // For a required key, `.get()` always returns the value type. For a non-required key, + // `.get()` returns the union of the value type and the type of the default argument + // (which defaults to `None`). + + // TODO: For now, we use two overloads here. They can be merged into a single function + // once the generics solver takes default arguments into account. + + let get_sig = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), + if field.is_required() { + field.declared_ty + } else { + UnionType::from_two_elements(db, field.declared_ty, Type::none(db)) + }, + ); + + let t_default = BoundTypeVarInstance::synthetic( + db, + Name::new_static("T"), + TypeVarVariance::Covariant, + ); + + let get_with_default_sig = Signature::new_generic( + Some(GenericContext::from_typevar_instances(db, [t_default])), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(Type::TypeVar(t_default)), + ], + ), + if field.is_required() { + field.declared_ty + } else { + UnionType::from_two_elements( + db, + field.declared_ty, + Type::TypeVar(t_default), + ) + }, + ); + + [get_sig, get_with_default_sig] + }) + // Fallback overloads for unknown keys + .chain(std::iter::once({ + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + ], + ), + UnionType::from_two_elements(db, Type::unknown(), Type::none(db)), + ) + })) + .chain(std::iter::once({ + let t_default = BoundTypeVarInstance::synthetic( + db, + Name::new_static("T"), + TypeVarVariance::Covariant, + ); + + Signature::new_generic( + Some(GenericContext::from_typevar_instances(db, [t_default])), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(Type::TypeVar(t_default)), + ], + ), + UnionType::from_two_elements( + db, + Type::unknown(), + Type::TypeVar(t_default), + ), + ) + })); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "pop") => { + let fields = self.fields(db, specialization, field_policy); + let overloads = fields + .iter() + .filter(|(_, field)| { + // Only synthesize `pop` for fields that are not required. + !field.is_required() + }) + .flat_map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + // TODO: Similar to above: consider merging these two overloads into one + + // `.pop()` without default + let pop_sig = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), + field.declared_ty, + ); + + // `.pop()` with a default value + let t_default = BoundTypeVarInstance::synthetic( + db, + Name::new_static("T"), + TypeVarVariance::Covariant, + ); + + let pop_with_default_sig = Signature::new_generic( + Some(GenericContext::from_typevar_instances(db, [t_default])), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(Type::TypeVar(t_default)), + ], + ), + UnionType::from_two_elements( + db, + field.declared_ty, + Type::TypeVar(t_default), + ), + ); + + [pop_sig, pop_with_default_sig] + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "setdefault") => { + let fields = self.fields(db, specialization, field_policy); + let overloads = fields.iter().map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + // `setdefault` always returns the field type + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(field.declared_ty), + ], + ), + field.declared_ty, + ) + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "update") => { + // TODO: synthesize a set of overloads with precise types + let signature = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::variadic(Name::new_static("args")), + Parameter::keyword_variadic(Name::new_static("kwargs")), + ], + ), + Type::none(db), + ); + + Some(Type::function_like_callable(db, signature)) + } + _ => None, + } + } + + /// Member lookup for classes that inherit from `typing.TypedDict`. + /// + /// This is implemented as a separate method because the item definitions on a `TypedDict`-based + /// class are *not* accessible as class members. Instead, this mostly defers to `TypedDictFallback`, + /// unless `name` corresponds to one of the specialized synthetic members like `__getitem__`. + pub(crate) fn typed_dict_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + if let Some(member) = self.own_synthesized_member(db, specialization, None, name) { + Place::bound(member).into() + } else { + KnownClass::TypedDictFallback + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + .expect("`find_name_in_mro_with_policy` will return `Some()` when called on class literal") + .map_type(|ty| + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: determine_upper_bound( + db, + self, + specialization, + ClassBase::is_typed_dict + ) + }, + TypeContext::default(), + ) + ) + } + } + + /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. + /// + /// See [`StaticClassLiteral::own_fields`] for more details. + #[salsa::tracked( + returns(ref), + cycle_initial=|_, _, _, _, _| FxIndexMap::default(), + heap_size=get_size2::GetSize::get_heap_size)] + pub(crate) fn fields( + self, + db: &'db dyn Db, + specialization: Option>, + field_policy: CodeGeneratorKind<'db>, + ) -> FxIndexMap> { + if field_policy == CodeGeneratorKind::NamedTuple { + // NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the + // fields of this class only. + return self.own_fields(db, specialization, field_policy); + } + + let matching_classes_in_mro: Vec<(StaticClassLiteral<'db>, Option>)> = + self.iter_mro(db, specialization) + .filter_map(|superclass| { + let class = superclass.into_class()?; + // Dynamic classes don't have fields (no class body). + let (class_literal, specialization) = class.static_class_literal(db)?; + if field_policy.matches(db, class_literal.into(), specialization) { + Some((class_literal, specialization)) + } else { + None + } + }) + // We need to collect into a `Vec` here because we iterate the MRO in reverse order + .collect(); + + matching_classes_in_mro + .into_iter() + .rev() + .flat_map(|(class, specialization)| class.own_fields(db, specialization, field_policy)) + // KW_ONLY sentinels are markers, not real fields. Exclude them so + // they cannot shadow an inherited field with the same name. + .filter(|(_, field)| !field.is_kw_only_sentinel(db)) + // We collect into a FxOrderMap here to deduplicate attributes + .collect() + } + + pub(crate) fn validate_members(self, context: &InferContext<'db, '_>) { + let db = context.db(); + let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self, None) else { + return; + }; + let class_body_scope = self.body_scope(db); + let table = place_table(db, class_body_scope); + let use_def = use_def_map(db, class_body_scope); + for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { + let result = place_from_declarations(db, declarations.clone()); + let attr = result.ignore_conflicting_declarations(); + let symbol = table.symbol(symbol_id); + let name = symbol.name(); + + let Some(Type::FunctionLiteral(literal)) = attr.place.ignore_possibly_undefined() + else { + continue; + }; + + match name.as_str() { + "__setattr__" | "__delattr__" => { + if let CodeGeneratorKind::DataclassLike(_) = field_policy + && self.is_frozen_dataclass(db) == Some(true) + { + if let Some(builder) = context.report_lint( + &INVALID_DATACLASS_OVERRIDE, + literal.node(db, context.file(), context.module()), + ) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot overwrite attribute `{}` in frozen dataclass `{}`", + name, + self.name(db) + )); + diagnostic.info(name); + } + } + } + "__lt__" | "__le__" | "__gt__" | "__ge__" => { + if let CodeGeneratorKind::DataclassLike(_) = field_policy + && self.has_dataclass_param(db, field_policy, DataclassFlags::ORDER) + { + if let Some(builder) = context.report_lint( + &INVALID_DATACLASS_OVERRIDE, + literal.node(db, context.file(), context.module()), + ) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot overwrite attribute `{}` in dataclass `{}` with `order=True`", + name, + self.name(db) + )); + diagnostic.info(name); + } + } + } + _ => {} + } + } + } + + /// Returns a map of all annotated attributes defined in the body of this class. + /// This extends the `__annotations__` attribute at runtime by also including default values + /// and computed field properties. + /// + /// For a class body like + /// ```py + /// @dataclass(kw_only=True) + /// class C: + /// x: int + /// y: str = "hello" + /// z: float = field(kw_only=False, default=1.0) + /// ``` + /// we return a map `{"x": Field, "y": Field, "z": Field}` where each `Field` contains + /// the annotated type, default value (if any), and field properties. + /// + /// **Important**: The returned `Field` objects represent our full understanding of the fields, + /// including properties inherited from class-level dataclass parameters (like `kw_only=True`) + /// and dataclass-transform parameters (like `kw_only_default=True`). They do not represent + /// only what is explicitly specified in each field definition. + pub(crate) fn own_fields( + self, + db: &'db dyn Db, + specialization: Option>, + field_policy: CodeGeneratorKind, + ) -> FxIndexMap> { + let mut attributes = FxIndexMap::default(); + + let class_body_scope = self.body_scope(db); + let table = place_table(db, class_body_scope); + + let use_def = use_def_map(db, class_body_scope); + + let typed_dict_params = self.typed_dict_params(db); + let mut kw_only_sentinel_field_seen = false; + + for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { + // Here, we exclude all declarations that are not annotated assignments. We need this because + // things like function definitions and nested classes would otherwise be considered dataclass + // fields. The check is too broad in the sense that it also excludes (weird) constructs where + // a symbol would have multiple declarations, one of which is an annotated assignment. If we + // want to improve this, we could instead pass a definition-kind filter to the use-def map + // query, or to the `symbol_from_declarations` call below. Doing so would potentially require + // us to generate a union of `__init__` methods. + if !declarations + .clone() + .all(|DeclarationWithConstraint { declaration, .. }| { + declaration.is_undefined_or(|declaration| { + matches!( + declaration.kind(db), + DefinitionKind::AnnotatedAssignment(..) + ) + }) + }) + { + continue; + } + + let symbol = table.symbol(symbol_id); + + let result = place_from_declarations(db, declarations.clone()); + let first_declaration = result.first_declaration; + let attr = result.ignore_conflicting_declarations(); + if attr.is_class_var() { + continue; + } + + 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) + .place + .ignore_possibly_undefined(); + + default_ty = + default_ty.map(|ty| ty.apply_optional_specialization(db, specialization)); + + let mut init = true; + let mut kw_only = None; + let mut alias = None; + if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty { + default_ty = field.default_type(db); + if self + .dataclass_params(db) + .map(|params| params.field_specifiers(db).is_empty()) + .unwrap_or(false) + { + // This happens when constructing a `dataclass` with a `dataclass_transform` + // without defining the `field_specifiers`, meaning it should ignore + // `dataclasses.field` and `dataclasses.Field`. + } else { + init = field.init(db); + kw_only = field.kw_only(db); + alias = field.alias(db); + } + } + + let kind = match field_policy { + CodeGeneratorKind::NamedTuple => FieldKind::NamedTuple { default_ty }, + CodeGeneratorKind::DataclassLike(_) => FieldKind::Dataclass { + default_ty, + init_only: attr.is_init_var(), + init, + kw_only, + alias, + }, + CodeGeneratorKind::TypedDict => { + let is_required = if attr.is_required() { + // Explicit Required[T] annotation - always required + true + } else if attr.is_not_required() { + // Explicit NotRequired[T] annotation - never required + false + } else { + // No explicit qualifier - use class default (`total` parameter) + typed_dict_params + .expect("TypedDictParams should be available for CodeGeneratorKind::TypedDict") + .contains(TypedDictParams::TOTAL) + }; + + FieldKind::TypedDict { + is_required, + is_read_only: attr.is_read_only(), + } + } + }; + + let mut field = Field { + declared_ty: attr_ty.apply_optional_specialization(db, specialization), + kind, + first_declaration, + }; + + // Check if this is a KW_ONLY sentinel and mark subsequent fields as keyword-only + if field.is_kw_only_sentinel(db) { + kw_only_sentinel_field_seen = true; + } + + // If no explicit kw_only setting and we've seen KW_ONLY sentinel, mark as keyword-only + if kw_only_sentinel_field_seen { + if let FieldKind::Dataclass { + kw_only: ref mut kw @ None, + .. + } = field.kind + { + *kw = Some(true); + } + } + + // Resolve the kw_only to the class-level default. This ensures that when fields + // are inherited by child classes, they use their defining class's kw_only default. + if let FieldKind::Dataclass { + kw_only: ref mut kw @ None, + .. + } = field.kind + { + let class_kw_only_default = self + .dataclass_params(db) + .is_some_and(|params| params.flags(db).contains(DataclassFlags::KW_ONLY)) + // TODO this next part should not be necessary, if we were properly + // initializing `dataclass_params` from the dataclass-transform params, for + // metaclass and base-class-based dataclass-transformers. + || matches!( + field_policy, + CodeGeneratorKind::DataclassLike(Some(transformer_params)) + if transformer_params.flags(db).contains(DataclassTransformerFlags::KW_ONLY_DEFAULT) + ); + *kw = Some(class_kw_only_default); + } + + attributes.insert(symbol.name().clone(), field); + } + } + + attributes + } + + /// Look up an instance attribute (available in `__dict__`) of the given name. + /// + /// See [`Type::instance_member`] for more details. + pub(super) fn instance_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + if self.is_typed_dict(db) { + return Place::Undefined.into(); + } + + match MroLookup::new(db, self.iter_mro(db, specialization)).instance_member(name) { + InstanceMemberResult::Done(result) => result, + InstanceMemberResult::TypedDict => KnownClass::TypedDictFallback + .to_instance(db) + .instance_member(db, name) + .map_type(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: Type::instance(db, self.unknown_specialization(db)), + }, + TypeContext::default(), + ) + }), + } + } + + /// Tries to find declarations/bindings of an attribute named `name` that are only + /// "implicitly" defined (`self.x = …`, `cls.x = …`) in a method of the class that + /// corresponds to `class_body_scope`. The `target_method_decorator` parameter is + /// used to skip methods that do not have the expected decorator. + fn implicit_attribute( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: &str, + target_method_decorator: MethodDecorator, + ) -> Member<'db> { + Self::implicit_attribute_inner( + db, + class_body_scope, + name.to_string(), + target_method_decorator, + ) + } + + #[salsa::tracked( + cycle_fn=implicit_attribute_cycle_recover, + cycle_initial=implicit_attribute_initial, + heap_size=ruff_memory_usage::heap_size, + )] + pub(super) fn implicit_attribute_inner( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: String, + target_method_decorator: MethodDecorator, + ) -> Member<'db> { + // If we do not see any declarations of an attribute, neither in the class body nor in + // any method, we build a union of `Unknown` with the inferred types of all bindings of + // that attribute. We include `Unknown` in that union to account for the fact that the + // attribute might be externally modified. + let mut union_of_inferred_types = UnionBuilder::new(db); + let mut qualifiers = TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE; + + let mut is_attribute_bound = false; + + let file = class_body_scope.file(db); + let module = parsed_module(db, file).load(db); + let index = semantic_index(db, file); + let class_map = use_def_map(db, class_body_scope); + let class_table = place_table(db, class_body_scope); + let is_valid_scope = |method_scope: &Scope| { + let Some(method_def) = method_scope.node().as_function() else { + return true; + }; + + // Check the decorators directly on the AST node to determine if this method + // is a classmethod or staticmethod. This is more reliable than checking the + // final evaluated type, which may be wrapped by other decorators like @cache. + let function_node = method_def.node(&module); + let definition = index.expect_single_definition(method_def); + + let mut is_classmethod = false; + let mut is_staticmethod = false; + + for decorator in &function_node.decorator_list { + let decorator_ty = + definition_expression_type(db, definition, &decorator.expression); + if let Type::ClassLiteral(class) = decorator_ty { + match class.known(db) { + Some(KnownClass::Classmethod) => is_classmethod = true, + Some(KnownClass::Staticmethod) => is_staticmethod = true, + _ => {} + } + } + } + + // Also check for implicit classmethods/staticmethods based on method name + let method_name = function_node.name.as_str(); + if is_implicit_classmethod(method_name) { + is_classmethod = true; + } + if is_implicit_staticmethod(method_name) { + is_staticmethod = true; + } + + match target_method_decorator { + MethodDecorator::None => !is_classmethod && !is_staticmethod, + MethodDecorator::ClassMethod => is_classmethod, + MethodDecorator::StaticMethod => is_staticmethod, + } + }; + + // First check declarations + for (attribute_declarations, method_scope_id) in + attribute_declarations(db, class_body_scope, &name) + { + let method_scope = index.scope(method_scope_id); + if !is_valid_scope(method_scope) { + continue; + } + + for attribute_declaration in attribute_declarations { + let DefinitionState::Defined(declaration) = attribute_declaration.declaration + else { + continue; + }; + + let DefinitionKind::AnnotatedAssignment(assignment) = declaration.kind(db) else { + continue; + }; + + // We found an annotated assignment of one of the following forms (using 'self' in these + // examples, but we support arbitrary names for the first parameters of methods): + // + // self.name: + // self.name: = … + + let annotation = declaration_type(db, declaration); + let annotation = Place::declared(annotation.inner).with_qualifiers( + annotation.qualifiers | TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE, + ); + + if let Some(all_qualifiers) = annotation.is_bare_final() { + if let Some(value) = assignment.value(&module) { + // If we see an annotated assignment with a bare `Final` as in + // `self.SOME_CONSTANT: Final = 1`, infer the type from the value + // on the right-hand side. + + let inferred_ty = infer_expression_type( + db, + index.expression(value), + TypeContext::default(), + ); + 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 + qualifiers |= all_qualifiers; + continue; + } + + return Member { inner: annotation }; + } + } + + if !qualifiers.contains(TypeQualifiers::FINAL) { + union_of_inferred_types = union_of_inferred_types.add(Type::unknown()); + } + + for (attribute_assignments, attribute_binding_scope_id) in + attribute_assignments(db, class_body_scope, &name) + { + let binding_scope = index.scope(attribute_binding_scope_id); + if !is_valid_scope(binding_scope) { + continue; + } + + let scope_for_reachability_analysis = { + if binding_scope.node().as_function().is_some() { + binding_scope + } else if binding_scope.is_eager() { + let mut eager_scope_parent = binding_scope; + while eager_scope_parent.is_eager() + && let Some(parent) = eager_scope_parent.parent() + { + eager_scope_parent = index.scope(parent); + } + eager_scope_parent + } else { + binding_scope + } + }; + + // The attribute assignment inherits the reachability of the method which contains it + let is_method_reachable = + if let Some(method_def) = scope_for_reachability_analysis.node().as_function() { + let method = index.expect_single_definition(method_def); + let method_place = class_table + .symbol_id(&method_def.node(&module).name) + .unwrap(); + class_map + .reachable_symbol_bindings(method_place) + .find_map(|bind| { + (bind.binding.is_defined_and(|def| def == method)) + .then(|| class_map.binding_reachability(db, &bind)) + }) + .unwrap_or(Truthiness::AlwaysFalse) + } else { + Truthiness::AlwaysFalse + }; + if is_method_reachable.is_always_false() { + continue; + } + + for attribute_assignment in attribute_assignments { + if let DefinitionState::Undefined = attribute_assignment.binding { + continue; + } + + let DefinitionState::Defined(binding) = attribute_assignment.binding else { + continue; + }; + + if !is_method_reachable.is_always_false() { + is_attribute_bound = true; + } + + match binding.kind(db) { + DefinitionKind::AnnotatedAssignment(_) => { + // Annotated assignments were handled above. This branch is not + // unreachable (because of the `continue` above), but there is + // nothing to do here. + } + DefinitionKind::Assignment(assign) => { + match assign.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // .., self.name, .. = + // (.., self.name, ..) = + // [.., self.name, ..] = + + let unpacked = infer_unpack_types(db, unpack); + + let inferred_ty = unpacked.expression_type(assign.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an un-annotated attribute assignment of the form: + // + // self.name = + + let inferred_ty = infer_expression_type( + db, + index.expression(assign.value(&module)), + TypeContext::default(), + ); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::For(for_stmt) => { + match for_stmt.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // for .., self.name, .. in : + + let unpacked = infer_unpack_types(db, unpack); + let inferred_ty = + unpacked.expression_type(for_stmt.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an attribute assignment like: + // + // for self.name in : + + let iterable_ty = infer_expression_type( + db, + index.expression(for_stmt.iterable(&module)), + TypeContext::default(), + ); + // TODO: Potential diagnostics resulting from the iterable are currently not reported. + let inferred_ty = + iterable_ty.iterate(db).homogeneous_element_type(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::WithItem(with_item) => { + match with_item.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // with as .., self.name, ..: + + let unpacked = infer_unpack_types(db, unpack); + let inferred_ty = + unpacked.expression_type(with_item.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an attribute assignment like: + // + // with as self.name: + + let context_ty = infer_expression_type( + db, + index.expression(with_item.context_expr(&module)), + TypeContext::default(), + ); + let inferred_ty = if with_item.is_async() { + context_ty.aenter(db) + } else { + context_ty.enter(db) + }; + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::Comprehension(comprehension) => { + match comprehension.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // [... for .., self.name, .. in ] + + let unpacked = infer_unpack_types(db, unpack); + + let inferred_ty = + unpacked.expression_type(comprehension.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an attribute assignment like: + // + // [... for self.name in ] + + let iterable_ty = infer_expression_type( + db, + index.expression(comprehension.iterable(&module)), + TypeContext::default(), + ); + // TODO: Potential diagnostics resulting from the iterable are currently not reported. + let inferred_ty = + iterable_ty.iterate(db).homogeneous_element_type(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::AugmentedAssignment(_) => { + // TODO: + } + DefinitionKind::NamedExpression(_) => { + // A named expression whose target is an attribute is syntactically prohibited + } + _ => {} + } + } + } + + 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 + /// this class, not on its superclasses. + pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + // TODO: There are many things that are not yet implemented here: + // - `typing.Final` + // - Proper diagnostics + + let body_scope = self.body_scope(db); + let table = place_table(db, body_scope); + + if let Some(symbol_id) = table.symbol_id(name) { + let use_def = use_def_map(db, body_scope); + + let declarations = use_def.end_of_scope_symbol_declarations(symbol_id); + let declared_and_qualifiers = + place_from_declarations(db, declarations).ignore_conflicting_declarations(); + + match declared_and_qualifiers { + PlaceAndQualifiers { + place: + mut declared @ Place::Defined(DefinedPlace { + ty: declared_ty, + definedness: declaredness, + .. + }), + qualifiers, + } => { + // For the purpose of finding instance attributes, ignore `ClassVar` + // declarations: + if qualifiers.contains(TypeQualifiers::CLASS_VAR) { + 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_undefined() + { + return Member::unbound(); + } + } + + // `KW_ONLY` sentinels are markers, not real instance attributes. + if declared_ty.is_instance_of(db, KnownClass::KwOnly) + && CodeGeneratorKind::from_static_class(db, self, None).is_some_and( + |policy| matches!(policy, CodeGeneratorKind::DataclassLike(_)), + ) + { + return Member::unbound(); + } + + // The attribute is declared in the class body. + + let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); + let inferred = place_from_bindings(db, bindings).place; + 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_undefined() + { + 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 { + inner: declared.with_qualifiers(qualifiers), + } + } else { + Member { + inner: Place::Defined(DefinedPlace { + ty: UnionType::from_two_elements( + db, + declared_ty, + implicit_ty, + ), + origin: TypeOrigin::Declared, + definedness: declaredness, + widening: Widening::None, + }) + .with_qualifiers(qualifiers), + } + } + } else if self.is_own_dataclass_instance_field(db, name) + && declared_ty + .class_member(db, "__get__".into()) + .place + .is_undefined() + { + // For dataclass-like classes, declared fields are assigned + // by the synthesized `__init__`, so they are instance + // attributes even without an explicit `self.x = ...` + // assignment in a method body. + // + // However, if the declared type is a descriptor (has + // `__get__`), we return unbound so that the descriptor + // protocol in `member_lookup_with_policy` can resolve + // the attribute type through `__get__`. + Member { + inner: declared.with_qualifiers(qualifiers), + } + } else { + // The symbol is declared and bound in the class body, + // but we did not find any attribute assignments in + // methods of the class. This means that the attribute + // has a class-level default value, but it would not be + // found in a `__dict__` lookup. + + Member::unbound() + } + } else { + // The attribute is declared but not bound in the class body. + // We take this as a sign that this is intended to be a pure + // instance attribute, and we trust the declared type, unless + // it is possibly-undeclared. In the latter case, we also + // union with the inferred type from attribute assignments. + + if declaredness == Definedness::AlwaysDefined { + Member { + inner: declared.with_qualifiers(qualifiers), + } + } else { + if let Some(implicit_ty) = Self::implicit_attribute( + db, + body_scope, + name, + MethodDecorator::None, + ) + .inner + .place + .ignore_possibly_undefined() + { + Member { + inner: Place::Defined(DefinedPlace { + ty: UnionType::from_two_elements( + db, + declared_ty, + implicit_ty, + ), + origin: TypeOrigin::Declared, + definedness: declaredness, + widening: Widening::None, + }) + .with_qualifiers(qualifiers), + } + } else { + Member { + inner: declared.with_qualifiers(qualifiers), + } + } + } + } + } + + PlaceAndQualifiers { + place: Place::Undefined, + qualifiers: _, + } => { + // The attribute is not *declared* in the class body. It could still be declared/bound + // in a method. + + Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) + } + } + } else { + // This attribute is neither declared nor bound in the class body. + // It could still be implicitly defined in a method. + + Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) + } + } + + /// Returns `true` if `name` is a non-init-only field directly declared on this + /// dataclass (i.e., a field that corresponds to an instance attribute). + /// + /// This is used to decide whether a bare class-body annotation like `x: int` + /// should be treated as defining an instance attribute: dataclass fields are + /// implicitly assigned in `__init__`, so they behave as instance attributes + /// even though no explicit binding exists in the class body. + fn is_own_dataclass_instance_field(self, db: &'db dyn Db, name: &str) -> bool { + let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self, None) else { + return false; + }; + if !matches!(field_policy, CodeGeneratorKind::DataclassLike(_)) { + return false; + } + + let fields = self.own_fields(db, None, field_policy); + let Some(field) = fields.get(name) else { + return false; + }; + matches!( + field.kind, + FieldKind::Dataclass { + init_only: false, + .. + } + ) + } + + pub(super) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { + Type::instance(db, ClassType::NonGeneric(self.into())) + } + + /// Return this class' involvement in an inheritance cycle, if any. + /// + /// A class definition like this will fail at runtime, + /// but we must be resilient to it or we could panic. + #[salsa::tracked(cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn inheritance_cycle(self, db: &'db dyn Db) -> Option { + /// Return `true` if the class is cyclically defined. + /// + /// Also, populates `visited_classes` with all base classes of `self`. + fn is_cyclically_defined_recursive<'db>( + db: &'db dyn Db, + class: StaticClassLiteral<'db>, + classes_on_stack: &mut FxIndexSet>, + visited_classes: &mut FxIndexSet>, + ) -> bool { + let mut result = false; + for explicit_base in class.explicit_bases(db) { + let explicit_base_class_literal = match explicit_base { + Type::ClassLiteral(class_literal) => class_literal.as_static(), + Type::GenericAlias(generic_alias) => Some(generic_alias.origin(db)), + _ => continue, + }; + let Some(explicit_base_class_literal) = explicit_base_class_literal else { + continue; + }; + if !classes_on_stack.insert(explicit_base_class_literal) { + return true; + } + + if visited_classes.insert(explicit_base_class_literal) { + // If we find a cycle, keep searching to check if we can reach the starting class. + result |= is_cyclically_defined_recursive( + db, + explicit_base_class_literal, + classes_on_stack, + visited_classes, + ); + } + classes_on_stack.pop(); + } + result + } + + tracing::trace!("Class::inheritance_cycle: {}", self.name(db)); + + let visited_classes = &mut FxIndexSet::default(); + if !is_cyclically_defined_recursive(db, self, &mut FxIndexSet::default(), visited_classes) { + None + } else if visited_classes.contains(&self) { + Some(InheritanceCycle::Participant) + } else { + Some(InheritanceCycle::Inherited) + } + } + + /// Returns a [`Span`] with the range of the class's header. + /// + /// See [`Self::header_range`] for more details. + pub(crate) fn header_span(self, db: &'db dyn Db) -> Span { + Span::from(self.file(db)).with_range(self.header_range(db)) + } + + /// Returns the range of the class's "header": the class name + /// and any arguments passed to the `class` statement. E.g. + /// + /// ```ignore + /// class Foo(Bar, metaclass=Baz): ... + /// ^^^^^^^^^^^^^^^^^^^^^^^ + /// ``` + pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + let class_scope = self.body_scope(db); + let module = parsed_module(db, class_scope.file(db)).load(db); + let class_node = class_scope.node(db).expect_class().node(&module); + let class_name = &class_node.name; + TextRange::new( + class_name.start(), + class_node + .arguments + .as_deref() + .map(Ranged::end) + .unwrap_or_else(|| class_name.end()), + ) + } +} + +#[salsa::tracked] +impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { + #[salsa::tracked(cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size)] + fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { + let typevar_in_generic_context = self + .generic_context(db) + .is_some_and(|generic_context| generic_context.variables(db).contains(&typevar)); + + if !typevar_in_generic_context { + return TypeVarVariance::Bivariant; + } + let class_body_scope = self.body_scope(db); + + let file = class_body_scope.file(db); + let index = semantic_index(db, file); + + let explicit_bases_variances = self + .explicit_bases(db) + .iter() + .map(|class| class.variance_of(db, typevar)); + + let default_attribute_variance = { + let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self.into(), None); + // Python 3.13 introduced a synthesized `__replace__` method on dataclasses which uses + // their field types in contravariant position, thus meaning a frozen dataclass must + // still be invariant in its field types. Other synthesized methods on dataclasses are + // not considered here, since they don't use field types in their signatures. TODO: + // ideally we'd have a single source of truth for information about synthesized + // methods, so we just look them up normally and don't hardcode this knowledge here. + let is_frozen_dataclass = Program::get(db).python_version(db) <= PythonVersion::PY312 + && self + .dataclass_params(db) + .is_some_and(|params| params.flags(db).contains(DataclassFlags::FROZEN)); + if is_namedtuple || is_frozen_dataclass { + TypeVarVariance::Covariant + } else { + TypeVarVariance::Invariant + } + }; + + let init_name: &Name = &"__init__".into(); + let new_name: &Name = &"__new__".into(); + + let use_def_map = index.use_def_map(class_body_scope.file_scope_id(db)); + let table = place_table(db, class_body_scope); + let attribute_places_and_qualifiers = + use_def_map + .all_end_of_scope_symbol_declarations() + .map(|(symbol_id, declarations)| { + let place_and_qual = + place_from_declarations(db, declarations).ignore_conflicting_declarations(); + (symbol_id, place_and_qual) + }) + .chain(use_def_map.all_end_of_scope_symbol_bindings().map( + |(symbol_id, bindings)| { + (symbol_id, place_from_bindings(db, bindings).place.into()) + }, + )) + .filter_map(|(symbol_id, place_and_qual)| { + if let Some(name) = table.place(symbol_id).as_symbol().map(Symbol::name) { + (![init_name, new_name].contains(&name)) + .then_some((name.to_string(), place_and_qual)) + } else { + None + } + }); + + // Dataclasses can have some additional synthesized methods (`__eq__`, `__hash__`, + // `__lt__`, etc.) but none of these will have field types type variables in their signatures, so we + // don't need to consider them for variance. + + let attribute_names = attribute_scopes(db, self.body_scope(db)) + .flat_map(|function_scope_id| { + index + .place_table(function_scope_id) + .members() + .filter_map(|member| member.as_instance_attribute()) + .filter(|name| *name != init_name && *name != new_name) + .map(std::string::ToString::to_string) + .collect::>() + }) + .dedup(); + + let attribute_variances = attribute_names + .map(|name| { + let place_and_quals = self.own_instance_member(db, &name).inner; + (name, place_and_quals) + }) + .chain(attribute_places_and_qualifiers) + .dedup() + .filter_map(|(name, place_and_qual)| { + place_and_qual.ignore_possibly_undefined().map(|ty| { + let variance = if place_and_qual + .qualifiers + // `CLASS_VAR || FINAL` is really `all()`, but + // we want to be robust against new qualifiers + .intersects(TypeQualifiers::CLASS_VAR | TypeQualifiers::FINAL) + // We don't allow mutation of methods or properties + || ty.is_function_literal() + || ty.is_property_instance() + // Underscore-prefixed attributes are assumed not to be externally mutated + || name.starts_with('_') + { + // CLASS_VAR: class vars generally shouldn't contain the + // type variable, but they could if it's a + // callable type. They can't be mutated on instances. + // + // FINAL: final attributes are immutable, and thus covariant + TypeVarVariance::Covariant + } else { + default_attribute_variance + }; + ty.with_polarity(variance).variance_of(db, typevar) + }) + }); + + attribute_variances + .chain(explicit_bases_variances) + .collect() + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub(crate) enum InheritanceCycle { + /// The class is cyclically defined and is a participant in the cycle. + /// i.e., it inherits either directly or indirectly from itself. + Participant, + /// The class inherits from a class that is a `Participant` in an inheritance cycle, + /// but is not itself a participant. + Inherited, +} + +impl InheritanceCycle { + pub(crate) const fn is_participant(self) -> bool { + matches!(self, InheritanceCycle::Participant) + } +} + +fn explicit_bases_cycle_initial<'db>( + db: &'db dyn Db, + id: salsa::Id, + literal: StaticClassLiteral<'db>, +) -> Box<[Type<'db>]> { + let module = parsed_module(db, literal.file(db)).load(db); + let class_stmt = literal.node(db, &module); + // Try to produce a list of `Divergent` types of the right length. However, if one or more of + // the bases is a starred expression, we don't know how many entries that will eventually + // expand to. + vec![Type::divergent(id); class_stmt.bases().len()].into_boxed_slice() +} + +fn explicit_bases_cycle_fn<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + previous: &[Type<'db>], + current: Box<[Type<'db>]>, + _literal: StaticClassLiteral<'db>, +) -> Box<[Type<'db>]> { + if previous.len() == current.len() { + // As long as the length of bases hasn't changed, use the same "monotonic widening" + // strategy that we use with most types, to avoid oscillations. + current + .iter() + .zip(previous.iter()) + .map(|(curr, prev)| curr.cycle_normalized(db, *prev, cycle)) + .collect() + } else { + // The length of bases has changed, presumably because we expanded a starred expression. We + // don't do "monotonic widening" here, because we don't want to make assumptions about + // which previous entries correspond to which current ones. An oscillation here would be + // unfortunate, but maybe only pathological programs can trigger such a thing. + current + } +} + +fn static_class_try_mro_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: StaticClassLiteral<'db>, + specialization: Option>, +) -> Result, StaticMroError<'db>> { + Err(StaticMroError::cycle( + db, + self_.apply_optional_specialization(db, specialization), + )) +} + +#[allow(clippy::unnecessary_wraps)] +fn try_metaclass_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self_: StaticClassLiteral<'db>, +) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { + Err(MetaclassError { + kind: MetaclassErrorKind::Cycle, + }) +} + +fn implicit_attribute_initial<'db>( + _db: &'db dyn Db, + id: salsa::Id, + _class_body_scope: ScopeId<'db>, + _name: String, + _target_method_decorator: MethodDecorator, +) -> Member<'db> { + Member { + inner: Place::bound(Type::divergent(id)).into(), + } +} + +#[allow(clippy::too_many_arguments)] +fn implicit_attribute_cycle_recover<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + previous_member: &Member<'db>, + member: Member<'db>, + _class_body_scope: ScopeId<'db>, + _name: String, + _target_method_decorator: MethodDecorator, +) -> Member<'db> { + let inner = member + .inner + .cycle_normalized(db, previous_member.inner, cycle); + Member { inner } +}