[ty] Fix handling of metaclasses in object.<CURSOR> completions

Basically, we weren't quite using `Type::member` in every case
correctly. Specifically, this example from @sharkdp:

```
class Meta(type):
    @property
    def meta_attr(self) -> int:
        return 0

class C(metaclass=Meta): ...

C.<CURSOR>
```

While we would return `C.meta_attr` here, we were claiming its type was
`property`. But its type should be `int`.

Ref https://github.com/astral-sh/ruff/pull/19216#discussion_r2197065241
This commit is contained in:
Andrew Gallant 2025-07-11 10:14:33 -04:00 committed by Andrew Gallant
parent 3560f86450
commit f7973ac870
2 changed files with 176 additions and 18 deletions

View file

@ -95,21 +95,21 @@ impl<'db> AllMembers<'db> {
}
Type::ClassLiteral(class_literal) => {
self.extend_with_class_members(db, class_literal);
self.extend_with_class_members(db, ty, class_literal);
if let Type::ClassLiteral(meta_class_literal) = ty.to_meta_type(db) {
self.extend_with_class_members(db, meta_class_literal);
self.extend_with_class_members(db, ty, meta_class_literal);
}
}
Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db);
self.extend_with_class_members(db, class_literal);
self.extend_with_class_members(db, ty, class_literal);
}
Type::SubclassOf(subclass_of_type) => {
if let Some(class_literal) = subclass_of_type.subclass_of().into_class() {
self.extend_with_class_members(db, class_literal.class_literal(db).0);
self.extend_with_class_members(db, ty, class_literal.class_literal(db).0);
}
}
@ -136,11 +136,11 @@ impl<'db> AllMembers<'db> {
| Type::BoundSuper(_)
| Type::TypeIs(_) => match ty.to_meta_type(db) {
Type::ClassLiteral(class_literal) => {
self.extend_with_class_members(db, class_literal);
self.extend_with_class_members(db, ty, class_literal);
}
Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db);
self.extend_with_class_members(db, class_literal);
self.extend_with_class_members(db, ty, class_literal);
}
_ => {}
},
@ -214,16 +214,41 @@ impl<'db> AllMembers<'db> {
}
}
fn extend_with_class_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) {
/// 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_ty = Type::ClassLiteral(parent);
let parent_scope = parent.body_scope(db);
for Member { name, .. } in all_declarations_and_bindings(db, parent_scope) {
let result = parent_ty.member(db, name.as_str());
let result = ty.member(db, name.as_str());
let Some(ty) = result.place.ignore_possibly_unbound() else {
continue;
};