[ty] Move `all_members`, and related types/routines, out of `ide_support.rs` (#21695)

This commit is contained in:
Alex Waygood 2025-12-02 14:45:24 +00:00 committed by GitHub
parent 644096ea8a
commit ac2552b11b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 533 additions and 518 deletions

View File

@ -11,7 +11,7 @@ use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module};
use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::FileScopeId; use crate::semantic_index::scope::FileScopeId;
use crate::semantic_index::semantic_index; 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::types::{Type, binding_type, infer_scope_types};
use crate::{Db, resolve_real_shadowable_module}; 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 (file_scope, _) in index.ancestor_scopes(file_scope) {
for memberdef in 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( members.insert(
memberdef.member.name, memberdef.member.name,
@ -221,12 +221,13 @@ impl<'db> SemanticModel<'db> {
let mut completions = vec![]; let mut completions = vec![];
for (file_scope, _) in index.ancestor_scopes(file_scope) { for (file_scope, _) in index.ancestor_scopes(file_scope) {
completions.extend( completions.extend(
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)).map(
.map(|memberdef| Completion { |memberdef| Completion {
name: memberdef.member.name, name: memberdef.member.name,
ty: Some(memberdef.member.ty), ty: Some(memberdef.member.ty),
builtin: false, builtin: false,
}), },
),
); );
} }
// Builtins are available in all scopes. // Builtins are available in all scopes.

View File

@ -96,6 +96,7 @@ mod generics;
pub mod ide_support; pub mod ide_support;
mod infer; mod infer;
mod instance; mod instance;
pub mod list_members;
mod member; mod member;
mod mro; mod mro;
mod narrow; mod narrow;

View File

@ -39,7 +39,7 @@ use crate::types::{
DataclassParams, FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, DataclassParams, FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType,
MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType, MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType,
TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, 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_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion}; use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion};
@ -888,7 +888,7 @@ impl<'db> Bindings<'db> {
if let [Some(ty)] = overload.parameter_types() { if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(Type::heterogeneous_tuple( overload.set_return_type(Type::heterogeneous_tuple(
db, db,
ide_support::all_members(db, *ty) list_members::all_members(db, *ty)
.into_iter() .into_iter()
.sorted() .sorted()
.map(|member| Type::string_literal(db, &member.name)), .map(|member| Type::string_literal(db, &member.name)),

View File

@ -74,7 +74,7 @@ use crate::types::diagnostic::{
}; };
use crate::types::display::DisplaySettings; use crate::types::display::DisplaySettings;
use crate::types::generics::{GenericContext, InferableTypeVars}; 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::narrow::ClassInfoConstraintFunction;
use crate::types::signatures::{CallableSignature, Signature}; use crate::types::signatures::{CallableSignature, Signature};
use crate::types::visitor::any_over_type; use crate::types::visitor::any_over_type;

View File

@ -1,27 +1,16 @@
use std::cmp::Ordering;
use std::collections::HashMap; use std::collections::HashMap;
use crate::place::{ use crate::place::builtins_module_scope;
Place, builtins_module_scope, imported_symbol, place_from_bindings, place_from_declarations,
};
use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::Definition;
use crate::semantic_index::definition::DefinitionKind; use crate::semantic_index::definition::DefinitionKind;
use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{attribute_scopes, global_scope, semantic_index, use_def_map};
use crate::semantic_index::{
attribute_scopes, global_scope, place_table, semantic_index, use_def_map,
};
use crate::types::call::{CallArguments, MatchedArgument}; use crate::types::call::{CallArguments, MatchedArgument};
use crate::types::generics::Specialization;
use crate::types::signatures::Signature; use crate::types::signatures::Signature;
use crate::types::{CallDunderError, SubclassOfInner, UnionType}; use crate::types::{CallDunderError, UnionType};
use crate::types::{ use crate::types::{CallableTypes, ClassBase, KnownClass, Type, TypeContext};
CallableTypes, ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext, use crate::{Db, DisplaySettings, HasType, SemanticModel};
TypeVarBoundOrConstraints, class::CodeGeneratorKind,
};
use crate::{Db, DisplaySettings, HasType, NameKind, SemanticModel};
use ruff_db::files::FileRange; use ruff_db::files::FileRange;
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
@ -29,497 +18,6 @@ use rustc_hash::FxHashSet;
pub use resolve_definition::{ImportAliasResolution, ResolvedDefinition, map_stub_definition}; pub use resolve_definition::{ImportAliasResolution, ResolvedDefinition, map_stub_definition};
use resolve_definition::{find_symbol_in_scope, resolve_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<Item = MemberWithDefinition<'db>> + '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<Member<'db>>,
}
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.<CURSOR>
/// ```
///
/// 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<Specialization<'db>>,
) {
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<Definition<'db>>,
}
/// 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<H: std::hash::Hasher>(&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<Ordering> {
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<Member<'db>> {
AllMembers::of(db, ty).members
}
/// Get the primary definition kind for a name expression within a specific file. /// 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. /// Returns the first definition kind that is reachable for this name in its scope.
/// This is useful for IDE features like semantic tokens. /// This is useful for IDE features like semantic tokens.

View File

@ -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<Item = MemberWithDefinition<'db>> + '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<Member<'db>>,
}
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.<CURSOR>
/// ```
///
/// 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<Specialization<'db>>,
) {
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<Definition<'db>>,
}
/// 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<H: std::hash::Hasher>(&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<Ordering> {
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<Member<'db>> {
AllMembers::of(db, ty).members
}

View File

@ -25,7 +25,7 @@ use crate::{
report_overridden_final_method, report_overridden_final_method,
}, },
function::{FunctionDecorators, FunctionType, KnownFunction}, 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 class_specialized = class.identity_specialization(db);
let own_class_members: FxHashSet<_> = let own_class_members: FxHashSet<_> = all_members_of_scope(db, class.body_scope(db)).collect();
all_declarations_and_bindings(db, class.body_scope(db)).collect();
for member in own_class_members { for member in own_class_members {
check_class_declaration(context, configuration, class_specialized, &member); check_class_declaration(context, configuration, class_specialized, &member);