Compare commits
7 Commits
dcreager/s
...
david/type
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0db8d41873 | ||
|
|
249a807665 | ||
|
|
9c4278da4e | ||
|
|
195669f33d | ||
|
|
596ee17ec3 | ||
|
|
7b7c8425b8 | ||
|
|
8d53802bc7 |
@@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) {
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY313,
|
||||
},
|
||||
100,
|
||||
110,
|
||||
);
|
||||
|
||||
bench_project(&benchmark, criterion);
|
||||
@@ -684,7 +684,7 @@ fn anyio(criterion: &mut Criterion) {
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY313,
|
||||
},
|
||||
100,
|
||||
150,
|
||||
);
|
||||
|
||||
bench_project(&benchmark, criterion);
|
||||
|
||||
@@ -210,7 +210,7 @@ static TANJUN: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
100,
|
||||
320,
|
||||
);
|
||||
|
||||
static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
@@ -226,7 +226,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
630,
|
||||
750,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -1957,14 +1957,34 @@ class Quux:
|
||||
",
|
||||
);
|
||||
|
||||
// FIXME: This should list completions on `self`, which should
|
||||
// include, at least, `foo` and `bar`. At time of writing
|
||||
// (2025-06-04), the type of `self` is inferred as `Unknown` in
|
||||
// this context. This in turn prevents us from getting a list
|
||||
// of available attributes.
|
||||
//
|
||||
// See: https://github.com/astral-sh/ty/issues/159
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
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__
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1798,23 +1798,23 @@ class BoundedContainer[T: int, U = str]:
|
||||
"T" @ 554..555: TypeParameter
|
||||
"value2" @ 557..563: Parameter
|
||||
"U" @ 565..566: TypeParameter
|
||||
"self" @ 577..581: Variable
|
||||
"self" @ 577..581: TypeParameter
|
||||
"value1" @ 582..588: Variable
|
||||
"T" @ 590..591: TypeParameter
|
||||
"value1" @ 594..600: Parameter
|
||||
"self" @ 609..613: Variable
|
||||
"self" @ 609..613: TypeParameter
|
||||
"value2" @ 614..620: Variable
|
||||
"U" @ 622..623: TypeParameter
|
||||
"value2" @ 626..632: Parameter
|
||||
"get_first" @ 642..651: Method [definition]
|
||||
"self" @ 652..656: SelfParameter
|
||||
"T" @ 661..662: TypeParameter
|
||||
"self" @ 679..683: Variable
|
||||
"self" @ 679..683: TypeParameter
|
||||
"value1" @ 684..690: Variable
|
||||
"get_second" @ 700..710: Method [definition]
|
||||
"self" @ 711..715: SelfParameter
|
||||
"U" @ 720..721: TypeParameter
|
||||
"self" @ 738..742: Variable
|
||||
"self" @ 738..742: TypeParameter
|
||||
"value2" @ 743..749: Variable
|
||||
"BoundedContainer" @ 798..814: Class [definition]
|
||||
"T" @ 815..816: TypeParameter [definition]
|
||||
|
||||
@@ -64,8 +64,7 @@ from typing import Self
|
||||
|
||||
class A:
|
||||
def implicit_self(self) -> Self:
|
||||
# TODO: This should be Self@implicit_self
|
||||
reveal_type(self) # revealed: Unknown
|
||||
reveal_type(self) # revealed: Self@implicit_self
|
||||
|
||||
return self
|
||||
|
||||
@@ -127,19 +126,16 @@ The name `self` is not special in any way.
|
||||
```py
|
||||
class B:
|
||||
def name_does_not_matter(this) -> Self:
|
||||
# TODO: Should reveal Self@name_does_not_matter
|
||||
reveal_type(this) # revealed: Unknown
|
||||
reveal_type(this) # revealed: Self@name_does_not_matter
|
||||
|
||||
return this
|
||||
|
||||
def positional_only(self, /, x: int) -> Self:
|
||||
# TODO: Should reveal Self@positional_only
|
||||
reveal_type(self) # revealed: Unknown
|
||||
reveal_type(self) # revealed: Self@positional_only
|
||||
return self
|
||||
|
||||
def keyword_only(self, *, x: int) -> Self:
|
||||
# TODO: Should reveal Self@keyword_only
|
||||
reveal_type(self) # revealed: Unknown
|
||||
reveal_type(self) # revealed: Self@keyword_only
|
||||
return self
|
||||
|
||||
@property
|
||||
@@ -165,8 +161,7 @@ T = TypeVar("T")
|
||||
|
||||
class G(Generic[T]):
|
||||
def id(self) -> Self:
|
||||
# TODO: Should reveal Self@id
|
||||
reveal_type(self) # revealed: Unknown
|
||||
reveal_type(self) # revealed: Self@id
|
||||
|
||||
return self
|
||||
|
||||
@@ -252,6 +247,20 @@ class LinkedList:
|
||||
reveal_type(LinkedList().next()) # revealed: LinkedList
|
||||
```
|
||||
|
||||
Attributes can also refer to a generic parameter:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
foo: T
|
||||
def method(self) -> None:
|
||||
reveal_type(self) # revealed: Self@method
|
||||
reveal_type(self.foo) # revealed: T@C
|
||||
```
|
||||
|
||||
## Generic Classes
|
||||
|
||||
```py
|
||||
@@ -342,31 +351,28 @@ b: Self
|
||||
|
||||
# TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self"
|
||||
class Foo:
|
||||
# TODO: rejected Self because self has a different type
|
||||
# TODO: This `self: T` annotation should be rejected because `T` is not `Self`
|
||||
def has_existing_self_annotation(self: T) -> Self:
|
||||
return self # error: [invalid-return-type]
|
||||
|
||||
def return_concrete_type(self) -> Self:
|
||||
# TODO: tell user to use "Foo" instead of "Self"
|
||||
# TODO: We could emit a hint that suggests annotating with `Foo` instead of `Self`
|
||||
# error: [invalid-return-type]
|
||||
return Foo()
|
||||
|
||||
@staticmethod
|
||||
# TODO: reject because of staticmethod
|
||||
# TODO: The usage of `Self` here should be rejected because this is a static method
|
||||
def make() -> Self:
|
||||
# error: [invalid-return-type]
|
||||
return Foo()
|
||||
|
||||
class Bar(Generic[T]):
|
||||
foo: T
|
||||
def bar(self) -> T:
|
||||
return self.foo
|
||||
class Bar(Generic[T]): ...
|
||||
|
||||
# error: [invalid-type-form]
|
||||
class Baz(Bar[Self]): ...
|
||||
|
||||
class MyMetaclass(type):
|
||||
# TODO: rejected
|
||||
# TODO: reject the Self usage. because self cannot be used within a metaclass.
|
||||
def __new__(cls) -> Self:
|
||||
return super().__new__(cls)
|
||||
```
|
||||
|
||||
@@ -26,9 +26,7 @@ class C:
|
||||
c_instance = C(1)
|
||||
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
|
||||
|
||||
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"]
|
||||
|
||||
# There is no special handling of attributes that are (directly) assigned to a declared parameter,
|
||||
# which means we union with `Unknown` here, since the attribute itself is not declared. This is
|
||||
@@ -177,8 +175,7 @@ c_instance = C(1)
|
||||
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
|
||||
|
||||
# TODO: Should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"]
|
||||
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
||||
|
||||
@@ -380,6 +377,11 @@ reveal_type(c_instance.y) # revealed: Unknown | int
|
||||
|
||||
#### Attributes defined in comprehensions
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
@@ -410,35 +412,75 @@ class D:
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.a) # revealed: Unknown
|
||||
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | str
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.c) # revealed: Unknown
|
||||
reveal_type(c_instance.c) # revealed: Unknown | str
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.d) # revealed: Unknown
|
||||
reveal_type(c_instance.d) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.e) # revealed: Unknown
|
||||
reveal_type(c_instance.e) # revealed: Unknown | int
|
||||
|
||||
# TODO: no error, reveal Unknown | int
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.f) # revealed: Unknown
|
||||
reveal_type(c_instance.f) # revealed: Unknown | int
|
||||
|
||||
# This one is correctly not resolved as an attribute:
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.g) # revealed: Unknown
|
||||
```
|
||||
|
||||
It does not matter how much the comprehension is nested.
|
||||
|
||||
Similarly attributes defined by the comprehension in a generic method are recognized.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def f[T](self):
|
||||
[... for self.a in [1]]
|
||||
[[... for self.b in [1]] for _ in [1]]
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
If the comprehension is inside another scope like function then that attribute is not inferred.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
def f():
|
||||
# error: [unresolved-attribute] "Unresolved attribute `a` on type `Self@__init__`."
|
||||
[... for self.a in IntIterable()]
|
||||
|
||||
def g():
|
||||
# error: [unresolved-attribute] "Unresolved attribute `b` on type `Self@__init__`."
|
||||
[... for self.b in IntIterable()]
|
||||
g()
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# This attribute is in the function f and is not reachable
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.a) # revealed: Unknown
|
||||
|
||||
# TODO: Even though g method is called and is reachable we do not record this attribute assignment
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the comprehension is nested in any other eager scope it still can assign attributes.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
class D:
|
||||
[[... for self.a in IntIterable()] for _ in IntIterable()]
|
||||
|
||||
reveal_type(C().a) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
#### Conditionally declared / bound attributes
|
||||
|
||||
We currently treat implicit instance attributes to be bound, even if they are only conditionally
|
||||
@@ -598,6 +640,8 @@ class C:
|
||||
self.c = c
|
||||
if False:
|
||||
def set_e(self, e: str) -> None:
|
||||
# TODO: Should not emit this diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
self.e = e
|
||||
|
||||
# TODO: this would ideally be `Unknown | Literal[1]`
|
||||
@@ -685,7 +729,7 @@ class C:
|
||||
pure_class_variable2: ClassVar = 1
|
||||
|
||||
def method(self):
|
||||
# TODO: this should be an error
|
||||
# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `Self@method`"
|
||||
self.pure_class_variable1 = "value set through instance"
|
||||
|
||||
reveal_type(C.pure_class_variable1) # revealed: str
|
||||
@@ -885,11 +929,9 @@ class Intermediate(Base):
|
||||
# TODO: This should be an error (violates Liskov)
|
||||
self.redeclared_in_method_with_wider_type: object = object()
|
||||
|
||||
# TODO: This should be an `invalid-assignment` error
|
||||
self.overwritten_in_subclass_method = None
|
||||
self.overwritten_in_subclass_method = None # error: [invalid-assignment]
|
||||
|
||||
# TODO: This should be an `invalid-assignment` error
|
||||
self.pure_overwritten_in_subclass_method = None
|
||||
self.pure_overwritten_in_subclass_method = None # error: [invalid-assignment]
|
||||
|
||||
self.pure_undeclared = "intermediate"
|
||||
|
||||
@@ -1839,6 +1881,7 @@ def external_getattribute(name) -> int:
|
||||
|
||||
class ThisFails:
|
||||
def __init__(self):
|
||||
# error: [invalid-assignment] "Implicit shadowing of function `__getattribute__`"
|
||||
self.__getattribute__ = external_getattribute
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
|
||||
@@ -205,7 +205,7 @@ class C:
|
||||
return str(key)
|
||||
|
||||
def f(self):
|
||||
# TODO: This should emit an `invalid-assignment` diagnostic once we understand the type of `self`
|
||||
# error: [invalid-assignment] "Implicit shadowing of function `__getitem__`"
|
||||
self.__getitem__ = None
|
||||
|
||||
# This is still fine, and simply calls the `__getitem__` method on the class
|
||||
|
||||
@@ -163,14 +163,13 @@ class A:
|
||||
|
||||
class B(A):
|
||||
def __init__(self, a: int):
|
||||
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, B>`
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, B>
|
||||
reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
|
||||
super().__init__(a)
|
||||
|
||||
@classmethod
|
||||
def f(cls):
|
||||
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
|
||||
# TODO: Once `cls` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
super().f()
|
||||
|
||||
@@ -358,15 +357,15 @@ from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: <class 'A'>, Unknown>
|
||||
reveal_type(super()) # revealed: <super: <class 'A'>, A>
|
||||
|
||||
class B:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, B>
|
||||
|
||||
class C(A.B):
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: <class 'C'>, Unknown>
|
||||
reveal_type(super()) # revealed: <super: <class 'C'>, C>
|
||||
|
||||
def inner(t: C):
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, C>
|
||||
@@ -616,7 +615,7 @@ class A:
|
||||
class B(A):
|
||||
def __init__(self, a: int):
|
||||
super().__init__(a)
|
||||
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
|
||||
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
|
||||
super().a
|
||||
|
||||
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
|
||||
|
||||
@@ -170,6 +170,7 @@ def f1(flag: bool):
|
||||
attr = DataDescriptor()
|
||||
|
||||
def f(self):
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `attr` on type `Self@f` with custom `__set__` method"
|
||||
self.attr = "normal"
|
||||
|
||||
reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]
|
||||
|
||||
@@ -208,8 +208,7 @@ class SuperUser(User):
|
||||
def now_called_robert(self):
|
||||
self.name = "Robert" # fine because overridden with a mutable attribute
|
||||
|
||||
# TODO: this should cause us to emit an error as we're assigning to a read-only property
|
||||
# inherited from the `NamedTuple` superclass (requires https://github.com/astral-sh/ty/issues/159)
|
||||
# error: 9 [invalid-assignment] "Cannot assign to read-only property `nickname` on object of type `Self@now_called_robert`"
|
||||
self.nickname = "Bob"
|
||||
|
||||
james = SuperUser(0, "James", 42, "Jimmy")
|
||||
|
||||
@@ -21,144 +21,143 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
|
||||
7 |
|
||||
8 | class B(A):
|
||||
9 | def __init__(self, a: int):
|
||||
10 | # TODO: Once `Self` is supported, this should be `<super: <class 'B'>, B>`
|
||||
11 | reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
12 | reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
|
||||
13 | super().__init__(a)
|
||||
14 |
|
||||
15 | @classmethod
|
||||
16 | def f(cls):
|
||||
17 | # TODO: Once `Self` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
|
||||
18 | reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
19 | super().f()
|
||||
20 |
|
||||
21 | super(B, B(42)).__init__(42)
|
||||
22 | super(B, B).f()
|
||||
23 | import enum
|
||||
24 | from typing import Any, Self, Never, Protocol, Callable
|
||||
25 | from ty_extensions import Intersection
|
||||
26 |
|
||||
27 | class BuilderMeta(type):
|
||||
28 | def __new__(
|
||||
29 | cls: type[Any],
|
||||
30 | name: str,
|
||||
31 | bases: tuple[type, ...],
|
||||
32 | dct: dict[str, Any],
|
||||
33 | ) -> BuilderMeta:
|
||||
34 | # revealed: <super: <class 'BuilderMeta'>, Any>
|
||||
35 | s = reveal_type(super())
|
||||
36 | # revealed: Any
|
||||
37 | return reveal_type(s.__new__(cls, name, bases, dct))
|
||||
38 |
|
||||
39 | class BuilderMeta2(type):
|
||||
40 | def __new__(
|
||||
41 | cls: type[BuilderMeta2],
|
||||
42 | name: str,
|
||||
43 | bases: tuple[type, ...],
|
||||
44 | dct: dict[str, Any],
|
||||
45 | ) -> BuilderMeta2:
|
||||
46 | # revealed: <super: <class 'BuilderMeta2'>, <class 'BuilderMeta2'>>
|
||||
47 | s = reveal_type(super())
|
||||
48 | # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501)
|
||||
49 | # revealed: Unknown
|
||||
50 | return reveal_type(s.__new__(cls, name, bases, dct))
|
||||
51 |
|
||||
52 | class Foo[T]:
|
||||
53 | x: T
|
||||
54 |
|
||||
55 | def method(self: Any):
|
||||
56 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
|
||||
57 |
|
||||
58 | if isinstance(self, Foo):
|
||||
59 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
|
||||
60 |
|
||||
61 | def method2(self: Foo[T]):
|
||||
62 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
|
||||
63 | reveal_type(super())
|
||||
64 |
|
||||
65 | def method3(self: Foo):
|
||||
66 | # revealed: <super: <class 'Foo'>, Foo[Unknown]>
|
||||
67 | reveal_type(super())
|
||||
68 |
|
||||
69 | def method4(self: Self):
|
||||
70 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
|
||||
71 | reveal_type(super())
|
||||
72 |
|
||||
73 | def method5[S: Foo[int]](self: S, other: S) -> S:
|
||||
74 | # revealed: <super: <class 'Foo'>, Foo[int]>
|
||||
75 | reveal_type(super())
|
||||
76 | return self
|
||||
77 |
|
||||
78 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
|
||||
79 | # revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
|
||||
80 | reveal_type(super())
|
||||
81 | return self
|
||||
82 |
|
||||
83 | def method7[S](self: S, other: S) -> S:
|
||||
84 | # error: [invalid-super-argument]
|
||||
85 | # revealed: Unknown
|
||||
86 | reveal_type(super())
|
||||
87 | return self
|
||||
88 |
|
||||
89 | def method8[S: int](self: S, other: S) -> S:
|
||||
90 | # error: [invalid-super-argument]
|
||||
91 | # revealed: Unknown
|
||||
92 | reveal_type(super())
|
||||
93 | return self
|
||||
94 |
|
||||
95 | def method9[S: (int, str)](self: S, other: S) -> S:
|
||||
96 | # error: [invalid-super-argument]
|
||||
97 | # revealed: Unknown
|
||||
98 | reveal_type(super())
|
||||
99 | return self
|
||||
100 |
|
||||
101 | def method10[S: Callable[..., str]](self: S, other: S) -> S:
|
||||
102 | # error: [invalid-super-argument]
|
||||
103 | # revealed: Unknown
|
||||
104 | reveal_type(super())
|
||||
105 | return self
|
||||
106 |
|
||||
107 | type Alias = Bar
|
||||
108 |
|
||||
109 | class Bar:
|
||||
110 | def method(self: Alias):
|
||||
111 | # revealed: <super: <class 'Bar'>, Bar>
|
||||
112 | reveal_type(super())
|
||||
113 |
|
||||
114 | def pls_dont_call_me(self: Never):
|
||||
115 | # revealed: <super: <class 'Bar'>, Unknown>
|
||||
116 | reveal_type(super())
|
||||
117 |
|
||||
118 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
|
||||
119 | # revealed: <super: <class 'Bar'>, Bar>
|
||||
120 | reveal_type(super())
|
||||
121 |
|
||||
122 | class P(Protocol):
|
||||
123 | def method(self: P):
|
||||
124 | # revealed: <super: <class 'P'>, P>
|
||||
125 | reveal_type(super())
|
||||
126 |
|
||||
127 | class E(enum.Enum):
|
||||
128 | X = 1
|
||||
129 |
|
||||
130 | def method(self: E):
|
||||
131 | match self:
|
||||
132 | case E.X:
|
||||
133 | # revealed: <super: <class 'E'>, E>
|
||||
134 | reveal_type(super())
|
||||
10 | reveal_type(super()) # revealed: <super: <class 'B'>, B>
|
||||
11 | reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
|
||||
12 | super().__init__(a)
|
||||
13 |
|
||||
14 | @classmethod
|
||||
15 | def f(cls):
|
||||
16 | # TODO: Once `cls` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
|
||||
17 | reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
|
||||
18 | super().f()
|
||||
19 |
|
||||
20 | super(B, B(42)).__init__(42)
|
||||
21 | super(B, B).f()
|
||||
22 | import enum
|
||||
23 | from typing import Any, Self, Never, Protocol, Callable
|
||||
24 | from ty_extensions import Intersection
|
||||
25 |
|
||||
26 | class BuilderMeta(type):
|
||||
27 | def __new__(
|
||||
28 | cls: type[Any],
|
||||
29 | name: str,
|
||||
30 | bases: tuple[type, ...],
|
||||
31 | dct: dict[str, Any],
|
||||
32 | ) -> BuilderMeta:
|
||||
33 | # revealed: <super: <class 'BuilderMeta'>, Any>
|
||||
34 | s = reveal_type(super())
|
||||
35 | # revealed: Any
|
||||
36 | return reveal_type(s.__new__(cls, name, bases, dct))
|
||||
37 |
|
||||
38 | class BuilderMeta2(type):
|
||||
39 | def __new__(
|
||||
40 | cls: type[BuilderMeta2],
|
||||
41 | name: str,
|
||||
42 | bases: tuple[type, ...],
|
||||
43 | dct: dict[str, Any],
|
||||
44 | ) -> BuilderMeta2:
|
||||
45 | # revealed: <super: <class 'BuilderMeta2'>, <class 'BuilderMeta2'>>
|
||||
46 | s = reveal_type(super())
|
||||
47 | # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501)
|
||||
48 | # revealed: Unknown
|
||||
49 | return reveal_type(s.__new__(cls, name, bases, dct))
|
||||
50 |
|
||||
51 | class Foo[T]:
|
||||
52 | x: T
|
||||
53 |
|
||||
54 | def method(self: Any):
|
||||
55 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
|
||||
56 |
|
||||
57 | if isinstance(self, Foo):
|
||||
58 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
|
||||
59 |
|
||||
60 | def method2(self: Foo[T]):
|
||||
61 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
|
||||
62 | reveal_type(super())
|
||||
63 |
|
||||
64 | def method3(self: Foo):
|
||||
65 | # revealed: <super: <class 'Foo'>, Foo[Unknown]>
|
||||
66 | reveal_type(super())
|
||||
67 |
|
||||
68 | def method4(self: Self):
|
||||
69 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
|
||||
70 | reveal_type(super())
|
||||
71 |
|
||||
72 | def method5[S: Foo[int]](self: S, other: S) -> S:
|
||||
73 | # revealed: <super: <class 'Foo'>, Foo[int]>
|
||||
74 | reveal_type(super())
|
||||
75 | return self
|
||||
76 |
|
||||
77 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
|
||||
78 | # revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
|
||||
79 | reveal_type(super())
|
||||
80 | return self
|
||||
81 |
|
||||
82 | def method7[S](self: S, other: S) -> S:
|
||||
83 | # error: [invalid-super-argument]
|
||||
84 | # revealed: Unknown
|
||||
85 | reveal_type(super())
|
||||
86 | return self
|
||||
87 |
|
||||
88 | def method8[S: int](self: S, other: S) -> S:
|
||||
89 | # error: [invalid-super-argument]
|
||||
90 | # revealed: Unknown
|
||||
91 | reveal_type(super())
|
||||
92 | return self
|
||||
93 |
|
||||
94 | def method9[S: (int, str)](self: S, other: S) -> S:
|
||||
95 | # error: [invalid-super-argument]
|
||||
96 | # revealed: Unknown
|
||||
97 | reveal_type(super())
|
||||
98 | return self
|
||||
99 |
|
||||
100 | def method10[S: Callable[..., str]](self: S, other: S) -> S:
|
||||
101 | # error: [invalid-super-argument]
|
||||
102 | # revealed: Unknown
|
||||
103 | reveal_type(super())
|
||||
104 | return self
|
||||
105 |
|
||||
106 | type Alias = Bar
|
||||
107 |
|
||||
108 | class Bar:
|
||||
109 | def method(self: Alias):
|
||||
110 | # revealed: <super: <class 'Bar'>, Bar>
|
||||
111 | reveal_type(super())
|
||||
112 |
|
||||
113 | def pls_dont_call_me(self: Never):
|
||||
114 | # revealed: <super: <class 'Bar'>, Unknown>
|
||||
115 | reveal_type(super())
|
||||
116 |
|
||||
117 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
|
||||
118 | # revealed: <super: <class 'Bar'>, Bar>
|
||||
119 | reveal_type(super())
|
||||
120 |
|
||||
121 | class P(Protocol):
|
||||
122 | def method(self: P):
|
||||
123 | # revealed: <super: <class 'P'>, P>
|
||||
124 | reveal_type(super())
|
||||
125 |
|
||||
126 | class E(enum.Enum):
|
||||
127 | X = 1
|
||||
128 |
|
||||
129 | def method(self: E):
|
||||
130 | match self:
|
||||
131 | case E.X:
|
||||
132 | # revealed: <super: <class 'E'>, E>
|
||||
133 | reveal_type(super())
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-super-argument]: `S@method7` is not an instance or subclass of `<class 'Foo'>` in `super(<class 'Foo'>, S@method7)` call
|
||||
--> src/mdtest_snippet.py:86:21
|
||||
--> src/mdtest_snippet.py:85:21
|
||||
|
|
||||
84 | # error: [invalid-super-argument]
|
||||
85 | # revealed: Unknown
|
||||
86 | reveal_type(super())
|
||||
83 | # error: [invalid-super-argument]
|
||||
84 | # revealed: Unknown
|
||||
85 | reveal_type(super())
|
||||
| ^^^^^^^
|
||||
87 | return self
|
||||
86 | return self
|
||||
|
|
||||
info: Type variable `S` has `object` as its implicit upper bound
|
||||
info: `object` is not an instance or subclass of `<class 'Foo'>`
|
||||
@@ -169,13 +168,13 @@ info: rule `invalid-super-argument` is enabled by default
|
||||
|
||||
```
|
||||
error[invalid-super-argument]: `S@method8` is not an instance or subclass of `<class 'Foo'>` in `super(<class 'Foo'>, S@method8)` call
|
||||
--> src/mdtest_snippet.py:92:21
|
||||
--> src/mdtest_snippet.py:91:21
|
||||
|
|
||||
90 | # error: [invalid-super-argument]
|
||||
91 | # revealed: Unknown
|
||||
92 | reveal_type(super())
|
||||
89 | # error: [invalid-super-argument]
|
||||
90 | # revealed: Unknown
|
||||
91 | reveal_type(super())
|
||||
| ^^^^^^^
|
||||
93 | return self
|
||||
92 | return self
|
||||
|
|
||||
info: Type variable `S` has upper bound `int`
|
||||
info: `int` is not an instance or subclass of `<class 'Foo'>`
|
||||
@@ -185,13 +184,13 @@ info: rule `invalid-super-argument` is enabled by default
|
||||
|
||||
```
|
||||
error[invalid-super-argument]: `S@method9` is not an instance or subclass of `<class 'Foo'>` in `super(<class 'Foo'>, S@method9)` call
|
||||
--> src/mdtest_snippet.py:98:21
|
||||
--> src/mdtest_snippet.py:97:21
|
||||
|
|
||||
96 | # error: [invalid-super-argument]
|
||||
97 | # revealed: Unknown
|
||||
98 | reveal_type(super())
|
||||
95 | # error: [invalid-super-argument]
|
||||
96 | # revealed: Unknown
|
||||
97 | reveal_type(super())
|
||||
| ^^^^^^^
|
||||
99 | return self
|
||||
98 | return self
|
||||
|
|
||||
info: Type variable `S` has constraints `int, str`
|
||||
info: `int | str` is not an instance or subclass of `<class 'Foo'>`
|
||||
@@ -201,13 +200,13 @@ info: rule `invalid-super-argument` is enabled by default
|
||||
|
||||
```
|
||||
error[invalid-super-argument]: `S@method10` is a type variable with an abstract/structural type as its bounds or constraints, in `super(<class 'Foo'>, S@method10)` call
|
||||
--> src/mdtest_snippet.py:104:21
|
||||
--> src/mdtest_snippet.py:103:21
|
||||
|
|
||||
102 | # error: [invalid-super-argument]
|
||||
103 | # revealed: Unknown
|
||||
104 | reveal_type(super())
|
||||
101 | # error: [invalid-super-argument]
|
||||
102 | # revealed: Unknown
|
||||
103 | reveal_type(super())
|
||||
| ^^^^^^^
|
||||
105 | return self
|
||||
104 | return self
|
||||
|
|
||||
info: Type variable `S` has upper bound `(...) -> str`
|
||||
info: rule `invalid-super-argument` is enabled by default
|
||||
|
||||
@@ -88,6 +88,8 @@ class C:
|
||||
self.FINAL_C: Final[int] = 1
|
||||
self.FINAL_D: Final = 1
|
||||
self.FINAL_E: Final
|
||||
# TODO: Should not be an error
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `FINAL_E` on type `Self@__init__`"
|
||||
self.FINAL_E = 1
|
||||
|
||||
reveal_type(C.FINAL_A) # revealed: int
|
||||
@@ -184,6 +186,7 @@ class C(metaclass=Meta):
|
||||
self.INSTANCE_FINAL_A: Final[int] = 1
|
||||
self.INSTANCE_FINAL_B: Final = 1
|
||||
self.INSTANCE_FINAL_C: Final[int]
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `INSTANCE_FINAL_C` on type `Self@__init__`"
|
||||
self.INSTANCE_FINAL_C = 1
|
||||
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type `<class 'C'>`"
|
||||
@@ -278,6 +281,8 @@ class C:
|
||||
def __init__(self):
|
||||
self.LEGAL_H: Final[int] = 1
|
||||
self.LEGAL_I: Final[int]
|
||||
# TODO: Should not be an error
|
||||
# error: [invalid-assignment]
|
||||
self.LEGAL_I = 1
|
||||
|
||||
# error: [invalid-type-form] "`Final` is not allowed in function parameter annotations"
|
||||
@@ -390,6 +395,8 @@ class C:
|
||||
DEFINED_IN_INIT: Final[int]
|
||||
|
||||
def __init__(self):
|
||||
# TODO: should not be an error
|
||||
# error: [invalid-assignment]
|
||||
self.DEFINED_IN_INIT = 1
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::iter::FusedIterator;
|
||||
use std::iter::{FusedIterator, once};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_db::files::File;
|
||||
@@ -154,29 +154,56 @@ pub(crate) fn attribute_declarations<'db, 's>(
|
||||
///
|
||||
/// 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>(
|
||||
pub(crate) fn attribute_scopes<'db>(
|
||||
db: &'db dyn Db,
|
||||
class_body_scope: ScopeId<'db>,
|
||||
) -> impl Iterator<Item = FileScopeId> + use<'s, 'db> {
|
||||
) -> impl Iterator<Item = FileScopeId> + '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.scopes, class_scope_id)
|
||||
.filter_map(move |(child_scope_id, scope)| {
|
||||
let (function_scope_id, function_scope) =
|
||||
if scope.node().scope_kind() == ScopeKind::TypeParams {
|
||||
// This could be a generic method with a type-params scope.
|
||||
// Go one level deeper to find the function scope. The first
|
||||
// descendant is the (potential) function scope.
|
||||
let function_scope_id = scope.descendants().start;
|
||||
(function_scope_id, index.scope(function_scope_id))
|
||||
} else {
|
||||
(child_scope_id, scope)
|
||||
};
|
||||
function_scope.node().as_function()?;
|
||||
Some(function_scope_id)
|
||||
})
|
||||
.flat_map(move |func_id| {
|
||||
// Add any descendent scope that is eager and have eager scopes between the scope
|
||||
// and the method scope. Since attributes can be defined in this scope.
|
||||
let nested = index.descendent_scopes(func_id).filter_map(move |(id, s)| {
|
||||
let is_eager = s.kind().is_eager();
|
||||
let parents_are_eager = {
|
||||
let mut all_parents_eager = true;
|
||||
let mut current = Some(id);
|
||||
|
||||
ChildrenIter::new(&index.scopes, class_scope_id).filter_map(move |(child_scope_id, scope)| {
|
||||
let (function_scope_id, function_scope) =
|
||||
if scope.node().scope_kind() == ScopeKind::TypeParams {
|
||||
// This could be a generic method with a type-params scope.
|
||||
// Go one level deeper to find the function scope. The first
|
||||
// descendant is the (potential) function scope.
|
||||
let function_scope_id = scope.descendants().start;
|
||||
(function_scope_id, index.scope(function_scope_id))
|
||||
} else {
|
||||
(child_scope_id, scope)
|
||||
};
|
||||
while let Some(scope_id) = current {
|
||||
if scope_id == func_id {
|
||||
break;
|
||||
}
|
||||
let scope = index.scope(scope_id);
|
||||
if !scope.is_eager() {
|
||||
all_parents_eager = false;
|
||||
break;
|
||||
}
|
||||
current = scope.parent();
|
||||
}
|
||||
|
||||
function_scope.node().as_function()?;
|
||||
Some(function_scope_id)
|
||||
})
|
||||
all_parents_eager
|
||||
};
|
||||
|
||||
(parents_are_eager && is_eager).then_some(id)
|
||||
});
|
||||
once(func_id).chain(nested)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the module global scope of `file`.
|
||||
@@ -751,7 +778,10 @@ mod tests {
|
||||
use crate::semantic_index::scope::{FileScopeId, Scope, ScopeKind};
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
use crate::semantic_index::use_def::UseDefMap;
|
||||
use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map};
|
||||
use crate::semantic_index::{
|
||||
attribute_declarations, attribute_scopes, global_scope, place_table, semantic_index,
|
||||
use_def_map,
|
||||
};
|
||||
|
||||
impl UseDefMap<'_> {
|
||||
fn first_public_binding(&self, symbol: ScopedSymbolId) -> Option<Definition<'_>> {
|
||||
@@ -1128,6 +1158,124 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||
}
|
||||
}
|
||||
|
||||
/// Test case to validate that comprehensions inside a method are correctly marked as attribute
|
||||
/// scopes
|
||||
#[test]
|
||||
fn comprehension_in_method_instance_attribute() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
class C:
|
||||
def __init__(self):
|
||||
[None for self.z in range(1)]
|
||||
",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
|
||||
let [(class_scope_id, class_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.collect::<Vec<_>>()[..]
|
||||
else {
|
||||
panic!("expected one child scope (the class)")
|
||||
};
|
||||
|
||||
assert_eq!(class_scope.kind(), ScopeKind::Class);
|
||||
let class_scope = class_scope_id.to_scope_id(&db, file);
|
||||
|
||||
let method_scopes: Vec<_> = index.child_scopes(class_scope_id).collect();
|
||||
assert_eq!(method_scopes.len(), 1, "expected __init__");
|
||||
let (method_scope_id, method_scope) = method_scopes[0];
|
||||
assert_eq!(method_scope.kind(), ScopeKind::Function);
|
||||
|
||||
let comp_scopes: Vec<_> = index.child_scopes(method_scope_id).collect();
|
||||
assert_eq!(comp_scopes.len(), 1, "expected one comprehension scope");
|
||||
let (comprehension_scope_id, comprehension_scope) = comp_scopes[0];
|
||||
assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension);
|
||||
|
||||
let attr_scopes: Vec<_> = attribute_scopes(&db, class_scope).collect();
|
||||
assert!(
|
||||
attr_scopes.contains(&method_scope_id),
|
||||
"attribute_scopes should include the method scope"
|
||||
);
|
||||
assert!(
|
||||
attr_scopes.contains(&comprehension_scope_id),
|
||||
"attribute_scopes should include the comprehension scope"
|
||||
);
|
||||
|
||||
let comprehension_place_table = index.place_table(comprehension_scope_id);
|
||||
let members: Vec<_> = comprehension_place_table.members().collect();
|
||||
|
||||
let has_z_instance_attr = members
|
||||
.iter()
|
||||
.any(|member| member.as_instance_attribute() == Some("z"));
|
||||
assert!(
|
||||
has_z_instance_attr,
|
||||
"self.z should be marked as an instance attribute in the comprehension scope found members: {members:?}"
|
||||
);
|
||||
|
||||
let z_declarations: Vec<_> = attribute_declarations(&db, class_scope, "z")
|
||||
.map(|(decls, scope_id)| {
|
||||
let decl_vec: Vec<_> = decls.collect();
|
||||
(decl_vec, scope_id)
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
!z_declarations.is_empty(),
|
||||
"attribute_declarations should find declarations for z"
|
||||
);
|
||||
|
||||
// Verify that at least one declaration of z is in the comprehension scope
|
||||
let has_comp_declaration = z_declarations
|
||||
.iter()
|
||||
.any(|(_, scope_id)| *scope_id == comprehension_scope_id);
|
||||
assert!(
|
||||
has_comp_declaration,
|
||||
"attribute_declarations should include a declaration from the comprehension scope"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test case to validate that when first method argument is shadowed by a comprehension variable,
|
||||
/// attributes accessed on the shadowed argument should NOT be tracked as instance attributes
|
||||
/// of the containing class.
|
||||
#[test]
|
||||
fn comprehension_shadowing_self() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
class D:
|
||||
g: int
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
[[None for self.g in range(1)] for self in [D()]]
|
||||
",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
|
||||
let all_scopes: Vec<_> = index.child_scopes(FileScopeId::global()).collect();
|
||||
|
||||
let (class_c_scope_id, class_c_scope) = all_scopes[1];
|
||||
assert_eq!(class_c_scope.kind(), ScopeKind::Class);
|
||||
let class_c_scope = class_c_scope_id.to_scope_id(&db, file);
|
||||
|
||||
let attr_scopes: Vec<_> = attribute_scopes(&db, class_c_scope).collect();
|
||||
let mut has_g_attribute = false;
|
||||
for scope_id in &attr_scopes {
|
||||
let place_table = index.place_table(*scope_id);
|
||||
if place_table
|
||||
.member_id_by_instance_attribute_name("g")
|
||||
.is_some()
|
||||
{
|
||||
has_g_attribute = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
!has_g_attribute,
|
||||
"self.g should NOT be tracked as an attribute of C because 'self' is shadowed by the outer comprehension variable"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test case to validate that the `x` variable used in the comprehension is referencing the
|
||||
/// `x` variable defined by the inner generator (`for x in iter2`) and not the outer one.
|
||||
#[test]
|
||||
|
||||
@@ -184,29 +184,34 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
self.current_scope_info().file_scope_id
|
||||
}
|
||||
|
||||
/// Returns the scope ID of the surrounding class body scope if the current scope
|
||||
/// is a method inside a class body. Returns `None` otherwise, e.g. if the current
|
||||
/// scope is a function body outside of a class, or if the current scope is not a
|
||||
/// Returns the scope ID of the method scope if the current scope
|
||||
/// is a method inside a class body or current scope is in eagerly executed scope in a method.
|
||||
/// Returns `None` otherwise, e.g. if the current scope is a function body outside of a class, or if the current scope is not a
|
||||
/// function body.
|
||||
fn is_method_of_class(&self) -> Option<FileScopeId> {
|
||||
let mut scopes_rev = self.scope_stack.iter().rev();
|
||||
fn is_method_or_eagerly_executed_in_method(&self) -> Option<FileScopeId> {
|
||||
let mut scopes_rev = self
|
||||
.scope_stack
|
||||
.iter()
|
||||
.rev()
|
||||
.skip_while(|scope| self.scopes[scope.file_scope_id].is_eager());
|
||||
let current = scopes_rev.next()?;
|
||||
|
||||
if self.scopes[current.file_scope_id].kind() != ScopeKind::Function {
|
||||
return None;
|
||||
}
|
||||
|
||||
let maybe_method = current.file_scope_id;
|
||||
let parent = scopes_rev.next()?;
|
||||
|
||||
match self.scopes[parent.file_scope_id].kind() {
|
||||
ScopeKind::Class => Some(parent.file_scope_id),
|
||||
ScopeKind::Class => Some(maybe_method),
|
||||
ScopeKind::TypeParams => {
|
||||
// If the function is generic, the parent scope is an annotation scope.
|
||||
// In this case, we need to go up one level higher to find the class scope.
|
||||
let grandparent = scopes_rev.next()?;
|
||||
|
||||
if self.scopes[grandparent.file_scope_id].kind() == ScopeKind::Class {
|
||||
Some(grandparent.file_scope_id)
|
||||
Some(maybe_method)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -215,6 +220,32 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a symbol name is bound in any intermediate eager scopes
|
||||
/// between the current scope and the specified method scope.
|
||||
///
|
||||
fn is_symbol_bound_in_intermediate_eager_scopes(
|
||||
&self,
|
||||
symbol_name: &str,
|
||||
method_scope_id: FileScopeId,
|
||||
) -> bool {
|
||||
for scope_info in self.scope_stack.iter().rev() {
|
||||
let scope_id = scope_info.file_scope_id;
|
||||
|
||||
if scope_id == method_scope_id {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(symbol_id) = self.place_tables[scope_id].symbol_id(symbol_name) {
|
||||
let symbol = self.place_tables[scope_id].symbol(symbol_id);
|
||||
if symbol.is_bound() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Push a new loop, returning the outer loop, if any.
|
||||
fn push_loop(&mut self) -> Option<Loop> {
|
||||
self.current_scope_info_mut()
|
||||
@@ -1647,7 +1678,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
self.visit_expr(&node.annotation);
|
||||
if let Some(value) = &node.value {
|
||||
self.visit_expr(value);
|
||||
if self.is_method_of_class().is_some() {
|
||||
if self.is_method_or_eagerly_executed_in_method().is_some() {
|
||||
// Record the right-hand side of the assignment as a standalone expression
|
||||
// if we're inside a method. This allows type inference to infer the type
|
||||
// of the value for annotated assignments like `self.CONSTANT: Final = 1`,
|
||||
@@ -2319,14 +2350,21 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
| ast::Expr::Attribute(ast::ExprAttribute { ctx, .. })
|
||||
| ast::Expr::Subscript(ast::ExprSubscript { ctx, .. }) => {
|
||||
if let Some(mut place_expr) = PlaceExpr::try_from_expr(expr) {
|
||||
if self.is_method_of_class().is_some() {
|
||||
if let Some(method_scope_id) = self.is_method_or_eagerly_executed_in_method() {
|
||||
if let PlaceExpr::Member(member) = &mut place_expr {
|
||||
if member.is_instance_attribute_candidate() {
|
||||
// We specifically mark attribute assignments to the first parameter of a method,
|
||||
// i.e. typically `self` or `cls`.
|
||||
let accessed_object_refers_to_first_parameter = self
|
||||
.current_first_parameter_name
|
||||
.is_some_and(|first| member.symbol_name() == first);
|
||||
// However, we must check that the symbol hasn't been shadowed by an intermediate
|
||||
// scope (e.g., a comprehension variable: `for self in [...]`).
|
||||
let accessed_object_refers_to_first_parameter =
|
||||
self.current_first_parameter_name.is_some_and(|first| {
|
||||
member.symbol_name() == first
|
||||
&& !self.is_symbol_bound_in_intermediate_eager_scopes(
|
||||
first,
|
||||
method_scope_id,
|
||||
)
|
||||
});
|
||||
|
||||
if accessed_object_refers_to_first_parameter {
|
||||
member.mark_instance_attribute();
|
||||
|
||||
@@ -206,7 +206,7 @@ enum ReduceResult<'db> {
|
||||
//
|
||||
// For now (until we solve https://github.com/astral-sh/ty/issues/957), keep this number
|
||||
// below 200, which is the salsa fixpoint iteration limit.
|
||||
const MAX_UNION_LITERALS: usize = 199;
|
||||
const MAX_UNION_LITERALS: usize = 190;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<UnionElement<'db>>,
|
||||
|
||||
@@ -3227,30 +3227,47 @@ impl<'db> ClassLiteral<'db> {
|
||||
union_of_inferred_types = union_of_inferred_types.add(Type::unknown());
|
||||
}
|
||||
|
||||
for (attribute_assignments, method_scope_id) in
|
||||
for (attribute_assignments, attribute_binding_scope_id) in
|
||||
attribute_assignments(db, class_body_scope, &name)
|
||||
{
|
||||
let method_scope = index.scope(method_scope_id);
|
||||
if !is_valid_scope(method_scope) {
|
||||
let binding_scope = index.scope(attribute_binding_scope_id);
|
||||
if !is_valid_scope(binding_scope) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The attribute assignment inherits the reachability of the method which contains it
|
||||
let is_method_reachable = if let Some(method_def) = method_scope.node().as_function() {
|
||||
let method = index.expect_single_definition(method_def);
|
||||
let method_place = class_table
|
||||
.symbol_id(&method_def.node(&module).name)
|
||||
.unwrap();
|
||||
class_map
|
||||
.all_reachable_symbol_bindings(method_place)
|
||||
.find_map(|bind| {
|
||||
(bind.binding.is_defined_and(|def| def == method))
|
||||
.then(|| class_map.binding_reachability(db, &bind))
|
||||
})
|
||||
.unwrap_or(Truthiness::AlwaysFalse)
|
||||
} else {
|
||||
Truthiness::AlwaysFalse
|
||||
let scope_for_reachability_analysis = {
|
||||
if binding_scope.node().as_function().is_some() {
|
||||
binding_scope
|
||||
} else if binding_scope.is_eager() {
|
||||
let mut eager_scope_parent = binding_scope;
|
||||
while eager_scope_parent.is_eager()
|
||||
&& let Some(parent) = eager_scope_parent.parent()
|
||||
{
|
||||
eager_scope_parent = index.scope(parent);
|
||||
}
|
||||
eager_scope_parent
|
||||
} else {
|
||||
binding_scope
|
||||
}
|
||||
};
|
||||
|
||||
// The attribute assignment inherits the reachability of the method which contains it
|
||||
let is_method_reachable =
|
||||
if let Some(method_def) = scope_for_reachability_analysis.node().as_function() {
|
||||
let method = index.expect_single_definition(method_def);
|
||||
let method_place = class_table
|
||||
.symbol_id(&method_def.node(&module).name)
|
||||
.unwrap();
|
||||
class_map
|
||||
.all_reachable_symbol_bindings(method_place)
|
||||
.find_map(|bind| {
|
||||
(bind.binding.is_defined_and(|def| def == method))
|
||||
.then(|| class_map.binding_reachability(db, &bind))
|
||||
})
|
||||
.unwrap_or(Truthiness::AlwaysFalse)
|
||||
} else {
|
||||
Truthiness::AlwaysFalse
|
||||
};
|
||||
if is_method_reachable.is_always_false() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -80,11 +80,11 @@ pub(crate) fn bind_typevar<'db>(
|
||||
/// Create a `typing.Self` type variable for a given class.
|
||||
pub(crate) fn typing_self<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope_id: ScopeId,
|
||||
function_scope_id: ScopeId,
|
||||
typevar_binding_context: Option<Definition<'db>>,
|
||||
class: ClassLiteral<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
let index = semantic_index(db, scope_id.file(db));
|
||||
let index = semantic_index(db, function_scope_id.file(db));
|
||||
|
||||
let identity = TypeVarIdentity::new(
|
||||
db,
|
||||
@@ -110,7 +110,7 @@ pub(crate) fn typing_self<'db>(
|
||||
bind_typevar(
|
||||
db,
|
||||
index,
|
||||
scope_id.file_scope_id(db),
|
||||
function_scope_id.file_scope_id(db),
|
||||
typevar_binding_context,
|
||||
typevar,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{iter, mem};
|
||||
use itertools::{Either, Itertools};
|
||||
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModuleRef;
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use ruff_python_ast::visitor::{Visitor, walk_expr};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext, PythonVersion};
|
||||
use ruff_python_stdlib::builtins::version_builtin_was_added;
|
||||
@@ -81,9 +81,9 @@ use crate::types::function::{
|
||||
};
|
||||
use crate::types::generics::{
|
||||
GenericContext, InferableTypeVars, LegacyGenericBase, SpecializationBuilder, bind_typevar,
|
||||
enclosing_generic_contexts,
|
||||
enclosing_generic_contexts, typing_self,
|
||||
};
|
||||
use crate::types::infer::nearest_enclosing_function;
|
||||
use crate::types::infer::{nearest_enclosing_class, nearest_enclosing_function};
|
||||
use crate::types::instance::SliceLiteral;
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::signatures::Signature;
|
||||
@@ -2495,6 +2495,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
} else {
|
||||
let ty = if let Some(default_ty) = default_ty {
|
||||
UnionType::from_elements(self.db(), [Type::unknown(), default_ty])
|
||||
} else if let Some(ty) = self.special_first_method_parameter_type(parameter) {
|
||||
ty
|
||||
} else {
|
||||
Type::unknown()
|
||||
};
|
||||
@@ -2535,6 +2537,53 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Special case for unannotated `cls` and `self` arguments to class methods and instance methods.
|
||||
fn special_first_method_parameter_type(
|
||||
&mut self,
|
||||
parameter: &ast::Parameter,
|
||||
) -> Option<Type<'db>> {
|
||||
let db = self.db();
|
||||
let scope_id = self.scope();
|
||||
let file = scope_id.file(db);
|
||||
let function_scope = scope_id.scope(db);
|
||||
let method = function_scope.node().as_function()?;
|
||||
|
||||
let parent_scope_id = function_scope.parent()?;
|
||||
let parent_scope = self.index.scope(parent_scope_id);
|
||||
parent_scope.node().as_class()?;
|
||||
|
||||
let method_definition = self.index.expect_single_definition(method);
|
||||
let DefinitionKind::Function(function_definition) = method_definition.kind(db) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let func_type = infer_definition_types(db, method_definition)
|
||||
.declaration_type(method_definition)
|
||||
.inner_type()
|
||||
.as_function_literal()?;
|
||||
|
||||
let module = parsed_module(db, file).load(db);
|
||||
if function_definition
|
||||
.node(&module)
|
||||
.parameters
|
||||
.index(parameter.name())
|
||||
.is_some_and(|index| index != 0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if func_type.is_classmethod(db) {
|
||||
// TODO: set the type for `cls` argument
|
||||
return None;
|
||||
} else if func_type.is_staticmethod(db) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let class = nearest_enclosing_class(db, self.index, scope_id).unwrap();
|
||||
|
||||
typing_self(db, self.scope(), Some(method_definition), class)
|
||||
}
|
||||
|
||||
/// Set initial declared/inferred types for a `*args` variadic positional parameter.
|
||||
///
|
||||
/// The annotated type is implicitly wrapped in a string-keyed dictionary.
|
||||
|
||||
@@ -139,7 +139,7 @@ class FuzzResult:
|
||||
case Executable.TY:
|
||||
panic_message = f"The following code triggers a {new}ty panic:"
|
||||
case _ as unreachable:
|
||||
assert_never(unreachable) # ty: ignore[type-assertion-failure]
|
||||
assert_never(unreachable)
|
||||
|
||||
print(colored(panic_message, "red"))
|
||||
print()
|
||||
|
||||
Reference in New Issue
Block a user