From 6368fd0ea54d8b2ad863814dfb85ba0afafb2823 Mon Sep 17 00:00:00 2001
From: Simon Lamon <32477463+silamon@users.noreply.github.com>
Date: Sat, 13 Dec 2025 17:49:10 +0100
Subject: [PATCH 1/5] Raise diagnostic when frozen dataclass inherits a
non-frozen dataclass and the other way around
---
.../src/types/diagnostic.rs | 60 ++++++++++++++++
.../src/types/infer/builder.rs | 72 +++++++++++--------
2 files changed, 103 insertions(+), 29 deletions(-)
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index dcc2f6b3c55e0..47767156a491d 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -121,6 +121,8 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_METHOD_OVERRIDE);
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
+ registry.register_lint(&FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS);
+ registry.register_lint(&NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -2220,6 +2222,64 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Checks for frozen dataclasses that inherit from non-frozen dataclasses.
+ ///
+ /// ## Why is this bad?
+ /// A frozen dataclass promises immutability. Inheriting from a non-frozen
+ /// dataclass breaks that guarantee because the base class allows mutation.
+ ///
+ /// ## Example
+ ///
+ /// ```python
+ /// from dataclasses import dataclass
+ ///
+ /// @dataclass
+ /// class Base:
+ /// x: int
+ ///
+ /// @dataclass(frozen=True)
+ /// class Child(Base): # Error raised here
+ /// y: int
+ /// ```
+ pub(crate) static FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS = {
+ summary: "detects frozen dataclasses inheriting from non-frozen dataclasses",
+ status: LintStatus::stable("0.0.1-alpha.35"),
+ default_level: Level::Error,
+ }
+}
+
+declare_lint! {
+ /// ## What it does
+ /// Checks for non-frozen dataclasses that inherit from frozen dataclasses.
+ ///
+ /// ## Why is this bad?
+ /// A frozen dataclass enforces immutability. Allowing a non-frozen subclass
+ /// would reintroduce mutability and violate the base class contract.
+ ///
+ /// ## Example
+ ///
+ /// ```python
+ /// from dataclasses import dataclass
+ ///
+ /// @dataclass(frozen=True)
+ /// class Base:
+ /// x: int
+ ///
+ /// @dataclass
+ /// class Child(Base): # Error raised here
+ /// y: int
+ /// ```
+ pub(crate) static NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS = {
+ summary: "detects non-frozen dataclasses inheriting from frozen dataclasses",
+ status: LintStatus::stable("0.0.1-alpha.35"),
+ default_level: Level::Error,
+ }
+}
+
+
+
/// A collection of type check diagnostics.
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
pub struct TypeCheckDiagnostics {
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 1b78a02c32f6e..340728572ed3a 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -55,34 +55,7 @@ use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorK
use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator};
use crate::types::context::{InNoTypeCheck, InferContext};
use crate::types::cyclic::CycleDetector;
-use crate::types::diagnostic::{
- self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
- CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY,
- INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS,
- INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY,
- INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE,
- INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL,
- INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
- INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE,
- POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
- SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
- UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
- hint_if_stdlib_attribute_exists_on_other_versions,
- hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
- report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
- report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
- report_instance_layout_conflict, report_invalid_arguments_to_annotated,
- report_invalid_assignment, report_invalid_attribute_assignment,
- report_invalid_exception_caught, report_invalid_exception_cause,
- report_invalid_exception_raised, report_invalid_exception_tuple_caught,
- report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
- report_invalid_or_unsupported_base, report_invalid_return_type,
- report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore,
- report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
- report_possibly_missing_attribute, report_possibly_unresolved_reference,
- report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
- report_unsupported_binary_operation, report_unsupported_comparison,
-};
+use crate::types::diagnostic::{self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds, report_instance_layout_conflict, report_invalid_arguments_to_annotated, report_invalid_assignment, report_invalid_attribute_assignment, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, report_unsupported_binary_operation, report_unsupported_comparison, FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS, NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
is_implicit_classmethod, is_implicit_staticmethod,
@@ -104,7 +77,7 @@ use crate::types::typed_dict::{
use crate::types::visitor::any_over_type;
use crate::types::{
BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypeKind,
- ClassLiteral, ClassType, DataclassParams, DynamicType, InternedType, IntersectionBuilder,
+ ClassLiteral, ClassType, DataclassFlags, DataclassParams, DynamicType, InternedType, IntersectionBuilder,
IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard,
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter,
ParameterForm, Parameters, Signature, SpecialFormType, SubclassOfType, TrackedConstraintSet,
@@ -755,6 +728,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
));
}
}
+
+ let (base_class_literal, _) = base_class.class_literal(self.db());
+
+ if let (Some(base_params), Some(class_params)) = (
+ base_class_literal.dataclass_params(self.db()),
+ class.dataclass_params(self.db()),
+ ) {
+ let base_is_frozen = base_params.flags(self.db())
+ .contains(DataclassFlags::FROZEN);
+
+ let class_is_frozen = class_params.flags(self.db())
+ .contains(DataclassFlags::FROZEN);
+
+ match (base_is_frozen, class_is_frozen) {
+ (true, false) => {
+ if let Some(builder) = self.context.report_lint(
+ &NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS,
+ &class_node.bases()[i],
+ ) {
+ builder.into_diagnostic(format_args!(
+ "A non-frozen class `{}` cannot inherit from a class `{}` that is frozen",
+ class.name(self.db()),
+ base_class.name(self.db()),
+ ));
+ }
+ }
+ (false, true) => {
+ if let Some(builder) = self.context.report_lint(
+ &FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS,
+ &class_node.bases()[i],
+ ) {
+ builder.into_diagnostic(format_args!(
+ "A frozen class `{}` cannot inherit from a class `{}` that is not frozen",
+ class.name(self.db()),
+ base_class.name(self.db()),
+ ));
+ }
+ }
+ _ => {}
+ }
+ }
}
// (4) Check that the class's MRO is resolvable
From d35e7f313db52d15c6eb7f4bbc7b58e9380e5e7d Mon Sep 17 00:00:00 2001
From: Simon Lamon <32477463+silamon@users.noreply.github.com>
Date: Sat, 13 Dec 2025 18:56:26 +0100
Subject: [PATCH 2/5] Add tests, address review
---
.../mdtest/dataclasses/dataclasses.md | 30 +++++++++++
.../src/types/diagnostic.rs | 10 ++--
.../src/types/infer/builder.rs | 53 +++++++++++++++----
3 files changed, 76 insertions(+), 17 deletions(-)
diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
index 7c6417565814d..4c2cd6ed90f9f 100644
--- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
+++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
@@ -521,6 +521,36 @@ frozen = MyFrozenChildClass()
del frozen.x # TODO this should emit an [invalid-assignment]
```
+A diagnostic is emitted if a non-frozen dataclass inherits from a frozen dataclass:
+
+```py
+from dataclasses import dataclass
+
+@dataclass(frozen=True)
+class FrozenBase:
+ x: int
+
+@dataclass
+class Child(FrozenBase): # error: [non-frozen-subclass-of-frozen-dataclass] "A non-frozen class `Child` cannot inherit from a class `FrozenBase` that is frozen"
+
+ y: int
+```
+
+A diagnostic is emitted if a frozen dataclass inherits from a non-frozen dataclass:
+
+```py
+from dataclasses import dataclass
+
+@dataclass
+class Base:
+ x: int
+
+@dataclass(frozen=True)
+class FrozenChild(Base): # error: [frozen-subclass-of-non-frozen-dataclass] "A frozen class `FrozenChild` cannot inherit from a class `Base` that is not frozen"
+
+ y: int
+```
+
### `match_args`
If `match_args` is set to `True` (the default), the `__match_args__` attribute is a tuple created
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 47767156a491d..6b2dbbfedbdc3 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -2227,8 +2227,8 @@ declare_lint! {
/// Checks for frozen dataclasses that inherit from non-frozen dataclasses.
///
/// ## Why is this bad?
- /// A frozen dataclass promises immutability. Inheriting from a non-frozen
- /// dataclass breaks that guarantee because the base class allows mutation.
+ /// Python raises a `TypeError` at runtime when a frozen dataclass
+ /// inherits from a non-frozen dataclass.
///
/// ## Example
///
@@ -2255,8 +2255,8 @@ declare_lint! {
/// Checks for non-frozen dataclasses that inherit from frozen dataclasses.
///
/// ## Why is this bad?
- /// A frozen dataclass enforces immutability. Allowing a non-frozen subclass
- /// would reintroduce mutability and violate the base class contract.
+ /// Python raises a `TypeError` at runtime when a non-frozen dataclass
+ /// inherits from a frozen dataclass.
///
/// ## Example
///
@@ -2278,8 +2278,6 @@ declare_lint! {
}
}
-
-
/// A collection of type check diagnostics.
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
pub struct TypeCheckDiagnostics {
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 340728572ed3a..a4b97b729db4a 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -55,7 +55,35 @@ use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorK
use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator};
use crate::types::context::{InNoTypeCheck, InferContext};
use crate::types::cyclic::CycleDetector;
-use crate::types::diagnostic::{self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds, report_instance_layout_conflict, report_invalid_arguments_to_annotated, report_invalid_assignment, report_invalid_attribute_assignment, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, report_unsupported_binary_operation, report_unsupported_comparison, FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS, NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS};
+use crate::types::diagnostic::{
+ self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
+ CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY,
+ FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE,
+ INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION,
+ INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS,
+ INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT,
+ INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM,
+ INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
+ NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE,
+ POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
+ UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
+ UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
+ hint_if_stdlib_attribute_exists_on_other_versions,
+ hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
+ report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
+ report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
+ report_instance_layout_conflict, report_invalid_arguments_to_annotated,
+ report_invalid_assignment, report_invalid_attribute_assignment,
+ report_invalid_exception_caught, report_invalid_exception_cause,
+ report_invalid_exception_raised, report_invalid_exception_tuple_caught,
+ report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
+ report_invalid_or_unsupported_base, report_invalid_return_type,
+ report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore,
+ report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
+ report_possibly_missing_attribute, report_possibly_unresolved_reference,
+ report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
+ report_unsupported_binary_operation, report_unsupported_comparison,
+};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
is_implicit_classmethod, is_implicit_staticmethod,
@@ -77,14 +105,15 @@ use crate::types::typed_dict::{
use crate::types::visitor::any_over_type;
use crate::types::{
BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypeKind,
- ClassLiteral, ClassType, DataclassFlags, DataclassParams, DynamicType, InternedType, IntersectionBuilder,
- IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard,
- MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter,
- ParameterForm, Parameters, Signature, SpecialFormType, SubclassOfType, TrackedConstraintSet,
- Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
- TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation,
- TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder,
- UnionType, UnionTypeInstance, binding_type, infer_scope_types, todo_type,
+ ClassLiteral, ClassType, DataclassFlags, DataclassParams, DynamicType, InternedType,
+ IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion,
+ LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
+ ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType,
+ SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers,
+ TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
+ TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
+ TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types,
+ todo_type,
};
use crate::types::{CallableTypes, overrides};
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
@@ -735,10 +764,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
base_class_literal.dataclass_params(self.db()),
class.dataclass_params(self.db()),
) {
- let base_is_frozen = base_params.flags(self.db())
+ let base_is_frozen = base_params
+ .flags(self.db())
.contains(DataclassFlags::FROZEN);
- let class_is_frozen = class_params.flags(self.db())
+ let class_is_frozen = class_params
+ .flags(self.db())
.contains(DataclassFlags::FROZEN);
match (base_is_frozen, class_is_frozen) {
From 19abe7fc0d8e76203fc3e805788cd5fda426baf0 Mon Sep 17 00:00:00 2001
From: Simon Lamon <32477463+silamon@users.noreply.github.com>
Date: Sat, 13 Dec 2025 19:47:06 +0100
Subject: [PATCH 3/5] Combine diagnostics, fix docs
---
crates/ty/docs/rules.md | 192 +++++++++++-------
.../mdtest/dataclasses/dataclasses.md | 4 +-
.../src/types/diagnostic.rs | 39 +---
.../src/types/infer/builder.rs | 34 +---
.../e2e__commands__debug_command.snap | 1 +
ty.schema.json | 10 +
6 files changed, 144 insertions(+), 136 deletions(-)
diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index e32e8d3e53fc5..c2b450ae10bd3 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -95,7 +95,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -126,7 +126,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -190,7 +190,7 @@ class B(A): ...
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -218,7 +218,7 @@ type B = A
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -245,7 +245,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -357,7 +357,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -387,7 +387,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -502,7 +502,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -557,7 +557,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -591,7 +591,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
@@ -627,7 +627,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -678,7 +678,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -707,7 +707,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -751,7 +751,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -787,13 +787,57 @@ class D(A):
def foo(self): ... # fine: overrides `A.foo`
```
+## `invalid-frozen-dataclass-subclass`
+
+
+Default level: error ·
+Added in 0.0.1-alpha.35 ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Checks for dataclasses with invalid frozen inheritance:
+- A frozen dataclass cannot inherit from a non-frozen dataclass.
+- A non-frozen dataclass cannot inherit from a frozen dataclass.
+
+**Why is this bad?**
+
+Python raises a `TypeError` at runtime when either of these inheritance
+patterns occurs.
+
+**Example**
+
+
+```python
+from dataclasses import dataclass
+
+@dataclass
+class Base:
+ x: int
+
+@dataclass(frozen=True)
+class Child(Base): # Error raised here
+ y: int
+
+@dataclass(frozen=True)
+class FrozenBase:
+ x: int
+
+@dataclass
+class NonFrozenChild(FrozenBase): # Error raised here
+ y: int
+```
+
## `invalid-generic-class`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -826,7 +870,7 @@ class C[U](Generic[T]): ...
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -865,7 +909,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -900,7 +944,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -934,7 +978,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1041,7 +1085,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
@@ -1095,7 +1139,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -1125,7 +1169,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
@@ -1175,7 +1219,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1201,7 +1245,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1232,7 +1276,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
@@ -1266,7 +1310,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
@@ -1315,7 +1359,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1340,7 +1384,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1398,7 +1442,7 @@ TODO #14889
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1425,7 +1469,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
@@ -1472,7 +1516,7 @@ Bar[int] # error: too few arguments
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1502,7 +1546,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1532,7 +1576,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
@@ -1566,7 +1610,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1600,7 +1644,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1635,7 +1679,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1660,7 +1704,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
@@ -1693,7 +1737,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1722,7 +1766,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1746,7 +1790,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1772,7 +1816,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -1805,7 +1849,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1832,7 +1876,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -1890,7 +1934,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1920,7 +1964,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
@@ -1949,7 +1993,7 @@ class B(A): ... # Error raised here
Default level: error ·
Preview (since 0.0.1-alpha.30) ·
Related issues ·
-View source
+View source
@@ -1983,7 +2027,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2010,7 +2054,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2038,7 +2082,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2084,7 +2128,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2111,7 +2155,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
@@ -2139,7 +2183,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2164,7 +2208,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2189,7 +2233,7 @@ print(x) # NameError: name 'x' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2226,7 +2270,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2254,7 +2298,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2279,7 +2323,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -2320,7 +2364,7 @@ class SubProto(BaseProto, Protocol):
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -2408,7 +2452,7 @@ a = 20 / 0 # type: ignore
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2436,7 +2480,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
@@ -2468,7 +2512,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2500,7 +2544,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2527,7 +2571,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2551,7 +2595,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2609,7 +2653,7 @@ def g():
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2648,7 +2692,7 @@ class D(C): ... # error: [unsupported-base]
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2711,7 +2755,7 @@ def foo(x: int | str) -> int | str:
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -2735,7 +2779,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
index 4c2cd6ed90f9f..54942f4a2e21b 100644
--- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
+++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
@@ -531,7 +531,7 @@ class FrozenBase:
x: int
@dataclass
-class Child(FrozenBase): # error: [non-frozen-subclass-of-frozen-dataclass] "A non-frozen class `Child` cannot inherit from a class `FrozenBase` that is frozen"
+class Child(FrozenBase): # error: [invalid-frozen-dataclass-subclass] "A non-frozen class `Child` cannot inherit from a class `FrozenBase` that is frozen"
y: int
```
@@ -546,7 +546,7 @@ class Base:
x: int
@dataclass(frozen=True)
-class FrozenChild(Base): # error: [frozen-subclass-of-non-frozen-dataclass] "A frozen class `FrozenChild` cannot inherit from a class `Base` that is not frozen"
+class FrozenChild(Base): # error: [invalid-frozen-dataclass-subclass] "A frozen class `FrozenChild` cannot inherit from a class `Base` that is not frozen"
y: int
```
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 6b2dbbfedbdc3..abd39ee2b41aa 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -121,8 +121,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_METHOD_OVERRIDE);
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
- registry.register_lint(&FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS);
- registry.register_lint(&NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS);
+ registry.register_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -2224,11 +2223,13 @@ declare_lint! {
declare_lint! {
/// ## What it does
- /// Checks for frozen dataclasses that inherit from non-frozen dataclasses.
+ /// Checks for dataclasses with invalid frozen inheritance:
+ /// - A frozen dataclass cannot inherit from a non-frozen dataclass.
+ /// - A non-frozen dataclass cannot inherit from a frozen dataclass.
///
/// ## Why is this bad?
- /// Python raises a `TypeError` at runtime when a frozen dataclass
- /// inherits from a non-frozen dataclass.
+ /// Python raises a `TypeError` at runtime when either of these inheritance
+ /// patterns occurs.
///
/// ## Example
///
@@ -2242,37 +2243,17 @@ declare_lint! {
/// @dataclass(frozen=True)
/// class Child(Base): # Error raised here
/// y: int
- /// ```
- pub(crate) static FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS = {
- summary: "detects frozen dataclasses inheriting from non-frozen dataclasses",
- status: LintStatus::stable("0.0.1-alpha.35"),
- default_level: Level::Error,
- }
-}
-
-declare_lint! {
- /// ## What it does
- /// Checks for non-frozen dataclasses that inherit from frozen dataclasses.
- ///
- /// ## Why is this bad?
- /// Python raises a `TypeError` at runtime when a non-frozen dataclass
- /// inherits from a frozen dataclass.
- ///
- /// ## Example
- ///
- /// ```python
- /// from dataclasses import dataclass
///
/// @dataclass(frozen=True)
- /// class Base:
+ /// class FrozenBase:
/// x: int
///
/// @dataclass
- /// class Child(Base): # Error raised here
+ /// class NonFrozenChild(FrozenBase): # Error raised here
/// y: int
/// ```
- pub(crate) static NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS = {
- summary: "detects non-frozen dataclasses inheriting from frozen dataclasses",
+ pub(crate) static INVALID_FROZEN_DATACLASS_SUBCLASS = {
+ summary: "detects dataclasses with invalid frozen/non-frozen subclassing",
status: LintStatus::stable("0.0.1-alpha.35"),
default_level: Level::Error,
}
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index a4b97b729db4a..a395949ba6686 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -55,35 +55,7 @@ use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorK
use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator};
use crate::types::context::{InNoTypeCheck, InferContext};
use crate::types::cyclic::CycleDetector;
-use crate::types::diagnostic::{
- self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
- CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY,
- FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE,
- INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION,
- INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS,
- INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT,
- INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM,
- INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
- NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE,
- POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
- UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
- UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
- hint_if_stdlib_attribute_exists_on_other_versions,
- hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
- report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
- report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
- report_instance_layout_conflict, report_invalid_arguments_to_annotated,
- report_invalid_assignment, report_invalid_attribute_assignment,
- report_invalid_exception_caught, report_invalid_exception_cause,
- report_invalid_exception_raised, report_invalid_exception_tuple_caught,
- report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
- report_invalid_or_unsupported_base, report_invalid_return_type,
- report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore,
- report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
- report_possibly_missing_attribute, report_possibly_unresolved_reference,
- report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
- report_unsupported_binary_operation, report_unsupported_comparison,
-};
+use crate::types::diagnostic::{self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INVALID_FROZEN_DATACLASS_SUBCLASS, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds, report_instance_layout_conflict, report_invalid_arguments_to_annotated, report_invalid_assignment, report_invalid_attribute_assignment, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, report_unsupported_binary_operation, report_unsupported_comparison};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
is_implicit_classmethod, is_implicit_staticmethod,
@@ -775,7 +747,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
match (base_is_frozen, class_is_frozen) {
(true, false) => {
if let Some(builder) = self.context.report_lint(
- &NON_FROZEN_SUBCLASS_OF_FROZEN_DATACLASS,
+ &INVALID_FROZEN_DATACLASS_SUBCLASS,
&class_node.bases()[i],
) {
builder.into_diagnostic(format_args!(
@@ -787,7 +759,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
(false, true) => {
if let Some(builder) = self.context.report_lint(
- &FROZEN_SUBCLASS_OF_NON_FROZEN_DATACLASS,
+ &INVALID_FROZEN_DATACLASS_SUBCLASS,
&class_node.bases()[i],
) {
builder.into_diagnostic(format_args!(
diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
index 21c4b0501433a..cf1b0afc645a6 100644
--- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
+++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
@@ -55,6 +55,7 @@ Settings: Settings {
"invalid-declaration": Error (Default),
"invalid-exception-caught": Error (Default),
"invalid-explicit-override": Error (Default),
+ "invalid-frozen-dataclass-subclass": Error (Default),
"invalid-generic-class": Error (Default),
"invalid-ignore-comment": Warning (Default),
"invalid-key": Error (Default),
diff --git a/ty.schema.json b/ty.schema.json
index 13d1ac9c478fc..87feeb250775c 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -583,6 +583,16 @@
}
]
},
+ "invalid-frozen-dataclass-subclass": {
+ "title": "detects dataclasses with invalid frozen/non-frozen subclassing",
+ "description": "## What it does\nChecks for dataclasses with invalid frozen inheritance:\n- A frozen dataclass cannot inherit from a non-frozen dataclass.\n- A non-frozen dataclass cannot inherit from a frozen dataclass.\n\n## Why is this bad?\nPython raises a `TypeError` at runtime when either of these inheritance\npatterns occurs.\n\n## Example\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass Base:\n x: int\n\n@dataclass(frozen=True)\nclass Child(Base): # Error raised here\n y: int\n\n@dataclass(frozen=True)\nclass FrozenBase:\n x: int\n\n@dataclass\nclass NonFrozenChild(FrozenBase): # Error raised here\n y: int\n```",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"invalid-generic-class": {
"title": "detects invalid generic classes",
"description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\n\n## Examples\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\") # okay\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)",
From 286649be5bec15cf2a5e3d61d20d386afd610904 Mon Sep 17 00:00:00 2001
From: Simon Lamon <32477463+silamon@users.noreply.github.com>
Date: Sat, 13 Dec 2025 19:50:22 +0100
Subject: [PATCH 4/5] Formatting
---
.../src/types/infer/builder.rs | 29 ++++++++++++++++++-
1 file changed, 28 insertions(+), 1 deletion(-)
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index a395949ba6686..b6ed9aa2c6f00 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -55,7 +55,34 @@ use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorK
use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator};
use crate::types::context::{InNoTypeCheck, InferContext};
use crate::types::cyclic::CycleDetector;
-use crate::types::diagnostic::{self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INVALID_FROZEN_DATACLASS_SUBCLASS, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds, report_instance_layout_conflict, report_invalid_arguments_to_annotated, report_invalid_assignment, report_invalid_attribute_assignment, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, report_unsupported_binary_operation, report_unsupported_comparison};
+use crate::types::diagnostic::{
+ self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
+ CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY,
+ INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS,
+ INVALID_BASE, INVALID_DECLARATION, INVALID_FROZEN_DATACLASS_SUBCLASS, INVALID_GENERIC_CLASS,
+ INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE,
+ INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC,
+ INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
+ INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE,
+ POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
+ SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
+ UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
+ hint_if_stdlib_attribute_exists_on_other_versions,
+ hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
+ report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
+ report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
+ report_instance_layout_conflict, report_invalid_arguments_to_annotated,
+ report_invalid_assignment, report_invalid_attribute_assignment,
+ report_invalid_exception_caught, report_invalid_exception_cause,
+ report_invalid_exception_raised, report_invalid_exception_tuple_caught,
+ report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
+ report_invalid_or_unsupported_base, report_invalid_return_type,
+ report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore,
+ report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
+ report_possibly_missing_attribute, report_possibly_unresolved_reference,
+ report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
+ report_unsupported_binary_operation, report_unsupported_comparison,
+};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
is_implicit_classmethod, is_implicit_staticmethod,
From 4dfad38c753e28ff1fca4c961097abcb9f19d4e1 Mon Sep 17 00:00:00 2001
From: Alex Waygood
Date: Sat, 13 Dec 2025 20:49:40 +0000
Subject: [PATCH 5/5] fancier diagnostics
---
.../mdtest/dataclasses/dataclasses.md | 51 +++++-
...zen_in\342\200\246_(9af2ab07b8e829e).snap" | 154 ++++++++++++++++++
crates/ty_python_semantic/src/types.rs | 6 +
crates/ty_python_semantic/src/types/class.rs | 22 +++
.../src/types/diagnostic.rs | 90 +++++++++-
.../src/types/infer/builder.rs | 90 ++++------
6 files changed, 349 insertions(+), 64 deletions(-)
create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para\342\200\246_-_frozen__non-frozen_in\342\200\246_(9af2ab07b8e829e).snap"
diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
index 54942f4a2e21b..60bfd36176948 100644
--- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
+++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
@@ -521,7 +521,14 @@ frozen = MyFrozenChildClass()
del frozen.x # TODO this should emit an [invalid-assignment]
```
-A diagnostic is emitted if a non-frozen dataclass inherits from a frozen dataclass:
+### frozen/non-frozen inheritance
+
+If a non-frozen dataclass inherits from a frozen dataclass, an exception is raised at runtime. We
+catch this error:
+
+
+
+`a.py`:
```py
from dataclasses import dataclass
@@ -530,13 +537,15 @@ from dataclasses import dataclass
class FrozenBase:
x: int
-@dataclass
-class Child(FrozenBase): # error: [invalid-frozen-dataclass-subclass] "A non-frozen class `Child` cannot inherit from a class `FrozenBase` that is frozen"
-
+@dataclass
+# error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
+class Child(FrozenBase):
y: int
```
-A diagnostic is emitted if a frozen dataclass inherits from a non-frozen dataclass:
+Frozen dataclasses inheriting from non-frozen dataclasses are also illegal:
+
+`b.py`:
```py
from dataclasses import dataclass
@@ -546,11 +555,39 @@ class Base:
x: int
@dataclass(frozen=True)
-class FrozenChild(Base): # error: [invalid-frozen-dataclass-subclass] "A frozen class `FrozenChild` cannot inherit from a class `Base` that is not frozen"
-
+# error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
+class FrozenChild(Base):
y: int
```
+Example of diagnostics when there are multiple files involved:
+
+`module.py`:
+
+```py
+import dataclasses
+
+@dataclasses.dataclass(frozen=False)
+class NotFrozenBase:
+ x: int
+```
+
+`main.py`:
+
+```py
+from functools import total_ordering
+from typing import final
+from dataclasses import dataclass
+
+from module import NotFrozenBase
+
+@final
+@dataclass(frozen=True)
+@total_ordering
+class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
+ y: str
+```
+
### `match_args`
If `match_args` is set to `True` (the default), the `__match_args__` attribute is a tuple created
diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para\342\200\246_-_frozen__non-frozen_in\342\200\246_(9af2ab07b8e829e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para\342\200\246_-_frozen__non-frozen_in\342\200\246_(9af2ab07b8e829e).snap"
new file mode 100644
index 0000000000000..3b3a006b6e19a
--- /dev/null
+++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para\342\200\246_-_frozen__non-frozen_in\342\200\246_(9af2ab07b8e829e).snap"
@@ -0,0 +1,154 @@
+---
+source: crates/ty_test/src/lib.rs
+expression: snapshot
+---
+---
+mdtest name: dataclasses.md - Dataclasses - Other dataclass parameters - frozen/non-frozen inheritance
+mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
+---
+
+# Python source files
+
+## a.py
+
+```
+ 1 | from dataclasses import dataclass
+ 2 |
+ 3 | @dataclass(frozen=True)
+ 4 | class FrozenBase:
+ 5 | x: int
+ 6 |
+ 7 | @dataclass
+ 8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
+ 9 | class Child(FrozenBase):
+10 | y: int
+```
+
+## b.py
+
+```
+ 1 | from dataclasses import dataclass
+ 2 |
+ 3 | @dataclass
+ 4 | class Base:
+ 5 | x: int
+ 6 |
+ 7 | @dataclass(frozen=True)
+ 8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
+ 9 | class FrozenChild(Base):
+10 | y: int
+```
+
+## module.py
+
+```
+1 | import dataclasses
+2 |
+3 | @dataclasses.dataclass(frozen=False)
+4 | class NotFrozenBase:
+5 | x: int
+```
+
+## main.py
+
+```
+ 1 | from functools import total_ordering
+ 2 | from typing import final
+ 3 | from dataclasses import dataclass
+ 4 |
+ 5 | from module import NotFrozenBase
+ 6 |
+ 7 | @final
+ 8 | @dataclass(frozen=True)
+ 9 | @total_ordering
+10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
+11 | y: str
+```
+
+# Diagnostics
+
+```
+error[invalid-frozen-dataclass-subclass]: Non-frozen dataclass cannot inherit from frozen dataclass
+ --> src/a.py:7:1
+ |
+ 5 | x: int
+ 6 |
+ 7 | @dataclass
+ | ---------- `Child` dataclass parameters
+ 8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
+ 9 | class Child(FrozenBase):
+ | ^^^^^^----------^ Subclass `Child` is not frozen but base class `FrozenBase` is
+10 | y: int
+ |
+info: This causes the class creation to fail
+info: Base class definition
+ --> src/a.py:3:1
+ |
+1 | from dataclasses import dataclass
+2 |
+3 | @dataclass(frozen=True)
+ | ----------------------- `FrozenBase` dataclass parameters
+4 | class FrozenBase:
+ | ^^^^^^^^^^ `FrozenBase` definition
+5 | x: int
+ |
+info: rule `invalid-frozen-dataclass-subclass` is enabled by default
+
+```
+
+```
+error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
+ --> src/b.py:7:1
+ |
+ 5 | x: int
+ 6 |
+ 7 | @dataclass(frozen=True)
+ | ----------------------- `FrozenChild` dataclass parameters
+ 8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
+ 9 | class FrozenChild(Base):
+ | ^^^^^^^^^^^^----^ Subclass `FrozenChild` is frozen but base class `Base` is not
+10 | y: int
+ |
+info: This causes the class creation to fail
+info: Base class definition
+ --> src/b.py:3:1
+ |
+1 | from dataclasses import dataclass
+2 |
+3 | @dataclass
+ | ---------- `Base` dataclass parameters
+4 | class Base:
+ | ^^^^ `Base` definition
+5 | x: int
+ |
+info: rule `invalid-frozen-dataclass-subclass` is enabled by default
+
+```
+
+```
+error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
+ --> src/main.py:8:1
+ |
+ 7 | @final
+ 8 | @dataclass(frozen=True)
+ | ----------------------- `FrozenChild` dataclass parameters
+ 9 | @total_ordering
+10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
+ | ^^^^^^^^^^^^-------------^ Subclass `FrozenChild` is frozen but base class `NotFrozenBase` is not
+11 | y: str
+ |
+info: This causes the class creation to fail
+info: Base class definition
+ --> src/module.py:3:1
+ |
+1 | import dataclasses
+2 |
+3 | @dataclasses.dataclass(frozen=False)
+ | ------------------------------------ `NotFrozenBase` dataclass parameters
+4 | class NotFrozenBase:
+ | ^^^^^^^^^^^^^ `NotFrozenBase` definition
+5 | x: int
+ |
+info: rule `invalid-frozen-dataclass-subclass` is enabled by default
+
+```
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index e9311547d7d2b..726decfc07fd9 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -677,6 +677,12 @@ 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),
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index 32962ea128090..022e63fe33431 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -1856,6 +1856,28 @@ impl<'db> ClassLiteral<'db> {
.filter_map(|decorator| decorator.known(db))
}
+ /// Iterate through the decorators on this class, returning the position of the first one
+ /// that matches the given predicate.
+ pub(super) fn find_decorator_position(
+ self,
+ db: &'db dyn Db,
+ predicate: impl Fn(Type<'db>) -> bool,
+ ) -> Option {
+ self.decorators(db)
+ .iter()
+ .position(|decorator| predicate(*decorator))
+ }
+
+ /// Iterate through the decorators on this class, returning the index of the first one
+ /// that is either `@dataclass` or `@dataclass(...)`.
+ pub(super) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option {
+ self.find_decorator_position(db, |ty| match ty {
+ Type::FunctionLiteral(function) => function.is_known(db, KnownFunction::Dataclass),
+ Type::DataclassDecorator(_) => true,
+ _ => false,
+ })
+ }
+
/// Is this class final?
pub(super) fn is_final(self, db: &'db dyn Db) -> bool {
self.known_function_decorators(db)
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index abd39ee2b41aa..3acd7b0a64191 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -30,7 +30,7 @@ use crate::types::{
ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type,
protocol_class::ProtocolClass,
};
-use crate::types::{KnownInstanceType, MemberLookupPolicy};
+use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy};
use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint};
use itertools::Itertools;
use ruff_db::{
@@ -4308,6 +4308,94 @@ fn report_unsupported_binary_operation_impl<'a>(
Some(diagnostic)
}
+pub(super) fn report_bad_frozen_dataclass_inheritance<'db>(
+ context: &InferContext<'db, '_>,
+ class: ClassLiteral<'db>,
+ class_node: &ast::StmtClassDef,
+ base_class: ClassLiteral<'db>,
+ base_class_node: &ast::Expr,
+ base_class_params: DataclassFlags,
+) {
+ let db = context.db();
+
+ let Some(builder) =
+ context.report_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS, class.header_range(db))
+ else {
+ return;
+ };
+
+ let mut diagnostic = if base_class_params.is_frozen() {
+ let mut diagnostic =
+ builder.into_diagnostic("Non-frozen dataclass cannot inherit from frozen dataclass");
+ diagnostic.set_concise_message(format_args!(
+ "Non-frozen dataclass `{}` cannot inherit from frozen dataclass `{}`",
+ class.name(db),
+ base_class.name(db)
+ ));
+ diagnostic.set_primary_message(format_args!(
+ "Subclass `{}` is not frozen but base class `{}` is",
+ class.name(db),
+ base_class.name(db)
+ ));
+ diagnostic
+ } else {
+ let mut diagnostic =
+ builder.into_diagnostic("Frozen dataclass cannot inherit from non-frozen dataclass");
+ diagnostic.set_concise_message(format_args!(
+ "Frozen dataclass `{}` cannot inherit from non-frozen dataclass `{}`",
+ class.name(db),
+ base_class.name(db)
+ ));
+ diagnostic.set_primary_message(format_args!(
+ "Subclass `{}` is frozen but base class `{}` is not",
+ class.name(db),
+ base_class.name(db)
+ ));
+ diagnostic
+ };
+
+ diagnostic.annotate(context.secondary(base_class_node));
+
+ if let Some(position) = class.find_dataclass_decorator_position(db) {
+ diagnostic.annotate(
+ context
+ .secondary(&class_node.decorator_list[position])
+ .message(format_args!("`{}` dataclass parameters", class.name(db))),
+ );
+ }
+ diagnostic.info("This causes the class creation to fail");
+
+ if let Some(decorator_position) = base_class.find_dataclass_decorator_position(db) {
+ let mut sub = SubDiagnostic::new(
+ SubDiagnosticSeverity::Info,
+ format_args!("Base class definition"),
+ );
+ sub.annotate(
+ Annotation::primary(base_class.header_span(db))
+ .message(format_args!("`{}` definition", base_class.name(db))),
+ );
+
+ let base_class_file = base_class.file(db);
+ let module = parsed_module(db, base_class_file).load(db);
+
+ let decorator_range = base_class
+ .body_scope(db)
+ .node(db)
+ .expect_class()
+ .node(&module)
+ .decorator_list[decorator_position]
+ .range();
+
+ sub.annotate(
+ Annotation::secondary(Span::from(base_class_file).with_range(decorator_range)).message(
+ format_args!("`{}` dataclass parameters", base_class.name(db)),
+ ),
+ );
+
+ diagnostic.sub(sub);
+ }
+}
+
/// This function receives an unresolved `from foo import bar` import,
/// where `foo` can be resolved to a module but that module does not
/// have a `bar` member or submodule.
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index b6ed9aa2c6f00..43940a4d889d4 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -59,25 +59,26 @@ use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY,
INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS,
- INVALID_BASE, INVALID_DECLARATION, INVALID_FROZEN_DATACLASS_SUBCLASS, INVALID_GENERIC_CLASS,
- INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE,
- INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC,
- INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
+ INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY,
+ INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE,
+ INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL,
+ INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE,
POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
- report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
- report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
- report_instance_layout_conflict, report_invalid_arguments_to_annotated,
- report_invalid_assignment, report_invalid_attribute_assignment,
- report_invalid_exception_caught, report_invalid_exception_cause,
- report_invalid_exception_raised, report_invalid_exception_tuple_caught,
- report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
- report_invalid_or_unsupported_base, report_invalid_return_type,
- report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore,
+ report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
+ report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases,
+ report_implicit_return_type, report_index_out_of_bounds, report_instance_layout_conflict,
+ report_invalid_arguments_to_annotated, report_invalid_assignment,
+ report_invalid_attribute_assignment, report_invalid_exception_caught,
+ report_invalid_exception_cause, report_invalid_exception_raised,
+ report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type,
+ report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base,
+ report_invalid_return_type, report_invalid_type_checking_constant,
+ report_named_tuple_field_with_leading_underscore,
report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
report_possibly_missing_attribute, report_possibly_unresolved_reference,
report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
@@ -104,15 +105,14 @@ use crate::types::typed_dict::{
use crate::types::visitor::any_over_type;
use crate::types::{
BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypeKind,
- ClassLiteral, ClassType, DataclassFlags, DataclassParams, DynamicType, InternedType,
- IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion,
- LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
- ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType,
- SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers,
- TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
- TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
- TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types,
- todo_type,
+ ClassLiteral, ClassType, DataclassParams, DynamicType, InternedType, IntersectionBuilder,
+ IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard,
+ MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter,
+ ParameterForm, Parameters, Signature, SpecialFormType, SubclassOfType, TrackedConstraintSet,
+ Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
+ TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation,
+ TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder,
+ UnionType, UnionTypeInstance, binding_type, infer_scope_types, todo_type,
};
use crate::types::{CallableTypes, overrides};
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
@@ -763,40 +763,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
base_class_literal.dataclass_params(self.db()),
class.dataclass_params(self.db()),
) {
- let base_is_frozen = base_params
- .flags(self.db())
- .contains(DataclassFlags::FROZEN);
-
- let class_is_frozen = class_params
- .flags(self.db())
- .contains(DataclassFlags::FROZEN);
-
- match (base_is_frozen, class_is_frozen) {
- (true, false) => {
- if let Some(builder) = self.context.report_lint(
- &INVALID_FROZEN_DATACLASS_SUBCLASS,
- &class_node.bases()[i],
- ) {
- builder.into_diagnostic(format_args!(
- "A non-frozen class `{}` cannot inherit from a class `{}` that is frozen",
- class.name(self.db()),
- base_class.name(self.db()),
- ));
- }
- }
- (false, true) => {
- if let Some(builder) = self.context.report_lint(
- &INVALID_FROZEN_DATACLASS_SUBCLASS,
- &class_node.bases()[i],
- ) {
- builder.into_diagnostic(format_args!(
- "A frozen class `{}` cannot inherit from a class `{}` that is not frozen",
- class.name(self.db()),
- base_class.name(self.db()),
- ));
- }
- }
- _ => {}
+ 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,
+ );
}
}
}