diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 1f71887712f1f..73b3032ed86bf 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -3069,11 +3069,11 @@ Added in 0.0.12
+reveal_mro(Point2) # revealed: (, )
+```
+
+## Fields with defaults
+
+The third element of a 3-tuple specifies a default value:
+
+```py
+from dataclasses import make_dataclass
+
+PointWithDefault = make_dataclass("PointWithDefault", [("x", int), ("y", int, 0)])
+
+# error: [missing-argument] "No argument provided for required parameter `x`"
+PointWithDefault()
+
+# Good - y has a default
+p1 = PointWithDefault(1)
+p2 = PointWithDefault(1, 2)
+
+reveal_type(PointWithDefault.__init__) # revealed: (self: PointWithDefault, x: int, y: int = 0) -> None
+```
+
+## Fields with `field()` defaults
+
+Using `dataclasses.field()` as the third element of a 3-tuple:
+
+```py
+from dataclasses import make_dataclass, field
+
+PointWithField = make_dataclass(
+ "PointWithField",
+ [
+ ("x", int),
+ ("y", int, field(default=0)),
+ ("z", list, field(default_factory=list)),
+ ],
+)
+
+# error: [missing-argument] "No argument provided for required parameter `x`"
+PointWithField()
+
+# Good - y and z have defaults
+p1 = PointWithField(1)
+p2 = PointWithField(1, 2)
+p3 = PointWithField(1, 2, [3])
+
+reveal_type(p1.x) # revealed: int
+reveal_type(p1.y) # revealed: int
+reveal_type(p1.z) # revealed: list[Unknown]
+```
+
+## Fields with `init=False` via `field()`
+
+```py
+from dataclasses import make_dataclass, field
+
+PointPartialInit = make_dataclass(
+ "PointPartialInit",
+ [
+ ("x", int),
+ ("y", int, field(init=False, default=0)),
+ ],
+)
+
+# Only x is in __init__
+p = PointPartialInit(1)
+reveal_type(p.x) # revealed: int
+reveal_type(p.y) # revealed: int
+
+# error: [unknown-argument] "Argument `y` does not match any known parameter"
+PointPartialInit(1, y=2)
+```
+
+## Fields with `kw_only=True` via `field()`
+
+```py
+from dataclasses import make_dataclass, field
+
+PointKwOnlyField = make_dataclass(
+ "PointKwOnlyField",
+ [
+ ("x", int),
+ ("y", int, field(kw_only=True)),
+ ],
+)
+
+# x is positional, y is keyword-only
+p1 = PointKwOnlyField(1, y=2)
+reveal_type(p1.x) # revealed: int
+reveal_type(p1.y) # revealed: int
+
+# error: [missing-argument] "No argument provided for required parameter `y`"
+# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2"
+PointKwOnlyField(1, 2)
+
+reveal_type(PointKwOnlyField.__init__) # revealed: (self: PointKwOnlyField, x: int, *, y: int) -> None
+```
+
+## Fields with `kw_only=False` overriding class-level `kw_only=True`
+
+Per-field `kw_only=False` overrides the class-level default:
+
+```py
+from dataclasses import make_dataclass, field
+
+MixedKwOnly = make_dataclass(
+ "MixedKwOnly",
+ [
+ ("x", int, field(kw_only=False)), # Override: positional
+ ("y", int), # Uses class default: keyword-only
+ ],
+ kw_only=True, # Default all fields to keyword-only
+)
+
+# x is positional (overridden), y is keyword-only (class default)
+p1 = MixedKwOnly(1, y=2)
+reveal_type(p1.x) # revealed: int
+reveal_type(p1.y) # revealed: int
+
+reveal_type(MixedKwOnly.__init__) # revealed: (self: MixedKwOnly, x: int, *, y: int) -> None
+```
+
+## Fields with combined `field()` options
+
+```py
+from dataclasses import make_dataclass, field
+
+ComplexFields = make_dataclass(
+ "ComplexFields",
+ [
+ ("required", int),
+ ("with_default", int, field(default=10)),
+ ("with_factory", list, field(default_factory=list)),
+ ("kw_with_default", str, field(kw_only=True, default="hello")),
+ ],
+)
+
+# Only 'required' is required; others have defaults
+c1 = ComplexFields(1)
+c2 = ComplexFields(1, 20)
+c3 = ComplexFields(1, 20, [1, 2, 3])
+c4 = ComplexFields(1, kw_with_default="world")
+
+reveal_type(c1.required) # revealed: int
+reveal_type(c1.with_default) # revealed: int
+reveal_type(c1.with_factory) # revealed: list[Unknown]
+reveal_type(c1.kw_with_default) # revealed: str
+
+# fmt: off
+reveal_type(ComplexFields.__init__) # revealed: (self: ComplexFields, required: int, with_default: int = 10, with_factory: list[Unknown] = ..., *, kw_with_default: str = "hello") -> None
+```
+
+## Dataclass methods
+
+### `__init__`
+
+```py
+from dataclasses import make_dataclass
+
+Point3 = make_dataclass("Point3", [("x", int), ("y", int)])
+
+# Good
+p1 = Point3(1, 2)
+p2 = Point3(x=1, y=2)
+
+# error: [missing-argument]
+p3 = Point3(1)
+
+# error: [missing-argument]
+p4 = Point3()
+```
+
+## Dataclass parameters
+
+### `init=False`
+
+```py
+from dataclasses import make_dataclass
+
+PointNoInit = make_dataclass("PointNoInit", [("x", int), ("y", int)], init=False)
+
+# error: [too-many-positional-arguments]
+p = PointNoInit(1, 2)
+```
+
+### `repr=False`
+
+```py
+from dataclasses import make_dataclass
+
+PointNoRepr = make_dataclass("PointNoRepr", [("x", int), ("y", int)], repr=False)
+
+p = PointNoRepr(1, 2)
+reveal_type(p.x) # revealed: int
+reveal_type(p.y) # revealed: int
+
+# The class is still created and usable, repr=False just affects __repr__
+reveal_type(p) # revealed: PointNoRepr
+```
+
+### `eq=False`
+
+```py
+from dataclasses import make_dataclass
+
+PointNoEq = make_dataclass("PointNoEq", [("x", int), ("y", int)], eq=False)
+
+p1 = PointNoEq(1, 2)
+p2 = PointNoEq(1, 2)
+
+# Falls back to object.__eq__
+reveal_type(p1 == p2) # revealed: bool
+```
+
+### `order=True`
+
+```py
+from dataclasses import make_dataclass
+
+PointOrder = make_dataclass("PointOrder", [("x", int), ("y", int)], order=True)
+
+p1 = PointOrder(1, 2)
+p2 = PointOrder(3, 4)
+
+reveal_type(p1 < p2) # revealed: bool
+reveal_type(p1 <= p2) # revealed: bool
+reveal_type(p1 > p2) # revealed: bool
+reveal_type(p1 >= p2) # revealed: bool
+```
+
+### `total_ordering` with `order=True`
+
+Using `total_ordering` on a dataclass with `order=True` is redundant since the comparison methods
+are already synthesized. However, this doesn't cause an error:
+
+```py
+from dataclasses import make_dataclass
+from functools import total_ordering
+
+# No error - but this is redundant since order=True already provides comparison methods
+PointOrdered = total_ordering(make_dataclass("PointOrdered", [("x", int)], order=True))
+
+p1 = PointOrdered(1)
+p2 = PointOrdered(2)
+reveal_type(p1 < p2) # revealed: bool
+```
+
+### `total_ordering` without `order=True`
+
+Using `total_ordering` on a dataclass without `order=True` requires at least one ordering method to
+be defined. Since `make_dataclass` with `order=False` doesn't synthesize any comparison methods,
+this results in an error:
+
+```py
+from dataclasses import make_dataclass
+from functools import total_ordering
+
+# error: [invalid-total-ordering] "`@functools.total_ordering` requires at least one ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`) to be defined: `PointNoOrder` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`"
+PointNoOrder = total_ordering(make_dataclass("PointNoOrder", [("x", int)], order=False))
+```
+
+### `frozen=True`
+
+```py
+from dataclasses import make_dataclass
+
+PointFrozen = make_dataclass("PointFrozen", [("x", int), ("y", int)], frozen=True)
+
+p = PointFrozen(1, 2)
+
+# frozen dataclasses generate __hash__
+reveal_type(hash(p)) # revealed: int
+
+# frozen dataclasses are immutable
+p.x = 42 # error: [invalid-assignment]
+p.y = 56 # error: [invalid-assignment]
+```
+
+### `unsafe_hash=True`
+
+```py
+from dataclasses import make_dataclass
+
+PointUnsafeHash = make_dataclass("PointUnsafeHash", [("x", int), ("y", int)], unsafe_hash=True)
+
+p = PointUnsafeHash(1, 2)
+
+# unsafe_hash=True generates __hash__ even without frozen=True
+reveal_type(hash(p)) # revealed: int
+```
+
+### `eq=True` without `frozen=True` sets `__hash__` to `None`
+
+```py
+from dataclasses import make_dataclass
+
+# By default, eq=True and frozen=False, which sets __hash__ to None
+PointDefaultHash = make_dataclass("PointDefaultHash", [("x", int)])
+
+p = PointDefaultHash(1)
+
+# __hash__ is None, so hash() fails at runtime
+reveal_type(PointDefaultHash.__hash__) # revealed: None
+```
+
+### `kw_only=True`
+
+```py
+from dataclasses import make_dataclass
+
+PointKwOnly = make_dataclass("PointKwOnly", [("x", int), ("y", int)], kw_only=True)
+
+# Good
+p1 = PointKwOnly(x=1, y=2)
+
+# error: [missing-argument] "No arguments provided for required parameters `x`, `y`"
+# error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 2"
+p2 = PointKwOnly(1, 2)
+```
+
+### `match_args=True` (default)
+
+```py
+from dataclasses import make_dataclass
+
+Point5 = make_dataclass("Point5", [("x", int), ("y", int)])
+
+reveal_type(Point5.__match_args__) # revealed: tuple[Literal["x"], Literal["y"]]
+```
+
+### `match_args=False`
+
+```py
+from dataclasses import make_dataclass
+
+PointNoMatchArgs = make_dataclass("PointNoMatchArgs", [("x", int), ("y", int)], match_args=False)
+
+# error: [unresolved-attribute] "Class `PointNoMatchArgs` has no attribute `__match_args__`"
+reveal_type(PointNoMatchArgs.__match_args__) # revealed: Unknown
+```
+
+### `slots=True`
+
+Functional dataclasses with `slots=True` and non-empty fields are understood as disjoint bases,
+causing an `instance-layout-conflict` error when combined with other slotted classes:
+
+```py
+from dataclasses import make_dataclass
+
+PointSlots = make_dataclass("PointSlots", [("x", int), ("y", int)], slots=True)
+
+p = PointSlots(1, 2)
+reveal_type(p.x) # revealed: int
+reveal_type(p.y) # revealed: int
+
+# Combining two slotted classes with non-empty __slots__ causes a layout conflict
+OtherSlots = make_dataclass("OtherSlots", [("z", int)], slots=True)
+
+class Combined(PointSlots, OtherSlots): ... # error: [instance-layout-conflict]
+
+# Empty slots are fine
+EmptySlots = make_dataclass("EmptySlots", [], slots=True)
+
+class CombinedWithEmpty(PointSlots, EmptySlots): ... # No error
+```
+
+### `weakref_slot=True`
+
+The `weakref_slot` parameter (Python 3.11+) adds a `__weakref__` slot when combined with
+`slots=True`:
+
+```toml
+[environment]
+python-version = "3.11"
+```
+
+```py
+from dataclasses import make_dataclass
+import weakref
+
+PointWeakref = make_dataclass("PointWeakref", [("x", int)], slots=True, weakref_slot=True)
+
+p = PointWeakref(1)
+reveal_type(p.x) # revealed: int
+
+# __weakref__ attribute is available
+reveal_type(p.__weakref__) # revealed: Any | None
+```
+
+### Combining multiple flags
+
+Multiple flags can be combined:
+
+```py
+from dataclasses import make_dataclass
+
+# frozen=True enables hashing and order=True enables comparisons
+PointFrozenOrdered = make_dataclass(
+ "PointFrozenOrdered",
+ [("x", int), ("y", int)],
+ frozen=True,
+ order=True,
+)
+
+p1 = PointFrozenOrdered(1, 2)
+p2 = PointFrozenOrdered(3, 4)
+
+# frozen dataclasses are hashable
+reveal_type(hash(p1)) # revealed: int
+
+# order=True enables comparisons
+reveal_type(p1 < p2) # revealed: bool
+reveal_type(p1 <= p2) # revealed: bool
+reveal_type(p1 > p2) # revealed: bool
+reveal_type(p1 >= p2) # revealed: bool
+
+# frozen dataclasses are immutable
+p1.x = 42 # error: [invalid-assignment]
+```
+
+### `slots=True` with `frozen=True`
+
+```py
+from dataclasses import make_dataclass
+
+SlottedFrozen = make_dataclass(
+ "SlottedFrozen",
+ [("x", int)],
+ slots=True,
+ frozen=True,
+)
+
+p = SlottedFrozen(1)
+reveal_type(hash(p)) # revealed: int
+
+# Frozen, so immutable
+p.x = 42 # error: [invalid-assignment]
+```
+
+### `kw_only=True` with `frozen=True`
+
+```py
+from dataclasses import make_dataclass
+
+KwOnlyFrozen = make_dataclass(
+ "KwOnlyFrozen",
+ [("x", int), ("y", int)],
+ kw_only=True,
+ frozen=True,
+)
+
+# All arguments must be keyword-only
+p = KwOnlyFrozen(x=1, y=2)
+reveal_type(hash(p)) # revealed: int
+
+# error: [missing-argument] "No arguments provided for required parameters `x`, `y`"
+# error: [too-many-positional-arguments]
+KwOnlyFrozen(1, 2)
+```
+
+## `__dataclass_fields__`
+
+```py
+from dataclasses import make_dataclass, Field
+
+Point6 = make_dataclass("Point6", [("x", int), ("y", int)])
+
+reveal_type(Point6.__dataclass_fields__) # revealed: dict[str, Field[Any]]
+```
+
+## Base classes
+
+The `bases` keyword argument specifies base classes:
+
+```py
+from dataclasses import make_dataclass
+from ty_extensions import reveal_mro
+
+class Base:
+ def greet(self) -> str:
+ return "Hello"
+
+Derived = make_dataclass("Derived", [("value", int)], bases=(Base,))
+reveal_mro(Derived) # revealed: (, , )
+
+d = Derived(42)
+reveal_type(d) # revealed: Derived
+reveal_type(d.value) # revealed: int
+reveal_type(d.greet()) # revealed: str
+```
+
+## Dynamic fields (unknown fields)
+
+When the fields argument is dynamic (not a literal), we fall back to gradual typing.
+
+```py
+from dataclasses import make_dataclass
+from ty_extensions import reveal_mro
+
+def get_fields():
+ return [("x", int)]
+
+fields = get_fields()
+PointDynamic = make_dataclass("PointDynamic", fields)
+
+p = PointDynamic(1) # No error - accepts any arguments
+reveal_type(p.x) # revealed: Any
+
+# The class is still inferred as inheriting directly from `object`
+# (`Unknown` is not inserted into the MRO)
+reveal_mro(PointDynamic) # revealed: (, )
+
+# ...but nonetheless, we assume that all attributes are available,
+# similar to attribute access on `Unknown`
+reveal_type(p.unknown) # revealed: Any
+```
+
+## Starred arguments
+
+When `*args` or `**kwargs` are used, we can't statically determine the arguments, so we fall back to
+gradual typing.
+
+```py
+from dataclasses import make_dataclass
+
+args = ("Point", [("x", int)])
+PointStarred = make_dataclass(*args)
+
+p = PointStarred(1) # No error - accepts any arguments
+reveal_type(p.x) # revealed: Unknown
+
+kwargs = {"cls_name": "Point2", "fields": [("y", str)]}
+Point2 = make_dataclass(**kwargs)
+
+p2 = Point2("hello") # No error - accepts any arguments
+reveal_type(p2.y) # revealed: Unknown
+```
+
+## Argument validation
+
+### Too few positional arguments
+
+Both `cls_name` and `fields` are required:
+
+```py
+from dataclasses import make_dataclass
+
+# error: [missing-argument] "No argument provided for required parameter `fields` of function `make_dataclass`"
+Point = make_dataclass("Point")
+```
+
+### Too many positional arguments
+
+Only `cls_name` and `fields` are positional arguments:
+
+```py
+from dataclasses import make_dataclass
+
+# error: [too-many-positional-arguments] "Too many positional arguments to function `make_dataclass`: expected 2, got 3"
+Point = make_dataclass("Point", [("x", int)], (object,))
+```
+
+### Unknown keyword argument
+
+```py
+from dataclasses import make_dataclass
+
+# error: [unknown-argument] "Argument `unknown` does not match any known parameter of function `make_dataclass`"
+Point = make_dataclass("Point", [("x", int)], unknown=True)
+```
+
+### Invalid type for `cls_name`
+
+```py
+from dataclasses import make_dataclass
+
+# error: [invalid-argument-type] "Invalid argument to parameter `cls_name` of `make_dataclass()`"
+Point = make_dataclass(123, [("x", int)])
+```
+
+### Invalid type for boolean parameters
+
+```py
+from dataclasses import make_dataclass
+
+# error: [invalid-argument-type] "Invalid argument to parameter `init` of `make_dataclass()`"
+C1 = make_dataclass("C1", [("x", int)], init="yes")
+
+# error: [invalid-argument-type] "Invalid argument to parameter `repr` of `make_dataclass()`"
+C2 = make_dataclass("C2", [("x", int)], repr="no")
+
+# error: [invalid-argument-type] "Invalid argument to parameter `eq` of `make_dataclass()`"
+C3 = make_dataclass("C3", [("x", int)], eq=None)
+
+# error: [invalid-argument-type] "Invalid argument to parameter `order` of `make_dataclass()`"
+C4 = make_dataclass("C4", [("x", int)], order=1)
+
+# error: [invalid-argument-type] "Invalid argument to parameter `frozen` of `make_dataclass()`"
+C5 = make_dataclass("C5", [("x", int)], frozen="true")
+
+# error: [invalid-argument-type] "Invalid argument to parameter `kw_only` of `make_dataclass()`"
+C6 = make_dataclass("C6", [("x", int)], kw_only=[])
+
+# error: [invalid-argument-type] "Invalid argument to parameter `unsafe_hash` of `make_dataclass()`"
+C7 = make_dataclass("C7", [("x", int)], unsafe_hash="yes")
+
+# error: [invalid-argument-type] "Invalid argument to parameter `match_args` of `make_dataclass()`"
+C8 = make_dataclass("C8", [("x", int)], match_args=1)
+
+# error: [invalid-argument-type] "Invalid argument to parameter `slots` of `make_dataclass()`"
+C9 = make_dataclass("C9", [("x", int)], slots="yes")
+
+# error: [invalid-argument-type] "Invalid argument to parameter `weakref_slot` of `make_dataclass()`"
+C10 = make_dataclass("C10", [("x", int)], weakref_slot=None)
+```
+
+### Invalid type for `namespace`
+
+```py
+from dataclasses import make_dataclass
+
+# error: [invalid-argument-type] "Invalid argument to parameter `namespace` of `make_dataclass()`"
+Point = make_dataclass("Point", [("x", int)], namespace="invalid")
+```
+
+### Invalid type for `module`
+
+```py
+from dataclasses import make_dataclass
+
+# error: [invalid-argument-type] "Invalid argument to parameter `module` of `make_dataclass()`"
+Point = make_dataclass("Point", [("x", int)], module=123)
+```
+
+### Invalid type for `bases`
+
+At runtime, `make_dataclass` requires `bases` to be a tuple (not a list or other iterable).
+
+```py
+from dataclasses import make_dataclass
+
+# error: [invalid-argument-type] "Invalid argument to parameter `bases` of `make_dataclass()`: Expected `tuple`, found `Literal[12345]`"
+Point1 = make_dataclass("Point1", [("x", int)], bases=12345)
+
+# error: [invalid-argument-type] "Invalid argument to parameter `bases` of `make_dataclass()`: Expected `tuple`, found `list[Unknown | ]`"
+Point2 = make_dataclass("Point2", [("x", int)], bases=[object])
+```
+
+### Valid `namespace` and `module`
+
+```py
+from dataclasses import make_dataclass
+
+# These are all valid
+Point1 = make_dataclass("Point1", [("x", int)], namespace=None)
+Point2 = make_dataclass("Point2", [("x", int)], namespace={"custom_attr": 42})
+Point3 = make_dataclass("Point3", [("x", int)], module=None)
+Point4 = make_dataclass("Point4", [("x", int)], module="my_module")
+```
+
+## Invalid bases
+
+### TypedDict and Generic bases
+
+These special forms are not allowed as bases for classes created via `make_dataclass()`.
+
+```py
+from dataclasses import make_dataclass
+from typing import TypedDict, Generic
+
+# error: [invalid-base] "Invalid base for class created via `make_dataclass()`"
+A = make_dataclass("A", [("x", int)], bases=(TypedDict,))
+
+# error: [invalid-base] "Invalid base for class created via `make_dataclass()`"
+B = make_dataclass("B", [("x", int)], bases=(Generic,))
+```
+
+### Protocol base
+
+Protocol bases use a different lint (`unsupported-dynamic-base`) because they're technically valid
+Python but not supported by ty for dynamic classes.
+
+```py
+from dataclasses import make_dataclass
+from typing import Protocol
+
+# error: [unsupported-dynamic-base] "Unsupported base for class created via `make_dataclass()`"
+C = make_dataclass("C", [("x", int)], bases=(Protocol,))
+```
+
+### Final class base
+
+Cannot inherit from a `@final` class.
+
+```py
+from dataclasses import make_dataclass
+from typing import final
+
+@final
+class FinalClass:
+ pass
+
+# error: [subclass-of-final-class] "Class `D` cannot inherit from final class `FinalClass`"
+D = make_dataclass("D", [("x", int)], bases=(FinalClass,))
+```
+
+### Enum base
+
+Creating an enum class via `make_dataclass()` is not supported.
+
+```py
+from dataclasses import make_dataclass
+from enum import Enum
+
+# error: [invalid-base] "Invalid base for class created via `make_dataclass()`"
+E = make_dataclass("E", [("x", int)], bases=(Enum,))
+```
+
+## Deferred evaluation
+
+### String annotations (forward references)
+
+String annotations (forward references) are properly evaluated to types:
+
+```py
+from dataclasses import make_dataclass
+
+Point = make_dataclass("Point", [("x", "int"), ("y", "int")])
+p = Point(1, 2)
+
+reveal_type(p.x) # revealed: int
+reveal_type(p.y) # revealed: int
+```
+
+### Recursive references
+
+Recursive references in functional syntax are supported:
+
+```py
+from dataclasses import make_dataclass
+
+Node = make_dataclass("Node", [("value", int), ("next", "Node | None")])
+n = Node(1, None)
+
+reveal_type(n.value) # revealed: int
+reveal_type(n.next) # revealed: Node | None
+```
+
+### Mutually recursive types
+
+Mutually recursive types work correctly:
+
+```py
+from dataclasses import make_dataclass
+
+A = make_dataclass("A", [("x", "B | None")])
+B = make_dataclass("B", [("x", "C")])
+C = make_dataclass("C", [("x", A)])
+
+a = A(x=B(x=C(x=A(x=None))))
+
+reveal_type(a.x) # revealed: B | None
+
+if a.x is not None:
+ reveal_type(a.x) # revealed: B
+ reveal_type(a.x.x) # revealed: C
+ reveal_type(a.x.x.x) # revealed: A
+ reveal_type(a.x.x.x.x) # revealed: B | None
+
+A(x=42) # error: [invalid-argument-type]
+
+# error: [invalid-argument-type]
+# error: [missing-argument]
+A(x=C())
+
+# error: [invalid-argument-type]
+A(x=C(x=A(x=None)))
+```
+
+### Complex recursive type with generics
+
+String annotations work with generic types:
+
+```py
+from dataclasses import make_dataclass
+
+TreeNode = make_dataclass("TreeNode", [("value", int), ("children", "list[TreeNode]")])
+
+t = TreeNode(1, [])
+reveal_type(t.value) # revealed: int
+reveal_type(t.children) # revealed: list[TreeNode]
+```
+
+### make_dataclass as base class with forward references
+
+When `make_dataclass` is used as a base class for a static class, forward references to the outer
+class are resolved:
+
+```py
+from dataclasses import make_dataclass
+
+class X(make_dataclass("XBase", [("next", "X | None")])):
+ pass
+
+x = X(next=None)
+reveal_type(x.next) # revealed: X | None
+
+# Recursive construction works
+x2 = X(next=X(next=None))
+reveal_type(x2.next) # revealed: X | None
+```
+
+## Edge cases
+
+### Empty fields list
+
+A dataclass with no fields is valid:
+
+```py
+from dataclasses import make_dataclass
+
+Empty = make_dataclass("Empty", [])
+
+e = Empty()
+reveal_type(e) # revealed: Empty
+
+# No fields, so __init__ takes no arguments
+# error: [too-many-positional-arguments]
+Empty(1)
+```
+
+### Equality methods with `eq=True` (default)
+
+```py
+from dataclasses import make_dataclass
+
+PointEq = make_dataclass("PointEq", [("x", int), ("y", int)])
+
+p1 = PointEq(1, 2)
+p2 = PointEq(1, 2)
+p3 = PointEq(3, 4)
+
+# __eq__ is synthesized
+reveal_type(p1 == p2) # revealed: bool
+reveal_type(p1 == p3) # revealed: bool
+
+# __ne__ is also available (from object, but works correctly)
+reveal_type(p1 != p2) # revealed: bool
+```
+
+### `namespace` parameter
+
+The `namespace` parameter allows adding custom attributes/methods to the class.
+
+```py
+from dataclasses import make_dataclass
+
+def custom_method(self) -> str:
+ return f"Point({self.x}, {self.y})"
+
+PointWithMethod = make_dataclass(
+ "PointWithMethod",
+ [("x", int), ("y", int)],
+ namespace={"describe": custom_method, "version": 1},
+)
+
+p = PointWithMethod(1, 2)
+reveal_type(p.x) # revealed: int
+
+# TODO: We don't currently track namespace additions, so these are unresolved
+# error: [unresolved-attribute]
+p.describe()
+
+# error: [unresolved-attribute]
+reveal_type(p.version) # revealed: Unknown
+```
+
+### Single field
+
+```py
+from dataclasses import make_dataclass
+
+Single = make_dataclass("Single", [("value", int)])
+
+s = Single(42)
+reveal_type(s.value) # revealed: int
+reveal_type(Single.__init__) # revealed: (self: Single, value: int) -> None
+```
+
+### Many fields
+
+```py
+from dataclasses import make_dataclass
+
+ManyFields = make_dataclass(
+ "ManyFields",
+ [
+ ("a", int),
+ ("b", str),
+ ("c", float),
+ ("d", bool),
+ ("e", list),
+ ],
+)
+
+m = ManyFields(1, "hello", 3.14, True, [])
+reveal_type(m.a) # revealed: int
+reveal_type(m.b) # revealed: str
+reveal_type(m.c) # revealed: int | float
+reveal_type(m.d) # revealed: bool
+reveal_type(m.e) # revealed: list[Unknown]
+```
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index ac325eb3680b1..e80b5f22e9203 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -49,7 +49,7 @@ use crate::suppression::check_suppressions;
use crate::types::bound_super::BoundSuperType;
use crate::types::builder::RecursivelyDefined;
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
-use crate::types::class::NamedTupleSpec;
+use crate::types::class::{DataclassSpec, NamedTupleSpec};
pub(crate) use crate::types::class_base::ClassBase;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
@@ -5601,6 +5601,10 @@ impl<'db> Type<'db> {
invalid_expressions: smallvec_inline![InvalidTypeExpression::NamedTupleSpec],
fallback_type: Type::unknown(),
}),
+ KnownInstanceType::DataclassSpec(_) => Err(InvalidTypeExpressionError {
+ invalid_expressions: smallvec_inline![InvalidTypeExpression::DataclassSpec],
+ fallback_type: Type::unknown(),
+ }),
KnownInstanceType::UnionType(instance) => {
// Cloning here is cheap if the result is a `Type` (which is `Copy`). It's more
// expensive if there are errors.
@@ -6024,6 +6028,7 @@ impl<'db> Type<'db> {
KnownInstanceType::Literal(_) |
KnownInstanceType::LiteralStringAlias(_) |
KnownInstanceType::NamedTupleSpec(_) |
+ KnownInstanceType::DataclassSpec(_) |
KnownInstanceType::NewType(_) => {
// TODO: For some of these, we may need to apply the type mapping to inner types.
self
@@ -6432,6 +6437,7 @@ impl<'db> Type<'db> {
| KnownInstanceType::Literal(_)
| KnownInstanceType::LiteralStringAlias(_)
| KnownInstanceType::NamedTupleSpec(_)
+ | KnownInstanceType::DataclassSpec(_)
| KnownInstanceType::NewType(_) => {
// TODO: For some of these, we may need to try to find legacy typevars in inner types.
}
@@ -7102,6 +7108,9 @@ pub enum KnownInstanceType<'db> {
/// The inferred spec for a functional `NamedTuple` class.
NamedTupleSpec(NamedTupleSpec<'db>),
+
+ /// The inferred spec for a functional `make_dataclass` class.
+ DataclassSpec(DataclassSpec<'db>),
}
fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
@@ -7153,6 +7162,14 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
visitor.visit_type(db, field.ty);
}
}
+ KnownInstanceType::DataclassSpec(spec) => {
+ for field in spec.fields(db) {
+ visitor.visit_type(db, field.ty);
+ if let Some(default) = field.default_ty {
+ visitor.visit_type(db, default);
+ }
+ }
+ }
}
}
@@ -7194,6 +7211,7 @@ impl<'db> KnownInstanceType<'db> {
.map_base_class_type(db, |class_type| class_type.normalized_impl(db, visitor)),
),
Self::NamedTupleSpec(spec) => Self::NamedTupleSpec(spec.normalized_impl(db, visitor)),
+ Self::DataclassSpec(spec) => Self::DataclassSpec(spec.normalized_impl(db, visitor)),
Self::Deprecated(_)
| Self::ConstraintSet(_)
| Self::GenericContext(_)
@@ -7253,6 +7271,9 @@ impl<'db> KnownInstanceType<'db> {
Self::NamedTupleSpec(spec) => spec
.recursive_type_normalized_impl(db, div, true)
.map(Self::NamedTupleSpec),
+ Self::DataclassSpec(spec) => spec
+ .recursive_type_normalized_impl(db, div, true)
+ .map(Self::DataclassSpec),
}
}
@@ -7279,7 +7300,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Callable(_) => KnownClass::GenericAlias,
Self::LiteralStringAlias(_) => KnownClass::Str,
Self::NewType(_) => KnownClass::NewType,
- Self::NamedTupleSpec(_) => KnownClass::Sequence,
+ Self::NamedTupleSpec(_) | Self::DataclassSpec(_) => KnownClass::Sequence,
}
}
@@ -7553,6 +7574,8 @@ enum InvalidTypeExpression<'db> {
Specialization,
/// Same for `NamedTupleSpec`
NamedTupleSpec,
+ /// Same for `DataclassSpec`
+ DataclassSpec,
/// Same for `typing.TypedDict`
TypedDict,
/// Same for `typing.TypeAlias`, anywhere except for as the sole annotation on an annotated
@@ -7614,6 +7637,9 @@ impl<'db> InvalidTypeExpression<'db> {
InvalidTypeExpression::NamedTupleSpec => {
f.write_str("`NamedTupleSpec` is not allowed in type expressions")
}
+ InvalidTypeExpression::DataclassSpec => {
+ f.write_str("`DataclassSpec` is not allowed in type expressions")
+ }
InvalidTypeExpression::TypedDict => f.write_str(
"The special form `typing.TypedDict` \
is not allowed in type expressions",
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index 6266187d4c19d..8b305f2117c3b 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -197,6 +197,7 @@ impl<'db> CodeGeneratorKind<'db> {
}
ClassLiteral::Dynamic(dynamic_class) => Self::from_dynamic_class(db, dynamic_class),
ClassLiteral::DynamicNamedTuple(_) => Some(Self::NamedTuple),
+ ClassLiteral::DynamicDataclass(_) => Some(Self::DataclassLike(None)),
}
}
@@ -468,6 +469,8 @@ pub enum ClassLiteral<'db> {
Dynamic(DynamicClassLiteral<'db>),
/// A class created via `collections.namedtuple()` or `typing.NamedTuple()`.
DynamicNamedTuple(DynamicNamedTupleLiteral<'db>),
+ /// A class created via `dataclasses.make_dataclass()`.
+ DynamicDataclass(DynamicDataclassLiteral<'db>),
}
impl<'db> ClassLiteral<'db> {
@@ -477,6 +480,7 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => class.name(db),
Self::Dynamic(class) => class.name(db),
Self::DynamicNamedTuple(namedtuple) => namedtuple.name(db),
+ Self::DynamicDataclass(dataclass) => dataclass.name(db),
}
}
@@ -502,6 +506,7 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => class.metaclass(db),
Self::Dynamic(class) => class.metaclass(db),
Self::DynamicNamedTuple(namedtuple) => namedtuple.metaclass(db),
+ Self::DynamicDataclass(dataclass) => dataclass.metaclass(db),
}
}
@@ -516,6 +521,7 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => class.class_member(db, name, policy),
Self::Dynamic(class) => class.class_member(db, name, policy),
Self::DynamicNamedTuple(namedtuple) => namedtuple.class_member(db, name, policy),
+ Self::DynamicDataclass(dataclass) => dataclass.class_member(db, name, policy),
}
}
@@ -531,7 +537,7 @@ impl<'db> ClassLiteral<'db> {
) -> PlaceAndQualifiers<'db> {
match self {
Self::Static(class) => class.class_member_from_mro(db, name, policy, mro_iter),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => {
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => {
// Dynamic classes don't have inherited generic context and are never `object`.
let result = MroLookup::new(db, mro_iter).class_member(name, policy, None, false);
match result {
@@ -557,7 +563,9 @@ impl<'db> ClassLiteral<'db> {
pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
match self {
Self::Static(class) => class.default_specialization(db),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => ClassType::NonGeneric(self),
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => {
+ ClassType::NonGeneric(self)
+ }
}
}
@@ -565,7 +573,9 @@ impl<'db> ClassLiteral<'db> {
pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
match self {
Self::Static(class) => class.identity_specialization(db),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => ClassType::NonGeneric(self),
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => {
+ ClassType::NonGeneric(self)
+ }
}
}
@@ -583,7 +593,7 @@ impl<'db> ClassLiteral<'db> {
pub fn is_typed_dict(self, db: &'db dyn Db) -> bool {
match self {
Self::Static(class) => class.is_typed_dict(db),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => false,
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => false,
}
}
@@ -591,7 +601,7 @@ impl<'db> ClassLiteral<'db> {
pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool {
match self {
Self::Static(class) => class.is_tuple(db),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => false,
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => false,
}
}
@@ -614,6 +624,7 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => class.file(db),
Self::Dynamic(class) => class.scope(db).file(db),
Self::DynamicNamedTuple(class) => class.scope(db).file(db),
+ Self::DynamicDataclass(class) => class.scope(db).file(db),
}
}
@@ -626,6 +637,7 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => class.header_range(db),
Self::Dynamic(class) => class.header_range(db),
Self::DynamicNamedTuple(class) => class.header_range(db),
+ Self::DynamicDataclass(class) => class.header_range(db),
}
}
@@ -640,8 +652,7 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => class.is_final(db),
// Dynamic classes created via `type()`, `collections.namedtuple()`, etc. cannot be
// marked as final.
- Self::Dynamic(_) => false,
- Self::DynamicNamedTuple(_) => false,
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => false,
}
}
@@ -659,6 +670,10 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => class.has_own_ordering_method(db),
Self::Dynamic(class) => class.has_own_ordering_method(db),
Self::DynamicNamedTuple(_) => false,
+ Self::DynamicDataclass(dataclass) => dataclass
+ .dataclass_params(db)
+ .flags(db)
+ .contains(DataclassFlags::ORDER),
}
}
@@ -666,7 +681,23 @@ impl<'db> ClassLiteral<'db> {
pub(crate) fn as_static(self) -> Option> {
match self {
Self::Static(class) => Some(class),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => None,
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => None,
+ }
+ }
+
+ /// Returns the dynamic dataclass definition if this is one.
+ pub(crate) fn as_dynamic_dataclass(self) -> Option> {
+ match self {
+ Self::DynamicDataclass(dataclass) => Some(dataclass),
+ Self::Static(_) | Self::Dynamic(_) | Self::DynamicNamedTuple(_) => None,
+ }
+ }
+
+ /// Returns the dynamic class definition (from `type()` call) if this is one.
+ pub(crate) fn as_dynamic(self) -> Option> {
+ match self {
+ Self::Dynamic(class) => Some(class),
+ Self::Static(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => None,
}
}
@@ -676,6 +707,7 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => Some(class.definition(db)),
Self::Dynamic(class) => class.definition(db),
Self::DynamicNamedTuple(namedtuple) => namedtuple.definition(db),
+ Self::DynamicDataclass(dataclass) => dataclass.definition(db),
}
}
@@ -684,13 +716,14 @@ impl<'db> ClassLiteral<'db> {
/// For static classes, returns `TypeDefinition::StaticClass`.
/// For dynamic classes, returns `TypeDefinition::DynamicClass` if a definition is available.
pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option> {
- match self {
- Self::Static(class) => Some(TypeDefinition::StaticClass(class.definition(db))),
- Self::Dynamic(class) => class.definition(db).map(TypeDefinition::DynamicClass),
- Self::DynamicNamedTuple(namedtuple) => {
- namedtuple.definition(db).map(TypeDefinition::DynamicClass)
- }
- }
+ let definition = match self {
+ Self::Dynamic(class) => class.definition(db),
+ Self::DynamicNamedTuple(namedtuple) => namedtuple.definition(db),
+ Self::DynamicDataclass(dataclass) => dataclass.definition(db),
+ Self::Static(class) => return Some(TypeDefinition::StaticClass(class.definition(db))),
+ };
+
+ definition.map(TypeDefinition::DynamicClass)
}
/// Returns the qualified name of this class.
@@ -707,6 +740,7 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => class.header_span(db),
Self::Dynamic(class) => class.header_span(db),
Self::DynamicNamedTuple(namedtuple) => namedtuple.header_span(db),
+ Self::DynamicDataclass(dataclass) => dataclass.header_span(db),
}
}
@@ -734,6 +768,22 @@ impl<'db> ClassLiteral<'db> {
// Dynamic namedtuples define `__slots__ = ()`, but `__slots__` must be
// non-empty for a class to be a disjoint base.
Self::DynamicNamedTuple(_) => None,
+ // Dynamic dataclasses can define `__slots__` if `slots=True`.
+ // However, empty slots (dataclass with no fields) are not disjoint bases.
+ Self::DynamicDataclass(dataclass) => {
+ let has_slots = dataclass
+ .dataclass_params(db)
+ .flags(db)
+ .contains(DataclassFlags::SLOTS);
+ let has_fields = !dataclass.fields(db).is_empty();
+ if has_slots && has_fields {
+ Some(DisjointBase::due_to_dunder_slots(
+ ClassLiteral::DynamicDataclass(dataclass),
+ ))
+ } else {
+ None
+ }
+ }
}
}
@@ -741,7 +791,7 @@ impl<'db> ClassLiteral<'db> {
pub(crate) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> {
match self {
Self::Static(class) => class.to_non_generic_instance(db),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => {
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => {
Type::instance(db, ClassType::NonGeneric(self))
}
}
@@ -764,7 +814,9 @@ impl<'db> ClassLiteral<'db> {
) -> ClassType<'db> {
match self {
Self::Static(class) => class.apply_specialization(db, f),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => ClassType::NonGeneric(self),
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => {
+ ClassType::NonGeneric(self)
+ }
}
}
@@ -779,6 +831,7 @@ impl<'db> ClassLiteral<'db> {
Self::Static(class) => class.instance_member(db, specialization, name),
Self::Dynamic(class) => class.instance_member(db, name),
Self::DynamicNamedTuple(namedtuple) => namedtuple.instance_member(db, name),
+ Self::DynamicDataclass(dataclass) => dataclass.instance_member(db, name),
}
}
@@ -786,7 +839,9 @@ impl<'db> ClassLiteral<'db> {
pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> {
match self {
Self::Static(class) => class.top_materialization(db),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => ClassType::NonGeneric(self),
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => {
+ ClassType::NonGeneric(self)
+ }
}
}
@@ -800,7 +855,9 @@ impl<'db> ClassLiteral<'db> {
) -> PlaceAndQualifiers<'db> {
match self {
Self::Static(class) => class.typed_dict_member(db, specialization, name, policy),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => Place::Undefined.into(),
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => {
+ Place::Undefined.into()
+ }
}
}
@@ -815,7 +872,7 @@ impl<'db> ClassLiteral<'db> {
Self::Dynamic(class) => {
Self::Dynamic(class.with_dataclass_params(db, dataclass_params))
}
- Self::DynamicNamedTuple(_) => self,
+ Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => self,
}
}
}
@@ -838,6 +895,12 @@ impl<'db> From> for ClassLiteral<'db> {
}
}
+impl<'db> From> for ClassLiteral<'db> {
+ fn from(literal: DynamicDataclassLiteral<'db>) -> Self {
+ ClassLiteral::DynamicDataclass(literal)
+ }
+}
+
/// Represents a class type, which might be a non-generic class, or a specialization of a generic
/// class.
#[derive(
@@ -930,7 +993,11 @@ impl<'db> ClassType<'db> {
) -> Option<(StaticClassLiteral<'db>, Option>)> {
match self {
Self::NonGeneric(ClassLiteral::Static(class)) => Some((class, None)),
- Self::NonGeneric(ClassLiteral::Dynamic(_) | ClassLiteral::DynamicNamedTuple(_)) => None,
+ Self::NonGeneric(
+ ClassLiteral::Dynamic(_)
+ | ClassLiteral::DynamicNamedTuple(_)
+ | ClassLiteral::DynamicDataclass(_),
+ ) => None,
Self::Generic(generic) => Some((generic.origin(db), Some(generic.specialization(db)))),
}
}
@@ -944,7 +1011,11 @@ impl<'db> ClassType<'db> {
) -> Option<(StaticClassLiteral<'db>, Option>)> {
match self {
Self::NonGeneric(ClassLiteral::Static(class)) => Some((class, None)),
- Self::NonGeneric(ClassLiteral::Dynamic(_) | ClassLiteral::DynamicNamedTuple(_)) => None,
+ Self::NonGeneric(
+ ClassLiteral::Dynamic(_)
+ | ClassLiteral::DynamicNamedTuple(_)
+ | ClassLiteral::DynamicDataclass(_),
+ ) => None,
Self::Generic(generic) => Some((
generic.origin(db),
Some(
@@ -1461,6 +1532,9 @@ impl<'db> ClassType<'db> {
Self::NonGeneric(ClassLiteral::DynamicNamedTuple(namedtuple)) => {
return namedtuple.own_class_member(db, name);
}
+ Self::NonGeneric(ClassLiteral::DynamicDataclass(dataclass)) => {
+ return dataclass.own_class_member(db, name);
+ }
Self::NonGeneric(ClassLiteral::Static(class)) => (class, None),
Self::Generic(generic) => (generic.origin(db), Some(generic.specialization(db))),
};
@@ -1754,6 +1828,9 @@ impl<'db> ClassType<'db> {
Self::NonGeneric(ClassLiteral::DynamicNamedTuple(namedtuple)) => {
namedtuple.instance_member(db, name)
}
+ Self::NonGeneric(ClassLiteral::DynamicDataclass(dataclass)) => {
+ dataclass.instance_member(db, name)
+ }
Self::NonGeneric(ClassLiteral::Static(class)) => {
if class.is_typed_dict(db) {
return Place::Undefined.into();
@@ -1785,6 +1862,9 @@ impl<'db> ClassType<'db> {
Self::NonGeneric(ClassLiteral::DynamicNamedTuple(namedtuple)) => {
namedtuple.own_instance_member(db, name)
}
+ Self::NonGeneric(ClassLiteral::DynamicDataclass(dataclass)) => {
+ dataclass.own_instance_member(db, name)
+ }
Self::NonGeneric(ClassLiteral::Static(class_literal)) => {
class_literal.own_instance_member(db, name)
}
@@ -2090,9 +2170,11 @@ impl<'db> VarianceInferable<'db> for ClassType<'db> {
fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
match self {
Self::NonGeneric(ClassLiteral::Static(class)) => class.variance_of(db, typevar),
- Self::NonGeneric(ClassLiteral::Dynamic(_) | ClassLiteral::DynamicNamedTuple(_)) => {
- TypeVarVariance::Bivariant
- }
+ Self::NonGeneric(
+ ClassLiteral::Dynamic(_)
+ | ClassLiteral::DynamicNamedTuple(_)
+ | ClassLiteral::DynamicDataclass(_),
+ ) => TypeVarVariance::Bivariant,
Self::Generic(generic) => generic.variance_of(db, typevar),
}
}
@@ -2346,6 +2428,13 @@ impl<'db> StaticClassLiteral<'db> {
}
// Dynamic namedtuples don't define their own ordering methods.
ClassLiteral::DynamicNamedTuple(_) => {}
+ // Dynamic dataclasses can have ordering methods if order=True.
+ ClassLiteral::DynamicDataclass(dataclass) => {
+ let member = dataclass.own_class_member(db, name);
+ if let Some(ty) = member.ignore_possibly_undefined() {
+ return Some(ty);
+ }
+ }
}
}
}
@@ -2802,7 +2891,7 @@ impl<'db> StaticClassLiteral<'db> {
.filter_map(ClassBase::into_class)
.any(|base| match base.class_literal(db) {
ClassLiteral::DynamicNamedTuple(_) => true,
- ClassLiteral::Dynamic(_) => false,
+ ClassLiteral::Dynamic(_) | ClassLiteral::DynamicDataclass(_) => false,
ClassLiteral::Static(class) => class
.explicit_bases(db)
.contains(&Type::SpecialForm(SpecialFormType::NamedTuple)),
@@ -2870,6 +2959,17 @@ impl<'db> StaticClassLiteral<'db> {
(dataclass_params, transformer_params)
}
+ /// Returns the combined dataclass flags from both `dataclass_params` and `transformer_params`.
+ fn combined_dataclass_flags(
+ self,
+ db: &'db dyn Db,
+ field_policy: CodeGeneratorKind<'db>,
+ ) -> DataclassFlags {
+ let (dataclass_params, transformer_params) = self.merged_dataclass_params(db, field_policy);
+ dataclass_params.map_or(DataclassFlags::empty(), |p| p.flags(db))
+ | transformer_params.map_or(DataclassFlags::empty(), |p| p.flags(db))
+ }
+
/// Checks if the given dataclass parameter flag is set for this class.
/// This checks both the `dataclass_params` and `transformer_params`.
fn has_dataclass_param(
@@ -2878,9 +2978,8 @@ impl<'db> StaticClassLiteral<'db> {
field_policy: CodeGeneratorKind<'db>,
param: DataclassFlags,
) -> bool {
- let (dataclass_params, transformer_params) = self.merged_dataclass_params(db, field_policy);
- dataclass_params.is_some_and(|params| params.flags(db).contains(param))
- || transformer_params.is_some_and(|params| params.flags(db).contains(param))
+ self.combined_dataclass_flags(db, field_policy)
+ .contains(param)
}
/// Return the explicit `metaclass` of this class, if one is defined.
@@ -3281,6 +3380,8 @@ impl<'db> StaticClassLiteral<'db> {
let field_policy = CodeGeneratorKind::from_class(db, self.into(), specialization)?;
+ let has_dataclass_param = |param| self.has_dataclass_param(db, field_policy, param);
+
let instance_ty =
Type::instance(db, self.apply_optional_specialization(db, specialization));
@@ -3457,74 +3558,40 @@ impl<'db> StaticClassLiteral<'db> {
specialization.map(|s| s.generic_context(db)),
)
}
- (CodeGeneratorKind::DataclassLike(_), "__lt__" | "__le__" | "__gt__" | "__ge__") => {
- if !self.has_dataclass_param(db, field_policy, DataclassFlags::ORDER) {
- return None;
- }
-
- let signature = Signature::new(
- Parameters::new(
- db,
- [
- Parameter::positional_or_keyword(Name::new_static("self"))
- // TODO: could be `Self`.
- .with_annotated_type(instance_ty),
- Parameter::positional_or_keyword(Name::new_static("other"))
- // TODO: could be `Self`.
- .with_annotated_type(instance_ty),
- ],
- ),
- KnownClass::Bool.to_instance(db),
- );
-
- Some(Type::function_like_callable(db, signature))
- }
- (CodeGeneratorKind::DataclassLike(_), "__hash__") => {
- let unsafe_hash =
- self.has_dataclass_param(db, field_policy, DataclassFlags::UNSAFE_HASH);
- let frozen = self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN);
- let eq = self.has_dataclass_param(db, field_policy, DataclassFlags::EQ);
-
- if unsafe_hash || (frozen && eq) {
- let signature = Signature::new(
- Parameters::new(
- db,
- [Parameter::positional_or_keyword(Name::new_static("self"))
- .with_annotated_type(instance_ty)],
- ),
- KnownClass::Int.to_instance(db),
- );
-
- Some(Type::function_like_callable(db, signature))
- } else if eq && !frozen {
- Some(Type::none(db))
- } else {
- // No `__hash__` is generated, fall back to `object.__hash__`
- None
- }
- }
+ (
+ CodeGeneratorKind::DataclassLike(_),
+ "__lt__" | "__le__" | "__gt__" | "__ge__" | "__hash__",
+ ) => synthesize_dataclass_dunder_method(
+ db,
+ name,
+ instance_ty,
+ self.combined_dataclass_flags(db, field_policy),
+ ),
(CodeGeneratorKind::DataclassLike(_), "__match_args__")
if Program::get(db).python_version(db) >= PythonVersion::PY310 =>
{
- if !self.has_dataclass_param(db, field_policy, DataclassFlags::MATCH_ARGS) {
- return None;
- }
-
- let kw_only_default =
- self.has_dataclass_param(db, field_policy, DataclassFlags::KW_ONLY);
-
+ let kw_only_default = has_dataclass_param(DataclassFlags::KW_ONLY);
let fields = self.fields(db, specialization, field_policy);
- let match_args = fields
- .iter()
- .filter(|(_, field)| {
- if let FieldKind::Dataclass { init, kw_only, .. } = &field.kind {
- *init && !kw_only.unwrap_or(kw_only_default)
- } else {
- false
- }
- })
- .map(|(name, _)| Type::string_literal(db, name));
- Some(Type::heterogeneous_tuple(db, match_args))
+ let fields_iter = fields.iter().filter_map(|(name, field)| {
+ if let FieldKind::Dataclass { init, kw_only, .. } = &field.kind {
+ Some(DataclassFieldInfo {
+ name: name.clone(),
+ ty: field.declared_ty,
+ default_ty: None,
+ init: *init,
+ kw_only: kw_only.unwrap_or(kw_only_default),
+ })
+ } else {
+ None
+ }
+ });
+ synthesize_dataclass_class_member(
+ db,
+ name,
+ instance_ty,
+ self.combined_dataclass_flags(db, field_policy),
+ fields_iter,
+ )
}
(CodeGeneratorKind::DataclassLike(_), "__weakref__")
if Program::get(db).python_version(db) >= PythonVersion::PY311 =>
@@ -3573,23 +3640,12 @@ impl<'db> StaticClassLiteral<'db> {
signature_from_fields(vec![self_parameter], instance_ty)
}
(CodeGeneratorKind::DataclassLike(_), "__setattr__") => {
- if self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN) {
- let signature = Signature::new(
- Parameters::new(
- db,
- [
- Parameter::positional_or_keyword(Name::new_static("self"))
- .with_annotated_type(instance_ty),
- Parameter::positional_or_keyword(Name::new_static("name")),
- Parameter::positional_or_keyword(Name::new_static("value")),
- ],
- ),
- Type::Never,
- );
-
- return Some(Type::function_like_callable(db, signature));
- }
- None
+ synthesize_dataclass_dunder_method(
+ db,
+ name,
+ instance_ty,
+ self.combined_dataclass_flags(db, field_policy),
+ )
}
(CodeGeneratorKind::DataclassLike(_), "__slots__")
if Program::get(db).python_version(db) >= PythonVersion::PY310 =>
@@ -4206,7 +4262,7 @@ impl<'db> StaticClassLiteral<'db> {
kw_only_sentinel_field_seen = true;
}
- // If no explicit kw_only setting and we've seen KW_ONLY sentinel, mark as keyword-only
+ // If no explicit `kw_only` setting and we've seen `KW_ONLY` sentinel, mark as keyword-only
if kw_only_sentinel_field_seen {
if let FieldKind::Dataclass {
kw_only: ref mut kw @ None,
@@ -4217,8 +4273,8 @@ impl<'db> StaticClassLiteral<'db> {
}
}
- // Resolve the kw_only to the class-level default. This ensures that when fields
- // are inherited by child classes, they use their defining class's kw_only default.
+ // Resolve the `kw_only` to the class-level default. This ensures that when fields
+ // are inherited by child classes, they use their defining class's `kw_only` default.
if let FieldKind::Dataclass {
kw_only: ref mut kw @ None,
..
@@ -4991,7 +5047,9 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
match self {
Self::Static(class) => class.variance_of(db, typevar),
- Self::Dynamic(_) | Self::DynamicNamedTuple(_) => TypeVarVariance::Bivariant,
+ Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicDataclass(_) => {
+ TypeVarVariance::Bivariant
+ }
}
}
}
@@ -5323,7 +5381,7 @@ impl<'db> DynamicClassLiteral<'db> {
/// an error (duplicate bases or C3 linearization failure).
#[salsa::tracked(returns(ref), heap_size = ruff_memory_usage::heap_size)]
pub(crate) fn try_mro(self, db: &'db dyn Db) -> Result, DynamicMroError<'db>> {
- Mro::of_dynamic_class(db, self)
+ Mro::of_dynamic(db, self.into())
}
/// Return `Some()` if this dynamic class is known to be a [`DisjointBase`].
@@ -5415,6 +5473,158 @@ fn create_field_property<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Type<'db>
Type::PropertyInstance(property)
}
+/// Field information for synthesizing dataclass methods.
+#[derive(Debug, Clone)]
+struct DataclassFieldInfo<'db> {
+ /// The field name (or alias if provided).
+ name: Name,
+ /// The declared type of the field.
+ ty: Type<'db>,
+ /// The default value type, if any.
+ default_ty: Option>,
+ /// Whether this field should be included in `__init__`.
+ init: bool,
+ /// Whether this field is keyword-only.
+ kw_only: bool,
+}
+
+/// Synthesize a dataclass class member given the dataclass flags, instance type, and fields.
+fn synthesize_dataclass_class_member<'db>(
+ db: &'db dyn Db,
+ name: &str,
+ instance_ty: Type<'db>,
+ flags: DataclassFlags,
+ fields: impl Iterator- >,
+) -> Option> {
+ match name {
+ "__init__" if flags.contains(DataclassFlags::INIT) => {
+ let mut parameters = vec![
+ Parameter::positional_or_keyword(Name::new_static("self"))
+ .with_annotated_type(instance_ty),
+ ];
+
+ for field in fields {
+ if !field.init {
+ continue;
+ }
+ let mut param = if field.kw_only {
+ Parameter::keyword_only(field.name)
+ } else {
+ Parameter::positional_or_keyword(field.name)
+ };
+ param = param.with_annotated_type(field.ty);
+ if let Some(default) = field.default_ty {
+ param = param.with_default_type(default);
+ }
+ parameters.push(param);
+ }
+
+ let signature = Signature::new(Parameters::new(db, parameters), Type::none(db));
+ Some(Type::function_like_callable(db, signature))
+ }
+ "__match_args__" if flags.contains(DataclassFlags::MATCH_ARGS) => {
+ // __match_args__ includes only fields that are in __init__ and not keyword-only
+ let match_args = fields
+ .filter(|field| field.init && !field.kw_only)
+ .map(|field| Type::string_literal(db, &field.name));
+ Some(Type::heterogeneous_tuple(db, match_args))
+ }
+ _ => synthesize_dataclass_dunder_method(db, name, instance_ty, flags),
+ }
+}
+
+/// Synthesize a dataclass dunder method that doesn't require field information.
+fn synthesize_dataclass_dunder_method<'db>(
+ db: &'db dyn Db,
+ name: &str,
+ instance_ty: Type<'db>,
+ flags: DataclassFlags,
+) -> Option> {
+ match name {
+ "__setattr__" if flags.contains(DataclassFlags::FROZEN) => {
+ // Frozen dataclasses have `__setattr__` that returns `Never` (immutable).
+ let signature = Signature::new(
+ Parameters::new(
+ db,
+ [
+ Parameter::positional_or_keyword(Name::new_static("self"))
+ .with_annotated_type(instance_ty),
+ Parameter::positional_or_keyword(Name::new_static("name")),
+ Parameter::positional_or_keyword(Name::new_static("value")),
+ ],
+ ),
+ Type::Never,
+ );
+ Some(Type::function_like_callable(db, signature))
+ }
+ "__lt__" | "__le__" | "__gt__" | "__ge__" if flags.contains(DataclassFlags::ORDER) => {
+ // Ordering methods: (self, other: Self) -> bool
+ let signature = Signature::new(
+ Parameters::new(
+ db,
+ [
+ Parameter::positional_or_keyword(Name::new_static("self"))
+ // TODO: could be `Self`.
+ .with_annotated_type(instance_ty),
+ Parameter::positional_or_keyword(Name::new_static("other"))
+ // TODO: could be `Self`.
+ .with_annotated_type(instance_ty),
+ ],
+ ),
+ KnownClass::Bool.to_instance(db),
+ );
+ Some(Type::function_like_callable(db, signature))
+ }
+ "__hash__" => {
+ let has_hash = flags.contains(DataclassFlags::UNSAFE_HASH)
+ || (flags.contains(DataclassFlags::FROZEN) && flags.contains(DataclassFlags::EQ));
+ if has_hash {
+ let signature = Signature::new(
+ Parameters::new(
+ db,
+ [Parameter::positional_or_keyword(Name::new_static("self"))
+ .with_annotated_type(instance_ty)],
+ ),
+ KnownClass::Int.to_instance(db),
+ );
+ Some(Type::function_like_callable(db, signature))
+ } else if flags.contains(DataclassFlags::EQ) && !flags.contains(DataclassFlags::FROZEN)
+ {
+ // eq=True without frozen=True sets __hash__ to None
+ Some(Type::none(db))
+ } else {
+ // No __hash__ is generated, fall back to object.__hash__
+ None
+ }
+ }
+ "__eq__" if flags.contains(DataclassFlags::EQ) => {
+ // __eq__(self, other: object) -> bool
+ let signature = Signature::new(
+ Parameters::new(
+ db,
+ [
+ Parameter::positional_or_keyword(Name::new_static("self"))
+ .with_annotated_type(instance_ty),
+ Parameter::positional_or_keyword(Name::new_static("other"))
+ .with_annotated_type(KnownClass::Object.to_instance(db)),
+ ],
+ ),
+ KnownClass::Bool.to_instance(db),
+ );
+ Some(Type::function_like_callable(db, signature))
+ }
+ "__dataclass_fields__" => {
+ // __dataclass_fields__: dict[str, Field[Any]]
+ let field_any = KnownClass::Field.to_specialized_instance(db, &[Type::any()]);
+ Some(
+ KnownClass::Dict
+ .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), field_any]),
+ )
+ }
+ _ => None,
+ }
+}
+
/// Synthesize a namedtuple class member given the field information.
///
/// This is used by both `DynamicNamedTupleLiteral` and `StaticClassLiteral` (for declarative
@@ -5988,6 +6198,514 @@ impl<'db> NamedTupleSpec<'db> {
impl get_size2::GetSize for NamedTupleSpec<'_> {}
+/// A single field in a dynamic `make_dataclass` class.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
+pub struct DataclassFieldSpec<'db> {
+ /// The field name.
+ pub name: Name,
+ /// The field type.
+ pub ty: Type<'db>,
+ /// The default value type, if any.
+ pub default_ty: Option>,
+ /// Whether this field should be included in `__init__`.
+ pub init: bool,
+ /// Whether this field is keyword-only.
+ pub kw_only: Option,
+ /// The alias name for this field (for use in `__init__`), if any.
+ pub alias: Option,
+}
+
+/// A specification describing the fields and bases of a dynamic `make_dataclass` class.
+///
+/// # Ordering
+///
+/// Ordering is based on the spec's salsa-assigned id and not on its values.
+/// The id may change between runs, or when the spec was garbage collected and recreated.
+#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)]
+#[derive(PartialOrd, Ord)]
+pub struct DataclassSpec<'db> {
+ /// The fields with their full metadata.
+ #[returns(deref)]
+ pub(crate) fields: Box<[DataclassFieldSpec<'db>]>,
+
+ /// Whether the fields are known statically.
+ pub(crate) has_known_fields: bool,
+
+ /// The base classes (from the `bases` keyword argument).
+ #[returns(deref)]
+ pub(crate) bases: Box<[ClassBase<'db>]>,
+}
+
+impl<'db> DataclassSpec<'db> {
+ /// Create a [`DataclassSpec`] with the given fields and bases.
+ pub(crate) fn known(
+ db: &'db dyn Db,
+ fields: Box<[DataclassFieldSpec<'db>]>,
+ bases: Box<[ClassBase<'db>]>,
+ ) -> Self {
+ Self::new(db, fields, true, bases)
+ }
+
+ /// Create a [`DataclassSpec`] that indicates a dataclass has unknown fields.
+ pub(crate) fn unknown(db: &'db dyn Db) -> Self {
+ Self::new(db, Box::default(), false, Box::default())
+ }
+
+ pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
+ let fields: Box<_> = self
+ .fields(db)
+ .iter()
+ .map(|field| DataclassFieldSpec {
+ name: field.name.clone(),
+ ty: field.ty.normalized_impl(db, visitor),
+ default_ty: field.default_ty.map(|d| d.normalized_impl(db, visitor)),
+ init: field.init,
+ kw_only: field.kw_only,
+ alias: field.alias.clone(),
+ })
+ .collect();
+
+ let bases: Box<_> = self
+ .bases(db)
+ .iter()
+ .map(|base| base.normalized_impl(db, visitor))
+ .collect();
+
+ Self::new(db, fields, self.has_known_fields(db), bases)
+ }
+
+ pub(crate) fn recursive_type_normalized_impl(
+ self,
+ db: &'db dyn Db,
+ div: Type<'db>,
+ nested: bool,
+ ) -> Option {
+ let fields = self
+ .fields(db)
+ .iter()
+ .map(|field| {
+ let normalized_ty = if nested {
+ field.ty.recursive_type_normalized_impl(db, div, nested)?
+ } else {
+ field
+ .ty
+ .recursive_type_normalized_impl(db, div, nested)
+ .unwrap_or(div)
+ };
+ let normalized_default = match field.default_ty {
+ Some(d) => Some(d.recursive_type_normalized_impl(db, div, nested)?),
+ None => None,
+ };
+ Some(DataclassFieldSpec {
+ name: field.name.clone(),
+ ty: normalized_ty,
+ default_ty: normalized_default,
+ init: field.init,
+ kw_only: field.kw_only,
+ alias: field.alias.clone(),
+ })
+ })
+ .collect::