[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

@ -1303,6 +1303,139 @@ quux.b<CURSOR>
"); ");
} }
#[test]
fn metaclass1() {
let test = cursor_test(
"\
class Meta(type):
@property
def meta_attr(self) -> int:
return 0
class C(metaclass=Meta): ...
C.<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
meta_attr :: int
mro :: bound method <class 'C'>.mro() -> list[type]
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
__basicsize__ :: int
__call__ :: bound method <class 'C'>.__call__(...) -> Any
__class__ :: <class 'Meta'>
__delattr__ :: def __delattr__(self, name: str, /) -> None
__dict__ :: MappingProxyType[str, Any]
__dictoffset__ :: int
__dir__ :: def __dir__(self) -> Iterable[str]
__doc__ :: str | None
__eq__ :: def __eq__(self, value: object, /) -> bool
__flags__ :: int
__format__ :: def __format__(self, format_spec: str, /) -> str
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
__getstate__ :: def __getstate__(self) -> object
__hash__ :: def __hash__(self) -> int
__init__ :: def __init__(self) -> None
__init_subclass__ :: def __init_subclass__(cls) -> None
__instancecheck__ :: bound method <class 'C'>.__instancecheck__(instance: Any, /) -> bool
__itemsize__ :: int
__module__ :: str
__mro__ :: tuple[<class 'C'>, <class 'object'>]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls) -> Self
__or__ :: bound method <class 'C'>.__or__(value: Any, /) -> UnionType
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: def __repr__(self) -> str
__ror__ :: bound method <class 'C'>.__ror__(value: Any, /) -> UnionType
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: bound method <class 'C'>.__subclasscheck__(subclass: type, /) -> bool
__subclasses__ :: bound method <class 'C'>.__subclasses__() -> list[Self]
__subclasshook__ :: bound method <class 'C'>.__subclasshook__(subclass: type, /) -> bool
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
__weakrefoffset__ :: int
");
}
#[test]
fn metaclass2() {
let test = cursor_test(
"\
class Meta(type):
@property
def meta_attr(self) -> int:
return 0
class C(metaclass=Meta): ...
Meta.<CURSOR>
",
);
insta::with_settings!({
// The formatting of some types are different depending on
// whether we're in release mode or not. These differences
// aren't really relevant for completion tests AFAIK, so
// just redact them. ---AG
filters => [(r"(?m)\s*__(annotations|new)__.+$", "")]},
{
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
meta_attr :: property
mro :: def mro(self) -> list[type]
__base__ :: type | None
__bases__ :: tuple[type, ...]
__basicsize__ :: int
__call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any
__class__ :: <class 'type'>
__delattr__ :: def __delattr__(self, name: str, /) -> None
__dict__ :: MappingProxyType[str, Any]
__dictoffset__ :: int
__dir__ :: def __dir__(self) -> Iterable[str]
__doc__ :: str | None
__eq__ :: def __eq__(self, value: object, /) -> bool
__flags__ :: int
__format__ :: def __format__(self, format_spec: str, /) -> str
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
__getstate__ :: def __getstate__(self) -> object
__hash__ :: def __hash__(self) -> int
__init__ :: Overload[(self, o: object, /) -> None, (self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None]
__init_subclass__ :: def __init_subclass__(cls) -> None
__instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool
__itemsize__ :: int
__module__ :: str
__mro__ :: tuple[<class 'Meta'>, <class 'type'>, <class 'object'>]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__or__ :: def __or__(self, value: Any, /) -> UnionType
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: def __repr__(self) -> str
__ror__ :: def __ror__(self, value: Any, /) -> UnionType
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool
__subclasses__ :: def __subclasses__(self: Self) -> list[Self]
__subclasshook__ :: bound method <class 'Meta'>.__subclasshook__(subclass: type, /) -> bool
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
__weakrefoffset__ :: int
");
}
);
}
#[test] #[test]
fn class_init3() { fn class_init3() {
let test = cursor_test( let test = cursor_test(
@ -1358,7 +1491,7 @@ Quux.<CURSOR>
); );
assert_snapshot!(test.completions_without_builtins_with_types(), @r" assert_snapshot!(test.completions_without_builtins_with_types(), @r"
mro :: def mro(self) -> list[type] mro :: bound method <class 'Quux'>.mro() -> list[type]
some_attribute :: int some_attribute :: int
some_class_method :: bound method <class 'Quux'>.some_class_method() -> int some_class_method :: bound method <class 'Quux'>.some_class_method() -> int
some_method :: def some_method(self) -> int some_method :: def some_method(self) -> int
@ -1368,7 +1501,7 @@ Quux.<CURSOR>
__base__ :: type | None __base__ :: type | None
__bases__ :: tuple[type, ...] __bases__ :: tuple[type, ...]
__basicsize__ :: int __basicsize__ :: int
__call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any __call__ :: bound method <class 'Quux'>.__call__(...) -> Any
__class__ :: <class 'type'> __class__ :: <class 'type'>
__delattr__ :: def __delattr__(self, name: str, /) -> None __delattr__ :: def __delattr__(self, name: str, /) -> None
__dict__ :: MappingProxyType[str, Any] __dict__ :: MappingProxyType[str, Any]
@ -1383,26 +1516,26 @@ Quux.<CURSOR>
__hash__ :: def __hash__(self) -> int __hash__ :: def __hash__(self) -> int
__init__ :: def __init__(self) -> Unknown __init__ :: def __init__(self) -> Unknown
__init_subclass__ :: def __init_subclass__(cls) -> None __init_subclass__ :: def __init_subclass__(cls) -> None
__instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool __instancecheck__ :: bound method <class 'Quux'>.__instancecheck__(instance: Any, /) -> bool
__itemsize__ :: int __itemsize__ :: int
__module__ :: str __module__ :: str
__mro__ :: tuple[<class 'type'>, <class 'object'>] __mro__ :: tuple[<class 'Quux'>, <class 'object'>]
__name__ :: str __name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool __ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls) -> Self __new__ :: def __new__(cls) -> Self
__or__ :: def __or__(self, value: Any, /) -> UnionType __or__ :: bound method <class 'Quux'>.__or__(value: Any, /) -> UnionType
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] __prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
__qualname__ :: str __qualname__ :: str
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...] __reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...] __reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: def __repr__(self) -> str __repr__ :: def __repr__(self) -> str
__ror__ :: def __ror__(self, value: Any, /) -> UnionType __ror__ :: bound method <class 'Quux'>.__ror__(value: Any, /) -> UnionType
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None __setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__sizeof__ :: def __sizeof__(self) -> int __sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str __str__ :: def __str__(self) -> str
__subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool __subclasscheck__ :: bound method <class 'Quux'>.__subclasscheck__(subclass: type, /) -> bool
__subclasses__ :: def __subclasses__(self: Self) -> list[Self] __subclasses__ :: bound method <class 'Quux'>.__subclasses__() -> list[Self]
__subclasshook__ :: bound method <class 'object'>.__subclasshook__(subclass: type, /) -> bool __subclasshook__ :: bound method <class 'Quux'>.__subclasshook__(subclass: type, /) -> bool
__text_signature__ :: str | None __text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
__weakrefoffset__ :: int __weakrefoffset__ :: int

View file

@ -95,21 +95,21 @@ impl<'db> AllMembers<'db> {
} }
Type::ClassLiteral(class_literal) => { 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) { 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) => { Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db); 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) => { Type::SubclassOf(subclass_of_type) => {
if let Some(class_literal) = subclass_of_type.subclass_of().into_class() { 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::BoundSuper(_)
| Type::TypeIs(_) => match ty.to_meta_type(db) { | Type::TypeIs(_) => match ty.to_meta_type(db) {
Type::ClassLiteral(class_literal) => { 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) => { Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db); 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 for parent in class_literal
.iter_mro(db, None) .iter_mro(db, None)
.filter_map(ClassBase::into_class) .filter_map(ClassBase::into_class)
.map(|class| class.class_literal(db).0) .map(|class| class.class_literal(db).0)
{ {
let parent_ty = Type::ClassLiteral(parent);
let parent_scope = parent.body_scope(db); let parent_scope = parent.body_scope(db);
for Member { name, .. } in all_declarations_and_bindings(db, parent_scope) { 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 { let Some(ty) = result.place.ignore_possibly_unbound() else {
continue; continue;
}; };