[ty] Support implicit type of `cls` in signatures (#21771)

## Summary

Extends https://github.com/astral-sh/ruff/pull/20517 to support the
implicit type of `cls` in `@classmethod` signatures. Part of
https://github.com/astral-sh/ty/issues/159.
This commit is contained in:
Ibraheem Ahmed 2025-12-10 16:56:20 -05:00 committed by GitHub
parent 1b44d7e2a7
commit 29bf2cd201
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 31 additions and 18 deletions

View File

@ -607,7 +607,7 @@ class X:
def __init__(self, val: int): ... def __init__(self, val: int): ...
def make_another(self) -> Self: def make_another(self) -> Self:
reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__ reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__
return self.__new__(X) return self.__new__(type(self))
``` ```
## Builtin functions and methods ## Builtin functions and methods

View File

@ -271,8 +271,7 @@ reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(itera
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any] reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace
# TODO: should be `Person` once we support implicit type of `self` reveal_type(Person._make(("Alice", 42))) # revealed: Person
reveal_type(Person._make(("Alice", 42))) # revealed: Unknown
person = Person("Alice", 42) person = Person("Alice", 42)

View File

@ -18,8 +18,8 @@ use ruff_python_ast::ParameterWithDefault;
use smallvec::{SmallVec, smallvec_inline}; use smallvec::{SmallVec, smallvec_inline};
use super::{ use super::{
DynamicType, Type, TypeVarVariance, definition_expression_type, infer_definition_types, ClassType, DynamicType, Type, TypeVarVariance, definition_expression_type,
semantic_index, infer_definition_types, semantic_index,
}; };
use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
@ -31,8 +31,8 @@ use crate::types::infer::nearest_enclosing_class;
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableTypeKind, ClassLiteral, ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableTypeKind, ClassLiteral,
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor,
KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, TypeContext, KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, SubclassOfInner,
TypeMapping, TypeRelation, VarianceInferable, todo_type, SubclassOfType, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
use ruff_python_ast::{self as ast, name::Name}; use ruff_python_ast::{self as ast, name::Name};
@ -1675,8 +1675,8 @@ impl<'db> Parameters<'db> {
}; };
let method_info = infer_method_information(db, definition); let method_info = infer_method_information(db, definition);
let is_static_or_classmethod = let is_staticmethod = method_info.is_some_and(|f| f.is_staticmethod);
method_info.is_some_and(|f| f.is_staticmethod || f.is_classmethod); let is_classmethod = method_info.is_some_and(|f| f.is_classmethod);
let inferred_annotation = |arg: &ParameterWithDefault| { let inferred_annotation = |arg: &ParameterWithDefault| {
if let Some(MethodInformation { if let Some(MethodInformation {
@ -1685,7 +1685,7 @@ impl<'db> Parameters<'db> {
class_is_generic, class_is_generic,
.. ..
}) = method_info }) = method_info
&& !is_static_or_classmethod && !is_staticmethod
&& arg.parameter.annotation().is_none() && arg.parameter.annotation().is_none()
&& parameters.index(arg.name().id()) == Some(0) && parameters.index(arg.name().id()) == Some(0)
{ {
@ -1700,17 +1700,31 @@ impl<'db> Parameters<'db> {
let index = semantic_index(db, scope_id.file(db)); let index = semantic_index(db, scope_id.file(db));
let class = nearest_enclosing_class(db, index, scope_id).unwrap(); let class = nearest_enclosing_class(db, index, scope_id).unwrap();
Some( let typing_self = typing_self(db, scope_id, typevar_binding_context, class)
typing_self(db, scope_id, typevar_binding_context, class) .expect("We should always find the surrounding class for an implicit self: Self annotation");
.map(Type::TypeVar)
.expect("We should always find the surrounding class for an implicit self: Self annotation"), if is_classmethod {
) Some(SubclassOfType::from(
db,
SubclassOfInner::TypeVar(typing_self),
))
} else {
Some(Type::TypeVar(typing_self))
}
} else { } else {
// For methods of non-generic classes that are not otherwise generic (e.g. return `Self` or // For methods of non-generic classes that are not otherwise generic (e.g. return `Self` or
// have additional type parameters), the implicit `Self` type of the `self` parameter would // have additional type parameters), the implicit `Self` type of the `self`, or the implicit
// be the only type variable, so we can just use the class directly. // `type[Self]` type of the `cls` parameter, would be the only type variable, so we can just
// use the class directly.
if is_classmethod {
Some(SubclassOfType::from(
db,
SubclassOfInner::Class(ClassType::NonGeneric(class_literal)),
))
} else {
Some(class_literal.to_non_generic_instance(db)) Some(class_literal.to_non_generic_instance(db))
} }
}
} else { } else {
None None
} }