Skip to content

Commit 585ce12

Browse files
authored
[ty] typing.Self is bound by the method, not the class (#19784)
This fixes our logic for binding a legacy typevar with its binding context. (To recap, a legacy typevar starts out "unbound" when it is first created, and each time it's used in a generic class or function, we "bind" it with the corresponding `Definition`.) We treat `typing.Self` the same as a legacy typevar, and so we apply this binding logic to it too. Before, we were using the enclosing class as its binding context. But that's not correct — it's the method where `typing.Self` is used that binds the typevar. (Each invocation of the method will find a new specialization of `Self` based on the specific instance type containing the invoked method.) This required plumbing through some additional state to the `in_type_expression` method. This also revealed that we weren't handling `Self`-typed instance attributes correctly (but were coincidentally not getting the expected false positive diagnostics).
1 parent 21ac16d commit 585ce12

File tree

9 files changed

+216
-69
lines changed

9 files changed

+216
-69
lines changed

crates/ty_ide/src/completion.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,7 +1247,7 @@ quux.<CURSOR>
12471247
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
12481248
__module__ :: str
12491249
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
1250-
__new__ :: bound method Quux.__new__() -> Self@object
1250+
__new__ :: bound method Quux.__new__() -> Self@__new__
12511251
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
12521252
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
12531253
__repr__ :: bound method Quux.__repr__() -> str
@@ -1292,7 +1292,7 @@ quux.b<CURSOR>
12921292
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
12931293
__module__ :: str
12941294
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
1295-
__new__ :: bound method Quux.__new__() -> Self@object
1295+
__new__ :: bound method Quux.__new__() -> Self@__new__
12961296
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
12971297
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
12981298
__repr__ :: bound method Quux.__repr__() -> str
@@ -1346,7 +1346,7 @@ C.<CURSOR>
13461346
__mro__ :: tuple[<class 'C'>, <class 'object'>]
13471347
__name__ :: str
13481348
__ne__ :: def __ne__(self, value: object, /) -> bool
1349-
__new__ :: def __new__(cls) -> Self@object
1349+
__new__ :: def __new__(cls) -> Self@__new__
13501350
__or__ :: bound method <class 'C'>.__or__(value: Any, /) -> UnionType
13511351
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
13521352
__qualname__ :: str
@@ -1522,7 +1522,7 @@ Quux.<CURSOR>
15221522
__mro__ :: tuple[<class 'Quux'>, <class 'object'>]
15231523
__name__ :: str
15241524
__ne__ :: def __ne__(self, value: object, /) -> bool
1525-
__new__ :: def __new__(cls) -> Self@object
1525+
__new__ :: def __new__(cls) -> Self@__new__
15261526
__or__ :: bound method <class 'Quux'>.__or__(value: Any, /) -> UnionType
15271527
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
15281528
__qualname__ :: str
@@ -1574,8 +1574,8 @@ Answer.<CURSOR>
15741574
__bool__ :: bound method <class 'Answer'>.__bool__() -> Literal[True]
15751575
__class__ :: <class 'EnumMeta'>
15761576
__contains__ :: bound method <class 'Answer'>.__contains__(value: object) -> bool
1577-
__copy__ :: def __copy__(self) -> Self@Enum
1578-
__deepcopy__ :: def __deepcopy__(self, memo: Any) -> Self@Enum
1577+
__copy__ :: def __copy__(self) -> Self@__copy__
1578+
__deepcopy__ :: def __deepcopy__(self, memo: Any) -> Self@__deepcopy__
15791579
__delattr__ :: def __delattr__(self, name: str, /) -> None
15801580
__dict__ :: MappingProxyType[str, Any]
15811581
__dictoffset__ :: int
@@ -1599,7 +1599,7 @@ Answer.<CURSOR>
15991599
__mro__ :: tuple[<class 'Answer'>, <class 'Enum'>, <class 'object'>]
16001600
__name__ :: str
16011601
__ne__ :: def __ne__(self, value: object, /) -> bool
1602-
__new__ :: def __new__(cls, value: object) -> Self@Enum
1602+
__new__ :: def __new__(cls, value: object) -> Self@__new__
16031603
__or__ :: bound method <class 'Answer'>.__or__(value: Any, /) -> UnionType
16041604
__order__ :: str
16051605
__prepare__ :: bound method <class 'EnumMeta'>.__prepare__(cls: str, bases: tuple[type, ...], **kwds: Any) -> _EnumDict

crates/ty_python_semantic/resources/mdtest/annotations/self.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,25 @@ from typing import Self
1616

1717
class Shape:
1818
def set_scale(self: Self, scale: float) -> Self:
19-
reveal_type(self) # revealed: Self@Shape
19+
reveal_type(self) # revealed: Self@set_scale
2020
return self
2121

2222
def nested_type(self: Self) -> list[Self]:
2323
return [self]
2424

2525
def nested_func(self: Self) -> Self:
2626
def inner() -> Self:
27-
reveal_type(self) # revealed: Self@Shape
27+
reveal_type(self) # revealed: Self@nested_func
2828
return self
2929
return inner()
3030

31+
def nested_func_without_enclosing_binding(self):
32+
def inner(x: Self):
33+
# TODO: revealed: Self@nested_func_without_enclosing_binding
34+
# (The outer method binds an implicit `Self`)
35+
reveal_type(x) # revealed: Self@inner
36+
inner(self)
37+
3138
def implicit_self(self) -> Self:
3239
# TODO: first argument in a method should be considered as "typing.Self"
3340
reveal_type(self) # revealed: Unknown
@@ -38,13 +45,13 @@ reveal_type(Shape().nested_func()) # revealed: Shape
3845

3946
class Circle(Shape):
4047
def set_scale(self: Self, scale: float) -> Self:
41-
reveal_type(self) # revealed: Self@Circle
48+
reveal_type(self) # revealed: Self@set_scale
4249
return self
4350

4451
class Outer:
4552
class Inner:
4653
def foo(self: Self) -> Self:
47-
reveal_type(self) # revealed: Self@Inner
54+
reveal_type(self) # revealed: Self@foo
4855
return self
4956
```
5057

@@ -99,6 +106,9 @@ reveal_type(Shape.bar()) # revealed: Unknown
99106
python-version = "3.11"
100107
```
101108

109+
TODO: The use of `Self` to annotate the `next_node` attribute should be
110+
[modeled as a property][self attribute], using `Self` in its parameter and return type.
111+
102112
```py
103113
from typing import Self
104114

@@ -108,6 +118,8 @@ class LinkedList:
108118

109119
def next(self: Self) -> Self:
110120
reveal_type(self.value) # revealed: int
121+
# TODO: no error
122+
# error: [invalid-return-type]
111123
return self.next_node
112124

113125
reveal_type(LinkedList().next()) # revealed: LinkedList
@@ -151,7 +163,7 @@ from typing import Self
151163

152164
class Shape:
153165
def union(self: Self, other: Self | None):
154-
reveal_type(other) # revealed: Self@Shape | None
166+
reveal_type(other) # revealed: Self@union | None
155167
return self
156168
```
157169

@@ -205,3 +217,5 @@ class MyMetaclass(type):
205217
def __new__(cls) -> Self:
206218
return super().__new__(cls)
207219
```
220+
221+
[self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations

crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
2727

2828
class Foo:
2929
def method(self, x: Self):
30-
reveal_type(x) # revealed: Self@Foo
30+
reveal_type(x) # revealed: Self@method
3131
```
3232

3333
## Type expressions

crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,33 @@ def g(x: T) -> T | None:
365365
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
366366
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
367367
```
368+
369+
## Opaque decorators don't affect typevar binding
370+
371+
Inside the body of a generic function, we should be able to see that the typevars bound by that
372+
function are in fact bound by that function. This requires being able to see the enclosing
373+
function's _undecorated_ type and signature, especially in the case where a gradually typed
374+
decorator "hides" the function type from outside callers.
375+
376+
```py
377+
from typing import cast, Any, Callable, TypeVar
378+
379+
F = TypeVar("F", bound=Callable[..., Any])
380+
T = TypeVar("T")
381+
382+
def opaque_decorator(f: Any) -> Any:
383+
return f
384+
385+
def transparent_decorator(f: F) -> F:
386+
return f
387+
388+
@opaque_decorator
389+
def decorated(t: T) -> None:
390+
# error: [redundant-cast]
391+
reveal_type(cast(T, t)) # revealed: T@decorated
392+
393+
@transparent_decorator
394+
def decorated(t: T) -> None:
395+
# error: [redundant-cast]
396+
reveal_type(cast(T, t)) # revealed: T@decorated
397+
```

crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,3 +377,30 @@ def f(
377377
# error: [invalid-argument-type] "does not satisfy upper bound"
378378
reveal_type(close_and_return(g)) # revealed: Unknown
379379
```
380+
381+
## Opaque decorators don't affect typevar binding
382+
383+
Inside the body of a generic function, we should be able to see that the typevars bound by that
384+
function are in fact bound by that function. This requires being able to see the enclosing
385+
function's _undecorated_ type and signature, especially in the case where a gradually typed
386+
decorator "hides" the function type from outside callers.
387+
388+
```py
389+
from typing import cast, Any, Callable
390+
391+
def opaque_decorator(f: Any) -> Any:
392+
return f
393+
394+
def transparent_decorator[F: Callable[..., Any]](f: F) -> F:
395+
return f
396+
397+
@opaque_decorator
398+
def decorated[T](t: T) -> None:
399+
# error: [redundant-cast]
400+
reveal_type(cast(T, t)) # revealed: T@decorated
401+
402+
@transparent_decorator
403+
def decorated[T](t: T) -> None:
404+
# error: [redundant-cast]
405+
reveal_type(cast(T, t)) # revealed: T@decorated
406+
```

crates/ty_python_semantic/resources/mdtest/named_tuple.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,9 @@ class Person(NamedTuple):
150150

151151
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
152152
reveal_type(Person._fields) # revealed: tuple[str, ...]
153-
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Self@NamedTupleFallback
153+
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Self@_make
154154
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
155-
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@NamedTupleFallback
155+
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace
156156

157157
# TODO: should be `Person` once we support `Self`
158158
reveal_type(Person._make(("Alice", 42))) # revealed: Unknown

crates/ty_python_semantic/src/types.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ use crate::types::function::{
4747
DataclassTransformerParams, FunctionSpans, FunctionType, KnownFunction,
4848
};
4949
use crate::types::generics::{
50-
GenericContext, PartialSpecialization, Specialization, walk_generic_context,
51-
walk_partial_specialization, walk_specialization,
50+
GenericContext, PartialSpecialization, Specialization, bind_legacy_typevar,
51+
walk_generic_context, walk_partial_specialization, walk_specialization,
5252
};
5353
pub use crate::types::ide_support::{
5454
CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name,
@@ -5268,12 +5268,13 @@ impl<'db> Type<'db> {
52685268
/// expression, it names the type `Type::NominalInstance(builtins.int)`, that is, all objects whose
52695269
/// `__class__` is `int`.
52705270
///
5271-
/// The `scope_id` argument must always be a scope from the file we are currently inferring, so
5271+
/// The `scope_id` and `legacy_typevar_binding_context` arguments must always come from the file we are currently inferring, so
52725272
/// as to avoid cross-module AST dependency.
52735273
pub(crate) fn in_type_expression(
52745274
&self,
52755275
db: &'db dyn Db,
52765276
scope_id: ScopeId<'db>,
5277+
legacy_typevar_binding_context: Option<Definition<'db>>,
52775278
) -> Result<Type<'db>, InvalidTypeExpressionError<'db>> {
52785279
match self {
52795280
// Special cases for `float` and `complex`
@@ -5402,16 +5403,26 @@ impl<'db> Type<'db> {
54025403
"nearest_enclosing_class must return type that can be instantiated",
54035404
);
54045405
let class_definition = class.definition(db);
5405-
Ok(Type::TypeVar(TypeVarInstance::new(
5406+
let typevar = TypeVarInstance::new(
54065407
db,
54075408
ast::name::Name::new_static("Self"),
54085409
Some(class_definition),
5409-
Some(class_definition),
5410+
None,
54105411
Some(TypeVarBoundOrConstraints::UpperBound(instance)),
54115412
TypeVarVariance::Invariant,
54125413
None,
54135414
TypeVarKind::Implicit,
5414-
)))
5415+
);
5416+
let typevar = bind_legacy_typevar(
5417+
db,
5418+
&module,
5419+
index,
5420+
scope_id.file_scope_id(db),
5421+
legacy_typevar_binding_context,
5422+
typevar,
5423+
)
5424+
.unwrap_or(typevar);
5425+
Ok(Type::TypeVar(typevar))
54155426
}
54165427
SpecialFormType::TypeAlias => Ok(Type::Dynamic(DynamicType::TodoTypeAlias)),
54175428
SpecialFormType::TypedDict => Err(InvalidTypeExpressionError {
@@ -5486,7 +5497,7 @@ impl<'db> Type<'db> {
54865497
let mut builder = UnionBuilder::new(db);
54875498
let mut invalid_expressions = smallvec::SmallVec::default();
54885499
for element in union.elements(db) {
5489-
match element.in_type_expression(db, scope_id) {
5500+
match element.in_type_expression(db, scope_id, legacy_typevar_binding_context) {
54905501
Ok(type_expr) => builder = builder.add(type_expr),
54915502
Err(InvalidTypeExpressionError {
54925503
fallback_type,
@@ -6660,7 +6671,7 @@ impl<'db> InvalidTypeExpression<'db> {
66606671
return;
66616672
};
66626673
if module_member_with_same_name
6663-
.in_type_expression(db, scope)
6674+
.in_type_expression(db, scope, None)
66646675
.is_err()
66656676
{
66666677
return;

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind};
99
use crate::semantic_index::{SemanticIndex, semantic_index};
1010
use crate::types::class::ClassType;
1111
use crate::types::class_base::ClassBase;
12+
use crate::types::infer::infer_definition_types;
1213
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
1314
use crate::types::signatures::{Parameter, Parameters, Signature};
1415
use crate::types::tuple::{TupleSpec, TupleType};
@@ -20,7 +21,7 @@ use crate::{Db, FxOrderSet};
2021

2122
/// Returns an iterator of any generic context introduced by the given scope or any enclosing
2223
/// scope.
23-
pub(crate) fn enclosing_generic_contexts<'db>(
24+
fn enclosing_generic_contexts<'db>(
2425
db: &'db dyn Db,
2526
module: &ParsedModuleRef,
2627
index: &SemanticIndex<'db>,
@@ -35,7 +36,9 @@ pub(crate) fn enclosing_generic_contexts<'db>(
3536
.generic_context(db)
3637
}
3738
NodeWithScopeKind::Function(function) => {
38-
binding_type(db, index.expect_single_definition(function.node(module)))
39+
infer_definition_types(db, index.expect_single_definition(function.node(module)))
40+
.undecorated_type()
41+
.expect("function should have undecorated type")
3942
.into_function_literal()?
4043
.signature(db)
4144
.iter()
@@ -59,6 +62,37 @@ fn bound_legacy_typevars<'db>(
5962
.filter(|typevar| typevar.is_legacy(db))
6063
}
6164

65+
/// Binds an unbound legacy typevar.
66+
///
67+
/// When a legacy typevar is first created, we will have a [`TypeVarInstance`] which does not have
68+
/// an associated binding context. When the typevar is used in a generic class or function, we
69+
/// "bind" it, adding the [`Definition`] of the generic class or function as its "binding context".
70+
///
71+
/// When an expression resolves to a legacy typevar, our inferred type will refer to the unbound
72+
/// [`TypeVarInstance`] from when the typevar was first created. This function walks the scopes
73+
/// that enclosing the expression, looking for the innermost binding context that binds the
74+
/// typevar.
75+
///
76+
/// If no enclosing scope has already bound the typevar, we might be in a syntactic position that
77+
/// is about to bind it (indicated by a non-`None` `legacy_typevar_binding_context`), in which case
78+
/// we bind the typevar with that new binding context.
79+
pub(crate) fn bind_legacy_typevar<'db>(
80+
db: &'db dyn Db,
81+
module: &ParsedModuleRef,
82+
index: &SemanticIndex<'db>,
83+
containing_scope: FileScopeId,
84+
legacy_typevar_binding_context: Option<Definition<'db>>,
85+
typevar: TypeVarInstance<'db>,
86+
) -> Option<TypeVarInstance<'db>> {
87+
enclosing_generic_contexts(db, module, index, containing_scope)
88+
.find_map(|enclosing_context| enclosing_context.binds_legacy_typevar(db, typevar))
89+
.or_else(|| {
90+
legacy_typevar_binding_context.map(|legacy_typevar_binding_context| {
91+
typevar.with_binding_context(db, legacy_typevar_binding_context)
92+
})
93+
})
94+
}
95+
6296
/// A list of formal type variables for a generic function, class, or type alias.
6397
///
6498
/// TODO: Handle nested generic contexts better, with actual parent links to the lexically
@@ -259,12 +293,13 @@ impl<'db> GenericContext<'db> {
259293
db: &'db dyn Db,
260294
typevar: TypeVarInstance<'db>,
261295
) -> Option<TypeVarInstance<'db>> {
262-
assert!(typevar.is_legacy(db));
296+
assert!(typevar.is_legacy(db) || typevar.is_implicit(db));
263297
let typevar_def = typevar.definition(db);
264298
self.variables(db)
265299
.iter()
266300
.find(|self_typevar| {
267-
self_typevar.is_legacy(db) && self_typevar.definition(db) == typevar_def
301+
(self_typevar.is_legacy(db) || self_typevar.is_implicit(db))
302+
&& self_typevar.definition(db) == typevar_def
268303
})
269304
.copied()
270305
}

0 commit comments

Comments
 (0)