From a8d1e96ef25adeac02a04f49e9df2282f75f4963 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 1 Dec 2025 14:15:03 +0000 Subject: [PATCH 1/2] [ty] Suppress false positives when `dataclasses.dataclass(...)(cls)` is called imperatively --- .../mdtest/dataclasses/dataclasses.md | 45 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 15 ++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index ba5151fa61ff2..40f04c0eda689 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -1462,3 +1462,48 @@ def test_c(): c = C(1) c.__lt__ = Mock() ``` + +## Imperatively calling `dataclasses.dataclass` + +While we do not currently recognize the special behaviour of `dataclasses.dataclass` if it is called +imperatively, we recognize that it can be called imperatively and do not emit any false-positive +diagnostics on such calls: + +```py +from dataclasses import dataclass +from typing_extensions import TypeVar, dataclass_transform + +U = TypeVar("U") + +@dataclass_transform(kw_only_default=True) +def sequence(cls: type[U]) -> type[U]: + d = dataclass( + repr=False, + eq=False, + match_args=False, + kw_only=True, + )(cls) + reveal_type(d) # revealed: type[U@sequence] + return d + +@dataclass_transform(kw_only_default=True) +def sequence2(cls: type) -> type: + d = dataclass( + repr=False, + eq=False, + match_args=False, + kw_only=True, + )(cls) + reveal_type(d) # revealed: type + return d + +@dataclass_transform(kw_only_default=True) +def sequence3(cls: type[U]) -> type[U]: + # TODO: should reveal `type[U@sequence3]` + return reveal_type(dataclass(cls)) # revealed: Unknown + +@dataclass_transform(kw_only_default=True) +def sequence4(cls: type) -> type: + # TODO: should reveal `type` + return reveal_type(dataclass(cls)) # revealed: Unknown +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 92aa8ae4e8715..3a9d9fd480e02 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6198,8 +6198,19 @@ impl<'db> Type<'db> { Binding::single(self, Signature::todo("Type::Intersection.call()")).into() } - // TODO: this is actually callable - Type::DataclassDecorator(_) => CallableBinding::not_callable(self).into(), + Type::DataclassDecorator(_) => { + let typevar = BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Invariant); + let typevar_meta = SubclassOfType::from(db, typevar); + let context = GenericContext::from_typevar_instances(db, [typevar]); + let parameters = [Parameter::positional_only(Some(Name::new_static("cls"))) + .with_annotated_type(typevar_meta)]; + let signature = Signature::new_generic( + Some(context), + Parameters::new(db, parameters), + Some(typevar_meta), + ); + Binding::single(self, signature).into() + } // TODO: some `SpecialForm`s are callable (e.g. TypedDicts) Type::SpecialForm(_) => CallableBinding::not_callable(self).into(), From 8dfa0d05fb86424f2a0d896c3b3cfae1566e0c2a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 1 Dec 2025 14:42:15 +0000 Subject: [PATCH 2/2] fix false positives --- .../resources/mdtest/dataclasses/dataclasses.md | 13 +++++++++++-- crates/ty_python_semantic/src/types.rs | 7 +++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 40f04c0eda689..fa6f76de753fc 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -1483,7 +1483,7 @@ def sequence(cls: type[U]) -> type[U]: match_args=False, kw_only=True, )(cls) - reveal_type(d) # revealed: type[U@sequence] + reveal_type(d) # revealed: type[U@sequence] & Any return d @dataclass_transform(kw_only_default=True) @@ -1494,7 +1494,7 @@ def sequence2(cls: type) -> type: match_args=False, kw_only=True, )(cls) - reveal_type(d) # revealed: type + reveal_type(d) # revealed: type & Any return d @dataclass_transform(kw_only_default=True) @@ -1506,4 +1506,13 @@ def sequence3(cls: type[U]) -> type[U]: def sequence4(cls: type) -> type: # TODO: should reveal `type` return reveal_type(dataclass(cls)) # revealed: Unknown + +class Foo: ... + +ordered_foo = dataclass(order=True)(Foo) +reveal_type(ordered_foo) # revealed: type[Foo] & Any +# TODO: should be `Foo & Any` +reveal_type(ordered_foo()) # revealed: @Todo(Type::Intersection.call) +# TODO: should be `Any` +reveal_type(ordered_foo() < ordered_foo()) # revealed: @Todo(Type::Intersection.call) ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3a9d9fd480e02..427fd7d113ee3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6195,7 +6195,7 @@ impl<'db> Type<'db> { ), Type::Intersection(_) => { - Binding::single(self, Signature::todo("Type::Intersection.call()")).into() + Binding::single(self, Signature::todo("Type::Intersection.call")).into() } Type::DataclassDecorator(_) => { @@ -6204,10 +6204,13 @@ impl<'db> Type<'db> { let context = GenericContext::from_typevar_instances(db, [typevar]); let parameters = [Parameter::positional_only(Some(Name::new_static("cls"))) .with_annotated_type(typevar_meta)]; + // Intersect with `Any` for the return type to reflect the fact that the `dataclass()` + // decorator adds methods to the class + let returns = IntersectionType::from_elements(db, [typevar_meta, Type::any()]); let signature = Signature::new_generic( Some(context), Parameters::new(db, parameters), - Some(typevar_meta), + Some(returns), ); Binding::single(self, signature).into() }