diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 236305cb73..f9ac728c40 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -11,7 +11,7 @@ use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module}; use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::FileScopeId; use crate::semantic_index::semantic_index; -use crate::types::ide_support::{Member, all_declarations_and_bindings, all_members}; +use crate::types::list_members::{Member, all_members, all_members_of_scope}; use crate::types::{Type, binding_type, infer_scope_types}; use crate::{Db, resolve_real_shadowable_module}; @@ -76,7 +76,7 @@ impl<'db> SemanticModel<'db> { for (file_scope, _) in index.ancestor_scopes(file_scope) { for memberdef in - all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file)) + all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file)) { members.insert( memberdef.member.name, @@ -221,12 +221,13 @@ impl<'db> SemanticModel<'db> { let mut completions = vec![]; for (file_scope, _) in index.ancestor_scopes(file_scope) { completions.extend( - all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file)) - .map(|memberdef| Completion { + all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file)).map( + |memberdef| Completion { name: memberdef.member.name, ty: Some(memberdef.member.ty), builtin: false, - }), + }, + ), ); } // Builtins are available in all scopes. diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 416e80b5c5..494f4efa83 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -96,6 +96,7 @@ mod generics; pub mod ide_support; mod infer; mod instance; +pub mod list_members; mod member; mod mro; mod narrow; diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index dce0c5b7eb..2093f2f377 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -39,7 +39,7 @@ use crate::types::{ DataclassParams, FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType, TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, - WrapperDescriptorKind, enums, ide_support, todo_type, + WrapperDescriptorKind, enums, list_members, todo_type, }; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion}; @@ -888,7 +888,7 @@ impl<'db> Bindings<'db> { if let [Some(ty)] = overload.parameter_types() { overload.set_return_type(Type::heterogeneous_tuple( db, - ide_support::all_members(db, *ty) + list_members::all_members(db, *ty) .into_iter() .sorted() .map(|member| Type::string_literal(db, &member.name)), diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 189520fa52..339689169c 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -74,7 +74,7 @@ use crate::types::diagnostic::{ }; use crate::types::display::DisplaySettings; use crate::types::generics::{GenericContext, InferableTypeVars}; -use crate::types::ide_support::all_members; +use crate::types::list_members::all_members; use crate::types::narrow::ClassInfoConstraintFunction; use crate::types::signatures::{CallableSignature, Signature}; use crate::types::visitor::any_over_type; diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 74eab52e72..2c53ea275f 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1,27 +1,16 @@ -use std::cmp::Ordering; use std::collections::HashMap; -use crate::place::{ - Place, builtins_module_scope, imported_symbol, place_from_bindings, place_from_declarations, -}; +use crate::place::builtins_module_scope; use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::DefinitionKind; -use crate::semantic_index::scope::ScopeId; -use crate::semantic_index::{ - attribute_scopes, global_scope, place_table, semantic_index, use_def_map, -}; +use crate::semantic_index::{attribute_scopes, global_scope, semantic_index, use_def_map}; use crate::types::call::{CallArguments, MatchedArgument}; -use crate::types::generics::Specialization; use crate::types::signatures::Signature; -use crate::types::{CallDunderError, SubclassOfInner, UnionType}; -use crate::types::{ - CallableTypes, ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext, - TypeVarBoundOrConstraints, class::CodeGeneratorKind, -}; -use crate::{Db, DisplaySettings, HasType, NameKind, SemanticModel}; +use crate::types::{CallDunderError, UnionType}; +use crate::types::{CallableTypes, ClassBase, KnownClass, Type, TypeContext}; +use crate::{Db, DisplaySettings, HasType, SemanticModel}; use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; -use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; @@ -29,497 +18,6 @@ use rustc_hash::FxHashSet; pub use resolve_definition::{ImportAliasResolution, ResolvedDefinition, map_stub_definition}; use resolve_definition::{find_symbol_in_scope, resolve_definition}; -// `__init__`, `__repr__`, `__eq__`, `__ne__` and `__hash__` are always included via `object`, -// so we don't need to list them here. -const SYNTHETIC_DATACLASS_ATTRIBUTES: &[&str] = &[ - "__lt__", - "__le__", - "__gt__", - "__ge__", - "__replace__", - "__setattr__", - "__delattr__", - "__slots__", - "__weakref__", - "__match_args__", - "__dataclass_fields__", - "__dataclass_params__", -]; - -pub(crate) fn all_declarations_and_bindings<'db>( - db: &'db dyn Db, - scope_id: ScopeId<'db>, -) -> impl Iterator> + 'db { - let use_def_map = use_def_map(db, scope_id); - let table = place_table(db, scope_id); - - use_def_map - .all_end_of_scope_symbol_declarations() - .filter_map(move |(symbol_id, declarations)| { - let place_result = place_from_declarations(db, declarations); - let definition = place_result.single_declaration; - place_result - .ignore_conflicting_declarations() - .place - .ignore_possibly_undefined() - .map(|ty| { - let symbol = table.symbol(symbol_id); - let member = Member { - name: symbol.name().clone(), - ty, - }; - MemberWithDefinition { member, definition } - }) - }) - .chain(use_def_map.all_end_of_scope_symbol_bindings().filter_map( - move |(symbol_id, bindings)| { - // It's not clear to AG how to using a bindings - // iterator here to get the correct definition for - // this binding. Below, we look through all bindings - // with a definition and only take one if there is - // exactly one. I don't think this can be wrong, but - // it's probably omitting definitions in some cases. - let mut definition = None; - for binding in bindings.clone() { - if let Some(def) = binding.binding.definition() { - if definition.is_some() { - definition = None; - break; - } - definition = Some(def); - } - } - place_from_bindings(db, bindings) - .ignore_possibly_undefined() - .map(|ty| { - let symbol = table.symbol(symbol_id); - let member = Member { - name: symbol.name().clone(), - ty, - }; - MemberWithDefinition { member, definition } - }) - }, - )) -} - -struct AllMembers<'db> { - members: FxHashSet>, -} - -impl<'db> AllMembers<'db> { - fn of(db: &'db dyn Db, ty: Type<'db>) -> Self { - let mut all_members = Self { - members: FxHashSet::default(), - }; - all_members.extend_with_type(db, ty); - all_members - } - - fn extend_with_type(&mut self, db: &'db dyn Db, ty: Type<'db>) { - match ty { - Type::Union(union) => self.members.extend( - union - .elements(db) - .iter() - .map(|ty| AllMembers::of(db, *ty).members) - .reduce(|acc, members| acc.intersection(&members).cloned().collect()) - .unwrap_or_default(), - ), - - Type::Intersection(intersection) => self.members.extend( - intersection - .positive(db) - .iter() - .map(|ty| AllMembers::of(db, *ty).members) - .reduce(|acc, members| acc.union(&members).cloned().collect()) - .unwrap_or_default(), - ), - - Type::NominalInstance(instance) => { - let (class_literal, specialization) = instance.class(db).class_literal(db); - self.extend_with_instance_members(db, ty, class_literal); - self.extend_with_synthetic_members(db, ty, class_literal, specialization); - } - - Type::NewTypeInstance(newtype) => { - self.extend_with_type(db, Type::instance(db, newtype.base_class_type(db))); - } - - Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => { - self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); - } - - Type::GenericAlias(generic_alias) if generic_alias.is_typed_dict(db) => { - self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); - } - - Type::SubclassOf(subclass_of_type) if subclass_of_type.is_typed_dict(db) => { - self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); - } - - Type::ClassLiteral(class_literal) => { - self.extend_with_class_members(db, ty, class_literal); - self.extend_with_synthetic_members(db, ty, class_literal, None); - if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { - self.extend_with_class_members(db, ty, metaclass); - } - } - - Type::GenericAlias(generic_alias) => { - let class_literal = generic_alias.origin(db); - self.extend_with_class_members(db, ty, class_literal); - self.extend_with_synthetic_members(db, ty, class_literal, None); - if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { - self.extend_with_class_members(db, ty, metaclass); - } - } - - Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - SubclassOfInner::Dynamic(_) => { - self.extend_with_type(db, KnownClass::Type.to_instance(db)); - } - _ => { - if let Some(class_type) = subclass_of_type.subclass_of().into_class(db) { - let (class_literal, specialization) = class_type.class_literal(db); - self.extend_with_class_members(db, ty, class_literal); - self.extend_with_synthetic_members(db, ty, class_literal, specialization); - if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { - self.extend_with_class_members(db, ty, metaclass); - } - } - } - }, - - Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy | Type::AlwaysFalsy => { - self.extend_with_type(db, Type::object()); - } - - Type::TypeAlias(alias) => self.extend_with_type(db, alias.value_type(db)), - - Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { - None => { - self.extend_with_type(db, Type::object()); - } - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - self.extend_with_type(db, bound); - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - self.members.extend( - constraints - .elements(db) - .iter() - .map(|ty| AllMembers::of(db, *ty).members) - .reduce(|acc, members| { - acc.intersection(&members).cloned().collect() - }) - .unwrap_or_default(), - ); - } - } - } - - Type::IntLiteral(_) - | Type::BooleanLiteral(_) - | Type::StringLiteral(_) - | Type::BytesLiteral(_) - | Type::EnumLiteral(_) - | Type::LiteralString - | Type::PropertyInstance(_) - | Type::FunctionLiteral(_) - | Type::BoundMethod(_) - | Type::KnownBoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::Callable(_) - | Type::ProtocolInstance(_) - | Type::SpecialForm(_) - | Type::KnownInstance(_) - | Type::BoundSuper(_) - | Type::TypeIs(_) => match ty.to_meta_type(db) { - Type::ClassLiteral(class_literal) => { - self.extend_with_class_members(db, ty, class_literal); - } - Type::SubclassOf(subclass_of) => { - if let Some(class) = subclass_of.subclass_of().into_class(db) { - self.extend_with_class_members(db, ty, class.class_literal(db).0); - } - } - Type::GenericAlias(generic_alias) => { - let class_literal = generic_alias.origin(db); - self.extend_with_class_members(db, ty, class_literal); - } - _ => {} - }, - - Type::TypedDict(_) => { - if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) { - self.extend_with_class_members(db, ty, class_literal); - } - - if let Type::ClassLiteral(class) = - KnownClass::TypedDictFallback.to_class_literal(db) - { - self.extend_with_instance_members(db, ty, class); - } - } - - Type::ModuleLiteral(literal) => { - self.extend_with_type(db, KnownClass::ModuleType.to_instance(db)); - let module = literal.module(db); - - let Some(file) = module.file(db) else { - return; - }; - - let module_scope = global_scope(db, file); - let use_def_map = use_def_map(db, module_scope); - let place_table = place_table(db, module_scope); - - for (symbol_id, _) in use_def_map.all_end_of_scope_symbol_declarations() { - let symbol_name = place_table.symbol(symbol_id).name(); - let Place::Defined(ty, _, _) = - imported_symbol(db, file, symbol_name, None).place - else { - continue; - }; - - // Filter private symbols from stubs if they appear to be internal types - let is_stub_file = file.path(db).extension() == Some("pyi"); - let is_private_symbol = match NameKind::classify(symbol_name) { - NameKind::Dunder | NameKind::Normal => false, - NameKind::Sunder => true, - }; - if is_private_symbol && is_stub_file { - match ty { - Type::NominalInstance(instance) - if matches!( - instance.known_class(db), - Some( - KnownClass::TypeVar - | KnownClass::TypeVarTuple - | KnownClass::ParamSpec - | KnownClass::UnionType - ) - ) => - { - continue; - } - Type::ClassLiteral(class) if class.is_protocol(db) => continue, - Type::KnownInstance( - KnownInstanceType::TypeVar(_) - | KnownInstanceType::TypeAliasType(_) - | KnownInstanceType::UnionType(_) - | KnownInstanceType::Literal(_) - | KnownInstanceType::Annotated(_), - ) => continue, - _ => {} - } - } - - self.members.insert(Member { - name: symbol_name.clone(), - ty, - }); - } - - self.members - .extend(literal.available_submodule_attributes(db).filter_map( - |submodule_name| { - let ty = literal.resolve_submodule(db, &submodule_name)?; - let name = submodule_name.clone(); - Some(Member { name, ty }) - }, - )); - } - } - } - - /// Add members from `class_literal` (including following its - /// parent classes). - /// - /// `ty` should be the original type that we're adding members for. - /// For example, in: - /// - /// ```text - /// class Meta(type): - /// @property - /// def meta_attr(self) -> int: - /// return 0 - /// - /// class C(metaclass=Meta): ... - /// - /// C. - /// ``` - /// - /// then `class_literal` might be `Meta`, but `ty` should be the - /// type of `C`. This ensures that the descriptor protocol is - /// correctly used (or not used) to get the type of each member of - /// `C`. - fn extend_with_class_members( - &mut self, - db: &'db dyn Db, - ty: Type<'db>, - class_literal: ClassLiteral<'db>, - ) { - for parent in class_literal - .iter_mro(db, None) - .filter_map(ClassBase::into_class) - .map(|class| class.class_literal(db).0) - { - let parent_scope = parent.body_scope(db); - for memberdef in all_declarations_and_bindings(db, parent_scope) { - let result = ty.member(db, memberdef.member.name.as_str()); - let Some(ty) = result.place.ignore_possibly_undefined() else { - continue; - }; - self.members.insert(Member { - name: memberdef.member.name, - ty, - }); - } - } - } - - fn extend_with_instance_members( - &mut self, - db: &'db dyn Db, - ty: Type<'db>, - class_literal: ClassLiteral<'db>, - ) { - for parent in class_literal - .iter_mro(db, None) - .filter_map(ClassBase::into_class) - .map(|class| class.class_literal(db).0) - { - let class_body_scope = parent.body_scope(db); - let file = class_body_scope.file(db); - let index = semantic_index(db, file); - for function_scope_id in attribute_scopes(db, class_body_scope) { - for place_expr in index.place_table(function_scope_id).members() { - let Some(name) = place_expr.as_instance_attribute() else { - continue; - }; - let result = ty.member(db, name); - let Some(ty) = result.place.ignore_possibly_undefined() else { - continue; - }; - self.members.insert(Member { - name: Name::new(name), - ty, - }); - } - } - - // This is very similar to `extend_with_class_members`, - // but uses the type of the class instance to query the - // class member. This gets us the right type for each - // member, e.g., `SomeClass.__delattr__` is not a bound - // method, but `instance_of_SomeClass.__delattr__` is. - for memberdef in all_declarations_and_bindings(db, class_body_scope) { - let result = ty.member(db, memberdef.member.name.as_str()); - let Some(ty) = result.place.ignore_possibly_undefined() else { - continue; - }; - self.members.insert(Member { - name: memberdef.member.name, - ty, - }); - } - } - } - - fn extend_with_synthetic_members( - &mut self, - db: &'db dyn Db, - ty: Type<'db>, - class_literal: ClassLiteral<'db>, - specialization: Option>, - ) { - match CodeGeneratorKind::from_class(db, class_literal, specialization) { - Some(CodeGeneratorKind::NamedTuple) => { - if ty.is_nominal_instance() { - self.extend_with_type(db, KnownClass::NamedTupleFallback.to_instance(db)); - } else { - self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db)); - } - } - Some(CodeGeneratorKind::TypedDict) => {} - Some(CodeGeneratorKind::DataclassLike(_)) => { - for attr in SYNTHETIC_DATACLASS_ATTRIBUTES { - if let Place::Defined(synthetic_member, _, _) = ty.member(db, attr).place { - self.members.insert(Member { - name: Name::from(*attr), - ty: synthetic_member, - }); - } - } - } - None => {} - } - } -} - -/// A member of a type with an optional definition. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct MemberWithDefinition<'db> { - pub member: Member<'db>, - pub definition: Option>, -} - -/// A member of a type. -/// -/// This represents a single item in (ideally) the list returned by -/// `dir(object)`. -/// -/// The equality, comparison and hashing traits implemented for -/// this type are done so by taking only the name into account. At -/// present, this is because we assume the name is enough to uniquely -/// identify each attribute on an object. This is perhaps complicated -/// by overloads, but they only get represented by one member for -/// now. Moreover, it is convenient to be able to sort collections of -/// members, and a `Type` currently (as of 2025-07-09) has no way to do -/// ordered comparisons. -#[derive(Clone, Debug)] -pub struct Member<'db> { - pub name: Name, - pub ty: Type<'db>, -} - -impl std::hash::Hash for Member<'_> { - fn hash(&self, state: &mut H) { - self.name.hash(state); - } -} - -impl Eq for Member<'_> {} - -impl<'db> PartialEq for Member<'db> { - fn eq(&self, rhs: &Member<'db>) -> bool { - self.name == rhs.name - } -} - -impl<'db> Ord for Member<'db> { - fn cmp(&self, rhs: &Member<'db>) -> Ordering { - self.name.cmp(&rhs.name) - } -} - -impl<'db> PartialOrd for Member<'db> { - fn partial_cmp(&self, rhs: &Member<'db>) -> Option { - Some(self.cmp(rhs)) - } -} - -/// List all members of a given type: anything that would be valid when accessed -/// as an attribute on an object of the given type. -pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet> { - AllMembers::of(db, ty).members -} - /// Get the primary definition kind for a name expression within a specific file. /// Returns the first definition kind that is reachable for this name in its scope. /// This is useful for IDE features like semantic tokens. diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs new file mode 100644 index 0000000000..66b92d69bc --- /dev/null +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -0,0 +1,516 @@ +//! Routines and types to list all members present on a given type or in a given scope. +//! +//! These two concepts are closely related, since listing all members of a given +//! module-literal type requires listing all members in the module's scope, and +//! listing all members on a nominal-instance type or a class-literal type requires +//! listing all members in the class's body scope. + +use std::cmp::Ordering; + +use ruff_python_ast::name::Name; +use rustc_hash::FxHashSet; + +use crate::{ + Db, NameKind, + place::{Place, imported_symbol, place_from_bindings, place_from_declarations}, + semantic_index::{ + attribute_scopes, definition::Definition, global_scope, place_table, scope::ScopeId, + semantic_index, use_def_map, + }, + types::{ + ClassBase, ClassLiteral, KnownClass, KnownInstanceType, SubclassOfInner, Type, + TypeVarBoundOrConstraints, class::CodeGeneratorKind, generics::Specialization, + }, +}; + +/// Iterate over all declarations and bindings in the given scope. +pub(crate) fn all_members_of_scope<'db>( + db: &'db dyn Db, + scope_id: ScopeId<'db>, +) -> impl Iterator> + 'db { + let use_def_map = use_def_map(db, scope_id); + let table = place_table(db, scope_id); + + use_def_map + .all_end_of_scope_symbol_declarations() + .filter_map(move |(symbol_id, declarations)| { + let place_result = place_from_declarations(db, declarations); + let definition = place_result.single_declaration; + place_result + .ignore_conflicting_declarations() + .place + .ignore_possibly_undefined() + .map(|ty| { + let symbol = table.symbol(symbol_id); + let member = Member { + name: symbol.name().clone(), + ty, + }; + MemberWithDefinition { member, definition } + }) + }) + .chain(use_def_map.all_end_of_scope_symbol_bindings().filter_map( + move |(symbol_id, bindings)| { + // It's not clear to AG how to using a bindings + // iterator here to get the correct definition for + // this binding. Below, we look through all bindings + // with a definition and only take one if there is + // exactly one. I don't think this can be wrong, but + // it's probably omitting definitions in some cases. + let mut definition = None; + for binding in bindings.clone() { + if let Some(def) = binding.binding.definition() { + if definition.is_some() { + definition = None; + break; + } + definition = Some(def); + } + } + place_from_bindings(db, bindings) + .ignore_possibly_undefined() + .map(|ty| { + let symbol = table.symbol(symbol_id); + let member = Member { + name: symbol.name().clone(), + ty, + }; + MemberWithDefinition { member, definition } + }) + }, + )) +} + +// `__init__`, `__repr__`, `__eq__`, `__ne__` and `__hash__` are always included via `object`, +// so we don't need to list them here. +const SYNTHETIC_DATACLASS_ATTRIBUTES: &[&str] = &[ + "__lt__", + "__le__", + "__gt__", + "__ge__", + "__replace__", + "__setattr__", + "__delattr__", + "__slots__", + "__weakref__", + "__match_args__", + "__dataclass_fields__", + "__dataclass_params__", +]; + +struct AllMembers<'db> { + members: FxHashSet>, +} + +impl<'db> AllMembers<'db> { + fn of(db: &'db dyn Db, ty: Type<'db>) -> Self { + let mut all_members = Self { + members: FxHashSet::default(), + }; + all_members.extend_with_type(db, ty); + all_members + } + + fn extend_with_type(&mut self, db: &'db dyn Db, ty: Type<'db>) { + match ty { + Type::Union(union) => self.members.extend( + union + .elements(db) + .iter() + .map(|ty| AllMembers::of(db, *ty).members) + .reduce(|acc, members| acc.intersection(&members).cloned().collect()) + .unwrap_or_default(), + ), + + Type::Intersection(intersection) => self.members.extend( + intersection + .positive(db) + .iter() + .map(|ty| AllMembers::of(db, *ty).members) + .reduce(|acc, members| acc.union(&members).cloned().collect()) + .unwrap_or_default(), + ), + + Type::NominalInstance(instance) => { + let (class_literal, specialization) = instance.class(db).class_literal(db); + self.extend_with_instance_members(db, ty, class_literal); + self.extend_with_synthetic_members(db, ty, class_literal, specialization); + } + + Type::NewTypeInstance(newtype) => { + self.extend_with_type(db, Type::instance(db, newtype.base_class_type(db))); + } + + Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => { + self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + } + + Type::GenericAlias(generic_alias) if generic_alias.is_typed_dict(db) => { + self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + } + + Type::SubclassOf(subclass_of_type) if subclass_of_type.is_typed_dict(db) => { + self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + } + + Type::ClassLiteral(class_literal) => { + self.extend_with_class_members(db, ty, class_literal); + self.extend_with_synthetic_members(db, ty, class_literal, None); + if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { + self.extend_with_class_members(db, ty, metaclass); + } + } + + Type::GenericAlias(generic_alias) => { + let class_literal = generic_alias.origin(db); + self.extend_with_class_members(db, ty, class_literal); + self.extend_with_synthetic_members(db, ty, class_literal, None); + if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { + self.extend_with_class_members(db, ty, metaclass); + } + } + + Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { + SubclassOfInner::Dynamic(_) => { + self.extend_with_type(db, KnownClass::Type.to_instance(db)); + } + _ => { + if let Some(class_type) = subclass_of_type.subclass_of().into_class(db) { + let (class_literal, specialization) = class_type.class_literal(db); + self.extend_with_class_members(db, ty, class_literal); + self.extend_with_synthetic_members(db, ty, class_literal, specialization); + if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { + self.extend_with_class_members(db, ty, metaclass); + } + } + } + }, + + Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy | Type::AlwaysFalsy => { + self.extend_with_type(db, Type::object()); + } + + Type::TypeAlias(alias) => self.extend_with_type(db, alias.value_type(db)), + + Type::TypeVar(bound_typevar) => { + match bound_typevar.typevar(db).bound_or_constraints(db) { + None => { + self.extend_with_type(db, Type::object()); + } + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + self.extend_with_type(db, bound); + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + self.members.extend( + constraints + .elements(db) + .iter() + .map(|ty| AllMembers::of(db, *ty).members) + .reduce(|acc, members| { + acc.intersection(&members).cloned().collect() + }) + .unwrap_or_default(), + ); + } + } + } + + Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::EnumLiteral(_) + | Type::LiteralString + | Type::PropertyInstance(_) + | Type::FunctionLiteral(_) + | Type::BoundMethod(_) + | Type::KnownBoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::Callable(_) + | Type::ProtocolInstance(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::BoundSuper(_) + | Type::TypeIs(_) => match ty.to_meta_type(db) { + Type::ClassLiteral(class_literal) => { + self.extend_with_class_members(db, ty, class_literal); + } + Type::SubclassOf(subclass_of) => { + if let Some(class) = subclass_of.subclass_of().into_class(db) { + self.extend_with_class_members(db, ty, class.class_literal(db).0); + } + } + Type::GenericAlias(generic_alias) => { + let class_literal = generic_alias.origin(db); + self.extend_with_class_members(db, ty, class_literal); + } + _ => {} + }, + + Type::TypedDict(_) => { + if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) { + self.extend_with_class_members(db, ty, class_literal); + } + + if let Type::ClassLiteral(class) = + KnownClass::TypedDictFallback.to_class_literal(db) + { + self.extend_with_instance_members(db, ty, class); + } + } + + Type::ModuleLiteral(literal) => { + self.extend_with_type(db, KnownClass::ModuleType.to_instance(db)); + let module = literal.module(db); + + let Some(file) = module.file(db) else { + return; + }; + + let module_scope = global_scope(db, file); + let use_def_map = use_def_map(db, module_scope); + let place_table = place_table(db, module_scope); + + for (symbol_id, _) in use_def_map.all_end_of_scope_symbol_declarations() { + let symbol_name = place_table.symbol(symbol_id).name(); + let Place::Defined(ty, _, _) = + imported_symbol(db, file, symbol_name, None).place + else { + continue; + }; + + // Filter private symbols from stubs if they appear to be internal types + let is_stub_file = file.path(db).extension() == Some("pyi"); + let is_private_symbol = match NameKind::classify(symbol_name) { + NameKind::Dunder | NameKind::Normal => false, + NameKind::Sunder => true, + }; + if is_private_symbol && is_stub_file { + match ty { + Type::NominalInstance(instance) + if matches!( + instance.known_class(db), + Some( + KnownClass::TypeVar + | KnownClass::TypeVarTuple + | KnownClass::ParamSpec + | KnownClass::UnionType + ) + ) => + { + continue; + } + Type::ClassLiteral(class) if class.is_protocol(db) => continue, + Type::KnownInstance( + KnownInstanceType::TypeVar(_) + | KnownInstanceType::TypeAliasType(_) + | KnownInstanceType::UnionType(_) + | KnownInstanceType::Literal(_) + | KnownInstanceType::Annotated(_), + ) => continue, + _ => {} + } + } + + self.members.insert(Member { + name: symbol_name.clone(), + ty, + }); + } + + self.members + .extend(literal.available_submodule_attributes(db).filter_map( + |submodule_name| { + let ty = literal.resolve_submodule(db, &submodule_name)?; + let name = submodule_name.clone(); + Some(Member { name, ty }) + }, + )); + } + } + } + + /// Add members from `class_literal` (including following its + /// parent classes). + /// + /// `ty` should be the original type that we're adding members for. + /// For example, in: + /// + /// ```text + /// class Meta(type): + /// @property + /// def meta_attr(self) -> int: + /// return 0 + /// + /// class C(metaclass=Meta): ... + /// + /// C. + /// ``` + /// + /// then `class_literal` might be `Meta`, but `ty` should be the + /// type of `C`. This ensures that the descriptor protocol is + /// correctly used (or not used) to get the type of each member of + /// `C`. + fn extend_with_class_members( + &mut self, + db: &'db dyn Db, + ty: Type<'db>, + class_literal: ClassLiteral<'db>, + ) { + for parent in class_literal + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + .map(|class| class.class_literal(db).0) + { + let parent_scope = parent.body_scope(db); + for memberdef in all_members_of_scope(db, parent_scope) { + let result = ty.member(db, memberdef.member.name.as_str()); + let Some(ty) = result.place.ignore_possibly_undefined() else { + continue; + }; + self.members.insert(Member { + name: memberdef.member.name, + ty, + }); + } + } + } + + fn extend_with_instance_members( + &mut self, + db: &'db dyn Db, + ty: Type<'db>, + class_literal: ClassLiteral<'db>, + ) { + for parent in class_literal + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + .map(|class| class.class_literal(db).0) + { + let class_body_scope = parent.body_scope(db); + let file = class_body_scope.file(db); + let index = semantic_index(db, file); + for function_scope_id in attribute_scopes(db, class_body_scope) { + for place_expr in index.place_table(function_scope_id).members() { + let Some(name) = place_expr.as_instance_attribute() else { + continue; + }; + let result = ty.member(db, name); + let Some(ty) = result.place.ignore_possibly_undefined() else { + continue; + }; + self.members.insert(Member { + name: Name::new(name), + ty, + }); + } + } + + // This is very similar to `extend_with_class_members`, + // but uses the type of the class instance to query the + // class member. This gets us the right type for each + // member, e.g., `SomeClass.__delattr__` is not a bound + // method, but `instance_of_SomeClass.__delattr__` is. + for memberdef in all_members_of_scope(db, class_body_scope) { + let result = ty.member(db, memberdef.member.name.as_str()); + let Some(ty) = result.place.ignore_possibly_undefined() else { + continue; + }; + self.members.insert(Member { + name: memberdef.member.name, + ty, + }); + } + } + } + + fn extend_with_synthetic_members( + &mut self, + db: &'db dyn Db, + ty: Type<'db>, + class_literal: ClassLiteral<'db>, + specialization: Option>, + ) { + match CodeGeneratorKind::from_class(db, class_literal, specialization) { + Some(CodeGeneratorKind::NamedTuple) => { + if ty.is_nominal_instance() { + self.extend_with_type(db, KnownClass::NamedTupleFallback.to_instance(db)); + } else { + self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db)); + } + } + Some(CodeGeneratorKind::TypedDict) => {} + Some(CodeGeneratorKind::DataclassLike(_)) => { + for attr in SYNTHETIC_DATACLASS_ATTRIBUTES { + if let Place::Defined(synthetic_member, _, _) = ty.member(db, attr).place { + self.members.insert(Member { + name: Name::from(*attr), + ty: synthetic_member, + }); + } + } + } + None => {} + } + } +} + +/// A member of a type or scope, with an optional definition. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct MemberWithDefinition<'db> { + pub member: Member<'db>, + pub definition: Option>, +} + +/// A member of a type or scope. +/// +/// In the context of the [`all_members`] routine, this represents +/// a single item in (ideally) the list returned by `dir(object)`. +/// +/// The equality, comparison and hashing traits implemented for +/// this type are done so by taking only the name into account. At +/// present, this is because we assume the name is enough to uniquely +/// identify each attribute on an object. This is perhaps complicated +/// by overloads, but they only get represented by one member for +/// now. Moreover, it is convenient to be able to sort collections of +/// members, and a [`Type`] currently (as of 2025-07-09) has no way to do +/// ordered comparisons. +#[derive(Clone, Debug)] +pub struct Member<'db> { + pub name: Name, + pub ty: Type<'db>, +} + +impl std::hash::Hash for Member<'_> { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + +impl Eq for Member<'_> {} + +impl<'db> PartialEq for Member<'db> { + fn eq(&self, rhs: &Member<'db>) -> bool { + self.name == rhs.name + } +} + +impl<'db> Ord for Member<'db> { + fn cmp(&self, rhs: &Member<'db>) -> Ordering { + self.name.cmp(&rhs.name) + } +} + +impl<'db> PartialOrd for Member<'db> { + fn partial_cmp(&self, rhs: &Member<'db>) -> Option { + Some(self.cmp(rhs)) + } +} + +/// List all members of a given type: anything that would be valid when accessed +/// as an attribute on an object of the given type. +pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet> { + AllMembers::of(db, ty).members +} diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index a56f04d1db..683f5549d1 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -25,7 +25,7 @@ use crate::{ report_overridden_final_method, }, function::{FunctionDecorators, FunctionType, KnownFunction}, - ide_support::{MemberWithDefinition, all_declarations_and_bindings}, + list_members::{MemberWithDefinition, all_members_of_scope}, }, }; @@ -53,8 +53,7 @@ pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLite } let class_specialized = class.identity_specialization(db); - let own_class_members: FxHashSet<_> = - all_declarations_and_bindings(db, class.body_scope(db)).collect(); + let own_class_members: FxHashSet<_> = all_members_of_scope(db, class.body_scope(db)).collect(); for member in own_class_members { check_class_declaration(context, configuration, class_specialized, &member);