diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 814eeae5068d0..178fb687ba991 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -8,7 +8,7 @@
Default level: error ·
Added in 0.0.13 ·
Related issues ·
-View source
+View source
@@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method`
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol):
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -157,7 +157,7 @@ def test(): -> "int":
Default level: error ·
Preview (since 0.0.16) ·
Related issues ·
-View source
+View source
@@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.7 ·
Related issues ·
-View source
+View source
@@ -261,7 +261,7 @@ def f(x: object):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -293,7 +293,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -324,7 +324,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -356,7 +356,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -388,7 +388,7 @@ class B(A): ...
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -416,7 +416,7 @@ type B = A
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -448,7 +448,7 @@ class Example:
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -504,7 +504,7 @@ false positives it can produce.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -531,7 +531,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -569,7 +569,7 @@ class A: # Crash at runtime
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -640,7 +640,7 @@ def foo() -> "intt\b": ...
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -766,7 +766,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -796,7 +796,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -822,7 +822,7 @@ t[3] # IndexError: tuple index out of range
Default level: warn ·
Added in 0.0.1-alpha.33 ·
Related issues ·
-View source
+View source
@@ -856,7 +856,7 @@ class MyClass: ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -945,7 +945,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -972,7 +972,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1000,7 +1000,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1034,7 +1034,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1070,7 +1070,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1094,7 +1094,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1121,7 +1121,7 @@ with 1:
Default level: error ·
Added in 0.0.12 ·
Related issues ·
-View source
+View source
@@ -1158,7 +1158,7 @@ class Foo(NamedTuple):
Default level: error ·
Added in 0.0.13 ·
Related issues ·
-View source
+View source
@@ -1190,7 +1190,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1219,7 +1219,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1263,7 +1263,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -1305,7 +1305,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.35 ·
Related issues ·
-View source
+View source
@@ -1349,7 +1349,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1387,7 +1387,7 @@ class D(Generic[U, T]): ...
Default level: error ·
Added in 0.0.12 ·
Related issues ·
-View source
+View source
@@ -1466,7 +1466,7 @@ a = 20 / 0 # type: ignore
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -1505,7 +1505,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: warn ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -1566,7 +1566,7 @@ def f(x, y, /): # Python 3.8+ syntax
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1601,7 +1601,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.18 ·
Related issues ·
-View source
+View source
@@ -1629,7 +1629,7 @@ match x:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1663,7 +1663,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1770,7 +1770,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1824,7 +1824,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Added in 0.0.1-alpha.27 ·
Related issues ·
-View source
+View source
@@ -1854,7 +1854,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1904,7 +1904,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1930,7 +1930,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1961,7 +1961,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1995,7 +1995,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2044,7 +2044,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2073,7 +2073,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2169,7 +2169,7 @@ class C: ...
Default level: error ·
Added in 0.0.10 ·
Related issues ·
-View source
+View source
@@ -2215,7 +2215,7 @@ class MyClass:
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -2242,7 +2242,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -2289,7 +2289,7 @@ Bar[int] # error: too few arguments
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2319,7 +2319,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2349,7 +2349,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -2383,7 +2383,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -2417,7 +2417,7 @@ class C:
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -2448,7 +2448,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2495,7 +2495,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type
Default level: error ·
Added in 0.0.16 ·
Related issues ·
-View source
+View source
@@ -2527,7 +2527,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -2562,7 +2562,7 @@ def f(x: dict):
Default level: error ·
Added in 0.0.9 ·
Related issues ·
-View source
+View source
@@ -2593,7 +2593,7 @@ class Foo(TypedDict):
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -2648,7 +2648,7 @@ def h(arg2: type):
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -2691,7 +2691,7 @@ def g(arg: object):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2716,7 +2716,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -2749,7 +2749,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2778,7 +2778,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2804,7 +2804,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2828,7 +2828,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -2861,7 +2861,7 @@ class B(A):
Default level: error ·
Added in 0.0.16 ·
Related issues ·
-View source
+View source
@@ -2894,7 +2894,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2921,7 +2921,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2948,7 +2948,7 @@ f(x=1) # Error raised here
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2976,7 +2976,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3008,7 +3008,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: ignore ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3045,7 +3045,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3109,7 +3109,7 @@ def test(): -> "int":
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3136,7 +3136,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.18 ·
Related issues ·
-View source
+View source
@@ -3168,7 +3168,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3198,7 +3198,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3227,7 +3227,7 @@ class B(A): ... # Error raised here
Default level: error ·
Added in 0.0.1-alpha.30 ·
Related issues ·
-View source
+View source
@@ -3261,7 +3261,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3288,7 +3288,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3316,7 +3316,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3362,7 +3362,7 @@ class A:
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3386,7 +3386,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3413,7 +3413,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3441,7 +3441,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -3499,7 +3499,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3524,7 +3524,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3549,7 +3549,7 @@ print(x) # NameError: name 'x' is not defined
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -3588,7 +3588,7 @@ class D(C): ... # error: [unsupported-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3625,7 +3625,7 @@ b1 < b2 < b1 # exception raised here
Default level: ignore ·
Added in 0.0.12 ·
Related issues ·
-View source
+View source
@@ -3666,7 +3666,7 @@ def factory(base: type[Base]) -> type:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3767,7 +3767,7 @@ to `false`.
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3830,7 +3830,7 @@ def foo(x: int | str) -> int | str:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md
index 650dd838f5df6..2f5c3994f5cf4 100644
--- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md
+++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md
@@ -421,9 +421,7 @@ class Mutable(DefaultFrozenModel, frozen=False, order=True):
name: str
m = Mutable(name="test")
-# TODO: This should not be an error. In order to support this, we need to implement the precise `frozen` semantics of
-# `dataclass_transform` described here: https://typing.python.org/en/latest/spec/dataclasses.html#dataclass-semantics
-m.name = "new" # error: [invalid-assignment]
+m.name = "new" # No error
reveal_type(Mutable(name="A") < Mutable(name="B")) # revealed: bool
```
@@ -459,6 +457,154 @@ m.name = "new" # No error
reveal_type(Mutable(name="A") < Mutable(name="B")) # revealed: bool
```
+### Frozen inheritance
+
+Just like for regular `@dataclass`es, mixing frozen and non-frozen `@dataclass_transform` classes in
+an inheritance chain is not allowed. However, the root class of a `@dataclass_transform` hierarchy
+(the class decorated with `@dataclass_transform()` or the class that directly specifies the
+`@dataclass_transform` metaclass) is "neither frozen nor non-frozen", so both frozen and non-frozen
+subclasses can inherit from it.
+
+#### Using function-based transformers
+
+For function-based transformers, all classes are either frozen or non-frozen. There is no special
+root class.
+
+```py
+from typing import dataclass_transform
+
+@dataclass_transform(frozen_default=True)
+def frozen_model(*, frozen: bool = True): ...
+
+@frozen_model()
+class FrozenParent:
+ x: int
+
+@frozen_model()
+class FrozenChild(FrozenParent):
+ y: int
+
+@frozen_model(frozen=False)
+# error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `NonFrozenChild` cannot inherit from frozen dataclass `FrozenParent`"
+class NonFrozenChild(FrozenParent):
+ y: int
+
+@frozen_model(frozen=False)
+class NonFrozenParent:
+ x: int
+
+@frozen_model()
+# error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenFromNonFrozen` cannot inherit from non-frozen dataclass `NonFrozenParent`"
+class FrozenFromNonFrozen(NonFrozenParent):
+ y: int
+```
+
+#### Using metaclass-based transformers
+
+For metaclass-based transformers, the class that is decorated with `@dataclass_transform` is the
+root class that is "neither frozen nor non-frozen" (`DefaultFrozenMeta` in the example below). So
+children of that class can be either frozen or non-frozen:
+
+```py
+from typing import dataclass_transform
+
+@dataclass_transform(frozen_default=True)
+class FrozenMeta(type):
+ def __new__(
+ cls,
+ name,
+ bases,
+ namespace,
+ *,
+ frozen: bool = True,
+ ): ...
+
+class DefaultFrozenModel(metaclass=FrozenMeta): ...
+```
+
+Both frozen and non-frozen classes can inherit from the root class:
+
+```py
+class FrozenParent(DefaultFrozenModel):
+ x: int
+
+class NonFrozenParent(DefaultFrozenModel, frozen=False):
+ x: int
+```
+
+Inheriting from these classes is fine as long as we keep the frozen/non-frozen status consistent:
+
+```py
+class FrozenChild(FrozenParent):
+ y: int
+
+class NonFrozenChild(NonFrozenParent, frozen=False):
+ y: int
+```
+
+But mixing frozen and non-frozen is not allowed at this level:
+
+```py
+# error: [invalid-frozen-dataclass-subclass]
+class InvalidFrozenChild(NonFrozenParent, frozen=True):
+ y: int
+
+# error: [invalid-frozen-dataclass-subclass]
+class InvalidNonFrozenChild(FrozenParent, frozen=False):
+ y: int
+```
+
+#### Using base-class-based transformers
+
+Similarly, for base-class-based transformers, the class that is decorated with
+`@dataclass_transform` is the root class that is "neither frozen nor non-frozen"
+(`DefaultFrozenModel` in the example below). So children of that class can be either frozen or
+non-frozen:
+
+```py
+from typing import dataclass_transform
+
+@dataclass_transform(frozen_default=True)
+class DefaultFrozenModel:
+ def __init_subclass__(
+ cls,
+ *,
+ frozen: bool = True,
+ ): ...
+```
+
+Both frozen and non-frozen classes can inherit from that root model:
+
+```py
+class FrozenParent(DefaultFrozenModel):
+ x: int
+
+class NonFrozenParent(DefaultFrozenModel, frozen=False):
+ x: int
+```
+
+Inheriting from these classes is fine as long as we keep the frozen/non-frozen status consistent:
+
+```py
+class FrozenChild(FrozenParent):
+ y: int
+
+class NonFrozenChild(NonFrozenParent, frozen=False):
+ y: int
+```
+
+But mixing frozen and non-frozen is not allowed at this level:
+
+```py
+# error: [invalid-frozen-dataclass-subclass]
+class InvalidFrozenChild(NonFrozenParent, frozen=True):
+ y: int
+
+# error: [invalid-frozen-dataclass-subclass]
+class InvalidNonFrozenChild(FrozenParent, frozen=False):
+ y: int
+```
+
### Override diagnostics on dataclass-like classes
#### Frozen override diagnostics
@@ -1206,7 +1352,7 @@ sure that we recognize all fields in a hierarchy like this:
from dataclasses import dataclass
from typing import dataclass_transform
-@dataclass_transform()
+@dataclass_transform(frozen_default=True)
class ModelMeta(type):
pass
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index 7445afd06edb3..d22ae65bb81cb 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -611,12 +611,6 @@ bitflags! {
}
}
-impl DataclassFlags {
- pub(crate) const fn is_frozen(self) -> bool {
- self.contains(Self::FROZEN)
- }
-}
-
pub(crate) const DATACLASS_FLAGS: &[(&str, DataclassFlags)] = &[
("init", DataclassFlags::INIT),
("repr", DataclassFlags::REPR),
@@ -12183,6 +12177,16 @@ pub(super) struct MetaclassCandidate<'db> {
explicit_metaclass_of: StaticClassLiteral<'db>,
}
+/// Information about a `@dataclass_transform`-decorated metaclass.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
+pub(super) struct MetaclassTransformInfo<'db> {
+ pub(super) params: DataclassTransformerParams<'db>,
+
+ /// Whether the metaclass providing these parameters was declared on the class itself
+ /// (via an explicit `metaclass=` keyword) rather than inherited from a base class.
+ pub(super) from_explicit_metaclass: bool,
+}
+
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct UnionType<'db> {
/// The union type includes values in any of these types.
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index 80dffc7745511..e24a03846f926 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -64,8 +64,8 @@ use crate::{
semantic_index, use_def_map,
},
types::{
- CallArguments, CallError, CallErrorKind, MetaclassCandidate, TypeDefinition, UnionType,
- definition_expression_type,
+ CallArguments, CallError, CallErrorKind, MetaclassCandidate, MetaclassTransformInfo,
+ TypeDefinition, UnionType, definition_expression_type,
},
};
use indexmap::IndexSet;
@@ -124,7 +124,7 @@ fn try_metaclass_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
_self_: StaticClassLiteral<'db>,
-) -> Result<(Type<'db>, Option>), MetaclassError<'db>> {
+) -> Result<(Type<'db>, Option>), MetaclassError<'db>> {
Err(MetaclassError {
kind: MetaclassErrorKind::Cycle,
})
@@ -185,8 +185,8 @@ impl<'db> CodeGeneratorKind<'db> {
) -> Option> {
if class.dataclass_params(db).is_some() {
Some(CodeGeneratorKind::DataclassLike(None))
- } else if let Ok((_, Some(transformer_params))) = class.try_metaclass(db) {
- Some(CodeGeneratorKind::DataclassLike(Some(transformer_params)))
+ } else if let Ok((_, Some(info))) = class.try_metaclass(db) {
+ Some(CodeGeneratorKind::DataclassLike(Some(info.params)))
} else if let Some(transformer_params) =
class.iter_mro(db, specialization).skip(1).find_map(|base| {
base.into_class().and_then(|class| {
@@ -2815,6 +2815,38 @@ impl<'db> StaticClassLiteral<'db> {
(dataclass_params, transformer_params)
}
+ /// Returns the effective frozen status of this class if it's a dataclass-like class.
+ ///
+ /// Returns `Some(true)` for a frozen dataclass-like class, `Some(false)` for a non-frozen one,
+ /// and `None` if the class is not a dataclass-like class, or if the dataclass is neither frozen
+ /// nor non-frozen.
+ pub(crate) fn is_frozen_dataclass(self, db: &'db dyn Db) -> Option {
+ // Check if this is a base-class-based transformer that has dataclass_transformer_params directly
+ // attached to it (because it is itself decorated with `@dataclass_transform`), or if this class
+ // has an explicit metaclass that is decorated with `@dataclass_transform`.
+ //
+ // In both cases, this signifies that this class is neither frozen nor non-frozen.
+ //
+ // See for details.
+ if self.dataclass_transformer_params(db).is_some()
+ || self
+ .try_metaclass(db)
+ .is_ok_and(|(_, info)| info.is_some_and(|i| i.from_explicit_metaclass))
+ {
+ return None;
+ }
+
+ if let field_policy @ CodeGeneratorKind::DataclassLike(_) =
+ CodeGeneratorKind::from_class(db, self.into(), None)?
+ {
+ // Otherwise, if this class is a dataclass-like class, determine its frozen status based on
+ // dataclass params and dataclass transformer params.
+ Some(self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN))
+ } else {
+ None
+ }
+ }
+
/// 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(
@@ -2864,7 +2896,7 @@ impl<'db> StaticClassLiteral<'db> {
pub(super) fn try_metaclass(
self,
db: &'db dyn Db,
- ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> {
+ ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> {
tracing::trace!("StaticClassLiteral::try_metaclass: {}", self.name(db));
// Identify the class's own metaclass (or take the first base class's metaclass).
@@ -2968,11 +3000,15 @@ impl<'db> StaticClassLiteral<'db> {
});
}
- let dataclass_transformer_params = candidate
+ let transform_info = candidate
.metaclass
.static_class_literal(db)
- .and_then(|(metaclass_literal, _)| metaclass_literal.dataclass_transformer_params(db));
- Ok((candidate.metaclass.into(), dataclass_transformer_params))
+ .and_then(|(metaclass_literal, _)| metaclass_literal.dataclass_transformer_params(db))
+ .map(|params| MetaclassTransformInfo {
+ params,
+ from_explicit_metaclass: candidate.explicit_metaclass_of == self,
+ });
+ Ok((candidate.metaclass.into(), transform_info))
}
/// Returns the class member of this class named `name`.
@@ -3538,7 +3574,7 @@ 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) {
+ if self.is_frozen_dataclass(db) == Some(true) {
let signature = Signature::new(
Parameters::new(
db,
@@ -4033,7 +4069,7 @@ impl<'db> StaticClassLiteral<'db> {
match name.as_str() {
"__setattr__" | "__delattr__" => {
if let CodeGeneratorKind::DataclassLike(_) = field_policy
- && self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN)
+ && self.is_frozen_dataclass(db) == Some(true)
{
if let Some(builder) = context.report_lint(
&INVALID_DATACLASS_OVERRIDE,
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index f708fd62e7ff9..6fcfec2fcc7d4 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -31,9 +31,7 @@ use crate::types::{
ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type,
protocol_class::ProtocolClass,
};
-use crate::types::{
- DataclassFlags, KnownInstanceType, MemberLookupPolicy, TypeVarInstance, UnionType,
-};
+use crate::types::{KnownInstanceType, MemberLookupPolicy, TypeVarInstance, UnionType};
use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint};
use itertools::Itertools;
use ruff_db::{
@@ -5566,7 +5564,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>(
class_node: &ast::StmtClassDef,
base_class: StaticClassLiteral<'db>,
base_class_node: &ast::Expr,
- base_class_params: DataclassFlags,
+ base_is_frozen: bool,
) {
let db = context.db();
@@ -5576,7 +5574,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>(
return;
};
- let mut diagnostic = if base_class_params.is_frozen() {
+ let mut diagnostic = if base_is_frozen {
let mut diagnostic =
builder.into_diagnostic("Non-frozen dataclass cannot inherit from frozen dataclass");
diagnostic.set_concise_message(format_args!(
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index afc5c2eb33e6d..82fe7304b053b 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -1061,24 +1061,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
if let Some((base_class_literal, _)) = base_class.static_class_literal(self.db())
- && let (Some(base_params), Some(class_params)) = (
- base_class_literal.dataclass_params(self.db()),
- class.dataclass_params(self.db()),
+ && let (Some(base_is_frozen), Some(class_is_frozen)) = (
+ base_class_literal.is_frozen_dataclass(self.db()),
+ class.is_frozen_dataclass(self.db()),
)
+ && base_is_frozen != class_is_frozen
{
- let base_params = base_params.flags(self.db());
- let class_is_frozen = class_params.flags(self.db()).is_frozen();
-
- if base_params.is_frozen() != class_is_frozen {
- report_bad_frozen_dataclass_inheritance(
- &self.context,
- class,
- class_node,
- base_class_literal,
- &class_node.bases()[i],
- base_params,
- );
- }
+ report_bad_frozen_dataclass_inheritance(
+ &self.context,
+ class,
+ class_node,
+ base_class_literal,
+ &class_node.bases()[i],
+ base_is_frozen,
+ );
}
}