[ty] Add type information to all_members API

Since we generally need (so far) to get the type information of each
suggestion to figure out its boundness anyway, we might as well expose
it here. Completions want to use this information to enhance the
metadata on each suggestion for a more pleasant user experience.

For the most part, this was pretty straight-forward. The most exciting
part was in computing the types for instance attributes. I'm not 100%
sure it's correct or is the best way to do it.
This commit is contained in:
Andrew Gallant 2025-07-08 14:43:50 -04:00 committed by Andrew Gallant
parent 79fe538458
commit fea84e8777
4 changed files with 269 additions and 102 deletions

View file

@ -1223,33 +1223,33 @@ quux.<CURSOR>
", ",
); );
assert_snapshot!(test.completions_without_builtins(), @r" assert_snapshot!(test.completions_without_builtins_with_types(), @r"
bar bar :: Unknown | Literal[2]
baz baz :: Unknown | Literal[3]
foo foo :: Unknown | Literal[1]
__annotations__ __annotations__ :: dict[str, Any]
__class__ __class__ :: type
__delattr__ __delattr__ :: bound method object.__delattr__(name: str, /) -> None
__dict__ __dict__ :: dict[str, Any]
__dir__ __dir__ :: bound method object.__dir__() -> Iterable[str]
__doc__ __doc__ :: str | None
__eq__ __eq__ :: bound method object.__eq__(value: object, /) -> bool
__format__ __format__ :: bound method object.__format__(format_spec: str, /) -> str
__getattribute__ __getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
__getstate__ __getstate__ :: bound method object.__getstate__() -> object
__hash__ __hash__ :: bound method object.__hash__() -> int
__init__ __init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ __init_subclass__ :: bound method object.__init_subclass__() -> None
__module__ __module__ :: str
__ne__ __ne__ :: bound method object.__ne__(value: object, /) -> bool
__new__ __new__ :: bound method object.__new__() -> Self
__reduce__ __reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ __reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ __repr__ :: bound method object.__repr__() -> str
__setattr__ __setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
__sizeof__ __sizeof__ :: bound method object.__sizeof__() -> int
__str__ __str__ :: bound method object.__str__() -> str
__subclasshook__ __subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
"); ");
} }
@ -1268,33 +1268,33 @@ quux.b<CURSOR>
", ",
); );
assert_snapshot!(test.completions_without_builtins(), @r" assert_snapshot!(test.completions_without_builtins_with_types(), @r"
bar bar :: Unknown | Literal[2]
baz baz :: Unknown | Literal[3]
foo foo :: Unknown | Literal[1]
__annotations__ __annotations__ :: dict[str, Any]
__class__ __class__ :: type
__delattr__ __delattr__ :: bound method object.__delattr__(name: str, /) -> None
__dict__ __dict__ :: dict[str, Any]
__dir__ __dir__ :: bound method object.__dir__() -> Iterable[str]
__doc__ __doc__ :: str | None
__eq__ __eq__ :: bound method object.__eq__(value: object, /) -> bool
__format__ __format__ :: bound method object.__format__(format_spec: str, /) -> str
__getattribute__ __getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
__getstate__ __getstate__ :: bound method object.__getstate__() -> object
__hash__ __hash__ :: bound method object.__hash__() -> int
__init__ __init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ __init_subclass__ :: bound method object.__init_subclass__() -> None
__module__ __module__ :: str
__ne__ __ne__ :: bound method object.__ne__(value: object, /) -> bool
__new__ __new__ :: bound method object.__new__() -> Self
__reduce__ __reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ __reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ __repr__ :: bound method object.__repr__() -> str
__setattr__ __setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
__sizeof__ __sizeof__ :: bound method object.__sizeof__() -> int
__str__ __str__ :: bound method object.__str__() -> str
__subclasshook__ __subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
"); ");
} }
@ -1321,6 +1321,89 @@ class Quux:
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>"); assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
} }
#[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.<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
mro :: def mro(self) -> list[type]
some_attribute :: int
some_class_method :: bound method <class 'Quux'>.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__ :: <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__ :: def __init__(self) -> Unknown
__init_subclass__ :: def __init_subclass__(cls) -> None
__instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool
__itemsize__ :: int
__module__ :: str
__mro__ :: tuple[<class 'type'>, <class 'object'>]
__name__ :: str
__ne__ :: def __ne__(self, value: object, /) -> bool
__new__ :: def __new__(cls) -> Self
__or__ :: def __or__(self, 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
__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
__text_signature__ :: str | None
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
__weakrefoffset__ :: int
");
}
// We don't yet take function parameters into account. // We don't yet take function parameters into account.
#[test] #[test]
fn call_prefix1() { fn call_prefix1() {
@ -2366,7 +2449,22 @@ importlib.<CURSOR>
self.completions_if(|c| !c.builtin) 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 { 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); let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
if completions.is_empty() { if completions.is_empty() {
return "<No completions found>".to_string(); return "<No completions found>".to_string();
@ -2374,7 +2472,7 @@ importlib.<CURSOR>
let included = completions let included = completions
.iter() .iter()
.filter(|label| predicate(label)) .filter(|label| predicate(label))
.map(|completion| completion.name.as_str().to_string()) .map(snapshot)
.collect::<Vec<String>>(); .collect::<Vec<String>>();
if included.is_empty() { if included.is_empty() {
// It'd be nice to include the actual number of // It'd be nice to include the actual number of

View file

@ -73,7 +73,7 @@ impl<'db> SemanticModel<'db> {
.into_iter() .into_iter()
.map(|member| Completion { .map(|member| Completion {
name: member.name, name: member.name,
ty: None, ty: member.ty,
builtin, builtin,
}) })
.collect() .collect()
@ -86,7 +86,7 @@ impl<'db> SemanticModel<'db> {
.into_iter() .into_iter()
.map(|member| Completion { .map(|member| Completion {
name: member.name, name: member.name,
ty: None, ty: member.ty,
builtin: false, builtin: false,
}) })
.collect() .collect()
@ -122,7 +122,7 @@ impl<'db> SemanticModel<'db> {
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file)) all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
.map(|member| Completion { .map(|member| Completion {
name: member.name, name: member.name,
ty: None, ty: member.ty,
builtin: false, builtin: false,
}), }),
); );
@ -172,8 +172,8 @@ impl NameKind {
pub struct Completion<'db> { pub struct Completion<'db> {
/// The label shown to the user for this suggestion. /// The label shown to the user for this suggestion.
pub name: Name, pub name: Name,
/// The type of this completion, if available. /// The type of this completion.
pub ty: Option<Type<'db>>, pub ty: Type<'db>,
/// Whether this suggestion came from builtins or not. /// Whether this suggestion came from builtins or not.
/// ///
/// At time of writing (2025-06-26), this information /// At time of writing (2025-06-26), this information

View file

@ -2048,7 +2048,11 @@ impl<'db> ClassLiteral<'db> {
/// A helper function for `instance_member` that looks up the `name` attribute only on /// A helper function for `instance_member` that looks up the `name` attribute only on
/// this class, not on its superclasses. /// 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: // TODO: There are many things that are not yet implemented here:
// - `typing.Final` // - `typing.Final`
// - Proper diagnostics // - Proper diagnostics

View file

@ -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::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
use crate::semantic_index::definition::DefinitionKind; use crate::semantic_index::definition::DefinitionKind;
use crate::semantic_index::place::ScopeId; use crate::semantic_index::place::ScopeId;
@ -14,7 +17,7 @@ use rustc_hash::FxHashSet;
pub(crate) fn all_declarations_and_bindings<'db>( pub(crate) fn all_declarations_and_bindings<'db>(
db: &'db dyn Db, db: &'db dyn Db,
scope_id: ScopeId<'db>, scope_id: ScopeId<'db>,
) -> impl Iterator<Item = Member> + 'db { ) -> impl Iterator<Item = Member<'db>> + 'db {
let use_def_map = use_def_map(db, scope_id); let use_def_map = use_def_map(db, scope_id);
let table = place_table(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) place_from_declarations(db, declarations)
.ok() .ok()
.and_then(|result| { .and_then(|result| {
result.place.ignore_possibly_unbound().and_then(|_| { result.place.ignore_possibly_unbound().and_then(|ty| {
table table
.place_expr(symbol_id) .place_expr(symbol_id)
.as_name() .as_name()
.cloned() .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)| { .filter_map(move |(symbol_id, bindings)| {
place_from_bindings(db, bindings) place_from_bindings(db, bindings)
.ignore_possibly_unbound() .ignore_possibly_unbound()
.and_then(|_| { .and_then(|ty| {
table table
.place_expr(symbol_id) .place_expr(symbol_id)
.as_name() .as_name()
.cloned() .cloned()
.map(|name| Member { name }) .map(|name| Member { name, ty })
}) })
}), }),
) )
} }
struct AllMembers { struct AllMembers<'db> {
members: FxHashSet<Member>, members: FxHashSet<Member<'db>>,
} }
impl AllMembers { impl<'db> AllMembers<'db> {
fn of<'db>(db: &'db dyn Db, ty: Type<'db>) -> Self { fn of(db: &'db dyn Db, ty: Type<'db>) -> Self {
let mut all_members = Self { let mut all_members = Self {
members: FxHashSet::default(), members: FxHashSet::default(),
}; };
@ -63,7 +66,7 @@ impl AllMembers {
all_members 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 { match ty {
Type::Union(union) => self.members.extend( Type::Union(union) => self.members.extend(
union union
@ -85,7 +88,6 @@ impl AllMembers {
Type::NominalInstance(instance) => { Type::NominalInstance(instance) => {
let (class_literal, _specialization) = instance.class.class_literal(db); 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); self.extend_with_instance_members(db, class_literal);
} }
@ -191,6 +193,7 @@ impl AllMembers {
self.members.insert(Member { self.members.insert(Member {
name: place_table.place_expr(symbol_id).expect_name().clone(), name: place_table.place_expr(symbol_id).expect_name().clone(),
ty,
}); });
} }
@ -198,71 +201,133 @@ impl AllMembers {
self.members.extend( self.members.extend(
imported_modules(db, literal.importing_file(db)) imported_modules(db, literal.importing_file(db))
.iter() .iter()
.filter_map(|submodule_name| submodule_name.relative_to(module_name)) .filter_map(|submodule_name| {
.filter_map(|relative_submodule_name| { let module = resolve_module(db, submodule_name)?;
Some(Member { let ty = Type::module_literal(db, file, &module);
name: Name::from(relative_submodule_name.components().next()?), 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) { fn extend_with_class_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) {
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>,
) {
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);
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>( fn extend_with_instance_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) {
&mut self,
db: &'db dyn 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_instance = Type::instance(db, parent.default_specialization(db));
let class_body_scope = parent.body_scope(db); let class_body_scope = parent.body_scope(db);
let file = class_body_scope.file(db); let file = class_body_scope.file(db);
let index = semantic_index(db, file); let index = semantic_index(db, file);
for function_scope_id in attribute_scopes(db, class_body_scope) { for function_scope_id in attribute_scopes(db, class_body_scope) {
let place_table = index.place_table(function_scope_id); let place_table = index.place_table(function_scope_id);
self.members.extend( for place_expr in place_table.places() {
place_table let Some(name) = place_expr.as_instance_attribute() else {
.instance_attributes() continue;
.cloned() };
.map(|name| Member { name }), 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)] /// A member of a type.
pub struct Member { ///
/// 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 name: Name,
pub ty: Type<'db>,
}
impl std::hash::Hash for Member<'_> {
fn hash<H: std::hash::Hasher>(&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<Ordering> {
Some(self.cmp(rhs))
}
} }
/// List all members of a given type: anything that would be valid when accessed /// List all members of a given type: anything that would be valid when accessed
/// as an attribute on an object of the given type. /// as an attribute on an object of the given type.
pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Member> { pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Member<'db>> {
AllMembers::of(db, ty).members AllMembers::of(db, ty).members
} }