From c5b8d551dff5e0ec8db776c6cb5951bbd4a03d57 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 3 Dec 2025 08:05:25 +0000 Subject: [PATCH] [ty] Suppress false positives when `dataclasses.dataclass(...)(cls)` is called imperatively (#21729) Fixes https://github.com/astral-sh/ty/issues/1705 --- .../mdtest/dataclasses/dataclasses.md | 54 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 20 +++++-- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index ba5151fa61..fa6f76de75 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -1462,3 +1462,57 @@ 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] & Any + 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 & Any + 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 + +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 e64d3802d5..2cafdcfcdd 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6226,11 +6226,25 @@ impl<'db> Type<'db> { ), Type::Intersection(_) => { - Binding::single(self, Signature::todo("Type::Intersection.call()")).into() + 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)]; + // 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(returns), + ); + Binding::single(self, signature).into() + } // TODO: some `SpecialForm`s are callable (e.g. TypedDicts) Type::SpecialForm(_) => CallableBinding::not_callable(self).into(),