From ce2bdb93573309910d6c94ada70ade921eeea926 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 8 Jul 2025 16:16:50 +0200 Subject: [PATCH] [ty] Conditionally defined dataclass fields (#19197) ## Summary Fixes a bug where conditionally defined dataclass fields were previously ignored. Thanks to @lipefree for reporting this. ## Test Plan New Markdown tests --- .../mdtest/dataclasses/dataclasses.md | 61 +++++++++++++++++++ ...taclasses.KW_ONLY…_(dd1b8f2f71487f16).snap | 29 +++++++++ crates/ty_python_semantic/src/types/class.rs | 2 +- 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 77a6b22ca0..b685b37fe0 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -558,6 +558,50 @@ class C(Base): reveal_type(C.__init__) # revealed: (self: C, x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None ``` +## Conditionally defined fields + +### Statically known conditions + +Fields that are defined in always-reachable branches are always present in the synthesized +`__init__` method. Fields that are defined in never-reachable branches are not present: + +```py +from dataclasses import dataclass + +@dataclass +class C: + normal: int + + if 1 + 2 == 3: + always_present: str + + if 1 + 2 == 4: + never_present: bool + +reveal_type(C.__init__) # revealed: (self: C, normal: int, always_present: str) -> None +``` + +### Dynamic conditions + +If a field is conditionally defined, we currently assume that it is always present. A more complex +alternative here would be to synthesized a union of all possible `__init__` signatures: + +```py +from dataclasses import dataclass + +def flag() -> bool: + return True + +@dataclass +class C: + normal: int + + if flag(): + conditionally_present: str + +reveal_type(C.__init__) # revealed: (self: C, normal: int, conditionally_present: str) -> None +``` + ## Generic dataclasses ```toml @@ -789,6 +833,23 @@ class Fails: # error: [duplicate-kw-only] reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None ``` +This also works if `KW_ONLY` is used in a conditional branch: + +```py +def flag() -> bool: + return True + +@dataclass +class D: # error: [duplicate-kw-only] + x: int + _1: KW_ONLY + + if flag(): + y: str + _2: KW_ONLY + z: float +``` + ## Other special cases ### `dataclasses.dataclass` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_`dataclasses.KW_ONLY…_(dd1b8f2f71487f16).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_`dataclasses.KW_ONLY…_(dd1b8f2f71487f16).snap index 8d360e167a..00b925ed42 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_`dataclasses.KW_ONLY…_(dd1b8f2f71487f16).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_`dataclasses.KW_ONLY…_(dd1b8f2f71487f16).snap @@ -37,6 +37,18 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses. 23 | e: bytes 24 | 25 | reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None +26 | def flag() -> bool: +27 | return True +28 | +29 | @dataclass +30 | class D: # error: [duplicate-kw-only] +31 | x: int +32 | _1: KW_ONLY +33 | +34 | if flag(): +35 | y: str +36 | _2: KW_ONLY +37 | z: float ``` # Diagnostics @@ -109,6 +121,23 @@ info[revealed-type]: Revealed type 24 | 25 | reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None | ^^^^^^^^^^^^^^ `(self: Fails, a: int, *, c: str, e: bytes) -> None` +26 | def flag() -> bool: +27 | return True | ``` + +``` +error[duplicate-kw-only]: Dataclass has more than one field annotated with `KW_ONLY` + --> src/mdtest_snippet.py:30:7 + | +29 | @dataclass +30 | class D: # error: [duplicate-kw-only] + | ^ +31 | x: int +32 | _1: KW_ONLY + | +info: `KW_ONLY` fields: `_1`, `_2` +info: rule `duplicate-kw-only` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a1104beeed..c7ab016c85 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1650,7 +1650,7 @@ impl<'db> ClassLiteral<'db> { if !declarations .clone() .all(|DeclarationWithConstraint { declaration, .. }| { - declaration.is_defined_and(|declaration| { + declaration.is_undefined_or(|declaration| { matches!( declaration.kind(db), DefinitionKind::AnnotatedAssignment(..)