[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
This commit is contained in:
David Peter 2025-07-08 16:16:50 +02:00 committed by GitHub
parent d78d10dd94
commit ce2bdb9357
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 91 additions and 1 deletions

View file

@ -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`

View file

@ -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
```

View file

@ -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(..)