From a66add41bea69ea2f6f4b9d2cc5528e0b3b3d690 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 8 Sep 2025 13:03:38 +0200 Subject: [PATCH] Make Self an inferrable TypeVar --- .../resources/mdtest/annotations/self.md | 6 +-- .../resources/mdtest/call/methods.md | 4 +- .../resources/mdtest/descriptor_protocol.md | 1 + .../resources/mdtest/generics/scoping.md | 4 +- .../resources/mdtest/named_tuple.md | 5 +- .../resources/mdtest/narrow/isinstance.md | 2 + .../resources/mdtest/with/async.md | 3 +- .../src/types/signatures.rs | 52 ++++++++++++++++--- 8 files changed, 62 insertions(+), 15 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index cf1efc939e..f0621aa462 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -79,8 +79,7 @@ class A: def static(x): ... a = A() -# TODO: Should reveal Self@implicit_self. Requires implicit self in method body(https://github.com/astral-sh/ruff/pull/18473) -reveal_type(a.implicit_self()) # revealed: Unknown +reveal_type(a.implicit_self()) # revealed: A reveal_type(a.implicit_self) # revealed: bound method A.implicit_self() -> A ``` @@ -130,8 +129,7 @@ class G(Generic[T]): g = G[int]() -# TODO: Should reveal Self@id Requires implicit self in method body(https://github.com/astral-sh/ruff/pull/18473) -reveal_type(G[int].id(g)) # revealed: Unknown +reveal_type(G[int].id(g)) # revealed: G[int] ``` Free functions and nested functions do not use implicit `Self`: diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 9185e1722e..6824a8ce31 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -69,7 +69,9 @@ reveal_type(bound_method(1)) # revealed: str When we call the function object itself, we need to pass the `instance` explicitly: ```py -C.f(1) # error: [missing-argument] +# error: [invalid-argument-type] "Argument to function `f` is incorrect: Argument type `Literal[1]` does not satisfy upper bound of type variable `Self`" +# error: [missing-argument] "No argument provided for required parameter `x` of function `f`" +C.f(1) reveal_type(C.f(C(), 1)) # revealed: str ``` diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 8c7363909b..288c3154d9 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -431,6 +431,7 @@ def _(flag: bool): reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2] C7.union_of_metaclass_attributes = 2 if flag else 1 + # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `union_of_metaclass_data_descriptor_and_attribute` on type `` with custom `__set__` method" C7.union_of_metaclass_data_descriptor_and_attribute = 2 if flag else 100 C7.union_of_class_attributes = 2 if flag else 1 C7.union_of_class_data_descriptor_and_attribute = 2 if flag else DataDescriptor() diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index f93baa8c8a..1da9994b21 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -117,7 +117,9 @@ reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str reveal_type(C[int]().f(1)) # revealed: str reveal_type(bound_method(1)) # revealed: str -C[int].f(1) # error: [missing-argument] +# error: [missing-argument] "No argument provided for required parameter `x` of function `f`" +# error: [invalid-argument-type] "Argument to function `f` is incorrect: Argument type `Literal[1]` does not satisfy upper bound of type variable `Self`" +C[int].f(1) reveal_type(C[int].f(C[int](), 1)) # revealed: str class D[U](C[U]): diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 2703f8554a..a3df21fd5d 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -277,8 +277,11 @@ reveal_type(Person._make(("Alice", 42))) # revealed: Unknown person = Person("Alice", 42) +# TODO: should not be an error +# error: [invalid-argument-type] reveal_type(person._asdict()) # revealed: dict[str, Any] -# TODO: should be `Person` once we support `Self` +# TODO: should be `Person` once we support `Self`, should not be an error +# error: [invalid-argument-type] reveal_type(person._replace(name="Bob")) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 153bc62112..9018d30fbf 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -358,9 +358,11 @@ def _(x: object): if isinstance(x, Invariant): reveal_type(x) # revealed: Top[Invariant[Unknown]] # error: [invalid-argument-type] "Argument to bound method `get` is incorrect: Expected `Self@get`, found `Top[Invariant[Unknown]]`" + # error: [invalid-argument-type] "Argument to bound method `get` is incorrect: Argument type `Top[Invariant[Unknown]]` does not satisfy upper bound of type variable `Self`" reveal_type(x.get()) # revealed: object # error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Never`, found `Literal[42]`" # error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Self@push`, found `Top[Invariant[Unknown]]`" + # error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Argument type `Top[Invariant[Unknown]]` does not satisfy upper bound of type variable `Self`" x.push(42) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md index 2d907fa381..5f0d864d46 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/async.md +++ b/crates/ty_python_semantic/resources/mdtest/with/async.md @@ -254,8 +254,7 @@ async def long_running_task(): async def main(): async with asyncio.TaskGroup() as tg: - # TODO: should be `TaskGroup` - reveal_type(tg) # revealed: Unknown + reveal_type(tg) # revealed: TaskGroup tg.create_task(long_running_task()) ``` diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index b20466fd8a..4d5bd6c3d7 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -20,13 +20,16 @@ use super::{ semantic_index, }; use crate::semantic_index::definition::{Definition, DefinitionKind}; +use crate::semantic_index::scope::ScopeId; use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension}; use crate::types::function::FunctionType; -use crate::types::generics::{GenericContext, walk_generic_context}; +use crate::types::generics::{GenericContext, bind_typevar, walk_generic_context}; +use crate::types::infer::nearest_enclosing_class; use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, NormalizedVisitor, - SpecialFormType, TypeMapping, TypeRelation, VarianceInferable, todo_type, + TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, + VarianceInferable, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_db::parsed::parsed_module; @@ -1227,6 +1230,42 @@ impl<'db> Parameters<'db> { let is_classmethod = method_type.is_some_and(|f| f.is_classmethod(db)); let is_staticmethod = method_type.is_some_and(|f| f.is_staticmethod(db)); + // TODO: remove duplication with Type::in_type_expression + let get_self_type = |scope_id: ScopeId, typevar_binding_context| { + { + let module = parsed_module(db, scope_id.file(db)).load(db); + let index = semantic_index(db, scope_id.file(db)); + let class = nearest_enclosing_class(db, index, scope_id).unwrap(); + let instance = Type::ClassLiteral(class) + .to_instance(db) + .expect("nearest_enclosing_class must return type that can be instantiated"); + let class_definition = class.definition(db); + let typevar = TypeVarInstance::new( + db, + ast::name::Name::new_static("Self"), + Some(class_definition), + Some(TypeVarBoundOrConstraints::UpperBound(instance).into()), + // According to the [spec], we can consider `Self` + // equivalent to an invariant type variable + // [spec]: https://typing.python.org/en/latest/spec/generics.html#self + Some(TypeVarVariance::Invariant), + None, + TypeVarKind::TypingSelf, + ); + Type::TypeVar( + bind_typevar( + db, + &module, + index, + scope_id.file_scope_id(db), + typevar_binding_context, + typevar, + ) + .unwrap(), + ) + } + }; + let positional_or_keyword = pos_or_keyword_iter.map(|arg| { if is_method && !is_staticmethod @@ -1234,11 +1273,12 @@ impl<'db> Parameters<'db> { && arg.parameter.annotation().is_none() && parameters.index(arg.name().id()) == Some(0) { - let implicit_annotation = Type::SpecialForm(SpecialFormType::TypingSelf) - .in_type_expression(db, definition.scope(db), Some(definition)) - .ok(); + let scope_id = definition.scope(db); + let typevar_binding_context = Some(definition); + let implicit_annotation = get_self_type(scope_id, typevar_binding_context); + Parameter { - annotated_type: implicit_annotation, + annotated_type: Some(implicit_annotation), synthetic_annotation: true, kind: ParameterKind::PositionalOrKeyword { name: arg.parameter.name.id.clone(),