[ty] Bind Self typevar to method context (#20366)

Fixes: https://github.com/astral-sh/ty/issues/1173

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR will change the logic of binding Self type variables to bind
self to the immediate function that it's used on.
Since we are binding `self` to methods and not the class itself we need
to ensure that we bind self consistently.

The fix is to traverse scopes containing the self and find the first
function inside a class and use that function to bind the typevar for
self.

If no such scope is found we fallback to the normal behavior. Using Self
outside of a class scope is not legal anyway.

## Test Plan

Added a new mdtest.

Checked the diagnostics that are not emitted anymore in [primer
results](https://github.com/astral-sh/ruff/pull/20366#issuecomment-3289411424).
It looks good altough I don't completely understand what was wrong
before.

---------

Co-authored-by: Douglas Creager <dcreager@dcreager.net>
This commit is contained in:
Shaygan Hooshyari 2025-09-17 20:58:54 +02:00 committed by GitHub
parent 3fcbe8bde6
commit 05622ae757
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 47 additions and 5 deletions

View file

@ -30,9 +30,7 @@ class Shape:
def nested_func_without_enclosing_binding(self):
def inner(x: Self):
# TODO: revealed: Self@nested_func_without_enclosing_binding
# (The outer method binds an implicit `Self`)
reveal_type(x) # revealed: Self@inner
reveal_type(x) # revealed: Self@nested_func_without_enclosing_binding
inner(self)
def implicit_self(self) -> Self:
@ -239,4 +237,36 @@ reveal_type(D().instance_method)
reveal_type(D.class_method)
```
In nested functions `self` binds to the method. So in the following example the `self` in `C.b` is
bound at `C.f`.
```py
from typing import Self
from ty_extensions import generic_context
class C[T]():
def f(self: Self):
def b(x: Self):
reveal_type(x) # revealed: Self@f
reveal_type(generic_context(b)) # revealed: None
reveal_type(generic_context(C.f)) # revealed: tuple[Self@f]
```
Even if the `Self` annotation appears first in the nested function, it is the method that binds
`Self`.
```py
from typing import Self
from ty_extensions import generic_context
class C:
def f(self: "C"):
def b(x: Self):
reveal_type(x) # revealed: Self@f
reveal_type(generic_context(b)) # revealed: None
reveal_type(generic_context(C.f)) # revealed: None
```
[self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations