mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-31 07:37:38 +00:00
[ty] List available members for a given type (#18251)
This PR adds initial support for listing all attributes of an object. It is exposed through a new `all_members` routine in `ty_extensions`, which is in turn used to test the functionality. The purpose of listing all members is for code completion. That is, given a `object.<CURSOR>`, we would like to list all available attributes on `object`.
This commit is contained in:
parent
d65bd69963
commit
e730f27f80
9 changed files with 882 additions and 12 deletions
53
crates/ty_python_semantic/resources/mdtest/binary/in.md
Normal file
53
crates/ty_python_semantic/resources/mdtest/binary/in.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Static binary operations using `in`
|
||||
|
||||
## Basic functionality
|
||||
|
||||
This demonstrates type inference support for `<str-literal> in <tuple>`:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert
|
||||
|
||||
static_assert("foo" in ("quux", "foo", "baz"))
|
||||
static_assert("foo" not in ("quux", "bar", "baz"))
|
||||
```
|
||||
|
||||
## With variables
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert
|
||||
|
||||
x = ("quux", "foo", "baz")
|
||||
static_assert("foo" in x)
|
||||
|
||||
x = ("quux", "bar", "baz")
|
||||
static_assert("foo" not in x)
|
||||
```
|
||||
|
||||
## Statically unknown results in a type error
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert
|
||||
|
||||
def _(a: str, b: str):
|
||||
static_assert("foo" in (a, b)) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
## Values being unknown doesn't mean the result is unknown
|
||||
|
||||
For example, when the types are completely disjoint:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert
|
||||
|
||||
def _(a: int, b: int):
|
||||
static_assert("foo" not in (a, b))
|
||||
```
|
||||
|
||||
## Failure cases
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert
|
||||
|
||||
# We don't support byte strings.
|
||||
static_assert(b"foo" not in (b"quux", b"foo", b"baz")) # error: [static-assert-error]
|
||||
```
|
|
@ -0,0 +1,488 @@
|
|||
# List all members
|
||||
|
||||
## Basic functionality
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
The `ty_extensions.all_members` function allows access to a list of accessible members/attributes on
|
||||
a given object. For example, all member functions of `str` are available on `"a"`:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
members_of_str = all_members("a")
|
||||
|
||||
static_assert("replace" in members_of_str)
|
||||
static_assert("startswith" in members_of_str)
|
||||
static_assert("isupper" in members_of_str)
|
||||
```
|
||||
|
||||
Similarly, special members such as `__add__` are also available:
|
||||
|
||||
```py
|
||||
static_assert("__add__" in members_of_str)
|
||||
static_assert("__gt__" in members_of_str)
|
||||
```
|
||||
|
||||
Members of base classes are also included (these dunder methods are defined on `object`):
|
||||
|
||||
```py
|
||||
static_assert("__doc__" in members_of_str)
|
||||
static_assert("__repr__" in members_of_str)
|
||||
```
|
||||
|
||||
Non-existent members are not included:
|
||||
|
||||
```py
|
||||
static_assert("non_existent" not in members_of_str)
|
||||
```
|
||||
|
||||
Note: The full list of all members is relatively long, but `reveal_type` can theoretically be used
|
||||
to see them all:
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
reveal_type(members_of_str) # error: [revealed-type]
|
||||
```
|
||||
|
||||
## Kinds of types
|
||||
|
||||
### Class instances
|
||||
|
||||
For instances of classes, `all_members` returns class members and implicit instance members of all
|
||||
classes in the MRO:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
class Base:
|
||||
base_class_attr: int = 1
|
||||
|
||||
def f_base(self):
|
||||
self.base_instance_attr: str = "Base"
|
||||
|
||||
class Intermediate(Base):
|
||||
intermediate_attr: int = 2
|
||||
|
||||
def f_intermediate(self):
|
||||
self.intermediate_instance_attr: str = "Intermediate"
|
||||
|
||||
class C(Intermediate):
|
||||
class_attr: int = 3
|
||||
|
||||
def f_c(self):
|
||||
self.instance_attr = "C"
|
||||
|
||||
@property
|
||||
def property_attr(self) -> int:
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def class_method(cls) -> int:
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def static_method() -> int:
|
||||
return 1
|
||||
|
||||
members_of_instance = all_members(C())
|
||||
|
||||
static_assert("base_class_attr" in members_of_instance)
|
||||
static_assert("intermediate_attr" in members_of_instance)
|
||||
static_assert("class_attr" in members_of_instance)
|
||||
|
||||
static_assert("base_instance_attr" in members_of_instance)
|
||||
static_assert("intermediate_instance_attr" in members_of_instance)
|
||||
static_assert("instance_attr" in members_of_instance)
|
||||
|
||||
static_assert("f_base" in members_of_instance)
|
||||
static_assert("f_intermediate" in members_of_instance)
|
||||
static_assert("f_c" in members_of_instance)
|
||||
|
||||
static_assert("property_attr" in members_of_instance)
|
||||
static_assert("class_method" in members_of_instance)
|
||||
static_assert("static_method" in members_of_instance)
|
||||
|
||||
static_assert("non_existent" not in members_of_instance)
|
||||
```
|
||||
|
||||
### Class objects
|
||||
|
||||
Class-level attributes can also be accessed through the class itself:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
class Base:
|
||||
base_attr: int = 1
|
||||
|
||||
class C(Base):
|
||||
class_attr: str = "c"
|
||||
|
||||
def f(self):
|
||||
self.instance_attr = True
|
||||
|
||||
members_of_class = all_members(C)
|
||||
|
||||
static_assert("class_attr" in members_of_class)
|
||||
static_assert("base_attr" in members_of_class)
|
||||
|
||||
static_assert("non_existent" not in members_of_class)
|
||||
```
|
||||
|
||||
But instance attributes can not be accessed this way:
|
||||
|
||||
```py
|
||||
static_assert("instance_attr" not in members_of_class)
|
||||
```
|
||||
|
||||
When a class has a metaclass, members of that metaclass (and bases of that metaclass) are also
|
||||
accessible:
|
||||
|
||||
```py
|
||||
class MetaBase(type):
|
||||
meta_base_attr = 1
|
||||
|
||||
class Meta(MetaBase):
|
||||
meta_attr = 2
|
||||
|
||||
class D(Base, metaclass=Meta):
|
||||
class_attr = 3
|
||||
|
||||
static_assert("meta_base_attr" in all_members(D))
|
||||
static_assert("meta_attr" in all_members(D))
|
||||
static_assert("base_attr" in all_members(D))
|
||||
static_assert("class_attr" in all_members(D))
|
||||
```
|
||||
|
||||
### Generic classes
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
base_attr: T
|
||||
|
||||
static_assert("base_attr" in all_members(C[int]))
|
||||
static_assert("base_attr" in all_members(C[int]()))
|
||||
```
|
||||
|
||||
### Other instance-like types
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
static_assert("__xor__" in all_members(True))
|
||||
static_assert("bit_length" in all_members(1))
|
||||
static_assert("startswith" in all_members("a"))
|
||||
static_assert("__buffer__" in all_members(b"a"))
|
||||
static_assert("is_integer" in all_members(3.14))
|
||||
|
||||
def _(literal_string: LiteralString):
|
||||
static_assert("startswith" in all_members(literal_string))
|
||||
|
||||
static_assert("count" in all_members(("some", "tuple", 1, 2)))
|
||||
|
||||
static_assert("__doc__" in all_members(len))
|
||||
static_assert("__doc__" in all_members("a".startswith))
|
||||
```
|
||||
|
||||
### Unions
|
||||
|
||||
For unions, `all_members` will only return members that are available on all elements of the union.
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
class A:
|
||||
on_both: int = 1
|
||||
only_on_a: str = "a"
|
||||
|
||||
class B:
|
||||
on_both: int = 2
|
||||
only_on_b: str = "b"
|
||||
|
||||
def f(union: A | B):
|
||||
static_assert("on_both" in all_members(union))
|
||||
static_assert("only_on_a" not in all_members(union))
|
||||
static_assert("only_on_b" not in all_members(union))
|
||||
```
|
||||
|
||||
### Intersections
|
||||
|
||||
#### Only positive types
|
||||
|
||||
Conversely, for intersections, `all_members` will list members that are available on any of the
|
||||
elements:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
class A:
|
||||
on_both: int = 1
|
||||
only_on_a: str = "a"
|
||||
|
||||
class B:
|
||||
on_both: int = 2
|
||||
only_on_b: str = "b"
|
||||
|
||||
def f(intersection: object):
|
||||
if isinstance(intersection, A):
|
||||
if isinstance(intersection, B):
|
||||
static_assert("on_both" in all_members(intersection))
|
||||
static_assert("only_on_a" in all_members(intersection))
|
||||
static_assert("only_on_b" in all_members(intersection))
|
||||
```
|
||||
|
||||
#### With negative types
|
||||
|
||||
It also works when negative types are introduced:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
class A:
|
||||
on_all: int = 1
|
||||
only_on_a: str = "a"
|
||||
only_on_ab: str = "a"
|
||||
only_on_ac: str = "a"
|
||||
|
||||
class B:
|
||||
on_all: int = 2
|
||||
only_on_b: str = "b"
|
||||
only_on_ab: str = "b"
|
||||
only_on_bc: str = "b"
|
||||
|
||||
class C:
|
||||
on_all: int = 3
|
||||
only_on_c: str = "c"
|
||||
only_on_ac: str = "c"
|
||||
only_on_bc: str = "c"
|
||||
|
||||
def f(intersection: object):
|
||||
if isinstance(intersection, A):
|
||||
if isinstance(intersection, B):
|
||||
if not isinstance(intersection, C):
|
||||
reveal_type(intersection) # revealed: A & B & ~C
|
||||
static_assert("on_all" in all_members(intersection))
|
||||
static_assert("only_on_a" in all_members(intersection))
|
||||
static_assert("only_on_b" in all_members(intersection))
|
||||
static_assert("only_on_c" not in all_members(intersection))
|
||||
static_assert("only_on_ab" in all_members(intersection))
|
||||
static_assert("only_on_ac" in all_members(intersection))
|
||||
static_assert("only_on_bc" in all_members(intersection))
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
### Basic support with sub-modules
|
||||
|
||||
`all_members` can also list attributes on modules:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
import math
|
||||
|
||||
static_assert("pi" in all_members(math))
|
||||
static_assert("cos" in all_members(math))
|
||||
```
|
||||
|
||||
This also works for submodules:
|
||||
|
||||
```py
|
||||
import os
|
||||
|
||||
static_assert("path" in all_members(os))
|
||||
|
||||
import os.path
|
||||
|
||||
static_assert("join" in all_members(os.path))
|
||||
```
|
||||
|
||||
Special members available on all modules are also included:
|
||||
|
||||
```py
|
||||
static_assert("__name__" in all_members(math))
|
||||
static_assert("__doc__" in all_members(math))
|
||||
```
|
||||
|
||||
### `__all__` is not respected for direct module access
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
import bar
|
||||
|
||||
static_assert("lion" in all_members(bar))
|
||||
static_assert("tiger" in all_members(bar))
|
||||
```
|
||||
|
||||
`bar.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["lion"]
|
||||
|
||||
lion = 1
|
||||
tiger = 1
|
||||
```
|
||||
|
||||
### `__all__` is respected for glob imports
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
import bar
|
||||
|
||||
static_assert("lion" in all_members(bar))
|
||||
static_assert("tiger" not in all_members(bar))
|
||||
```
|
||||
|
||||
`bar.py`:
|
||||
|
||||
```py
|
||||
from quux import *
|
||||
```
|
||||
|
||||
`quux.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["lion"]
|
||||
|
||||
lion = 1
|
||||
tiger = 1
|
||||
```
|
||||
|
||||
### `__all__` is respected for stub files
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
def evaluate(x=None):
|
||||
if x is None:
|
||||
return 0
|
||||
return x
|
||||
```
|
||||
|
||||
`module.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import Optional
|
||||
|
||||
__all__ = ["evaluate"]
|
||||
|
||||
def evaluate(x: Optional[int] = None) -> int: ...
|
||||
```
|
||||
|
||||
`play.py`:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
import module
|
||||
|
||||
static_assert("evaluate" in all_members(module))
|
||||
static_assert("Optional" not in all_members(module))
|
||||
```
|
||||
|
||||
## Conditionally available members
|
||||
|
||||
Some members are only conditionally available. For example, `int.bit_count` was only introduced in
|
||||
Python 3.10:
|
||||
|
||||
### 3.9
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
static_assert("bit_count" not in all_members(42))
|
||||
```
|
||||
|
||||
### 3.10
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
static_assert("bit_count" in all_members(42))
|
||||
```
|
||||
|
||||
## Failures cases
|
||||
|
||||
### Dynamically added members
|
||||
|
||||
Dynamically added members can not be accessed:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
class C:
|
||||
static_attr = 1
|
||||
|
||||
def __setattr__(self, name: str, value: str) -> None:
|
||||
pass
|
||||
|
||||
def __getattr__(self, name: str) -> str:
|
||||
return "a"
|
||||
|
||||
c = C()
|
||||
c.dynamic_attr = "a"
|
||||
|
||||
static_assert("static_attr" in all_members(c))
|
||||
static_assert("dynamic_attr" not in all_members(c))
|
||||
```
|
||||
|
||||
### Dataclasses
|
||||
|
||||
So far, we do not include synthetic members of dataclasses.
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(order=True)
|
||||
class Person:
|
||||
name: str
|
||||
age: int
|
||||
|
||||
static_assert("name" in all_members(Person))
|
||||
static_assert("age" in all_members(Person))
|
||||
|
||||
# These are always available, since they are also defined on `object`:
|
||||
static_assert("__init__" in all_members(Person))
|
||||
static_assert("__repr__" in all_members(Person))
|
||||
static_assert("__eq__" in all_members(Person))
|
||||
|
||||
# TODO: this should ideally be available:
|
||||
static_assert("__lt__" in all_members(Person)) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
### Attributes not available at runtime
|
||||
|
||||
Typeshed includes some attributes in `object` that are not available for some (builtin) types. For
|
||||
example, `__annotations__` does not exist on `int` at runtime, but it is available as an attribute
|
||||
on `object` in typeshed:
|
||||
|
||||
```py
|
||||
from ty_extensions import all_members, static_assert
|
||||
|
||||
# TODO: this should ideally not be available:
|
||||
static_assert("__annotations__" not in all_members(3)) # error: [static-assert-error]
|
||||
```
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: all_members.md - List all members - Basic functionality
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from ty_extensions import all_members, static_assert
|
||||
2 |
|
||||
3 | members_of_str = all_members("a")
|
||||
4 |
|
||||
5 | static_assert("replace" in members_of_str)
|
||||
6 | static_assert("startswith" in members_of_str)
|
||||
7 | static_assert("isupper" in members_of_str)
|
||||
8 | static_assert("__add__" in members_of_str)
|
||||
9 | static_assert("__gt__" in members_of_str)
|
||||
10 | static_assert("__doc__" in members_of_str)
|
||||
11 | static_assert("__repr__" in members_of_str)
|
||||
12 | static_assert("non_existent" not in members_of_str)
|
||||
13 | from typing_extensions import reveal_type
|
||||
14 |
|
||||
15 | reveal_type(members_of_str) # error: [revealed-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
info[revealed-type]: Revealed type
|
||||
--> src/mdtest_snippet.py:15:13
|
||||
|
|
||||
13 | from typing_extensions import reveal_type
|
||||
14 |
|
||||
15 | reveal_type(members_of_str) # error: [revealed-type]
|
||||
| ^^^^^^^^^^^^^^ `tuple[Literal["__add__"], Literal["__annotations__"], Literal["__class__"], Literal["__contains__"], Literal["__delattr__"], Literal["__dict__"], Literal["__dir__"], Literal["__doc__"], Literal["__eq__"], Literal["__format__"], Literal["__ge__"], Literal["__getattribute__"], Literal["__getitem__"], Literal["__getnewargs__"], Literal["__gt__"], Literal["__hash__"], Literal["__init__"], Literal["__init_subclass__"], Literal["__iter__"], Literal["__le__"], Literal["__len__"], Literal["__lt__"], Literal["__mod__"], Literal["__module__"], Literal["__mul__"], Literal["__ne__"], Literal["__new__"], Literal["__reduce__"], Literal["__reduce_ex__"], Literal["__repr__"], Literal["__reversed__"], Literal["__rmul__"], Literal["__setattr__"], Literal["__sizeof__"], Literal["__str__"], Literal["__subclasshook__"], Literal["capitalize"], Literal["casefold"], Literal["center"], Literal["count"], Literal["encode"], Literal["endswith"], Literal["expandtabs"], Literal["find"], Literal["format"], Literal["format_map"], Literal["index"], Literal["isalnum"], Literal["isalpha"], Literal["isascii"], Literal["isdecimal"], Literal["isdigit"], Literal["isidentifier"], Literal["islower"], Literal["isnumeric"], Literal["isprintable"], Literal["isspace"], Literal["istitle"], Literal["isupper"], Literal["join"], Literal["ljust"], Literal["lower"], Literal["lstrip"], Literal["maketrans"], Literal["partition"], Literal["removeprefix"], Literal["removesuffix"], Literal["replace"], Literal["rfind"], Literal["rindex"], Literal["rjust"], Literal["rpartition"], Literal["rsplit"], Literal["rstrip"], Literal["split"], Literal["splitlines"], Literal["startswith"], Literal["strip"], Literal["swapcase"], Literal["title"], Literal["translate"], Literal["upper"], Literal["zfill"]]`
|
||||
|
|
||||
|
||||
```
|
|
@ -99,7 +99,9 @@ pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<UseD
|
|||
index.use_def_map(scope.file_scope_id(db))
|
||||
}
|
||||
|
||||
/// Returns all attribute assignments (and their method scope IDs) for a specific class body scope.
|
||||
/// Returns all attribute assignments (and their method scope IDs) with a symbol name matching
|
||||
/// the one given for a specific class body scope.
|
||||
///
|
||||
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
|
||||
/// introduces a direct dependency on that file's AST.
|
||||
pub(crate) fn attribute_assignments<'db, 's>(
|
||||
|
@ -109,6 +111,28 @@ pub(crate) fn attribute_assignments<'db, 's>(
|
|||
) -> impl Iterator<Item = (BindingWithConstraintsIterator<'db, 'db>, FileScopeId)> + use<'s, 'db> {
|
||||
let file = class_body_scope.file(db);
|
||||
let index = semantic_index(db, file);
|
||||
|
||||
attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| {
|
||||
let attribute_table = index.instance_attribute_table(function_scope_id);
|
||||
let symbol = attribute_table.symbol_id_by_name(name)?;
|
||||
let use_def = &index.use_def_maps[function_scope_id];
|
||||
Some((
|
||||
use_def.instance_attribute_bindings(symbol),
|
||||
function_scope_id,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns all attribute assignments as scope IDs for a specific class body scope.
|
||||
///
|
||||
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
|
||||
/// introduces a direct dependency on that file's AST.
|
||||
pub(crate) fn attribute_scopes<'db, 's>(
|
||||
db: &'db dyn Db,
|
||||
class_body_scope: ScopeId<'db>,
|
||||
) -> impl Iterator<Item = FileScopeId> + use<'s, 'db> {
|
||||
let file = class_body_scope.file(db);
|
||||
let index = semantic_index(db, file);
|
||||
let class_scope_id = class_body_scope.file_scope_id(db);
|
||||
|
||||
ChildrenIter::new(index, class_scope_id).filter_map(|(child_scope_id, scope)| {
|
||||
|
@ -124,13 +148,7 @@ pub(crate) fn attribute_assignments<'db, 's>(
|
|||
};
|
||||
|
||||
function_scope.node().as_function()?;
|
||||
let attribute_table = index.instance_attribute_table(function_scope_id);
|
||||
let symbol = attribute_table.symbol_id_by_name(name)?;
|
||||
let use_def = &index.use_def_maps[function_scope_id];
|
||||
Some((
|
||||
use_def.instance_attribute_bindings(symbol),
|
||||
function_scope_id,
|
||||
))
|
||||
Some(function_scope_id)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -519,7 +537,7 @@ pub struct ChildrenIter<'a> {
|
|||
}
|
||||
|
||||
impl<'a> ChildrenIter<'a> {
|
||||
fn new(module_symbol_table: &'a SemanticIndex, parent: FileScopeId) -> Self {
|
||||
pub(crate) fn new(module_symbol_table: &'a SemanticIndex, parent: FileScopeId) -> Self {
|
||||
let descendants = DescendantsIter::new(module_symbol_table, parent);
|
||||
|
||||
Self {
|
||||
|
|
|
@ -65,6 +65,7 @@ mod context;
|
|||
mod diagnostic;
|
||||
mod display;
|
||||
mod generics;
|
||||
mod ide_support;
|
||||
mod infer;
|
||||
mod instance;
|
||||
mod mro;
|
||||
|
@ -7662,6 +7663,8 @@ pub enum KnownFunction {
|
|||
GenericContext,
|
||||
/// `ty_extensions.dunder_all_names`
|
||||
DunderAllNames,
|
||||
/// `ty_extensions.all_members`
|
||||
AllMembers,
|
||||
}
|
||||
|
||||
impl KnownFunction {
|
||||
|
@ -7721,7 +7724,8 @@ impl KnownFunction {
|
|||
| Self::IsSubtypeOf
|
||||
| Self::GenericContext
|
||||
| Self::DunderAllNames
|
||||
| Self::StaticAssert => module.is_ty_extensions(),
|
||||
| Self::StaticAssert
|
||||
| Self::AllMembers => module.is_ty_extensions(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9390,7 +9394,8 @@ pub(crate) mod tests {
|
|||
| KnownFunction::IsSingleValued
|
||||
| KnownFunction::IsAssignableTo
|
||||
| KnownFunction::IsEquivalentTo
|
||||
| KnownFunction::IsGradualEquivalentTo => KnownModule::TyExtensions,
|
||||
| KnownFunction::IsGradualEquivalentTo
|
||||
| KnownFunction::AllMembers => KnownModule::TyExtensions,
|
||||
};
|
||||
|
||||
let function_definition = known_module_symbol(&db, module, function_name)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//! [signatures][crate::types::signatures], we have to handle the fact that the callable might be a
|
||||
//! union of types, each of which might contain multiple overloads.
|
||||
|
||||
use itertools::Itertools;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
|
||||
use super::{
|
||||
|
@ -22,7 +23,8 @@ use crate::types::signatures::{Parameter, ParameterForm};
|
|||
use crate::types::{
|
||||
BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, FunctionType,
|
||||
KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType,
|
||||
SpecialFormType, TupleType, TypeMapping, UnionType, WrapperDescriptorKind, todo_type,
|
||||
SpecialFormType, TupleType, TypeMapping, UnionType, WrapperDescriptorKind, ide_support,
|
||||
todo_type,
|
||||
};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
|
||||
use ruff_python_ast as ast;
|
||||
|
@ -656,6 +658,18 @@ impl<'db> Bindings<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::AllMembers) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(TupleType::from_elements(
|
||||
db,
|
||||
ide_support::all_members(db, *ty)
|
||||
.into_iter()
|
||||
.sorted()
|
||||
.map(|member| Type::string_literal(db, &member)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::Len) => {
|
||||
if let [Some(first_arg)] = overload.parameter_types() {
|
||||
if let Some(len_ty) = first_arg.len(db) {
|
||||
|
|
183
crates/ty_python_semantic/src/types/ide_support.rs
Normal file
183
crates/ty_python_semantic/src/types/ide_support.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
use crate::Db;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::semantic_index::{
|
||||
attribute_scopes, global_scope, semantic_index, symbol_table, use_def_map,
|
||||
};
|
||||
use crate::symbol::{imported_symbol, symbol_from_bindings, symbol_from_declarations};
|
||||
use crate::types::{ClassBase, ClassLiteral, KnownClass, Type};
|
||||
use ruff_python_ast::name::Name;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
struct AllMembers {
|
||||
members: FxHashSet<Name>,
|
||||
}
|
||||
|
||||
impl AllMembers {
|
||||
fn of<'db>(db: &'db dyn Db, ty: Type<'db>) -> Self {
|
||||
let mut all_members = Self {
|
||||
members: FxHashSet::default(),
|
||||
};
|
||||
all_members.extend_with_type(db, ty);
|
||||
all_members
|
||||
}
|
||||
|
||||
fn extend_with_type<'db>(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
match ty {
|
||||
Type::Union(union) => self.members.extend(
|
||||
union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| AllMembers::of(db, *ty).members)
|
||||
.reduce(|acc, members| acc.intersection(&members).cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
|
||||
Type::Intersection(intersection) => self.members.extend(
|
||||
intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.map(|ty| AllMembers::of(db, *ty).members)
|
||||
.reduce(|acc, members| acc.union(&members).cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class_literal) => {
|
||||
self.extend_with_class_members(db, class_literal);
|
||||
|
||||
if let Type::ClassLiteral(meta_class_literal) = ty.to_meta_type(db) {
|
||||
self.extend_with_class_members(db, meta_class_literal);
|
||||
}
|
||||
}
|
||||
|
||||
Type::GenericAlias(generic_alias) => {
|
||||
let class_literal = generic_alias.origin(db);
|
||||
self.extend_with_class_members(db, 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);
|
||||
}
|
||||
}
|
||||
|
||||
Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy | Type::AlwaysFalsy => {}
|
||||
|
||||
Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::Tuple(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::Callable(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::SpecialForm(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::BoundSuper(_) => {
|
||||
if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) {
|
||||
self.extend_with_class_members(db, class_literal);
|
||||
}
|
||||
}
|
||||
|
||||
Type::ModuleLiteral(literal) => {
|
||||
self.extend_with_type(db, KnownClass::ModuleType.to_instance(db));
|
||||
|
||||
let Some(file) = literal.module(db).file() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let module_scope = global_scope(db, file);
|
||||
let use_def_map = use_def_map(db, module_scope);
|
||||
let symbol_table = symbol_table(db, module_scope);
|
||||
|
||||
for (symbol_id, _) in use_def_map.all_public_declarations() {
|
||||
let symbol_name = symbol_table.symbol(symbol_id).name();
|
||||
if !imported_symbol(db, file, symbol_name, None)
|
||||
.symbol
|
||||
.is_unbound()
|
||||
{
|
||||
self.members
|
||||
.insert(symbol_table.symbol(symbol_id).name().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_with_declarations_and_bindings(&mut self, db: &dyn Db, scope_id: ScopeId) {
|
||||
let use_def_map = use_def_map(db, scope_id);
|
||||
let symbol_table = symbol_table(db, scope_id);
|
||||
|
||||
for (symbol_id, declarations) in use_def_map.all_public_declarations() {
|
||||
if symbol_from_declarations(db, declarations)
|
||||
.is_ok_and(|result| !result.symbol.is_unbound())
|
||||
{
|
||||
self.members
|
||||
.insert(symbol_table.symbol(symbol_id).name().clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (symbol_id, bindings) in use_def_map.all_public_bindings() {
|
||||
if !symbol_from_bindings(db, bindings).is_unbound() {
|
||||
self.members
|
||||
.insert(symbol_table.symbol(symbol_id).name().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_with_class_members<'db>(
|
||||
&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_scope = parent.body_scope(db);
|
||||
self.extend_with_declarations_and_bindings(db, parent_scope);
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_with_instance_members<'db>(
|
||||
&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 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 attribute_table = index.instance_attribute_table(function_scope_id);
|
||||
for symbol in attribute_table.symbols() {
|
||||
self.members.insert(symbol.name().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(crate) fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Name> {
|
||||
AllMembers::of(db, ty).members
|
||||
}
|
|
@ -6719,6 +6719,37 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
right: Type<'db>,
|
||||
range: TextRange,
|
||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||
let is_str_literal_in_tuple = |literal: Type<'db>, tuple: TupleType<'db>| {
|
||||
// Protect against doing a lot of work for pathologically large
|
||||
// tuples.
|
||||
//
|
||||
// Ref: https://github.com/astral-sh/ruff/pull/18251#discussion_r2115909311
|
||||
if tuple.len(self.db()) > 1 << 12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut definitely_true = false;
|
||||
let mut definitely_false = true;
|
||||
for element in tuple.elements(self.db()) {
|
||||
if element.is_string_literal() {
|
||||
if literal == *element {
|
||||
definitely_true = true;
|
||||
definitely_false = false;
|
||||
}
|
||||
} else if !literal.is_disjoint_from(self.db(), *element) {
|
||||
definitely_false = false;
|
||||
}
|
||||
}
|
||||
|
||||
if definitely_true {
|
||||
Some(true)
|
||||
} else if definitely_false {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Note: identity (is, is not) for equal builtin types is unreliable and not part of the
|
||||
// language spec.
|
||||
// - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal
|
||||
|
@ -6850,6 +6881,30 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
}
|
||||
}
|
||||
}
|
||||
(Type::StringLiteral(_), Type::Tuple(tuple)) if op == ast::CmpOp::In => {
|
||||
if let Some(answer) = is_str_literal_in_tuple(left, tuple) {
|
||||
return Ok(Type::BooleanLiteral(answer));
|
||||
}
|
||||
|
||||
self.infer_binary_type_comparison(
|
||||
KnownClass::Str.to_instance(self.db()),
|
||||
op,
|
||||
right,
|
||||
range,
|
||||
)
|
||||
}
|
||||
(Type::StringLiteral(_), Type::Tuple(tuple)) if op == ast::CmpOp::NotIn => {
|
||||
if let Some(answer) = is_str_literal_in_tuple(left, tuple) {
|
||||
return Ok(Type::BooleanLiteral(!answer));
|
||||
}
|
||||
|
||||
self.infer_binary_type_comparison(
|
||||
KnownClass::Str.to_instance(self.db()),
|
||||
op,
|
||||
right,
|
||||
range,
|
||||
)
|
||||
}
|
||||
(Type::StringLiteral(_), _) => self.infer_binary_type_comparison(
|
||||
KnownClass::Str.to_instance(self.db()),
|
||||
op,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue