[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 nested_func_without_enclosing_binding(self):
def inner(x: Self): def inner(x: Self):
# TODO: revealed: Self@nested_func_without_enclosing_binding reveal_type(x) # revealed: Self@nested_func_without_enclosing_binding
# (The outer method binds an implicit `Self`)
reveal_type(x) # revealed: Self@inner
inner(self) inner(self)
def implicit_self(self) -> Self: def implicit_self(self) -> Self:
@ -239,4 +237,36 @@ reveal_type(D().instance_method)
reveal_type(D.class_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 [self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations

View file

@ -2,6 +2,7 @@ use std::borrow::Cow;
use crate::types::constraints::ConstraintSet; use crate::types::constraints::ConstraintSet;
use itertools::Itertools;
use ruff_db::parsed::ParsedModuleRef; use ruff_db::parsed::ParsedModuleRef;
use ruff_python_ast as ast; use ruff_python_ast as ast;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
@ -18,8 +19,8 @@ use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor,
Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
UnionType, binding_type, declaration_type, TypeVarVariance, UnionType, binding_type, declaration_type,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
@ -82,6 +83,17 @@ pub(crate) fn bind_typevar<'db>(
typevar_binding_context: Option<Definition<'db>>, typevar_binding_context: Option<Definition<'db>>,
typevar: TypeVarInstance<'db>, typevar: TypeVarInstance<'db>,
) -> Option<BoundTypeVarInstance<'db>> { ) -> Option<BoundTypeVarInstance<'db>> {
// typing.Self is treated like a legacy typevar, but doesn't follow the same scoping rules. It is always bound to the outermost method in the containing class.
if matches!(typevar.kind(db), TypeVarKind::TypingSelf) {
for ((_, inner), (_, outer)) in index.ancestor_scopes(containing_scope).tuple_windows() {
if outer.kind().is_class() {
if let NodeWithScopeKind::Function(function) = inner.node() {
let definition = index.expect_single_definition(function.node(module));
return Some(typevar.with_binding_context(db, definition));
}
}
}
}
enclosing_generic_contexts(db, module, index, containing_scope) enclosing_generic_contexts(db, module, index, containing_scope)
.find_map(|enclosing_context| enclosing_context.binds_typevar(db, typevar)) .find_map(|enclosing_context| enclosing_context.binds_typevar(db, typevar))
.or_else(|| { .or_else(|| {