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, + ); } }