diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 15caef7d46..f689a04533 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1223,33 +1223,33 @@ quux. ", ); - assert_snapshot!(test.completions_without_builtins(), @r" - bar - baz - foo - __annotations__ - __class__ - __delattr__ - __dict__ - __dir__ - __doc__ - __eq__ - __format__ - __getattribute__ - __getstate__ - __hash__ - __init__ - __init_subclass__ - __module__ - __ne__ - __new__ - __reduce__ - __reduce_ex__ - __repr__ - __setattr__ - __sizeof__ - __str__ - __subclasshook__ + assert_snapshot!(test.completions_without_builtins_with_types(), @r" + bar :: Unknown | Literal[2] + baz :: Unknown | Literal[3] + foo :: Unknown | Literal[1] + __annotations__ :: dict[str, Any] + __class__ :: type + __delattr__ :: bound method object.__delattr__(name: str, /) -> None + __dict__ :: dict[str, Any] + __dir__ :: bound method object.__dir__() -> Iterable[str] + __doc__ :: str | None + __eq__ :: bound method object.__eq__(value: object, /) -> bool + __format__ :: bound method object.__format__(format_spec: str, /) -> str + __getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any + __getstate__ :: bound method object.__getstate__() -> object + __hash__ :: bound method object.__hash__() -> int + __init__ :: bound method Quux.__init__() -> Unknown + __init_subclass__ :: bound method object.__init_subclass__() -> None + __module__ :: str + __ne__ :: bound method object.__ne__(value: object, /) -> bool + __new__ :: bound method object.__new__() -> Self + __reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...] + __reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...] + __repr__ :: bound method object.__repr__() -> str + __setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None + __sizeof__ :: bound method object.__sizeof__() -> int + __str__ :: bound method object.__str__() -> str + __subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool "); } @@ -1268,33 +1268,33 @@ quux.b ", ); - assert_snapshot!(test.completions_without_builtins(), @r" - bar - baz - foo - __annotations__ - __class__ - __delattr__ - __dict__ - __dir__ - __doc__ - __eq__ - __format__ - __getattribute__ - __getstate__ - __hash__ - __init__ - __init_subclass__ - __module__ - __ne__ - __new__ - __reduce__ - __reduce_ex__ - __repr__ - __setattr__ - __sizeof__ - __str__ - __subclasshook__ + assert_snapshot!(test.completions_without_builtins_with_types(), @r" + bar :: Unknown | Literal[2] + baz :: Unknown | Literal[3] + foo :: Unknown | Literal[1] + __annotations__ :: dict[str, Any] + __class__ :: type + __delattr__ :: bound method object.__delattr__(name: str, /) -> None + __dict__ :: dict[str, Any] + __dir__ :: bound method object.__dir__() -> Iterable[str] + __doc__ :: str | None + __eq__ :: bound method object.__eq__(value: object, /) -> bool + __format__ :: bound method object.__format__(format_spec: str, /) -> str + __getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any + __getstate__ :: bound method object.__getstate__() -> object + __hash__ :: bound method object.__hash__() -> int + __init__ :: bound method Quux.__init__() -> Unknown + __init_subclass__ :: bound method object.__init_subclass__() -> None + __module__ :: str + __ne__ :: bound method object.__ne__(value: object, /) -> bool + __new__ :: bound method object.__new__() -> Self + __reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...] + __reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...] + __repr__ :: bound method object.__repr__() -> str + __setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None + __sizeof__ :: bound method object.__sizeof__() -> int + __str__ :: bound method object.__str__() -> str + __subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool "); } @@ -1321,6 +1321,89 @@ class Quux: assert_snapshot!(test.completions_without_builtins(), @""); } + #[test] + fn class_attributes1() { + let test = cursor_test( + "\ +class Quux: + some_attribute: int = 1 + + def __init__(self): + self.foo = 1 + self.bar = 2 + self.baz = 3 + + def some_method(self) -> int: + return 1 + + @property + def some_property(self) -> int: + return 1 + + @classmethod + def some_class_method(self) -> int: + return 1 + + @staticmethod + def some_static_method(self) -> int: + return 1 + +Quux. +", + ); + + assert_snapshot!(test.completions_without_builtins_with_types(), @r" + mro :: def mro(self) -> list[type] + some_attribute :: int + some_class_method :: bound method .some_class_method() -> int + some_method :: def some_method(self) -> int + some_property :: property + some_static_method :: def some_static_method(self) -> int + __annotations__ :: dict[str, Any] + __base__ :: type | None + __bases__ :: tuple[type, ...] + __basicsize__ :: int + __call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any + __class__ :: + __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) -> Unknown + __init_subclass__ :: def __init_subclass__(cls) -> None + __instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool + __itemsize__ :: int + __module__ :: str + __mro__ :: tuple[, ] + __name__ :: str + __ne__ :: def __ne__(self, value: object, /) -> bool + __new__ :: def __new__(cls) -> Self + __or__ :: def __or__(self, value: Any, /) -> UnionType + __prepare__ :: bound method .__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 .__subclasshook__(subclass: type, /) -> bool + __text_signature__ :: str | None + __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] + __weakrefoffset__ :: int + "); + } + // We don't yet take function parameters into account. #[test] fn call_prefix1() { @@ -2366,7 +2449,22 @@ importlib. self.completions_if(|c| !c.builtin) } + fn completions_without_builtins_with_types(&self) -> String { + self.completions_if_snapshot( + |c| !c.builtin, + |c| format!("{} :: {}", c.name, c.ty.display(&self.db)), + ) + } + fn completions_if(&self, predicate: impl Fn(&Completion) -> bool) -> String { + self.completions_if_snapshot(predicate, |c| c.name.as_str().to_string()) + } + + fn completions_if_snapshot( + &self, + predicate: impl Fn(&Completion) -> bool, + snapshot: impl Fn(&Completion) -> String, + ) -> String { let completions = completion(&self.db, self.cursor.file, self.cursor.offset); if completions.is_empty() { return "".to_string(); @@ -2374,7 +2472,7 @@ importlib. let included = completions .iter() .filter(|label| predicate(label)) - .map(|completion| completion.name.as_str().to_string()) + .map(snapshot) .collect::>(); if included.is_empty() { // It'd be nice to include the actual number of diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 8a794f2e6d..61ae1bb0ff 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -73,7 +73,7 @@ impl<'db> SemanticModel<'db> { .into_iter() .map(|member| Completion { name: member.name, - ty: None, + ty: member.ty, builtin, }) .collect() @@ -86,7 +86,7 @@ impl<'db> SemanticModel<'db> { .into_iter() .map(|member| Completion { name: member.name, - ty: None, + ty: member.ty, builtin: false, }) .collect() @@ -122,7 +122,7 @@ impl<'db> SemanticModel<'db> { all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file)) .map(|member| Completion { name: member.name, - ty: None, + ty: member.ty, builtin: false, }), ); @@ -172,8 +172,8 @@ impl NameKind { pub struct Completion<'db> { /// The label shown to the user for this suggestion. pub name: Name, - /// The type of this completion, if available. - pub ty: Option>, + /// The type of this completion. + pub ty: Type<'db>, /// Whether this suggestion came from builtins or not. /// /// At time of writing (2025-06-26), this information diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 23f91a01cb..ab69186726 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2048,7 +2048,11 @@ impl<'db> ClassLiteral<'db> { /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. - fn own_instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub(crate) fn own_instance_member( + self, + db: &'db dyn Db, + name: &str, + ) -> PlaceAndQualifiers<'db> { // TODO: There are many things that are not yet implemented here: // - `typing.Final` // - Proper diagnostics diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 0efe7881ad..bd934aa41f 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1,3 +1,6 @@ +use std::cmp::Ordering; + +use crate::module_resolver::resolve_module; use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations}; use crate::semantic_index::definition::DefinitionKind; use crate::semantic_index::place::ScopeId; @@ -14,7 +17,7 @@ use rustc_hash::FxHashSet; pub(crate) fn all_declarations_and_bindings<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, -) -> impl Iterator + 'db { +) -> impl Iterator> + 'db { let use_def_map = use_def_map(db, scope_id); let table = place_table(db, scope_id); @@ -24,12 +27,12 @@ pub(crate) fn all_declarations_and_bindings<'db>( place_from_declarations(db, declarations) .ok() .and_then(|result| { - result.place.ignore_possibly_unbound().and_then(|_| { + result.place.ignore_possibly_unbound().and_then(|ty| { table .place_expr(symbol_id) .as_name() .cloned() - .map(|name| Member { name }) + .map(|name| Member { name, ty }) }) }) }) @@ -39,23 +42,23 @@ pub(crate) fn all_declarations_and_bindings<'db>( .filter_map(move |(symbol_id, bindings)| { place_from_bindings(db, bindings) .ignore_possibly_unbound() - .and_then(|_| { + .and_then(|ty| { table .place_expr(symbol_id) .as_name() .cloned() - .map(|name| Member { name }) + .map(|name| Member { name, ty }) }) }), ) } -struct AllMembers { - members: FxHashSet, +struct AllMembers<'db> { + members: FxHashSet>, } -impl AllMembers { - fn of<'db>(db: &'db dyn Db, ty: Type<'db>) -> Self { +impl<'db> AllMembers<'db> { + fn of(db: &'db dyn Db, ty: Type<'db>) -> Self { let mut all_members = Self { members: FxHashSet::default(), }; @@ -63,7 +66,7 @@ impl AllMembers { all_members } - fn extend_with_type<'db>(&mut self, db: &'db dyn Db, ty: Type<'db>) { + fn extend_with_type(&mut self, db: &'db dyn Db, ty: Type<'db>) { match ty { Type::Union(union) => self.members.extend( union @@ -85,7 +88,6 @@ impl AllMembers { Type::NominalInstance(instance) => { let (class_literal, _specialization) = instance.class.class_literal(db); - self.extend_with_class_members(db, class_literal); self.extend_with_instance_members(db, class_literal); } @@ -191,6 +193,7 @@ impl AllMembers { self.members.insert(Member { name: place_table.place_expr(symbol_id).expect_name().clone(), + ty, }); } @@ -198,71 +201,133 @@ impl AllMembers { self.members.extend( imported_modules(db, literal.importing_file(db)) .iter() - .filter_map(|submodule_name| submodule_name.relative_to(module_name)) - .filter_map(|relative_submodule_name| { - Some(Member { - name: Name::from(relative_submodule_name.components().next()?), - }) + .filter_map(|submodule_name| { + let module = resolve_module(db, submodule_name)?; + let ty = Type::module_literal(db, file, &module); + Some((submodule_name, ty)) + }) + .filter_map(|(submodule_name, ty)| { + let relative = submodule_name.relative_to(module_name)?; + Some((relative, ty)) + }) + .filter_map(|(relative_submodule_name, ty)| { + let name = Name::from(relative_submodule_name.components().next()?); + Some(Member { name, ty }) }), ); } } } - fn extend_with_declarations_and_bindings(&mut self, db: &dyn Db, scope_id: ScopeId) { - self.members - .extend(all_declarations_and_bindings(db, scope_id)); - } - - fn extend_with_class_members<'db>( - &mut self, - db: &'db dyn Db, - class_literal: ClassLiteral<'db>, - ) { + fn extend_with_class_members(&mut self, db: &'db dyn 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); - self.extend_with_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 Some(ty) = result.place.ignore_possibly_unbound() else { + continue; + }; + self.members.insert(Member { name, ty }); + } } } - fn extend_with_instance_members<'db>( - &mut self, - db: &'db dyn Db, - class_literal: ClassLiteral<'db>, - ) { + fn extend_with_instance_members(&mut self, db: &'db dyn 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_instance = Type::instance(db, parent.default_specialization(db)); 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) { let place_table = index.place_table(function_scope_id); - self.members.extend( - place_table - .instance_attributes() - .cloned() - .map(|name| Member { name }), - ); + for place_expr in place_table.places() { + let Some(name) = place_expr.as_instance_attribute() else { + continue; + }; + let result = parent_instance.member(db, name.as_str()); + let Some(ty) = result.place.ignore_possibly_unbound() else { + continue; + }; + self.members.insert(Member { + name: name.clone(), + 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 Member { name, .. } in all_declarations_and_bindings(db, class_body_scope) { + let result = parent_instance.member(db, name.as_str()); + let Some(ty) = result.place.ignore_possibly_unbound() else { + continue; + }; + self.members.insert(Member { name, ty }); } } } } -#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub struct Member { +/// 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(&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 { + 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 { +pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet> { AllMembers::of(db, ty).members }