mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 07:04:37 +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]
|
||||
fn class_init3() {
|
||||
let test = cursor_test(
|
||||
|
@ -1358,7 +1491,7 @@ Quux.<CURSOR>
|
|||
);
|
||||
|
||||
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_class_method :: bound method <class 'Quux'>.some_class_method() -> int
|
||||
some_method :: def some_method(self) -> int
|
||||
|
@ -1368,7 +1501,7 @@ Quux.<CURSOR>
|
|||
__base__ :: type | None
|
||||
__bases__ :: tuple[type, ...]
|
||||
__basicsize__ :: int
|
||||
__call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any
|
||||
__call__ :: bound method <class 'Quux'>.__call__(...) -> Any
|
||||
__class__ :: <class 'type'>
|
||||
__delattr__ :: def __delattr__(self, name: str, /) -> None
|
||||
__dict__ :: MappingProxyType[str, Any]
|
||||
|
@ -1383,26 +1516,26 @@ Quux.<CURSOR>
|
|||
__hash__ :: def __hash__(self) -> int
|
||||
__init__ :: def __init__(self) -> Unknown
|
||||
__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
|
||||
__module__ :: str
|
||||
__mro__ :: tuple[<class 'type'>, <class 'object'>]
|
||||
__mro__ :: tuple[<class 'Quux'>, <class 'object'>]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__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]
|
||||
__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
|
||||
__ror__ :: bound method <class 'Quux'>.__ror__(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 'object'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__subclasscheck__ :: bound method <class 'Quux'>.__subclasscheck__(subclass: type, /) -> bool
|
||||
__subclasses__ :: bound method <class 'Quux'>.__subclasses__() -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'Quux'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
__weakrefoffset__ :: int
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue