diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index bceff924fb081..e932aabd15cdd 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -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); diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index dfa76d2b338e4..be6195d96aa99 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -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] diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 915fa0c030962..feb4e45fc65bf 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -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(), @""); + 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] diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 63dcaa0cb6364..4e66881f485e8 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -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] diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 91a2bcaf075b2..621db497c4ab4 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -56,7 +56,7 @@ In instance methods, the first parameter (regardless of its name) is assumed to ```toml [environment] -python-version = "3.11" +python-version = "3.12" ``` ```py @@ -64,16 +64,30 @@ 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 - def a_method(self) -> int: - def first_arg_is_not_self(a: int) -> int: + def implicit_self_generic[T](self, x: T) -> T: + reveal_type(self) # revealed: Self@implicit_self_generic + + return x + + def method_a(self) -> None: + def first_param_is_not_self(a: int): reveal_type(a) # revealed: int - return a - return first_arg_is_not_self(1) + reveal_type(self) # revealed: Self@method_a + + def first_param_is_not_self_unannotated(a): + reveal_type(a) # revealed: Unknown + reveal_type(self) # revealed: Self@method_a + + def first_param_is_also_not_self(self) -> None: + reveal_type(self) # revealed: Unknown + + def first_param_is_explicit_self(this: Self) -> None: + reveal_type(this) # revealed: Self@method_a + reveal_type(self) # revealed: Self@method_a @classmethod def a_classmethod(cls) -> Self: @@ -127,19 +141,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 +176,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 +262,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 +366,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) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index f6c20d0c5ff88..82d7f0c57b5a9 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -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 @@ -399,9 +396,19 @@ class TupleIterable: class C: def __init__(self) -> None: + # TODO: Should not emit this diagnostic + # error: [unresolved-attribute] [... for self.a in IntIterable()] + # TODO: Should not emit this diagnostic + # error: [unresolved-attribute] + # error: [unresolved-attribute] [... for (self.b, self.c) in TupleIterable()] + # TODO: Should not emit this diagnostic + # error: [unresolved-attribute] + # error: [unresolved-attribute] [... for self.d in IntIterable() for self.e in IntIterable()] + # TODO: Should not emit this diagnostic + # error: [unresolved-attribute] [[... for self.f in IntIterable()] for _ in IntIterable()] [[... for self.g in IntIterable()] for self in [D()]] @@ -598,6 +605,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 +694,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 +894,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 +1846,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] diff --git a/crates/ty_python_semantic/resources/mdtest/call/dunder.md b/crates/ty_python_semantic/resources/mdtest/call/dunder.md index 09b83f0035f89..721517eac4f90 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/dunder.md +++ b/crates/ty_python_semantic/resources/mdtest/call/dunder.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index 1862866764d5b..d08c5777c14c7 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -163,14 +163,13 @@ class A: class B(A): def __init__(self, a: int): - # TODO: Once `Self` is supported, this should be `, B>` - reveal_type(super()) # revealed: , Unknown> + reveal_type(super()) # revealed: , B> reveal_type(super(object, super())) # revealed: , super> super().__init__(a) @classmethod def f(cls): - # TODO: Once `Self` is supported, this should be `, >` + # TODO: Once `cls` is supported, this should be `, >` reveal_type(super()) # revealed: , Unknown> super().f() @@ -358,15 +357,15 @@ from __future__ import annotations class A: def test(self): - reveal_type(super()) # revealed: , Unknown> + reveal_type(super()) # revealed: , A> class B: def test(self): - reveal_type(super()) # revealed: , Unknown> + reveal_type(super()) # revealed: , B> class C(A.B): def test(self): - reveal_type(super()) # revealed: , Unknown> + reveal_type(super()) # revealed: , C> def inner(t: C): reveal_type(super()) # revealed: , 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 `, B>` has no attribute `a`" super().a # error: [unresolved-attribute] "Object of type `, B>` has no attribute `a`" diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index f5e0f254d0c11..f4b5854ed7825 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -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"] diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 3a2b7ce14b697..f8c8a330f27ae 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -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") diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" index b13937e6e53be..c9fcea3d5a868 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" @@ -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 `, B>` - 11 | reveal_type(super()) # revealed: , Unknown> - 12 | reveal_type(super(object, super())) # revealed: , super> - 13 | super().__init__(a) - 14 | - 15 | @classmethod - 16 | def f(cls): - 17 | # TODO: Once `Self` is supported, this should be `, >` - 18 | reveal_type(super()) # revealed: , 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: , 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: , > - 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: , Any> - 57 | - 58 | if isinstance(self, Foo): - 59 | reveal_type(super()) # revealed: , Any> - 60 | - 61 | def method2(self: Foo[T]): - 62 | # revealed: , Foo[T@Foo]> - 63 | reveal_type(super()) - 64 | - 65 | def method3(self: Foo): - 66 | # revealed: , Foo[Unknown]> - 67 | reveal_type(super()) - 68 | - 69 | def method4(self: Self): - 70 | # revealed: , Foo[T@Foo]> - 71 | reveal_type(super()) - 72 | - 73 | def method5[S: Foo[int]](self: S, other: S) -> S: - 74 | # revealed: , 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: , Foo[int]> | , 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: , Bar> -112 | reveal_type(super()) -113 | -114 | def pls_dont_call_me(self: Never): -115 | # revealed: , Unknown> -116 | reveal_type(super()) -117 | -118 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): -119 | # revealed: , Bar> -120 | reveal_type(super()) -121 | -122 | class P(Protocol): -123 | def method(self: P): -124 | # revealed: , 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: , E> -134 | reveal_type(super()) + 10 | reveal_type(super()) # revealed: , B> + 11 | reveal_type(super(object, super())) # revealed: , super> + 12 | super().__init__(a) + 13 | + 14 | @classmethod + 15 | def f(cls): + 16 | # TODO: Once `cls` is supported, this should be `, >` + 17 | reveal_type(super()) # revealed: , 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: , 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: , > + 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: , Any> + 56 | + 57 | if isinstance(self, Foo): + 58 | reveal_type(super()) # revealed: , Any> + 59 | + 60 | def method2(self: Foo[T]): + 61 | # revealed: , Foo[T@Foo]> + 62 | reveal_type(super()) + 63 | + 64 | def method3(self: Foo): + 65 | # revealed: , Foo[Unknown]> + 66 | reveal_type(super()) + 67 | + 68 | def method4(self: Self): + 69 | # revealed: , Foo[T@Foo]> + 70 | reveal_type(super()) + 71 | + 72 | def method5[S: Foo[int]](self: S, other: S) -> S: + 73 | # revealed: , 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: , Foo[int]> | , 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: , Bar> +111 | reveal_type(super()) +112 | +113 | def pls_dont_call_me(self: Never): +114 | # revealed: , Unknown> +115 | reveal_type(super()) +116 | +117 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): +118 | # revealed: , Bar> +119 | reveal_type(super()) +120 | +121 | class P(Protocol): +122 | def method(self: P): +123 | # revealed: , 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: , E> +133 | reveal_type(super()) ``` # Diagnostics ``` error[invalid-super-argument]: `S@method7` is not an instance or subclass of `` in `super(, 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 `` @@ -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 `` in `super(, 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 `` @@ -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 `` in `super(, 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 `` @@ -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(, 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 diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md index 7c68534e10c2c..ce2879e248b38 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -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 ``" @@ -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 ``` diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 152f9ba325f03..de413c00a9cf5 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -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>, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 249c406a1bee5..eb62c74102c17 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -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>, class: ClassLiteral<'db>, ) -> Option> { - 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, ) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 99df77b71d619..00bf13db47404 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -81,7 +81,7 @@ 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::instance::SliceLiteral; @@ -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,65 @@ 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> { + let db = self.db(); + let file = self.file(); + + let function_scope_id = self.scope(); + let function_scope = function_scope_id.scope(db); + let function = function_scope.node().as_function()?; + + let parent_file_scope_id = function_scope.parent()?; + let mut parent_scope_id = parent_file_scope_id.to_scope_id(db, file); + + // Skip type parameter scopes, if the method itself is generic. + if parent_scope_id.is_annotation(db) { + let parent_scope = parent_scope_id.scope(db); + parent_scope_id = parent_scope.parent()?.to_scope_id(db, file); + } + + // Return early if this is not a method inside a class. + let class = parent_scope_id.scope(db).node().as_class()?; + + let method_definition = self.index.expect_single_definition(function); + let DefinitionKind::Function(function_definition) = method_definition.kind(db) else { + return None; + }; + + if function_definition + .node(self.module()) + .parameters + .index(parameter.name()) + .is_none_or(|index| index != 0) + { + return None; + } + + let method = infer_definition_types(db, method_definition) + .declaration_type(method_definition) + .inner_type() + .as_function_literal()?; + + if method.is_classmethod(db) { + // TODO: set the type for `cls` argument + return None; + } else if method.is_staticmethod(db) { + return None; + } + + let class_definition = self.index.expect_single_definition(class); + let class_literal = infer_definition_types(db, class_definition) + .declaration_type(class_definition) + .inner_type() + .as_class_literal()?; + + typing_self(db, self.scope(), Some(method_definition), class_literal) + } + /// Set initial declared/inferred types for a `*args` variadic positional parameter. /// /// The annotated type is implicitly wrapped in a string-keyed dictionary. diff --git a/python/py-fuzzer/fuzz.py b/python/py-fuzzer/fuzz.py index 76cc52d2a1c2e..e113d7e17981c 100644 --- a/python/py-fuzzer/fuzz.py +++ b/python/py-fuzzer/fuzz.py @@ -139,7 +139,7 @@ def print_description(self, index: int, num_seeds: int) -> None: 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()