mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 15:14:42 +00:00
[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:
parent
3560f86450
commit
f7973ac870
2 changed files with 176 additions and 18 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue